diff options
Diffstat (limited to 'src')
42 files changed, 2721 insertions, 1321 deletions
diff --git a/src/eu/siacs/conversations/crypto/OtrEngine.java b/src/eu/siacs/conversations/crypto/OtrEngine.java index 01ba5e49..7960aa2b 100644 --- a/src/eu/siacs/conversations/crypto/OtrEngine.java +++ b/src/eu/siacs/conversations/crypto/OtrEngine.java @@ -154,13 +154,16 @@ public class OtrEngine implements OtrEngineHost { @Override public void injectMessage(SessionID session, String body) throws OtrException { MessagePacket packet = new MessagePacket(); - packet.setFrom(account.getFullJid()); //sender - packet.setTo(session.getAccountID()+"/"+session.getUserID()); //reciepient + packet.setFrom(account.getFullJid()); + if (session.getUserID().isEmpty()) { + packet.setTo(session.getAccountID()); + } else { + packet.setTo(session.getAccountID()+"/"+session.getUserID()); + } packet.setBody(body); packet.addChild("private","urn:xmpp:carbons:2"); packet.addChild("no-copy","urn:xmpp:hints"); packet.setType(MessagePacket.TYPE_CHAT); - //Log.d(LOGTAG,packet.toString()); account.getXmppConnection().sendMessagePacket(packet); } diff --git a/src/eu/siacs/conversations/crypto/PgpEngine.java b/src/eu/siacs/conversations/crypto/PgpEngine.java index 2d0c56e1..65b7ccc7 100644 --- a/src/eu/siacs/conversations/crypto/PgpEngine.java +++ b/src/eu/siacs/conversations/crypto/PgpEngine.java @@ -54,9 +54,18 @@ public class PgpEngine { switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: - message.setBody(os.toString()); - message.setEncryption(Message.ENCRYPTION_DECRYPTED); - callback.success(message); + try { + os.flush(); + if (message.getEncryption() == Message.ENCRYPTION_PGP) { + message.setBody(os.toString()); + message.setEncryption(Message.ENCRYPTION_DECRYPTED); + callback.success(message); + } + } catch (IOException e) { + callback.error(R.string.openpgp_error, message); + return; + } + return; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: callback.userInputRequried((PendingIntent) result @@ -64,6 +73,8 @@ public class PgpEngine { message); return; case OpenPgpApi.RESULT_CODE_ERROR: + OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR); + Log.d("xmppService",error.getMessage()); callback.error(R.string.openpgp_error, message); return; default: @@ -153,14 +164,20 @@ public class PgpEngine { switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: - StringBuilder encryptedMessageBody = new StringBuilder(); - String[] lines = os.toString().split("\n"); - for (int i = 3; i < lines.length - 1; ++i) { - encryptedMessageBody.append(lines[i].trim()); + try { + os.flush(); + StringBuilder encryptedMessageBody = new StringBuilder(); + String[] lines = os.toString().split("\n"); + for (int i = 3; i < lines.length - 1; ++i) { + encryptedMessageBody.append(lines[i].trim()); + } + message.setEncryptedBody(encryptedMessageBody + .toString()); + callback.success(message); + } catch (IOException e) { + callback.error(R.string.openpgp_error, message); } - message.setEncryptedBody(encryptedMessageBody - .toString()); - callback.success(message); + break; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: callback.userInputRequried((PendingIntent) result diff --git a/src/eu/siacs/conversations/entities/Account.java b/src/eu/siacs/conversations/entities/Account.java index b19889bf..5de2c532 100644 --- a/src/eu/siacs/conversations/entities/Account.java +++ b/src/eu/siacs/conversations/entities/Account.java @@ -1,7 +1,6 @@ package eu.siacs.conversations.entities; import java.security.interfaces.DSAPublicKey; -import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.concurrent.CopyOnWriteArrayList; @@ -13,10 +12,14 @@ import org.json.JSONException; import org.json.JSONObject; import eu.siacs.conversations.crypto.OtrEngine; +import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.XmppConnection; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; public class Account extends AbstractEntity{ @@ -42,8 +45,6 @@ public class Account extends AbstractEntity{ public static final int STATUS_UNAUTHORIZED = 3; public static final int STATUS_SERVER_NOT_FOUND = 5; - public static final int STATUS_SERVER_REQUIRES_TLS = 6; - public static final int STATUS_REGISTRATION_FAILED = 7; public static final int STATUS_REGISTRATION_CONFLICT = 8; public static final int STATUS_REGISTRATION_SUCCESSFULL = 9; @@ -57,6 +58,7 @@ public class Account extends AbstractEntity{ protected String resource = "mobile"; protected int status = -1; protected JSONObject keys = new JSONObject(); + protected String avatar; protected boolean online = false; @@ -68,7 +70,7 @@ public class Account extends AbstractEntity{ private Roster roster = null; - private List<Bookmark> bookmarks = new ArrayList<Bookmark>(); + private List<Bookmark> bookmarks = new CopyOnWriteArrayList<Bookmark>(); public List<Conversation> pendingConferenceJoins = new CopyOnWriteArrayList<Conversation>(); public List<Conversation> pendingConferenceLeaves = new CopyOnWriteArrayList<Conversation>(); @@ -142,6 +144,11 @@ public class Account extends AbstractEntity{ } } + public boolean errorStatus() { + int s = getStatus(); + return (s == STATUS_OFFLINE || s == STATUS_SERVER_NOT_FOUND || s == STATUS_UNAUTHORIZED); + } + public boolean hasErrorStatus() { return getStatus() > STATUS_NO_INTERNET && (getXmppConnection().getAttempt() >= 2); } @@ -319,4 +326,25 @@ public class Account extends AbstractEntity{ } return false; } + + public Bitmap getImage(Context context, int size) { + if (this.avatar!=null) { + Bitmap bm = FileBackend.getAvatar(this.avatar, size, context); + if (bm==null) { + return UIHelper.getContactPicture(getJid(), size, context, false); + } else { + return bm; + } + } else { + return UIHelper.getContactPicture(getJid(), size, context, false); + } + } + + public void setAvatar(String filename) { + this.avatar = filename; + } + + public String getAvatar() { + return this.avatar; + } } diff --git a/src/eu/siacs/conversations/entities/Contact.java b/src/eu/siacs/conversations/entities/Contact.java index 8f8e38a5..47a3a0d7 100644 --- a/src/eu/siacs/conversations/entities/Contact.java +++ b/src/eu/siacs/conversations/entities/Contact.java @@ -8,12 +8,14 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Element; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; public class Contact implements ListItem { public static final String TABLENAME = "contacts"; @@ -34,6 +36,7 @@ public class Contact implements ListItem { protected int subscription = 0; protected String systemAccount; protected String photoUri; + protected String avatar; protected JSONObject keys = new JSONObject(); protected Presences presences = new Presences(); @@ -316,7 +319,20 @@ public class Contact implements ListItem { } @Override - public Bitmap getImage(int dpSize, Context context) { - return UIHelper.getContactPicture(this, dpSize, context, false); + public Bitmap getImage(int size, Context context) { + if (this.avatar!=null) { + Bitmap bm = FileBackend.getAvatar(avatar, size, context); + if (bm==null) { + return UIHelper.getContactPicture(this, size, context, false); + } else { + return bm; + } + } else { + return UIHelper.getContactPicture(this, size, context, false); + } + } + + public void setAvatar(String filename) { + this.avatar = filename; } } diff --git a/src/eu/siacs/conversations/entities/Conversation.java b/src/eu/siacs/conversations/entities/Conversation.java index 76fe84cf..be641ea4 100644 --- a/src/eu/siacs/conversations/entities/Conversation.java +++ b/src/eu/siacs/conversations/entities/Conversation.java @@ -1,8 +1,10 @@ package eu.siacs.conversations.entities; import java.security.interfaces.DSAPublicKey; -import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import eu.siacs.conversations.utils.UIHelper; import net.java.otr4j.OtrException; import net.java.otr4j.crypto.OtrCryptoEngineImpl; @@ -13,7 +15,7 @@ import net.java.otr4j.session.SessionStatus; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; -import android.net.Uri; +import android.graphics.Bitmap; public class Conversation extends AbstractEntity { public static final String TABLENAME = "conversations"; @@ -43,7 +45,7 @@ public class Conversation extends AbstractEntity { private String nextPresence; - private transient List<Message> messages = null; + private transient CopyOnWriteArrayList<Message> messages = null; private transient Account account = null; private transient SessionImpl otrSession; @@ -85,8 +87,9 @@ public class Conversation extends AbstractEntity { } public List<Message> getMessages() { - if (messages == null) - this.messages = new ArrayList<Message>(); // prevent null pointer + if (messages == null) { + this.messages = new CopyOnWriteArrayList<Message>(); // prevent null pointer + } // populate with Conversation (this) @@ -133,7 +136,7 @@ public class Conversation extends AbstractEntity { } } - public void setMessages(List<Message> msgs) { + public void setMessages(CopyOnWriteArrayList<Message> msgs) { this.messages = msgs; } @@ -173,13 +176,6 @@ public class Conversation extends AbstractEntity { return this.contactJid; } - public Uri getProfilePhotoUri() { - if (this.getProfilePhotoString() != null) { - return Uri.parse(this.getProfilePhotoString()); - } - return null; - } - public int getStatus() { return this.status; } @@ -395,4 +391,12 @@ public class Conversation extends AbstractEntity { public Bookmark getBookmark() { return this.bookmark; } + + public Bitmap getImage(Context context, int size) { + if (mode==MODE_SINGLE) { + return getContact().getImage(size, context); + } else { + return UIHelper.getContactPicture(this, size, context, false); + } + } } diff --git a/src/eu/siacs/conversations/entities/Message.java b/src/eu/siacs/conversations/entities/Message.java index 49c5ce58..17b4e5b8 100644 --- a/src/eu/siacs/conversations/entities/Message.java +++ b/src/eu/siacs/conversations/entities/Message.java @@ -33,9 +33,11 @@ public class Message extends AbstractEntity { public static final int TYPE_IMAGE = 1; public static final int TYPE_AUDIO = 2; public static final int TYPE_STATUS = 3; + public static final int TYPE_PRIVATE = 4; public static String CONVERSATION = "conversationUuid"; public static String COUNTERPART = "counterpart"; + public static String TRUE_COUNTERPART = "trueCounterpart"; public static String BODY = "body"; public static String TIME_SENT = "timeSent"; public static String ENCRYPTION = "encryption"; @@ -44,6 +46,7 @@ public class Message extends AbstractEntity { protected String conversationUuid; protected String counterpart; + protected String trueCounterpart; protected String body; protected String encryptedBody; protected long timeSent; @@ -62,21 +65,22 @@ 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, + conversation.getContactJid(), null, body, System.currentTimeMillis(), encryption, 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,TYPE_TEXT); + this(java.util.UUID.randomUUID().toString(), conversation.getUuid(),counterpart, null, body, System.currentTimeMillis(), encryption,status,TYPE_TEXT); this.conversation = conversation; } - public Message(String uuid, String conversationUUid, String counterpart, + public Message(String uuid, String conversationUUid, String counterpart, String trueCounterpart, String body, long timeSent, int encryption, int status, int type) { this.uuid = uuid; this.conversationUuid = conversationUUid; this.counterpart = counterpart; + this.trueCounterpart = trueCounterpart; this.body = body; this.timeSent = timeSent; this.encryption = encryption; @@ -90,6 +94,7 @@ public class Message extends AbstractEntity { values.put(UUID, uuid); values.put(CONVERSATION, conversationUuid); values.put(COUNTERPART, counterpart); + values.put(TRUE_COUNTERPART,trueCounterpart); values.put(BODY, body); values.put(TIME_SENT, timeSent); values.put(ENCRYPTION, encryption); @@ -109,6 +114,24 @@ public class Message extends AbstractEntity { public String getCounterpart() { return counterpart; } + + public Contact getContact() { + if (this.conversation.getMode() == Conversation.MODE_SINGLE) { + return this.conversation.getContact(); + } else { + if (this.trueCounterpart == null) { + return null; + } else { + Account account = this.conversation.getAccount(); + Contact contact = account.getRoster().getContact(this.trueCounterpart); + if (contact.showInRoster()) { + return contact; + } else { + return null; + } + } + } + } public String getBody() { return body; @@ -144,6 +167,7 @@ public class Message extends AbstractEntity { return new Message(cursor.getString(cursor.getColumnIndex(UUID)), cursor.getString(cursor.getColumnIndex(CONVERSATION)), cursor.getString(cursor.getColumnIndex(COUNTERPART)), + cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART)), cursor.getString(cursor.getColumnIndex(BODY)), cursor.getLong(cursor.getColumnIndex(TIME_SENT)), cursor.getInt(cursor.getColumnIndex(ENCRYPTION)), @@ -200,13 +224,17 @@ public class Message extends AbstractEntity { } public void setPresence(String presence) { - if (presence == null) { + if (presence == null || presence.isEmpty()) { this.counterpart = this.counterpart.split("/")[0]; } else { this.counterpart = this.counterpart.split("/")[0] + "/" + presence; } } + public void setTrueCounterpart(String trueCounterpart) { + this.trueCounterpart = trueCounterpart; + } + public String getPresence() { String[] counterparts = this.counterpart.split("/"); if (counterparts.length == 2) { @@ -230,4 +258,8 @@ public class Message extends AbstractEntity { message.setConversation(conversation); return message; } + + public void setCounterpart(String counterpart) { + this.counterpart = counterpart; + } } diff --git a/src/eu/siacs/conversations/entities/MucOptions.java b/src/eu/siacs/conversations/entities/MucOptions.java index 0bb9b295..61b2732d 100644 --- a/src/eu/siacs/conversations/entities/MucOptions.java +++ b/src/eu/siacs/conversations/entities/MucOptions.java @@ -2,6 +2,7 @@ package eu.siacs.conversations.entities; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.xml.Element; @@ -13,11 +14,11 @@ public class MucOptions { public static final int ERROR_NO_ERROR = 0; public static final int ERROR_NICK_IN_USE = 1; public static final int ERROR_ROOM_NOT_FOUND = 2; - + public interface OnRenameListener { public void onRename(boolean success); } - + public class User { public static final int ROLE_MODERATOR = 3; public static final int ROLE_NONE = 0; @@ -28,22 +29,33 @@ public class MucOptions { public static final int AFFILIATION_MEMBER = 2; public static final int AFFILIATION_OUTCAST = 1; public static final int AFFILIATION_NONE = 0; - + private int role; private int affiliation; private String name; + private String jid; private long pgpKeyId = 0; - + public String getName() { return name; } + public void setName(String user) { this.name = user; } - + + public void setJid(String jid) { + this.jid = jid; + } + + public String getJid() { + return this.jid; + } + public int getRole() { return this.role; } + public void setRole(String role) { role = role.toLowerCase(); if (role.equals("moderator")) { @@ -56,9 +68,11 @@ public class MucOptions { this.role = ROLE_NONE; } } + public int getAffiliation() { return this.affiliation; } + public void setAffiliation(String affiliation) { if (affiliation.equalsIgnoreCase("admin")) { this.affiliation = AFFILIATION_ADMIN; @@ -72,16 +86,18 @@ public class MucOptions { this.affiliation = AFFILIATION_NONE; } } + public void setPgpKeyId(long id) { this.pgpKeyId = id; } - + public long getPgpKeyId() { return this.pgpKeyId; } } + private Account account; - private ArrayList<User> users = new ArrayList<User>(); + private List<User> users = new CopyOnWriteArrayList<User>(); private Conversation conversation; private boolean isOnline = false; private int error = ERROR_ROOM_NOT_FOUND; @@ -94,44 +110,47 @@ public class MucOptions { public MucOptions(Account account) { this.account = account; } - + public void deleteUser(String name) { - for(int i = 0; i < users.size(); ++i) { + for (int i = 0; i < users.size(); ++i) { if (users.get(i).getName().equals(name)) { users.remove(i); return; } } } - + public void addUser(User user) { - for(int i = 0; i < users.size(); ++i) { + for (int i = 0; i < users.size(); ++i) { if (users.get(i).getName().equals(user.getName())) { users.set(i, user); return; } } users.add(user); - } - + } + public void processPacket(PresencePacket packet, PgpEngine pgp) { String[] fromParts = packet.getFrom().split("/"); - if (fromParts.length>=2) { + if (fromParts.length >= 2) { String name = fromParts[1]; String type = packet.getAttribute("type"); - if (type==null) { + if (type == null) { User user = new User(); - Element item = packet.findChild("x","http://jabber.org/protocol/muc#user").findChild("item"); + Element item = packet.findChild("x", + "http://jabber.org/protocol/muc#user") + .findChild("item"); user.setName(name); user.setAffiliation(item.getAttribute("affiliation")); user.setRole(item.getAttribute("role")); + user.setJid(item.getAttribute("jid")); user.setName(name); if (name.equals(this.joinnick)) { this.isOnline = true; this.error = ERROR_NO_ERROR; self = user; if (aboutToRename) { - if (renameListener!=null) { + if (renameListener != null) { renameListener.onRename(true); } aboutToRename = false; @@ -140,8 +159,7 @@ public class MucOptions { addUser(user); } if (pgp != null) { - Element x = packet.findChild("x", - "jabber:x:signed"); + Element x = packet.findChild("x", "jabber:x:signed"); if (x != null) { Element status = packet.findChild("status"); String msg; @@ -150,7 +168,8 @@ public class MucOptions { } else { msg = ""; } - user.setPgpKeyId(pgp.fetchKeyId(account,msg, x.getContent())); + user.setPgpKeyId(pgp.fetchKeyId(account, msg, + x.getContent())); } } } else if (type.equals("unavailable")) { @@ -159,26 +178,27 @@ public class MucOptions { Element error = packet.findChild("error"); if (error.hasChild("conflict")) { if (aboutToRename) { - if (renameListener!=null) { + if (renameListener != null) { renameListener.onRename(false); } aboutToRename = false; this.setJoinNick(getActualNick()); } else { - this.error = ERROR_NICK_IN_USE; + this.error = ERROR_NICK_IN_USE; } } } } } - + public List<User> getUsers() { return this.users; } - + public String getProposedNick() { String[] mucParts = conversation.getContactJid().split("/"); - if (conversation.getBookmark() != null && conversation.getBookmark().getNick() != null) { + if (conversation.getBookmark() != null + && conversation.getBookmark().getNick() != null) { return conversation.getBookmark().getNick(); } else { if (mucParts.length == 2) { @@ -188,27 +208,27 @@ public class MucOptions { } } } - + public String getActualNick() { - if (this.self.getName()!=null) { + if (this.self.getName() != null) { return this.self.getName(); } else { return this.getProposedNick(); } } - + public void setJoinNick(String nick) { this.joinnick = nick; } - + public void setConversation(Conversation conversation) { this.conversation = conversation; } - + public boolean online() { return this.isOnline; } - + public int getError() { return this.error; } @@ -216,7 +236,7 @@ public class MucOptions { public void setOnRenameListener(OnRenameListener listener) { this.renameListener = listener; } - + public OnRenameListener getOnRenameListener() { return this.renameListener; } @@ -234,7 +254,7 @@ public class MucOptions { public void setSubject(String content) { this.subject = content; } - + public String getSubject() { return this.subject; } @@ -242,33 +262,33 @@ public class MucOptions { public void flagAboutToRename() { this.aboutToRename = true; } - + public long[] getPgpKeyIds() { List<Long> ids = new ArrayList<Long>(); - for(User user : getUsers()) { - if(user.getPgpKeyId()!=0) { + for (User user : getUsers()) { + if (user.getPgpKeyId() != 0) { ids.add(user.getPgpKeyId()); } } long[] primitivLongArray = new long[ids.size()]; - for(int i = 0; i < ids.size(); ++i) { + for (int i = 0; i < ids.size(); ++i) { primitivLongArray[i] = ids.get(i); } return primitivLongArray; } - + public boolean pgpKeysInUse() { - for(User user : getUsers()) { - if (user.getPgpKeyId()!=0) { + for (User user : getUsers()) { + if (user.getPgpKeyId() != 0) { return true; } } return false; } - + public boolean everybodyHasKeys() { - for(User user : getUsers()) { - if (user.getPgpKeyId()==0) { + for (User user : getUsers()) { + if (user.getPgpKeyId() == 0) { return false; } } @@ -276,6 +296,16 @@ public class MucOptions { } public String getJoinJid() { - return this.conversation.getContactJid().split("/")[0]+"/"+this.joinnick; + return this.conversation.getContactJid().split("/")[0] + "/" + + this.joinnick; + } + + public String getTrueCounterpart(String counterpart) { + for(User user : this.getUsers()) { + if (user.getName().equals(counterpart)) { + return user.getJid(); + } + } + return null; } }
\ No newline at end of file diff --git a/src/eu/siacs/conversations/generator/AbstractGenerator.java b/src/eu/siacs/conversations/generator/AbstractGenerator.java index 05d5799c..d9839572 100644 --- a/src/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/eu/siacs/conversations/generator/AbstractGenerator.java @@ -18,7 +18,8 @@ public abstract class AbstractGenerator { "http://jabber.org/protocol/muc", "jabber:x:conference", "http://jabber.org/protocol/caps", - "http://jabber.org/protocol/disco#info"}; + "http://jabber.org/protocol/disco#info", + "urn:xmpp:avatar:metadata+notify"}; public final String IDENTITY_NAME = "Conversations 0.5"; public final String IDENTITY_TYPE = "phone"; /*public final String[] FEATURES = { "http://jabber.org/protocol/muc","http://jabber.org/protocol/disco#info", "http://jabber.org/protocol/disco#items", "http://jabber.org/protocol/caps" }; @@ -45,6 +46,6 @@ public abstract class AbstractGenerator { s.append(feature+"<"); } byte[] sha1 = md.digest(s.toString().getBytes()); - return new String(Base64.encode(sha1, Base64.DEFAULT)); + return new String(Base64.encode(sha1, Base64.DEFAULT)).trim(); } } diff --git a/src/eu/siacs/conversations/generator/IqGenerator.java b/src/eu/siacs/conversations/generator/IqGenerator.java index 7b3350d4..259538c2 100644 --- a/src/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/eu/siacs/conversations/generator/IqGenerator.java @@ -5,6 +5,7 @@ import java.util.Collections; import java.util.List; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class IqGenerator extends AbstractGenerator { @@ -28,4 +29,61 @@ public class IqGenerator extends AbstractGenerator { } return packet; } + + protected IqPacket publish(String node, Element item) { + IqPacket packet = new IqPacket(IqPacket.TYPE_SET); + Element pubsub = packet.addChild("pubsub", "http://jabber.org/protocol/pubsub"); + Element publish = pubsub.addChild("publish"); + publish.setAttribute("node", node); + publish.addChild(item); + return packet; + } + + protected IqPacket retrieve(String node, Element item) { + IqPacket packet = new IqPacket(IqPacket.TYPE_GET); + Element pubsub = packet.addChild("pubsub", "http://jabber.org/protocol/pubsub"); + Element items = pubsub.addChild("items"); + items.setAttribute("node", node); + if (item!=null) { + items.addChild(item); + } + return packet; + } + + public IqPacket publishAvatar(Avatar avatar) { + Element item = new Element("item"); + item.setAttribute("id", avatar.sha1sum); + Element data = item.addChild("data","urn:xmpp:avatar:data"); + data.setContent(avatar.image); + return publish("urn:xmpp:avatar:data", item); + } + + public IqPacket publishAvatarMetadata(Avatar avatar) { + Element item = new Element("item"); + item.setAttribute("id", avatar.sha1sum); + Element metadata = item.addChild("metadata","urn:xmpp:avatar:metadata"); + Element info = metadata.addChild("info"); + info.setAttribute("bytes",avatar.size); + info.setAttribute("id",avatar.sha1sum); + info.setAttribute("height",avatar.height); + info.setAttribute("width",avatar.height); + info.setAttribute("type", avatar.type); + return publish("urn:xmpp:avatar:metadata",item); + } + + public IqPacket retrieveAvatar(Avatar avatar) { + Element item = new Element("item"); + item.setAttribute("id", avatar.sha1sum); + IqPacket packet = retrieve("urn:xmpp:avatar:data", item); + packet.setTo(avatar.owner); + return packet; + } + + public IqPacket retrieveAvatarMetaData(String to) { + IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null); + if (to!=null) { + packet.setTo(to); + } + return packet; + } } diff --git a/src/eu/siacs/conversations/generator/MessageGenerator.java b/src/eu/siacs/conversations/generator/MessageGenerator.java index 4449a7ec..26182aad 100644 --- a/src/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/eu/siacs/conversations/generator/MessageGenerator.java @@ -22,6 +22,9 @@ public class MessageGenerator { packet.setTo(message.getCounterpart()); packet.setType(MessagePacket.TYPE_CHAT); packet.addChild("markable", "urn:xmpp:chat-markers:0"); + } else if (message.getType() == Message.TYPE_PRIVATE) { + packet.setTo(message.getCounterpart()); + packet.setType(MessagePacket.TYPE_CHAT); } else { packet.setTo(message.getCounterpart().split("/")[0]); packet.setType(MessagePacket.TYPE_GROUPCHAT); diff --git a/src/eu/siacs/conversations/parser/AbstractParser.java b/src/eu/siacs/conversations/parser/AbstractParser.java index c4c6720a..96d11508 100644 --- a/src/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/eu/siacs/conversations/parser/AbstractParser.java @@ -63,6 +63,8 @@ public abstract class AbstractParser { String presence = null; if (fromParts.length >= 2) { presence = fromParts[1]; + } else { + presence = ""; } Contact contact = account.getRoster().getContact(from); long timestamp = getTimestamp(packet); @@ -73,4 +75,16 @@ public abstract class AbstractParser { } } } + + protected String avatarData(Element items) { + Element item = items.findChild("item"); + if (item==null) { + return null; + } + Element data = item.findChild("data","urn:xmpp:avatar:data"); + if (data==null) { + return null; + } + return data.getContent(); + } } diff --git a/src/eu/siacs/conversations/parser/IqParser.java b/src/eu/siacs/conversations/parser/IqParser.java index 023fb4df..a22ff6a5 100644 --- a/src/eu/siacs/conversations/parser/IqParser.java +++ b/src/eu/siacs/conversations/parser/IqParser.java @@ -27,19 +27,33 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { if (!contact.getOption(Contact.Options.DIRTY_PUSH)) { contact.setServerName(name); } - if (subscription.equals("remove")) { - contact.resetOption(Contact.Options.IN_ROSTER); - contact.resetOption(Contact.Options.DIRTY_DELETE); - contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); - } else { - contact.setOption(Contact.Options.IN_ROSTER); - contact.resetOption(Contact.Options.DIRTY_PUSH); - contact.parseSubscriptionFromElement(item); + if (subscription!=null) { + if (subscription.equals("remove")) { + contact.resetOption(Contact.Options.IN_ROSTER); + contact.resetOption(Contact.Options.DIRTY_DELETE); + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + } else { + contact.setOption(Contact.Options.IN_ROSTER); + contact.resetOption(Contact.Options.DIRTY_PUSH); + contact.parseSubscriptionFromElement(item); + } } } } mXmppConnectionService.updateRosterUi(); } + + public String avatarData(IqPacket packet) { + Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub"); + if (pubsub==null) { + return null; + } + Element items = pubsub.findChild("items"); + if (items==null) { + return null; + } + return super.avatarData(items); + } @Override public void onIqPacketReceived(Account account, IqPacket packet) { diff --git a/src/eu/siacs/conversations/parser/MessageParser.java b/src/eu/siacs/conversations/parser/MessageParser.java index a4fcc810..7de7e9e1 100644 --- a/src/eu/siacs/conversations/parser/MessageParser.java +++ b/src/eu/siacs/conversations/parser/MessageParser.java @@ -1,15 +1,18 @@ package eu.siacs.conversations.parser; import android.os.SystemClock; +import android.util.Log; import net.java.otr4j.session.Session; import net.java.otr4j.session.SessionStatus; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnMessagePacketReceived; +import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; public class MessageParser extends AbstractParser implements @@ -37,6 +40,14 @@ public class MessageParser extends AbstractParser implements packet.getBody(), Message.ENCRYPTION_NONE, Message.STATUS_RECIEVED); } + if (conversation.getMode() == Conversation.MODE_MULTI + && fromParts.length >= 2) { + finishedMessage.setType(Message.TYPE_PRIVATE); + finishedMessage.setPresence(fromParts[1]); + finishedMessage.setTrueCounterpart(conversation.getMucOptions() + .getTrueCounterpart(fromParts[1])); + + } finishedMessage.setTime(getTimestamp(packet)); return finishedMessage; } @@ -47,25 +58,31 @@ public class MessageParser extends AbstractParser implements String[] fromParts = packet.getFrom().split("/"); Conversation conversation = mXmppConnectionService .findOrCreateConversation(account, fromParts[0], false); + String presence; + if (fromParts.length >= 2) { + presence = fromParts[1]; + } else { + presence = ""; + } updateLastseen(packet, account, true); String body = packet.getBody(); if (!conversation.hasValidOtrSession()) { if (properlyAddressed) { conversation.startOtrSession( mXmppConnectionService.getApplicationContext(), - fromParts[1], false); + presence, false); } else { return null; } } else { String foreignPresence = conversation.getOtrSession() .getSessionID().getUserID(); - if (!foreignPresence.equals(fromParts[1])) { + if (!foreignPresence.equals(presence)) { conversation.endOtrIfNeeded(); if (properlyAddressed) { conversation.startOtrSession( mXmppConnectionService.getApplicationContext(), - fromParts[1], false); + presence, false); } else { return null; } @@ -110,7 +127,8 @@ public class MessageParser extends AbstractParser implements private Message parseGroupchat(MessagePacket packet, Account account) { int status; String[] fromParts = packet.getFrom().split("/"); - if (mXmppConnectionService.find(account.pendingConferenceLeaves,account,fromParts[0]) != null) { + if (mXmppConnectionService.find(account.pendingConferenceLeaves, + account, fromParts[0]) != null) { return null; } Conversation conversation = mXmppConnectionService @@ -146,6 +164,10 @@ public class MessageParser extends AbstractParser implements Message.ENCRYPTION_PGP, status); } finishedMessage.setTime(getTimestamp(packet)); + if (status == Message.STATUS_RECIEVED) { + finishedMessage.setTrueCounterpart(conversation.getMucOptions() + .getTrueCounterpart(counterPart)); + } return finishedMessage; } @@ -167,24 +189,29 @@ public class MessageParser extends AbstractParser implements } Element message = forwarded.findChild("message"); if ((message == null) || (!message.hasChild("body"))) { - if (status == Message.STATUS_RECIEVED) { + if (status == Message.STATUS_RECIEVED && message.getAttribute("from")!=null) { parseNormal(message, account); } return null; } if (status == Message.STATUS_RECIEVED) { fullJid = message.getAttribute("from"); - updateLastseen(message, account, true); + if (fullJid == null) { + return null; + } else { + updateLastseen(message, account, true); + } } else { fullJid = message.getAttribute("to"); - } - if (fullJid==null) { - return null; + if (fullJid == null) { + return null; + } } String[] parts = fullJid.split("/"); Conversation conversation = mXmppConnectionService .findOrCreateConversation(account, parts[0], false); conversation.setLatestMarkableMessageId(getMarkableMessageId(packet)); + String pgpBody = getPgpBody(message); Message finishedMessage; if (pgpBody != null) { @@ -196,6 +223,16 @@ public class MessageParser extends AbstractParser implements Message.ENCRYPTION_NONE, status); } finishedMessage.setTime(getTimestamp(message)); + + if (conversation.getMode() == Conversation.MODE_MULTI + && parts.length >= 2) { + finishedMessage.setType(Message.TYPE_PRIVATE); + finishedMessage.setPresence(parts[1]); + finishedMessage.setTrueCounterpart(conversation.getMucOptions() + .getTrueCounterpart(parts[1])); + + } + return finishedMessage; } @@ -206,6 +243,11 @@ public class MessageParser extends AbstractParser implements } private void parseNormal(Element packet, Account account) { + if (packet.hasChild("event", "http://jabber.org/protocol/pubsub#event")) { + Element event = packet.findChild("event", + "http://jabber.org/protocol/pubsub#event"); + parseEvent(event, packet.getAttribute("from"), account); + } if (packet.hasChild("displayed", "urn:xmpp:chat-markers:0")) { String id = packet .findChild("displayed", "urn:xmpp:chat-markers:0") @@ -221,8 +263,9 @@ public class MessageParser extends AbstractParser implements updateLastseen(packet, account, false); mXmppConnectionService.markMessage(account, fromParts[0], id, Message.STATUS_SEND_RECEIVED); - } else if (packet.hasChild("x","http://jabber.org/protocol/muc#user")) { - Element x = packet.findChild("x","http://jabber.org/protocol/muc#user"); + } else if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) { + Element x = packet.findChild("x", + "http://jabber.org/protocol/muc#user"); if (x.hasChild("invite")) { Conversation conversation = mXmppConnectionService .findOrCreateConversation(account, @@ -230,15 +273,15 @@ public class MessageParser extends AbstractParser implements if (!conversation.getMucOptions().online()) { mXmppConnectionService.joinMuc(conversation); mXmppConnectionService.updateConversationUi(); - } + } } } else if (packet.hasChild("x", "jabber:x:conference")) { Element x = packet.findChild("x", "jabber:x:conference"); String jid = x.getAttribute("jid"); - if (jid!=null) { + if (jid != null) { Conversation conversation = mXmppConnectionService - .findOrCreateConversation(account,jid, true); + .findOrCreateConversation(account, jid, true); if (!conversation.getMucOptions().online()) { mXmppConnectionService.joinMuc(conversation); mXmppConnectionService.updateConversationUi(); @@ -247,6 +290,35 @@ public class MessageParser extends AbstractParser implements } } + private void parseEvent(Element event, String from, Account account) { + Element items = event.findChild("items"); + String node = items.getAttribute("node"); + if (node != null) { + if (node.equals("urn:xmpp:avatar:metadata")) { + Avatar avatar = Avatar.parseMetadata(items); + if (avatar!=null) { + avatar.owner = from; + if (mXmppConnectionService.getFileBackend().isAvatarCached( + avatar)) { + if (account.getJid().equals(from)) { + account.setAvatar(avatar.getFilename()); + } else { + Contact contact = account.getRoster().getContact(from); + contact.setAvatar(avatar.getFilename()); + } + } else { + mXmppConnectionService.fetchAvatar(account, avatar); + } + } + } else { + Log.d("xmppService", account.getJid() + ": " + node + " from " + + from); + } + } else { + Log.d("xmppService", event.toString()); + } + } + private String getPgpBody(Element message) { Element child = message.findChild("x", "jabber:x:encrypted"); if (child == null) { @@ -306,8 +378,7 @@ public class MessageParser extends AbstractParser implements message.markUnread(); } else { message.getConversation().markRead(); - lastCarbonMessageReceived = SystemClock - .elapsedRealtime(); + lastCarbonMessageReceived = SystemClock.elapsedRealtime(); notify = false; } } @@ -317,6 +388,9 @@ public class MessageParser extends AbstractParser implements } else if (packet.getType() == MessagePacket.TYPE_NORMAL) { this.parseNormal(packet, account); return; + } else if (packet.getType() == MessagePacket.TYPE_HEADLINE) { + this.parseHeadline(packet, account); + return; } if ((message == null) || (message.getBody() == null)) { return; @@ -324,11 +398,15 @@ public class MessageParser extends AbstractParser implements if ((mXmppConnectionService.confirmMessages()) && ((packet.getId() != null))) { if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) { - MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account, packet, "urn:xmpp:chat-markers:0"); + MessagePacket receipt = mXmppConnectionService + .getMessageGenerator().received(account, packet, + "urn:xmpp:chat-markers:0"); mXmppConnectionService.sendMessagePacket(account, receipt); } if (packet.hasChild("request", "urn:xmpp:receipts")) { - MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account, packet, "urn:xmpp:receipts"); + MessagePacket receipt = mXmppConnectionService + .getMessageGenerator().received(account, packet, + "urn:xmpp:receipts"); mXmppConnectionService.sendMessagePacket(account, receipt); } } @@ -339,4 +417,12 @@ public class MessageParser extends AbstractParser implements } mXmppConnectionService.notifyUi(conversation, notify); } + + private void parseHeadline(MessagePacket packet, Account account) { + if (packet.hasChild("event", "http://jabber.org/protocol/pubsub#event")) { + Element event = packet.findChild("event", + "http://jabber.org/protocol/pubsub#event"); + parseEvent(event, packet.getFrom(), account); + } + } } diff --git a/src/eu/siacs/conversations/parser/PresenceParser.java b/src/eu/siacs/conversations/parser/PresenceParser.java index 33f4185f..ea19df6f 100644 --- a/src/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/eu/siacs/conversations/parser/PresenceParser.java @@ -13,7 +13,7 @@ import eu.siacs.conversations.xmpp.stanzas.PresencePacket; public class PresenceParser extends AbstractParser implements OnPresencePacketReceived { - + public PresenceParser(XmppConnectionService service) { super(service); } @@ -21,23 +21,31 @@ public class PresenceParser extends AbstractParser implements public void parseConferencePresence(PresencePacket packet, Account account) { PgpEngine mPgpEngine = mXmppConnectionService.getPgpEngine(); if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) { - Conversation muc = mXmppConnectionService.find(account,packet + Conversation muc = mXmppConnectionService.find(account, packet .getAttribute("from").split("/")[0]); if (muc != null) { + boolean before = muc.getMucOptions().online(); muc.getMucOptions().processPacket(packet, mPgpEngine); + if (before!=muc.getMucOptions().online()) { + mXmppConnectionService.updateConversationUi(); + } } } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { - Conversation muc = mXmppConnectionService.find(account,packet + Conversation muc = mXmppConnectionService.find(account, packet .getAttribute("from").split("/")[0]); if (muc != null) { + boolean before = muc.getMucOptions().online(); muc.getMucOptions().processPacket(packet, mPgpEngine); + if (before!=muc.getMucOptions().online()) { + mXmppConnectionService.updateConversationUi(); + } } } - mXmppConnectionService.updateConversationUi(); } public void parseContactPresence(PresencePacket packet, Account account) { - PresenceGenerator mPresenceGenerator = mXmppConnectionService.getPresenceGenerator(); + PresenceGenerator mPresenceGenerator = mXmppConnectionService + .getPresenceGenerator(); if (packet.getFrom() == null) { return; } @@ -56,30 +64,34 @@ public class PresenceParser extends AbstractParser implements } else { Contact contact = account.getRoster().getContact(packet.getFrom()); if (type == null) { - if (fromParts.length == 2) { - int sizeBefore = contact.getPresences().size(); - contact.updatePresence(fromParts[1], - Presences.parseShow(packet.findChild("show"))); - PgpEngine pgp = mXmppConnectionService.getPgpEngine(); - if (pgp != null) { - Element x = packet.findChild("x", "jabber:x:signed"); - if (x != null) { - Element status = packet.findChild("status"); - String msg; - if (status != null) { - msg = status.getContent(); - } else { - msg = ""; - } - contact.setPgpKeyId(pgp.fetchKeyId(account, msg, - x.getContent())); + String presence; + if (fromParts.length >= 2) { + presence = fromParts[1]; + } else { + presence = ""; + } + int sizeBefore = contact.getPresences().size(); + contact.updatePresence(presence, + Presences.parseShow(packet.findChild("show"))); + PgpEngine pgp = mXmppConnectionService.getPgpEngine(); + if (pgp != null) { + Element x = packet.findChild("x", "jabber:x:signed"); + if (x != null) { + Element status = packet.findChild("status"); + String msg; + if (status != null) { + msg = status.getContent(); + } else { + msg = ""; } + contact.setPgpKeyId(pgp.fetchKeyId(account, msg, + x.getContent())); } - boolean online = sizeBefore < contact.getPresences().size(); - updateLastseen(packet, account, true); - mXmppConnectionService.onContactStatusChanged - .onContactStatusChanged(contact, online); } + boolean online = sizeBefore < contact.getPresences().size(); + updateLastseen(packet, account, true); + mXmppConnectionService.onContactStatusChanged + .onContactStatusChanged(contact, online); } else if (type.equals("unavailable")) { if (fromParts.length != 2) { contact.clearPresences(); @@ -90,7 +102,8 @@ public class PresenceParser extends AbstractParser implements .onContactStatusChanged(contact, false); } else if (type.equals("subscribe")) { if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) { - mXmppConnectionService.sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact)); + mXmppConnectionService.sendPresencePacket(account, + mPresenceGenerator.sendPresenceUpdatesTo(contact)); } else { contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); } diff --git a/src/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/eu/siacs/conversations/persistance/DatabaseBackend.java index 7643076a..c3f0d9ee 100644 --- a/src/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -20,15 +20,17 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static DatabaseBackend instance = null; private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 5; + private static final int DATABASE_VERSION = 6; private static String CREATE_CONTATCS_STATEMENT = "create table " - + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " + Contact.SERVERNAME + " TEXT, " - + Contact.SYSTEMNAME + " TEXT," + Contact.JID + " TEXT," - + Contact.KEYS + " TEXT," + Contact.PHOTOURI + " TEXT," - + Contact.OPTIONS + " NUMBER," + Contact.SYSTEMACCOUNT - + " NUMBER, " + "FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES " - + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, UNIQUE("+Contact.ACCOUNT+", "+Contact.JID+") ON CONFLICT REPLACE);"; + + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " + + Contact.SERVERNAME + " TEXT, " + Contact.SYSTEMNAME + " TEXT," + + Contact.JID + " TEXT," + Contact.KEYS + " TEXT," + + Contact.PHOTOURI + " TEXT," + Contact.OPTIONS + " NUMBER," + + Contact.SYSTEMACCOUNT + " NUMBER, " + "FOREIGN KEY(" + + Contact.ACCOUNT + ") REFERENCES " + Account.TABLENAME + "(" + + Account.UUID + ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + + ", " + Contact.JID + ") ON CONFLICT REPLACE);"; public DatabaseBackend(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); @@ -54,8 +56,9 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL("create table " + Message.TABLENAME + "( " + Message.UUID + " TEXT PRIMARY KEY, " + Message.CONVERSATION + " TEXT, " + Message.TIME_SENT + " NUMBER, " + Message.COUNTERPART - + " TEXT, " + Message.BODY + " TEXT, " + Message.ENCRYPTION - + " NUMBER, " + Message.STATUS + " NUMBER," + Message.TYPE + + " TEXT, " + Message.TRUE_COUNTERPART + " TEXT," + + Message.BODY + " TEXT, " + Message.ENCRYPTION + " NUMBER, " + + Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, FOREIGN KEY(" + Message.CONVERSATION + ") REFERENCES " + Conversation.TABLENAME + "(" + Conversation.UUID + ") ON DELETE CASCADE);"); @@ -74,9 +77,14 @@ public class DatabaseBackend extends SQLiteOpenHelper { + Message.TYPE + " NUMBER"); } if (oldVersion < 5 && newVersion >= 5) { - db.execSQL("DROP TABLE "+Contact.TABLENAME); + db.execSQL("DROP TABLE " + Contact.TABLENAME); db.execSQL(CREATE_CONTATCS_STATEMENT); - db.execSQL("UPDATE "+Account.TABLENAME+ " SET "+Account.ROSTERVERSION+" = NULL"); + db.execSQL("UPDATE " + Account.TABLENAME + " SET " + + Account.ROSTERVERSION + " = NULL"); + } + if (oldVersion < 6 && newVersion >= 6) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.TRUE_COUNTERPART + " TEXT"); } } @@ -128,24 +136,27 @@ public class DatabaseBackend extends SQLiteOpenHelper { } return list; } - - public List<Message> getMessages(Conversation conversations, int limit) { - return getMessages(conversations, limit,-1); + + public CopyOnWriteArrayList<Message> getMessages( + Conversation conversations, int limit) { + return getMessages(conversations, limit, -1); } - public List<Message> getMessages(Conversation conversation, int limit, long timestamp) { - List<Message> list = new CopyOnWriteArrayList<Message>(); + public CopyOnWriteArrayList<Message> getMessages(Conversation conversation, + int limit, long timestamp) { + CopyOnWriteArrayList<Message> list = new CopyOnWriteArrayList<Message>(); SQLiteDatabase db = this.getReadableDatabase(); Cursor cursor; - if (timestamp==-1) { + if (timestamp == -1) { String[] selectionArgs = { conversation.getUuid() }; cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION - + "=?", selectionArgs, null, null, Message.TIME_SENT + " DESC", - String.valueOf(limit)); + + "=?", selectionArgs, null, null, Message.TIME_SENT + + " DESC", String.valueOf(limit)); } else { - String[] selectionArgs = { conversation.getUuid() , ""+timestamp}; + String[] selectionArgs = { conversation.getUuid(), "" + timestamp }; cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION - + "=? and "+Message.TIME_SENT+"<?", selectionArgs, null, null, Message.TIME_SENT + " DESC", + + "=? and " + Message.TIME_SENT + "<?", selectionArgs, + null, null, Message.TIME_SENT + " DESC", String.valueOf(limit)); } if (cursor.getCount() > 0) { @@ -225,16 +236,16 @@ public class DatabaseBackend extends SQLiteOpenHelper { roster.initContact(Contact.fromCursor(cursor)); } } - + public void writeRoster(Roster roster) { Account account = roster.getAccount(); SQLiteDatabase db = this.getWritableDatabase(); - for(Contact contact : roster.getContacts()) { + for (Contact contact : roster.getContacts()) { if (contact.getOption(Contact.Options.IN_ROSTER)) { db.insert(Contact.TABLENAME, null, contact.getContentValues()); } else { - String where = Contact.ACCOUNT + "=? AND "+Contact.JID+"=?"; - String[] whereArgs = {account.getUuid(), contact.getJid()}; + String where = Contact.ACCOUNT + "=? AND " + Contact.JID + "=?"; + String[] whereArgs = { account.getUuid(), contact.getJid() }; db.delete(Contact.TABLENAME, where, whereArgs); } } diff --git a/src/eu/siacs/conversations/persistance/FileBackend.java b/src/eu/siacs/conversations/persistance/FileBackend.java index 1ee68a27..f4067f87 100644 --- a/src/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/eu/siacs/conversations/persistance/FileBackend.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.persistance; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -7,19 +8,33 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import android.content.Context; +import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Canvas; import android.graphics.Matrix; +import android.graphics.RectF; import android.media.ExifInterface; import android.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.util.Base64; +import android.util.Base64OutputStream; import android.util.Log; import android.util.LruCache; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.ImageProvider; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.jingle.JingleFile; +import eu.siacs.conversations.xmpp.pep.Avatar; public class FileBackend { @@ -45,11 +60,11 @@ public class FileBackend { return thumbnailCache; } - public JingleFile getJingleFile(Message message) { - return getJingleFile(message, true); + public JingleFile getJingleFileLegacy(Message message) { + return getJingleFileLegacy(message, true); } - public JingleFile getJingleFile(Message message, boolean decrypted) { + public JingleFile getJingleFileLegacy(Message message, boolean decrypted) { Conversation conversation = message.getConversation(); String prefix = context.getFilesDir().getAbsolutePath(); String path = prefix + "/" + conversation.getAccount().getJid() + "/" @@ -66,7 +81,28 @@ public class FileBackend { } return new JingleFile(path + "/" + filename); } + + public JingleFile getJingleFile(Message message) { + return getJingleFile(message, true); + } + public JingleFile getJingleFile(Message message, boolean decrypted) { + StringBuilder filename = new StringBuilder(); + filename.append(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath()); + filename.append("/Conversations/"); + filename.append(message.getUuid()); + if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) { + filename.append(".webp"); + } else { + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + filename.append(".webp"); + } else { + filename.append(".webp.pgp"); + } + } + return new JingleFile(filename.toString()); + } + public Bitmap resize(Bitmap originalBitmap, int size) { int w = originalBitmap.getWidth(); int h = originalBitmap.getHeight(); @@ -125,21 +161,11 @@ public class FileBackend { if (originalBitmap == null) { throw new ImageCopyException(R.string.error_not_an_image_file); } - if (image == null) { - getIncomingFile().delete(); - } Bitmap scalledBitmap = resize(originalBitmap, IMAGE_SIZE); originalBitmap = null; - ExifInterface exif = new ExifInterface(image.toString()); - if (exif.getAttribute(ExifInterface.TAG_ORIENTATION) - .equalsIgnoreCase("6")) { - scalledBitmap = rotate(scalledBitmap, 90); - } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION) - .equalsIgnoreCase("8")) { - scalledBitmap = rotate(scalledBitmap, 270); - } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION) - .equalsIgnoreCase("3")) { - scalledBitmap = rotate(scalledBitmap, 180); + int rotation = getRotation(image); + if (rotation > 0) { + scalledBitmap = rotate(scalledBitmap, rotation); } OutputStream os = new FileOutputStream(file); boolean success = scalledBitmap.compress( @@ -170,6 +196,38 @@ public class FileBackend { } } } + + private int getRotation(Uri image) { + if ("content".equals(image.getScheme())) { + Cursor cursor = context.getContentResolver().query(image, + new String[] { MediaStore.Images.ImageColumns.ORIENTATION }, null, null, null); + + if (cursor.getCount() != 1) { + return -1; + } + cursor.moveToFirst(); + return cursor.getInt(0); + } else { + ExifInterface exif; + try { + exif = new ExifInterface(image.toString()); + if (exif.getAttribute(ExifInterface.TAG_ORIENTATION) + .equalsIgnoreCase("6")) { + return 90; + } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION) + .equalsIgnoreCase("8")) { + return 270; + } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION) + .equalsIgnoreCase("3")) { + return 180; + } else { + return 0; + } + } catch (IOException e) { + return -1; + } + } + } public Bitmap getImageFromMessage(Message message) { return BitmapFactory.decodeFile(getJingleFile(message) @@ -180,8 +238,11 @@ public class FileBackend { throws FileNotFoundException { Bitmap thumbnail = thumbnailCache.get(message.getUuid()); if ((thumbnail == null) && (!cacheOnly)) { - Bitmap fullsize = BitmapFactory.decodeFile(getJingleFile(message) - .getAbsolutePath()); + File file = getJingleFile(message); + if (!file.exists()) { + file = getJingleFileLegacy(message); + } + Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath()); if (fullsize == null) { throw new FileNotFoundException(); } @@ -215,10 +276,148 @@ public class FileBackend { public File getIncomingFile() { return new File(context.getFilesDir().getAbsolutePath() + "/incoming"); } - + public Uri getIncomingUri() { return Uri.parse(context.getFilesDir().getAbsolutePath() + "/incoming"); } + + public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) { + try { + Avatar avatar = new Avatar(); + Bitmap bm = cropCenterSquare(image, size); + if (bm==null) { + return null; + } + ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream(); + Base64OutputStream mBase64OutputSttream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT); + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + DigestOutputStream mDigestOutputStream = new DigestOutputStream(mBase64OutputSttream, digest); + if (!bm.compress(format, 75, mDigestOutputStream)) { + return null; + } + mDigestOutputStream.flush(); + mDigestOutputStream.close(); + avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest()); + avatar.image = new String(mByteArrayOutputStream.toByteArray()); + return avatar; + } catch (NoSuchAlgorithmException e) { + return null; + } catch (IOException e) { + return null; + } + } + + public boolean isAvatarCached(Avatar avatar) { + File file = new File(getAvatarPath(context, avatar.getFilename())); + return file.exists(); + } + + public boolean save(Avatar avatar) { + if (isAvatarCached(avatar)) { + return true; + } + String filename = getAvatarPath(context, avatar.getFilename()); + File file = new File(filename+".tmp"); + file.getParentFile().mkdirs(); + try { + file.createNewFile(); + FileOutputStream mFileOutputStream = new FileOutputStream(file); + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + DigestOutputStream mDigestOutputStream = new DigestOutputStream(mFileOutputStream, digest); + mDigestOutputStream.write(avatar.getImageAsBytes()); + mDigestOutputStream.flush(); + mDigestOutputStream.close(); + avatar.size = file.length(); + String sha1sum = CryptoHelper.bytesToHex(digest.digest()); + if (sha1sum.equals(avatar.sha1sum)) { + file.renameTo(new File(filename)); + return true; + } else { + Log.d("xmppService","sha1sum mismatch for "+avatar.owner); + file.delete(); + return false; + } + } catch (FileNotFoundException e) { + return false; + } catch (IOException e) { + return false; + } catch (NoSuchAlgorithmException e) { + return false; + } + } + + public static String getAvatarPath(Context context, String avatar) { + return context.getFilesDir().getAbsolutePath() + "/avatars/"+avatar; + } + + public Bitmap cropCenterSquare(Uri image, int size) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = calcSampleSize(image, size); + InputStream is = context.getContentResolver() + .openInputStream(image); + Bitmap input = BitmapFactory.decodeStream(is, null, options); + if (input==null) { + return null; + } else { + return cropCenterSquare(input, size); + } + } catch (FileNotFoundException e) { + return null; + } + } + + public static Bitmap cropCenterSquare(Bitmap input, int size) { + int w = input.getWidth(); + int h = input.getHeight(); + + float scale = Math.max((float) size / h, (float) size / w); + + float outWidth = scale * w; + float outHeight = scale * h; + float left = (size - outWidth) / 2; + float top = (size - outHeight) / 2; + RectF target = new RectF(left, top, left + outWidth, top + + outHeight); + + Bitmap output = Bitmap.createBitmap(size, size, input.getConfig()); + Canvas canvas = new Canvas(output); + canvas.drawBitmap(input, null, target, null); + return output; + } + + private int calcSampleSize(Uri image, int size) + throws FileNotFoundException { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(context.getContentResolver() + .openInputStream(image), null, options); + int height = options.outHeight; + int width = options.outWidth; + int inSampleSize = 1; + + if (height > size || width > size) { + int halfHeight = height / 2; + int halfWidth = width / 2; + + while ((halfHeight / inSampleSize) > size + && (halfWidth / inSampleSize) > size) { + inSampleSize *= 2; + } + } + return inSampleSize; + + } + + public Uri getJingleFileUri(Message message) { + File file = getJingleFile(message); + if (file.exists()) { + return Uri.parse("file://"+file.getAbsolutePath()); + } else { + return ImageProvider.getProviderUri(message); + } + } public class ImageCopyException extends Exception { private static final long serialVersionUID = -1010013599132881427L; @@ -232,4 +431,12 @@ public class FileBackend { return resId; } } + + public static Bitmap getAvatar(String avatar, int size, Context context) { + Bitmap bm = BitmapFactory.decodeFile(FileBackend.getAvatarPath(context, avatar)); + if (bm==null) { + return null; + } + return cropCenterSquare(bm, UIHelper.getRealPx(size, context)); + } } diff --git a/src/eu/siacs/conversations/services/Defaults.java b/src/eu/siacs/conversations/services/Defaults.java new file mode 100644 index 00000000..c942dd48 --- /dev/null +++ b/src/eu/siacs/conversations/services/Defaults.java @@ -0,0 +1,11 @@ +package eu.siacs.conversations.services; + +import android.graphics.Bitmap; + +public final class Defaults { + public static final int AVATAR_SIZE = 192; + public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP; + private Defaults() { + + } +} diff --git a/src/eu/siacs/conversations/services/ImageProvider.java b/src/eu/siacs/conversations/services/ImageProvider.java index 80cfbd95..ff8eec0b 100644 --- a/src/eu/siacs/conversations/services/ImageProvider.java +++ b/src/eu/siacs/conversations/services/ImageProvider.java @@ -61,7 +61,7 @@ public class ImageProvider extends ContentProvider { message.setConversation(conversation); conversation.setAccount(account); - File file = fileBackend.getJingleFile(message); + File file = fileBackend.getJingleFileLegacy(message); pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); return pfd; @@ -110,7 +110,7 @@ public class ImageProvider extends ContentProvider { return 0; } - public static Uri getContentUri(Message message) { + public static Uri getProviderUri(Message message) { return Uri .parse("content://eu.siacs.conversations.images/" + message.getConversationUuid() diff --git a/src/eu/siacs/conversations/services/XmppConnectionService.java b/src/eu/siacs/conversations/services/XmppConnectionService.java index c9813d82..2c27315e 100644 --- a/src/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/eu/siacs/conversations/services/XmppConnectionService.java @@ -20,6 +20,7 @@ import de.duenndns.ssl.MemorizingTrustManager; import net.java.otr4j.OtrException; import net.java.otr4j.session.Session; import net.java.otr4j.session.SessionStatus; +import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Bookmark; @@ -53,6 +54,7 @@ import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; @@ -64,6 +66,7 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.database.ContentObserver; +import android.graphics.Bitmap; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; @@ -109,6 +112,7 @@ public class XmppConnectionService extends Service { private OnConversationUpdate mOnConversationUpdate = null; private int convChangedListenerCount = 0; private OnAccountUpdate mOnAccountUpdate = null; + private int accountChangedListenerCount = 0; private OnRosterUpdate mOnRosterUpdate = null; public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() { @@ -525,7 +529,7 @@ public class XmppConnectionService extends Service { } else { message.getConversation().endOtrIfNeeded(); failWaitingOtrMessages(message.getConversation()); - if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { + if (message.getConversation().getMode() == Conversation.MODE_SINGLE || message.getType() == Message.TYPE_PRIVATE) { message.setStatus(Message.STATUS_SEND); } packet = mMessageGenerator.generateChat(message); @@ -668,7 +672,7 @@ public class XmppConnectionService extends Service { @Override public void onIqPacketReceived(Account account, IqPacket packet) { Element query = packet.query(); - List<Bookmark> bookmarks = new ArrayList<Bookmark>(); + List<Bookmark> bookmarks = new CopyOnWriteArrayList<Bookmark>(); Element storage = query.findChild("storage", "storage:bookmarks"); if (storage!=null) { for(Element item : storage.getChildren()) { @@ -921,10 +925,14 @@ public class XmppConnectionService extends Service { public void setOnAccountListChangedListener(OnAccountUpdate listener) { this.mOnAccountUpdate = listener; + this.accountChangedListenerCount++; } public void removeOnAccountListChangedListener() { - this.mOnAccountUpdate = null; + this.accountChangedListenerCount--; + if (this.accountChangedListenerCount == 0) { + this.mOnAccountUpdate = null; + } } public void setOnRosterUpdateListener(OnRosterUpdate listener) { @@ -1183,6 +1191,117 @@ public class XmppConnectionService extends Service { } } + + public void publishAvatar(Account account, Uri image, final UiCallback<Avatar> callback) { + final Bitmap.CompressFormat format = Defaults.AVATAR_FORMAT; + final int size = Defaults.AVATAR_SIZE; + final Avatar avatar = getFileBackend().getPepAvatar(image, size, format); + if (avatar!=null) { + avatar.height = size; + avatar.width = size; + if (format.equals(Bitmap.CompressFormat.WEBP)) { + avatar.type = "image/webp"; + } else if (format.equals(Bitmap.CompressFormat.JPEG)) { + avatar.type = "image/jpeg"; + } else if (format.equals(Bitmap.CompressFormat.PNG)) { + avatar.type = "image/png"; + } + if (!getFileBackend().save(avatar)) { + callback.error(R.string.error_saving_avatar, avatar); + return; + } + IqPacket packet = this.mIqGenerator.publishAvatar(avatar); + this.sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket result) { + if (result.getType() == IqPacket.TYPE_RESULT) { + IqPacket packet = XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar); + sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket result) { + if (result.getType() == IqPacket.TYPE_RESULT) { + account.setAvatar(avatar.getFilename()); + callback.success(avatar); + } else { + callback.error(R.string.error_publish_avatar_server_reject, avatar); + } + } + }); + } else { + callback.error(R.string.error_publish_avatar_server_reject, avatar); + } + } + }); + } else { + callback.error(R.string.error_publish_avatar_converting, null); + } + } + + public void fetchAvatar(Account account, Avatar avatar) { + fetchAvatar(account, avatar, null); + } + + public void fetchAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) { + Log.d(LOGTAG,account.getJid()+": retrieving avatar for "+avatar.owner); + IqPacket packet = this.mIqGenerator.retrieveAvatar(avatar); + sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket result) { + avatar.image = mIqParser.avatarData(result); + if (avatar.image!=null) { + if (getFileBackend().save(avatar)) { + if (account.getJid().equals(avatar.owner)) { + account.setAvatar(avatar.getFilename()); + } else { + Contact contact = account.getRoster().getContact(avatar.owner); + contact.setAvatar(avatar.getFilename()); + } + if (callback!=null) { + callback.success(avatar); + } + return; + } + } + if (callback!=null) { + callback.error(0, null); + } + } + }); + } + + public void checkForAvatar(Account account, final UiCallback<Avatar> callback) { + IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null); + this.sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_RESULT) { + Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub"); + if (pubsub!=null) { + Element items = pubsub.findChild("items"); + if (items!=null) { + Avatar avatar = Avatar.parseMetadata(items); + if (avatar!=null) { + avatar.owner = account.getJid(); + if (fileBackend.isAvatarCached(avatar)) { + account.setAvatar(avatar.getFilename()); + callback.success(avatar); + } else { + fetchAvatar(account, avatar,callback); + } + return; + } + } + } + } + callback.error(0, null); + } + }); + } + public void deleteContactOnServer(Contact contact) { contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); contact.resetOption(Contact.Options.DIRTY_PUSH); diff --git a/src/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 56903da8..c33277f9 100644 --- a/src/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -7,6 +7,8 @@ import org.openintents.openpgp.util.OpenPgpUtils; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.OnRenameListener; @@ -54,7 +56,7 @@ public class ConferenceDetailsActivity extends XmppActivity { private List<User> users = new ArrayList<MucOptions.User>(); private OnConversationUpdate onConvChanged = new OnConversationUpdate() { - + @Override public void onConversationUpdate() { runOnUiThread(new Runnable() { @@ -150,13 +152,14 @@ public class ConferenceDetailsActivity extends XmppActivity { this.uuid = getIntent().getExtras().getString("uuid"); } if (uuid != null) { - this.conversation = xmppConnectionService.findConversationByUuid(uuid); + this.conversation = xmppConnectionService + .findConversationByUuid(uuid); if (this.conversation != null) { populateView(); } } } - + @Override protected void onStop() { if (xmppConnectionServiceBound) { @@ -164,39 +167,39 @@ public class ConferenceDetailsActivity extends XmppActivity { } super.onStop(); } - + protected void registerListener() { if (xmppConnectionServiceBound) { xmppConnectionService .setOnConversationListChangedListener(this.onConvChanged); - xmppConnectionService.setOnRenameListener(new OnRenameListener() { + xmppConnectionService.setOnRenameListener(new OnRenameListener() { - @Override - public void onRename(final boolean success) { - runOnUiThread(new Runnable() { + @Override + public void onRename(final boolean success) { + runOnUiThread(new Runnable() { - @Override - public void run() { - populateView(); - if (success) { - Toast.makeText(ConferenceDetailsActivity.this, - getString(R.string.your_nick_has_been_changed), - Toast.LENGTH_SHORT).show(); - } else { - Toast.makeText(ConferenceDetailsActivity.this, - getString(R.string.nick_in_use), - Toast.LENGTH_SHORT).show(); + @Override + public void run() { + populateView(); + if (success) { + Toast.makeText( + ConferenceDetailsActivity.this, + getString(R.string.your_nick_has_been_changed), + Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(ConferenceDetailsActivity.this, + getString(R.string.nick_in_use), + Toast.LENGTH_SHORT).show(); + } } - } - }); - } - }); + }); + } + }); } } private void populateView() { - mYourPhoto.setImageBitmap(UIHelper.getContactPicture(conversation - .getMucOptions().getActualNick(), 48, this, false)); + mYourPhoto.setImageBitmap(conversation.getAccount().getImage(this, 48)); setTitle(conversation.getName(true)); mFullJid.setText(conversation.getContactJid().split("/")[0]); mYourNick.setText(conversation.getMucOptions().getActualNick()); @@ -222,28 +225,44 @@ public class ConferenceDetailsActivity extends XmppActivity { this.users.addAll(conversation.getMucOptions().getUsers()); LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); membersView.removeAllViews(); - for (final User contact : conversation.getMucOptions().getUsers()) { - View view = (View) inflater.inflate(R.layout.contact, null); - TextView displayName = (TextView) view + Account account = conversation.getAccount(); + for (final User user : conversation.getMucOptions().getUsers()) { + View view = (View) inflater.inflate(R.layout.contact, membersView, + false); + TextView name = (TextView) view .findViewById(R.id.contact_display_name); TextView key = (TextView) view.findViewById(R.id.key); - displayName.setText(contact.getName()); TextView role = (TextView) view.findViewById(R.id.contact_jid); - role.setText(getReadableRole(contact.getRole())); - if (contact.getPgpKeyId() != 0) { + if (user.getPgpKeyId() != 0) { key.setVisibility(View.VISIBLE); key.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - viewPgpKey(contact); + viewPgpKey(user); } }); - key.setText(OpenPgpUtils.convertKeyIdToHex(contact - .getPgpKeyId())); + key.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId())); + } + Bitmap bm; + if (user.getJid() != null) { + Contact contact = account.getRoster().getContact(user.getJid()); + if (contact.showInRoster()) { + bm = contact.getImage(48, this); + name.setText(contact.getDisplayName()); + role.setText(user.getName() + " \u2022 " + getReadableRole(user.getRole())); + } else { + bm = UIHelper.getContactPicture(user.getName(), 48, this, + false); + name.setText(user.getName()); + role.setText(getReadableRole(user.getRole())); + } + } else { + bm = UIHelper + .getContactPicture(user.getName(), 48, this, false); + name.setText(user.getName()); + role.setText(getReadableRole(user.getRole())); } - Bitmap bm = UIHelper.getContactPicture(contact.getName(), 48, this, - false); ImageView iv = (ImageView) view.findViewById(R.id.contact_photo); iv.setImageBitmap(bm); membersView.addView(view); diff --git a/src/eu/siacs/conversations/ui/ConversationActivity.java b/src/eu/siacs/conversations/ui/ConversationActivity.java index aa4fda4e..1e1acb53 100644 --- a/src/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/eu/siacs/conversations/ui/ConversationActivity.java @@ -4,6 +4,7 @@ import java.io.FileNotFoundException; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.RejectedExecutionException; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Contact; @@ -11,6 +12,7 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.ImageProvider; import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate; +import eu.siacs.conversations.ui.adapter.ConversationAdapter; import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.utils.UIHelper; import android.net.Uri; @@ -18,10 +20,10 @@ import android.os.AsyncTask; import android.os.Bundle; import android.preference.PreferenceManager; import android.provider.MediaStore; +import android.app.ActionBar; import android.app.AlertDialog; import android.app.FragmentTransaction; import android.app.PendingIntent; -import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.IntentSender.SendIntentException; @@ -29,20 +31,15 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.support.v4.widget.SlidingPaneLayout; import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener; import android.util.DisplayMetrics; -import android.util.Log; import android.view.KeyEvent; -import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; @@ -50,7 +47,6 @@ import android.widget.CheckBox; import android.widget.ListView; import android.widget.PopupMenu; import android.widget.PopupMenu.OnMenuItemClickListener; -import android.widget.TextView; import android.widget.ImageView; import android.widget.Toast; @@ -61,17 +57,17 @@ public class ConversationActivity extends XmppActivity { 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 REQUEST_ATTACH_FILE_DIALOG = 0x48502; - private static final int REQUEST_IMAGE_CAPTURE = 0x33788; - private static final int REQUEST_RECORD_AUDIO = 0x46189; - private static final int REQUEST_SEND_PGP_IMAGE = 0x53883; - public static final int REQUEST_ENCRYPT_MESSAGE = 0x378018; + public static final int REQUEST_SEND_MESSAGE = 0x0201; + public static final int REQUEST_DECRYPT_PGP = 0x0202; + private static final int REQUEST_ATTACH_FILE_DIALOG = 0x0203; + private static final int REQUEST_IMAGE_CAPTURE = 0x0204; + private static final int REQUEST_RECORD_AUDIO = 0x0205; + private static final int REQUEST_SEND_PGP_IMAGE = 0x0206; + public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207; - private static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x92734; - private static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x84123; - private static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x75291; + private static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301; + private static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302; + private static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0303; protected SlidingPaneLayout spl; @@ -150,93 +146,11 @@ public class ConversationActivity extends XmppActivity { setContentView(R.layout.fragment_conversations_overview); listView = (ListView) findViewById(R.id.list); + + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setHomeButtonEnabled(false); - this.listAdapter = new ArrayAdapter<Conversation>(this, - R.layout.conversation_list_row, conversationList) { - @Override - public View getView(int position, View view, ViewGroup parent) { - if (view == null) { - LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); - view = (View) inflater.inflate( - R.layout.conversation_list_row, null); - } - Conversation conv; - if (conversationList.size() > position) { - conv = getItem(position); - } else { - return view; - } - if (!spl.isSlideable()) { - if (conv == getSelectedConversation()) { - view.setBackgroundColor(0xffdddddd); - } else { - view.setBackgroundColor(Color.TRANSPARENT); - } - } else { - view.setBackgroundColor(Color.TRANSPARENT); - } - TextView convName = (TextView) view - .findViewById(R.id.conversation_name); - convName.setText(conv.getName(useSubject)); - TextView convLastMsg = (TextView) view - .findViewById(R.id.conversation_lastmsg); - ImageView imagePreview = (ImageView) view - .findViewById(R.id.conversation_lastimage); - - Message latestMessage = conv.getLatestMessage(); - - if (latestMessage.getType() == Message.TYPE_TEXT) { - if ((latestMessage.getEncryption() != Message.ENCRYPTION_PGP) - && (latestMessage.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)) { - convLastMsg.setText(conv.getLatestMessage().getBody()); - } else { - convLastMsg - .setText(getText(R.string.encrypted_message_received)); - } - convLastMsg.setVisibility(View.VISIBLE); - imagePreview.setVisibility(View.GONE); - } else if (latestMessage.getType() == Message.TYPE_IMAGE) { - if (latestMessage.getStatus() >= Message.STATUS_RECIEVED) { - convLastMsg.setVisibility(View.GONE); - imagePreview.setVisibility(View.VISIBLE); - loadBitmap(latestMessage, imagePreview); - } else { - convLastMsg.setVisibility(View.VISIBLE); - imagePreview.setVisibility(View.GONE); - if (latestMessage.getStatus() == Message.STATUS_RECEIVED_OFFER) { - convLastMsg - .setText(getText(R.string.image_offered_for_download)); - } else if (latestMessage.getStatus() == Message.STATUS_RECIEVING) { - convLastMsg - .setText(getText(R.string.receiving_image)); - } else { - convLastMsg.setText(""); - } - } - } - - if (!conv.isRead()) { - convName.setTypeface(null, Typeface.BOLD); - convLastMsg.setTypeface(null, Typeface.BOLD); - } else { - convName.setTypeface(null, Typeface.NORMAL); - convLastMsg.setTypeface(null, Typeface.NORMAL); - } - - ((TextView) view.findViewById(R.id.conversation_lastupdate)) - .setText(UIHelper.readableTimeDifference(getContext(), - conv.getLatestMessage().getTimeSent())); - - ImageView profilePicture = (ImageView) view - .findViewById(R.id.conversation_image); - profilePicture.setImageBitmap(UIHelper.getContactPicture(conv, - 56, activity.getApplicationContext(), false)); - - return view; - } - - }; - + this.listAdapter = new ConversationAdapter(this, conversationList); listView.setAdapter(this.listAdapter); listView.setOnItemClickListener(new OnItemClickListener() { @@ -247,7 +161,7 @@ public class ConversationActivity extends XmppActivity { paneShouldBeOpen = false; if (getSelectedConversation() != conversationList.get(position)) { setSelectedConversation(conversationList.get(position)); - swapConversationFragment(); // .onBackendConnected(conversationList.get(position)); + swapConversationFragment(); } else { spl.closePane(); } @@ -262,9 +176,12 @@ public class ConversationActivity extends XmppActivity { @Override public void onPanelOpened(View arg0) { paneShouldBeOpen = true; - getActionBar().setDisplayHomeAsUpEnabled(false); - getActionBar().setHomeButtonEnabled(false); - getActionBar().setTitle(R.string.app_name); + ActionBar ab = getActionBar(); + if (ab!=null) { + ab.setDisplayHomeAsUpEnabled(false); + ab.setHomeButtonEnabled(false); + ab.setTitle(R.string.app_name); + } invalidateOptionsMenu(); hideKeyboard(); } @@ -274,10 +191,13 @@ public class ConversationActivity extends XmppActivity { paneShouldBeOpen = false; if ((conversationList.size() > 0) && (getSelectedConversation() != null)) { - getActionBar().setDisplayHomeAsUpEnabled(true); - getActionBar().setHomeButtonEnabled(true); - getActionBar().setTitle( + ActionBar ab = getActionBar(); + if (ab!=null) { + ab.setDisplayHomeAsUpEnabled(true); + ab.setHomeButtonEnabled(true); + ab.setTitle( getSelectedConversation().getName(useSubject)); + } invalidateOptionsMenu(); if (!getSelectedConversation().isRead()) { xmppConnectionService @@ -427,7 +347,7 @@ public class ConversationActivity extends XmppActivity { switch (item.getItemId()) { case android.R.id.home: spl.openPane(); - break; + return true; case R.id.action_attach_file: View menuAttachFile = findViewById(R.id.action_attach_file); if (menuAttachFile==null) { @@ -602,12 +522,15 @@ public class ConversationActivity extends XmppActivity { protected ConversationFragment swapConversationFragment() { ConversationFragment selectedFragment = new ConversationFragment(); + if (!isFinishing()) { FragmentTransaction transaction = getFragmentManager() .beginTransaction(); transaction.replace(R.id.selected_conversation, selectedFragment, "conversation"); - transaction.commitAllowingStateLoss(); + + transaction.commitAllowingStateLoss(); + } return selectedFragment; } @@ -624,19 +547,24 @@ public class ConversationActivity extends XmppActivity { @Override protected void onNewIntent(Intent intent) { - if ((Intent.ACTION_VIEW.equals(intent.getAction()) && (VIEW_CONVERSATION - .equals(intent.getType())))) { - String convToView = (String) intent.getExtras().get(CONVERSATION); - updateConversationList(); - for (int i = 0; i < conversationList.size(); ++i) { - if (conversationList.get(i).getUuid().equals(convToView)) { - setSelectedConversation(conversationList.get(i)); - break; + if (xmppConnectionServiceBound) { + if ((Intent.ACTION_VIEW.equals(intent.getAction()) && (VIEW_CONVERSATION + .equals(intent.getType())))) { + String convToView = (String) intent.getExtras().get(CONVERSATION); + updateConversationList(); + for (int i = 0; i < conversationList.size(); ++i) { + if (conversationList.get(i).getUuid().equals(convToView)) { + setSelectedConversation(conversationList.get(i)); + break; + } } + paneShouldBeOpen = false; + String text = intent.getExtras().getString(TEXT, null); + swapConversationFragment().setText(text); } - paneShouldBeOpen = false; - String text = intent.getExtras().getString(TEXT, null); - swapConversationFragment().setText(text); + } else { + handledViewIntent = false; + setIntent(intent); } } @@ -747,17 +675,14 @@ public class ConversationActivity extends XmppActivity { } else if (requestCode == REQUEST_IMAGE_CAPTURE) { attachImageToConversation(getSelectedConversation(), null); } else if (requestCode == REQUEST_RECORD_AUDIO) { - Log.d("xmppService", data.getData().toString()); attachAudioToConversation(getSelectedConversation(), data.getData()); - } else { - Log.d(LOGTAG, "unknown result code:" + requestCode); } } } private void attachAudioToConversation(Conversation conversation, Uri uri) { - + } private void attachImageToConversation(Conversation conversation, Uri uri) { @@ -818,9 +743,7 @@ public class ConversationActivity extends XmppActivity { try { this.startIntentSenderForResult(pi.getIntentSender(), requestCode, null, 0, 0, 0); - } catch (SendIntentException e1) { - Log.d("xmppService", "failed to start intent to send message"); - } + } catch (SendIntentException e1) {} } class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> { @@ -838,7 +761,6 @@ public class ConversationActivity extends XmppActivity { return xmppConnectionService.getFileBackend().getThumbnail( message, (int) (metrics.density * 288), false); } catch (FileNotFoundException e) { - Log.d("xmppService", "file not found!"); return null; } } @@ -873,7 +795,11 @@ public class ConversationActivity extends XmppActivity { final AsyncDrawable asyncDrawable = new AsyncDrawable( getResources(), null, task); imageView.setImageDrawable(asyncDrawable); - task.execute(message); + try { + task.execute(message); + } catch (RejectedExecutionException e) { + return; + } } } } diff --git a/src/eu/siacs/conversations/ui/ConversationFragment.java b/src/eu/siacs/conversations/ui/ConversationFragment.java index 1df59843..1270d23a 100644 --- a/src/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/eu/siacs/conversations/ui/ConversationFragment.java @@ -13,13 +13,16 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.EditMessage.OnEnterPressed; import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected; import eu.siacs.conversations.ui.adapter.MessageAdapter; import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked; +import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked; import eu.siacs.conversations.utils.UIHelper; import android.app.AlertDialog; import android.app.Fragment; import android.app.PendingIntent; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentSender; @@ -30,14 +33,17 @@ import android.preference.PreferenceManager; import android.text.Editable; import android.text.Selection; import android.view.Gravity; +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; import android.widget.AbsListView.OnScrollListener; +import android.widget.TextView.OnEditorActionListener; import android.widget.AbsListView; -import android.widget.EditText; import android.widget.ListView; import android.widget.ImageButton; import android.widget.RelativeLayout; @@ -55,7 +61,7 @@ public class ConversationFragment extends Fragment { protected String queuedPqpMessage = null; - private EditText chatMsg; + private EditMessage mEditMessage; private String pastedText = null; private RelativeLayout snackbar; private TextView snackbarMessage; @@ -65,22 +71,27 @@ public class ConversationFragment extends Fragment { private boolean messagesLoaded = false; private IntentSender askForPassphraseIntent = null; + + private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() { + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE) { + InputMethodManager imm = (InputMethodManager) v.getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + return true; + } else { + return false; + } + } + }; - private OnClickListener sendMsgListener = new OnClickListener() { + private OnClickListener mSendButtonListener = new OnClickListener() { @Override public void onClick(View v) { - if (chatMsg.getText().length() < 1) - return; - Message message = new Message(conversation, chatMsg.getText() - .toString(), conversation.getNextEncryption()); - if (conversation.getNextEncryption() == Message.ENCRYPTION_OTR) { - sendOtrMessage(message); - } else if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { - sendPgpMessage(message); - } else { - sendPlainTextMessage(message); - } + sendMessage(); } }; protected OnClickListener clickToDecryptListener = new OnClickListener() { @@ -147,17 +158,44 @@ public class ConversationFragment extends Fragment { }; private ConversationActivity activity; + + + private void sendMessage() { + if (mEditMessage.getText().length() < 1) { + if (this.conversation.getMode() == Conversation.MODE_MULTI) { + conversation.setNextPresence(null); + updateChatMsgHint(); + } + return; + } + Message message = new Message(conversation, mEditMessage.getText() + .toString(), conversation.getNextEncryption()); + if (conversation.getMode() == Conversation.MODE_MULTI) { + if (conversation.getNextPresence() != null) { + message.setPresence(conversation.getNextPresence()); + message.setType(Message.TYPE_PRIVATE); + conversation.setNextPresence(null); + } + } + if (conversation.getNextEncryption() == Message.ENCRYPTION_OTR) { + sendOtrMessage(message); + } else if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { + sendPgpMessage(message); + } else { + sendPlainTextMessage(message); + } + } public void updateChatMsgHint() { switch (conversation.getNextEncryption()) { case Message.ENCRYPTION_NONE: - chatMsg.setHint(getString(R.string.send_plain_text_message)); + mEditMessage.setHint(getString(R.string.send_plain_text_message)); break; case Message.ENCRYPTION_OTR: - chatMsg.setHint(getString(R.string.send_otr_message)); + mEditMessage.setHint(getString(R.string.send_otr_message)); break; case Message.ENCRYPTION_PGP: - chatMsg.setHint(getString(R.string.send_pgp_message)); + mEditMessage.setHint(getString(R.string.send_pgp_message)); break; default: break; @@ -169,8 +207,8 @@ public class ConversationFragment extends Fragment { ViewGroup container, Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.fragment_conversation, container, false); - chatMsg = (EditText) view.findViewById(R.id.textinput); - chatMsg.setOnClickListener(new OnClickListener() { + mEditMessage = (EditMessage) view.findViewById(R.id.textinput); + mEditMessage.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { @@ -179,10 +217,18 @@ public class ConversationFragment extends Fragment { } } }); - + mEditMessage.setOnEditorActionListener(mEditorActionListener); + mEditMessage.setOnEnterPressedListener(new OnEnterPressed() { + + @Override + public void onEnterPressed() { + sendMessage(); + } + }); + ImageButton sendButton = (ImageButton) view .findViewById(R.id.textSendButton); - sendButton.setOnClickListener(this.sendMsgListener); + sendButton.setOnClickListener(this.mSendButtonListener); snackbar = (RelativeLayout) view.findViewById(R.id.snackbar); snackbarMessage = (TextView) view.findViewById(R.id.snackbar_message); @@ -197,7 +243,24 @@ public class ConversationFragment extends Fragment { @Override public void onContactPictureClicked(Message message) { if (message.getConversation().getMode() == Conversation.MODE_MULTI) { - highlightInConference(message.getCounterpart()); + if (message.getPresence() != null) { + highlightInConference(message.getPresence()); + } else { + highlightInConference(message.getCounterpart()); + } + } + } + }); + messageListAdapter.setOnContactPictureLongClicked(new OnContactPictureLongClicked() { + + @Override + public void onContactPictureLongClicked(Message message) { + if (message.getConversation().getMode() == Conversation.MODE_MULTI) { + if (message.getPresence() != null) { + privateMessageWith(message.getPresence()); + } else { + privateMessageWith(message.getCounterpart()); + } } } }); @@ -205,16 +268,22 @@ public class ConversationFragment extends Fragment { return view; } + + protected void privateMessageWith(String counterpart) { + this.mEditMessage.setHint(getString(R.string.send_private_message_to,counterpart)); + this.mEditMessage.setText(""); + this.conversation.setNextPresence(counterpart); + } protected void highlightInConference(String nick) { - String oldString = chatMsg.getText().toString().trim(); + String oldString = mEditMessage.getText().toString().trim(); if (oldString.isEmpty()) { - chatMsg.setText(nick + ": "); + mEditMessage.setText(nick + ": "); } else { - chatMsg.setText(oldString + " " + nick + " "); + mEditMessage.setText(oldString + " " + nick + " "); } - int position = chatMsg.length(); - Editable etext = chatMsg.getText(); + int position = mEditMessage.length(); + Editable etext = mEditMessage.getText(); Selection.setSelection(etext, position); } @@ -234,29 +303,30 @@ public class ConversationFragment extends Fragment { public void onStop() { super.onStop(); if (this.conversation != null) { - this.conversation.setNextMessage(chatMsg.getText().toString()); + this.conversation.setNextMessage(mEditMessage.getText().toString()); } } public void onBackendConnected() { + this.activity = (ConversationActivity) getActivity(); this.conversation = activity.getSelectedConversation(); if (this.conversation == null) { return; } String oldString = conversation.getNextMessage().trim(); if (this.pastedText == null) { - this.chatMsg.setText(oldString); + this.mEditMessage.setText(oldString); } else { if (oldString.isEmpty()) { - chatMsg.setText(pastedText); + mEditMessage.setText(pastedText); } else { - chatMsg.setText(oldString + " " + pastedText); + mEditMessage.setText(oldString + " " + pastedText); } pastedText = null; } - int position = chatMsg.length(); - Editable etext = chatMsg.getText(); + int position = mEditMessage.length(); + Editable etext = mEditMessage.getText(); Selection.setSelection(etext, position); updateMessages(); if (activity.getSlidingPaneLayout().isSlideable()) { @@ -269,6 +339,9 @@ public class ConversationFragment extends Fragment { activity.invalidateOptionsMenu(); } } + if (this.conversation.getMode() == Conversation.MODE_MULTI) { + conversation.setNextPresence(null); + } } private void decryptMessage(Message message) { @@ -371,7 +444,8 @@ public class ConversationFragment extends Fragment { if (size >= 1) { messagesView.setSelection(size - 1); } - chatMsg.setText(""); + mEditMessage.setText(""); + updateChatMsgHint(); } protected void updateStatusMessages() { @@ -425,7 +499,9 @@ public class ConversationFragment extends Fragment { protected void showSnackbar(int message, int action, OnClickListener clickListener) { snackbar.setVisibility(View.VISIBLE); + snackbar.setOnClickListener(null); snackbarMessage.setText(message); + snackbarMessage.setOnClickListener(null); snackbarAction.setText(action); snackbarAction.setOnClickListener(clickListener); } @@ -560,6 +636,6 @@ public class ConversationFragment extends Fragment { } public void clearInputField() { - this.chatMsg.setText(""); + this.mEditMessage.setText(""); } } diff --git a/src/eu/siacs/conversations/ui/EditAccountActivity.java b/src/eu/siacs/conversations/ui/EditAccountActivity.java new file mode 100644 index 00000000..e5920cdf --- /dev/null +++ b/src/eu/siacs/conversations/ui/EditAccountActivity.java @@ -0,0 +1,294 @@ +package eu.siacs.conversations.ui; + +import android.app.PendingIntent; +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.AutoCompleteTextView; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.CompoundButton.OnCheckedChangeListener; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; +import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; +import eu.siacs.conversations.utils.Validator; +import eu.siacs.conversations.xmpp.pep.Avatar; + +public class EditAccountActivity extends XmppActivity { + + private AutoCompleteTextView mAccountJid; + private EditText mPassword; + private EditText mPasswordConfirm; + private CheckBox mRegisterNew; + private Button mCancelButton; + private Button mSaveButton; + + private String jidToEdit; + private Account mAccount; + + private boolean mUserInputIsValid = false; + private boolean mFetchingAvatar = false; + + private OnClickListener mSaveButtonClickListener = new OnClickListener() { + + @Override + public void onClick(View v) { + if (mAccount != null && mAccount.errorStatus() + && !mUserInputIsValid) { + xmppConnectionService.reconnectAccount(mAccount, true); + return; + } + boolean registerNewAccount = mRegisterNew.isChecked(); + String[] jidParts = mAccountJid.getText().toString().split("@"); + String username = jidParts[0]; + String server; + if (jidParts.length >= 2) { + server = jidParts[1]; + } else { + server = ""; + } + String password = mPassword.getText().toString(); + String passwordConfirm = mPasswordConfirm.getText().toString(); + if (registerNewAccount) { + if (!password.equals(passwordConfirm)) { + mPasswordConfirm + .setError(getString(R.string.passwords_do_not_match)); + return; + } + } + if (mAccount != null) { + mAccount.setPassword(password); + mAccount.setUsername(username); + mAccount.setServer(server); + mAccount.setOption(Account.OPTION_REGISTER, mRegisterNew.isChecked()); + xmppConnectionService.updateAccount(mAccount); + } else { + if (xmppConnectionService.findAccountByJid(mAccountJid.getText().toString())!=null) { + mAccountJid.setError(getString(R.string.account_already_exists)); + return; + } + mAccount = new Account(username, server, password); + mAccount.setOption(Account.OPTION_USETLS, true); + mAccount.setOption(Account.OPTION_USECOMPRESSION, true); + if (registerNewAccount) { + mAccount.setOption(Account.OPTION_REGISTER, true); + } + xmppConnectionService.createAccount(mAccount); + } + if (jidToEdit != null) { + finish(); + } else { + mUserInputIsValid = false; + updateSaveButton(); + updateAccountInformation(); + } + + } + }; + private OnClickListener mCancelButtonClickListener = new OnClickListener() { + + @Override + public void onClick(View v) { + finish(); + } + }; + private TextWatcher mTextWatcher = new TextWatcher() { + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + mUserInputIsValid = inputDataDiffersFromAccount() && Validator.isValidJid(mAccountJid.getText().toString()); + updateSaveButton(); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + } + + @Override + public void afterTextChanged(Editable s) { + } + }; + private OnAccountUpdate mOnAccountUpdateListener = new OnAccountUpdate() { + + @Override + public void onAccountUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + if (jidToEdit==null && mAccount!=null && mAccount.getStatus() == Account.STATUS_ONLINE) { + if (!mFetchingAvatar) { + mFetchingAvatar = true; + xmppConnectionService.checkForAvatar(mAccount, mAvatarFetchCallback); + } + } else { + updateSaveButton(); + } + } + }); + } + }; + private UiCallback<Avatar> mAvatarFetchCallback = new UiCallback<Avatar>() { + + @Override + public void userInputRequried(PendingIntent pi, Avatar avatar) { + finishInitialSetup(avatar); + } + + @Override + public void success(Avatar avatar) { + finishInitialSetup(avatar); + } + + @Override + public void error(int errorCode, Avatar avatar) { + finishInitialSetup(avatar); + } + }; + + protected void finishInitialSetup(final Avatar avatar) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + Intent intent; + if (avatar!=null) { + intent = new Intent(getApplicationContext(), StartConversationActivity.class); + } else { + intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class); + intent.putExtra("account", mAccount.getJid()); + } + startActivity(intent); + finish(); + } + }); + } + + protected boolean inputDataDiffersFromAccount() { + if (mAccount == null) { + return true; + } else { + return (!mAccount.getJid().equals(mAccountJid.getText().toString())) + || (!mAccount.getPassword().equals( + mPassword.getText().toString()) || mAccount + .isOptionSet(Account.OPTION_REGISTER) != mRegisterNew + .isChecked()); + } + } + + protected void updateSaveButton() { + if (mAccount != null + && mAccount.getStatus() == Account.STATUS_CONNECTING + && !mUserInputIsValid) { + this.mSaveButton.setEnabled(false); + this.mSaveButton.setTextColor(getSecondaryTextColor()); + this.mSaveButton.setText(R.string.account_status_connecting); + } else if (mAccount != null && mAccount.errorStatus() + && !mUserInputIsValid) { + this.mSaveButton.setEnabled(true); + this.mSaveButton.setTextColor(getPrimaryTextColor()); + this.mSaveButton.setText(R.string.connect); + } else if (mUserInputIsValid) { + this.mSaveButton.setEnabled(true); + this.mSaveButton.setTextColor(getPrimaryTextColor()); + if (jidToEdit!=null) { + this.mSaveButton.setText(R.string.save); + } else { + this.mSaveButton.setText(R.string.next); + } + } else { + this.mSaveButton.setEnabled(false); + this.mSaveButton.setTextColor(getSecondaryTextColor()); + if (jidToEdit!=null) { + this.mSaveButton.setText(R.string.save); + } else { + this.mSaveButton.setText(R.string.next); + } + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_edit_account); + this.mAccountJid = (AutoCompleteTextView) findViewById(R.id.account_jid); + this.mPassword = (EditText) findViewById(R.id.account_password); + this.mPasswordConfirm = (EditText) findViewById(R.id.account_password_confirm); + this.mRegisterNew = (CheckBox) findViewById(R.id.account_register_new); + this.mSaveButton = (Button) findViewById(R.id.save_button); + this.mCancelButton = (Button) findViewById(R.id.cancel_button); + this.mSaveButton.setOnClickListener(this.mSaveButtonClickListener); + this.mCancelButton.setOnClickListener(this.mCancelButtonClickListener); + this.mRegisterNew + .setOnCheckedChangeListener(new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, + boolean isChecked) { + if (isChecked) { + mPasswordConfirm.setVisibility(View.VISIBLE); + } else { + mPasswordConfirm.setVisibility(View.GONE); + } + mUserInputIsValid = inputDataDiffersFromAccount() && Validator.isValidJid(mAccountJid.getText().toString()); + updateSaveButton(); + } + }); + this.mAccountJid.addTextChangedListener(this.mTextWatcher); + this.mPassword.addTextChangedListener(this.mTextWatcher); + } + + @Override + protected void onStart() { + super.onStart(); + if (getIntent() != null) { + this.jidToEdit = getIntent().getStringExtra("jid"); + if (this.jidToEdit != null) { + this.mRegisterNew.setVisibility(View.GONE); + getActionBar().setTitle(R.string.mgmt_account_edit); + } else { + getActionBar().setTitle(R.string.action_add_account); + } + } + } + + @Override + protected void onBackendConnected() { + this.xmppConnectionService + .setOnAccountListChangedListener(this.mOnAccountUpdateListener); + this.mAccountJid.setAdapter(null); + if (this.jidToEdit != null) { + this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit); + updateAccountInformation(); + } else if (this.xmppConnectionService.getAccounts().size() == 0) { + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setDisplayShowHomeEnabled(false); + this.mCancelButton.setEnabled(false); + } + this.mAccountJid.setAdapter(new KnownHostsAdapter(this, + android.R.layout.simple_list_item_1, xmppConnectionService + .getKnownHosts())); + updateSaveButton(); + } + + private void updateAccountInformation() { + this.mAccountJid.setText(this.mAccount.getJid()); + this.mPassword.setText(this.mAccount.getPassword()); + if (this.mAccount.isOptionSet(Account.OPTION_REGISTER)) { + this.mRegisterNew.setVisibility(View.VISIBLE); + this.mRegisterNew.setChecked(true); + this.mPasswordConfirm.setText(this.mAccount.getPassword()); + } else { + this.mRegisterNew.setVisibility(View.GONE); + this.mRegisterNew.setChecked(false); + } + } +} diff --git a/src/eu/siacs/conversations/ui/EditAccountDialog.java b/src/eu/siacs/conversations/ui/EditAccountDialog.java deleted file mode 100644 index 7c135fc1..00000000 --- a/src/eu/siacs/conversations/ui/EditAccountDialog.java +++ /dev/null @@ -1,157 +0,0 @@ -package eu.siacs.conversations.ui; - -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; -import eu.siacs.conversations.utils.Validator; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.DialogFragment; -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.AutoCompleteTextView; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.CompoundButton; -import android.widget.CompoundButton.OnCheckedChangeListener; -import android.widget.EditText; -import android.widget.TextView; - -public class EditAccountDialog extends DialogFragment { - - protected Account account; - - protected AutoCompleteTextView mAccountJid; - - public void setAccount(Account account) { - this.account = account; - } - - public interface EditAccountListener { - public void onAccountEdited(Account account); - } - - protected EditAccountListener listener = null; - - private KnownHostsAdapter mKnownHostsAdapter; - - public void setEditAccountListener(EditAccountListener listener) { - this.listener = listener; - } - - public void setKnownHosts(List<String> hosts, Context context) { - this.mKnownHostsAdapter = new KnownHostsAdapter(context, android.R.layout.simple_list_item_1, hosts); - if (this.mAccountJid != null) { - this.mAccountJid.setAdapter(this.mKnownHostsAdapter); - } - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - LayoutInflater inflater = getActivity().getLayoutInflater(); - View view = inflater.inflate(R.layout.edit_account_dialog, null); - mAccountJid = (AutoCompleteTextView) view.findViewById(R.id.account_jid); - if (this.mKnownHostsAdapter!=null) { - mAccountJid.setAdapter(this.mKnownHostsAdapter); - } - final TextView confirmPwDesc = (TextView) view - .findViewById(R.id.account_confirm_password_desc); - - final EditText password = (EditText) view - .findViewById(R.id.account_password); - final EditText passwordConfirm = (EditText) view - .findViewById(R.id.account_password_confirm2); - final CheckBox registerAccount = (CheckBox) view - .findViewById(R.id.edit_account_register_new); - - if (account != null) { - mAccountJid.setText(account.getJid()); - password.setText(account.getPassword()); - if (account.isOptionSet(Account.OPTION_REGISTER)) { - registerAccount.setChecked(true); - passwordConfirm.setVisibility(View.VISIBLE); - passwordConfirm.setText(account.getPassword()); - } else { - registerAccount.setVisibility(View.GONE); - } - } - builder.setTitle(R.string.account_settings); - - - registerAccount - .setOnCheckedChangeListener(new OnCheckedChangeListener() { - - @Override - public void onCheckedChanged(CompoundButton buttonView, - boolean isChecked) { - if (isChecked) { - passwordConfirm.setVisibility(View.VISIBLE); - confirmPwDesc.setVisibility(View.VISIBLE); - } else { - passwordConfirm.setVisibility(View.GONE); - confirmPwDesc.setVisibility(View.GONE); - } - } - }); - - builder.setView(view); - builder.setNeutralButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.save), null); - return builder.create(); - } - - @Override - public void onStart() { - super.onStart(); - final AlertDialog d = (AlertDialog) getDialog(); - Button positiveButton = (Button) d.getButton(Dialog.BUTTON_POSITIVE); - positiveButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - EditText jidEdit = (EditText) d.findViewById(R.id.account_jid); - String jid = jidEdit.getText().toString(); - EditText passwordEdit = (EditText) d - .findViewById(R.id.account_password); - EditText passwordConfirmEdit = (EditText) d.findViewById(R.id.account_password_confirm2); - String password = passwordEdit.getText().toString(); - String passwordConfirm = passwordConfirmEdit.getText().toString(); - CheckBox register = (CheckBox) d.findViewById(R.id.edit_account_register_new); - String username; - String server; - if (Validator.isValidJid(jid)) { - String[] parts = jid.split("@"); - username = parts[0]; - server = parts[1]; - } else { - jidEdit.setError(getString(R.string.invalid_jid)); - return; - } - if (register.isChecked()) { - if (!passwordConfirm.equals(password)) { - passwordConfirmEdit.setError(getString(R.string.passwords_do_not_match)); - return; - } - } - if (account != null) { - account.setPassword(password); - account.setUsername(username); - account.setServer(server); - } else { - account = new Account(username, server, password); - account.setOption(Account.OPTION_USETLS, true); - account.setOption(Account.OPTION_USECOMPRESSION, true); - } - account.setOption(Account.OPTION_REGISTER, register.isChecked()); - if (listener != null) { - listener.onAccountEdited(account); - d.dismiss(); - } - } - }); - } -} diff --git a/src/eu/siacs/conversations/ui/EditMessage.java b/src/eu/siacs/conversations/ui/EditMessage.java new file mode 100644 index 00000000..f8302050 --- /dev/null +++ b/src/eu/siacs/conversations/ui/EditMessage.java @@ -0,0 +1,39 @@ +package eu.siacs.conversations.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.widget.EditText; + +public class EditMessage extends EditText { + + public EditMessage(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public EditMessage(Context context) { + super(context); + } + + protected OnEnterPressed mOnEnterPressed; + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_ENTER) { + if (mOnEnterPressed != null) { + mOnEnterPressed.onEnterPressed(); + } + return true; + } + return super.onKeyDown(keyCode, event); + } + + public void setOnEnterPressedListener(OnEnterPressed listener) { + this.mOnEnterPressed = listener; + } + + public interface OnEnterPressed { + public void onEnterPressed(); + } + +} diff --git a/src/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/eu/siacs/conversations/ui/ManageAccountActivity.java index e56e2db9..803917b9 100644 --- a/src/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -6,27 +6,22 @@ import java.util.List; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; -import eu.siacs.conversations.ui.EditAccountDialog.EditAccountListener; +import eu.siacs.conversations.ui.adapter.AccountAdapter; import eu.siacs.conversations.xmpp.XmppConnection; -import android.app.Activity; import android.app.AlertDialog; -import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.os.Bundle; import android.os.SystemClock; import android.view.ActionMode; -import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemLongClickListener; -import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; @@ -41,7 +36,7 @@ public class ManageAccountActivity extends XmppActivity { protected List<Account> accountList = new ArrayList<Account>(); protected ListView accountListView; - protected ArrayAdapter<Account> accountListViewAdapter; + protected AccountAdapter mAccountAdapter; protected OnAccountUpdate accountChanged = new OnAccountUpdate() { @Override @@ -52,12 +47,176 @@ public class ManageAccountActivity extends XmppActivity { @Override public void run() { - accountListViewAdapter.notifyDataSetChanged(); + mAccountAdapter.notifyDataSetChanged(); } }); } }; + protected ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + if (selectedAccountForActionMode + .isOptionSet(Account.OPTION_DISABLED)) { + menu.findItem(R.id.mgmt_account_enable).setVisible(true); + menu.findItem(R.id.mgmt_account_disable).setVisible(false); + } else { + menu.findItem(R.id.mgmt_account_disable).setVisible(true); + menu.findItem(R.id.mgmt_account_enable).setVisible(false); + } + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + // TODO Auto-generated method stub + + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.manageaccounts_context, menu); + return true; + } + + @Override + public boolean onActionItemClicked(final ActionMode mode, MenuItem item) { + if (item.getItemId() == R.id.mgmt_account_edit) { + editAccount(selectedAccountForActionMode); + } else if (item.getItemId() == R.id.mgmt_account_disable) { + selectedAccountForActionMode.setOption(Account.OPTION_DISABLED, + true); + xmppConnectionService + .updateAccount(selectedAccountForActionMode); + mode.finish(); + } else if (item.getItemId() == R.id.mgmt_account_enable) { + selectedAccountForActionMode.setOption(Account.OPTION_DISABLED, + false); + xmppConnectionService + .updateAccount(selectedAccountForActionMode); + mode.finish(); + } else if (item.getItemId() == R.id.mgmt_account_publish_avatar) { + Intent intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class); + intent.putExtra("account", selectedAccountForActionMode.getJid()); + startActivity(intent); + } else if (item.getItemId() == R.id.mgmt_account_delete) { + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(getString(R.string.mgmt_account_are_you_sure)); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage(getString(R.string.mgmt_account_delete_confirm_text)); + builder.setPositiveButton(getString(R.string.delete), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + xmppConnectionService + .deleteAccount(selectedAccountForActionMode); + selectedAccountForActionMode = null; + mode.finish(); + } + }); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.create().show(); + } else if (item.getItemId() == R.id.mgmt_account_announce_pgp) { + if (activity.hasPgp()) { + mode.finish(); + announcePgp(selectedAccountForActionMode, null); + } else { + activity.showInstallPgpDialog(); + } + } else if (item.getItemId() == R.id.mgmt_otr_key) { + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle("OTR Fingerprint"); + String fingerprintTxt = selectedAccountForActionMode + .getOtrFingerprint(getApplicationContext()); + View view = (View) getLayoutInflater().inflate( + R.layout.otr_fingerprint, null); + if (fingerprintTxt != null) { + TextView fingerprint = (TextView) view + .findViewById(R.id.otr_fingerprint); + TextView noFingerprintView = (TextView) view + .findViewById(R.id.otr_no_fingerprint); + fingerprint.setText(fingerprintTxt); + fingerprint.setVisibility(View.VISIBLE); + noFingerprintView.setVisibility(View.GONE); + } + builder.setView(view); + builder.setPositiveButton(getString(R.string.done), null); + builder.create().show(); + } else if (item.getItemId() == R.id.mgmt_account_info) { + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(getString(R.string.account_info)); + if (selectedAccountForActionMode.getStatus() == Account.STATUS_ONLINE) { + XmppConnection xmpp = selectedAccountForActionMode + .getXmppConnection(); + long connectionAge = (SystemClock.elapsedRealtime() - xmpp.lastConnect) / 60000; + long sessionAge = (SystemClock.elapsedRealtime() - xmpp.lastSessionStarted) / 60000; + long connectionAgeHours = connectionAge / 60; + long sessionAgeHours = sessionAge / 60; + View view = (View) getLayoutInflater().inflate( + R.layout.server_info, null); + TextView connection = (TextView) view + .findViewById(R.id.connection); + TextView session = (TextView) view + .findViewById(R.id.session); + TextView pcks_sent = (TextView) view + .findViewById(R.id.pcks_sent); + TextView pcks_received = (TextView) view + .findViewById(R.id.pcks_received); + TextView carbon = (TextView) view.findViewById(R.id.carbon); + TextView stream = (TextView) view.findViewById(R.id.stream); + TextView roster = (TextView) view.findViewById(R.id.roster); + TextView presences = (TextView) view + .findViewById(R.id.number_presences); + presences.setText(selectedAccountForActionMode + .countPresences() + ""); + pcks_received.setText("" + xmpp.getReceivedStanzas()); + pcks_sent.setText("" + xmpp.getSentStanzas()); + if (connectionAgeHours >= 2) { + connection.setText(connectionAgeHours + " " + + getString(R.string.hours)); + } else { + connection.setText(connectionAge + " " + + getString(R.string.mins)); + } + if (xmpp.getFeatures().sm()) { + if (sessionAgeHours >= 2) { + session.setText(sessionAgeHours + " " + + getString(R.string.hours)); + } else { + session.setText(sessionAge + " " + + getString(R.string.mins)); + } + stream.setText(getString(R.string.yes)); + } else { + stream.setText(getString(R.string.no)); + session.setText(connection.getText()); + } + if (xmpp.getFeatures().carbons()) { + carbon.setText(getString(R.string.yes)); + } else { + carbon.setText(getString(R.string.no)); + } + if (xmpp.getFeatures().rosterVersioning()) { + roster.setText(getString(R.string.yes)); + } else { + roster.setText(getString(R.string.no)); + } + builder.setView(view); + } else { + builder.setMessage(getString(R.string.mgmt_account_account_offline)); + } + builder.setPositiveButton(getString(R.string.hide), null); + builder.create().show(); + } + return true; + } + + }; + @Override protected void onCreate(Bundle savedInstanceState) { @@ -66,107 +225,16 @@ public class ManageAccountActivity extends XmppActivity { setContentView(R.layout.manage_accounts); accountListView = (ListView) findViewById(R.id.account_list); - accountListViewAdapter = new ArrayAdapter<Account>( - getApplicationContext(), R.layout.account_row, this.accountList) { - @Override - public View getView(int position, View view, ViewGroup parent) { - Account account = getItem(position); - if (view == null) { - LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); - view = (View) inflater.inflate(R.layout.account_row, null); - } - ((TextView) view.findViewById(R.id.account_jid)) - .setText(account.getJid()); - TextView statusView = (TextView) view - .findViewById(R.id.account_status); - switch (account.getStatus()) { - case Account.STATUS_DISABLED: - statusView - .setText(getString(R.string.account_status_disabled)); - statusView.setTextColor(0xFF1da9da); - break; - case Account.STATUS_ONLINE: - statusView - .setText(getString(R.string.account_status_online)); - statusView.setTextColor(0xFF83b600); - break; - case Account.STATUS_CONNECTING: - statusView - .setText(getString(R.string.account_status_connecting)); - statusView.setTextColor(0xFF1da9da); - break; - case Account.STATUS_OFFLINE: - statusView - .setText(getString(R.string.account_status_offline)); - statusView.setTextColor(0xFFe92727); - break; - case Account.STATUS_UNAUTHORIZED: - statusView - .setText(getString(R.string.account_status_unauthorized)); - statusView.setTextColor(0xFFe92727); - break; - case Account.STATUS_SERVER_NOT_FOUND: - statusView - .setText(getString(R.string.account_status_not_found)); - statusView.setTextColor(0xFFe92727); - break; - case Account.STATUS_NO_INTERNET: - statusView - .setText(getString(R.string.account_status_no_internet)); - statusView.setTextColor(0xFFe92727); - break; - case Account.STATUS_SERVER_REQUIRES_TLS: - statusView - .setText(getString(R.string.account_status_requires_tls)); - statusView.setTextColor(0xFFe92727); - break; - case Account.STATUS_REGISTRATION_FAILED: - statusView - .setText(getString(R.string.account_status_regis_fail)); - statusView.setTextColor(0xFFe92727); - break; - case Account.STATUS_REGISTRATION_CONFLICT: - statusView - .setText(getString(R.string.account_status_regis_conflict)); - statusView.setTextColor(0xFFe92727); - break; - case Account.STATUS_REGISTRATION_SUCCESSFULL: - statusView - .setText(getString(R.string.account_status_regis_success)); - statusView.setTextColor(0xFF83b600); - break; - case Account.STATUS_REGISTRATION_NOT_SUPPORTED: - statusView - .setText(getString(R.string.account_status_regis_not_sup)); - statusView.setTextColor(0xFFe92727); - break; - default: - statusView.setText(""); - break; - } - - return view; - } - }; final XmppActivity activity = this; - accountListView.setAdapter(this.accountListViewAdapter); + this.mAccountAdapter = new AccountAdapter(this, accountList); + accountListView.setAdapter(this.mAccountAdapter); accountListView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> arg0, View view, int position, long arg3) { if (!isActionMode) { - Account account = accountList.get(position); - if (account.getStatus() == Account.STATUS_OFFLINE) { - activity.xmppConnectionService.reconnectAccount( - accountList.get(position), true); - } else if (account.getStatus() == Account.STATUS_ONLINE) { - activity.startActivity(new Intent(activity - .getApplicationContext(), - StartConversationActivity.class)); - } else if (account.getStatus() != Account.STATUS_DISABLED) { - editAccount(account); - } + editAccount(accountList.get(position)); } else { selectedAccountForActionMode = accountList.get(position); actionMode.invalidate(); @@ -186,221 +254,7 @@ public class ManageAccountActivity extends XmppActivity { selectedAccountForActionMode = accountList .get(position); actionMode = activity - .startActionMode((new ActionMode.Callback() { - - @Override - public boolean onPrepareActionMode( - ActionMode mode, Menu menu) { - if (selectedAccountForActionMode - .isOptionSet(Account.OPTION_DISABLED)) { - menu.findItem( - R.id.mgmt_account_enable) - .setVisible(true); - menu.findItem( - R.id.mgmt_account_disable) - .setVisible(false); - } else { - menu.findItem( - R.id.mgmt_account_disable) - .setVisible(true); - menu.findItem( - R.id.mgmt_account_enable) - .setVisible(false); - } - return true; - } - - @Override - public void onDestroyActionMode( - ActionMode mode) { - // TODO Auto-generated method stub - - } - - @Override - public boolean onCreateActionMode( - ActionMode mode, Menu menu) { - MenuInflater inflater = mode - .getMenuInflater(); - inflater.inflate( - R.menu.manageaccounts_context, - menu); - return true; - } - - @Override - public boolean onActionItemClicked( - final ActionMode mode, - MenuItem item) { - if (item.getItemId() == R.id.mgmt_account_edit) { - editAccount(selectedAccountForActionMode); - } else if (item.getItemId() == R.id.mgmt_account_disable) { - selectedAccountForActionMode - .setOption( - Account.OPTION_DISABLED, - true); - xmppConnectionService - .updateAccount(selectedAccountForActionMode); - mode.finish(); - } else if (item.getItemId() == R.id.mgmt_account_enable) { - selectedAccountForActionMode - .setOption( - Account.OPTION_DISABLED, - false); - xmppConnectionService - .updateAccount(selectedAccountForActionMode); - mode.finish(); - } else if (item.getItemId() == R.id.mgmt_account_delete) { - AlertDialog.Builder builder = new AlertDialog.Builder( - activity); - builder.setTitle(getString(R.string.mgmt_account_are_you_sure)); - builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setMessage(getString(R.string.mgmt_account_delete_confirm_text)); - builder.setPositiveButton( - getString(R.string.delete), - new OnClickListener() { - - @Override - public void onClick( - DialogInterface dialog, - int which) { - xmppConnectionService - .deleteAccount(selectedAccountForActionMode); - selectedAccountForActionMode = null; - mode.finish(); - } - }); - builder.setNegativeButton( - getString(R.string.cancel), - null); - builder.create().show(); - } else if (item.getItemId() == R.id.mgmt_account_announce_pgp) { - if (activity.hasPgp()) { - mode.finish(); - announcePgp( - selectedAccountForActionMode, - null); - } else { - activity.showInstallPgpDialog(); - } - } else if (item.getItemId() == R.id.mgmt_otr_key) { - AlertDialog.Builder builder = new AlertDialog.Builder( - activity); - builder.setTitle("OTR Fingerprint"); - String fingerprintTxt = selectedAccountForActionMode - .getOtrFingerprint(getApplicationContext()); - View view = (View) getLayoutInflater() - .inflate( - R.layout.otr_fingerprint, - null); - if (fingerprintTxt != null) { - TextView fingerprint = (TextView) view - .findViewById(R.id.otr_fingerprint); - TextView noFingerprintView = (TextView) view - .findViewById(R.id.otr_no_fingerprint); - fingerprint - .setText(fingerprintTxt); - fingerprint - .setVisibility(View.VISIBLE); - noFingerprintView - .setVisibility(View.GONE); - } - builder.setView(view); - builder.setPositiveButton( - getString(R.string.done), - null); - builder.create().show(); - } else if (item.getItemId() == R.id.mgmt_account_info) { - AlertDialog.Builder builder = new AlertDialog.Builder( - activity); - builder.setTitle(getString(R.string.account_info)); - if (selectedAccountForActionMode - .getStatus() == Account.STATUS_ONLINE) { - XmppConnection xmpp = selectedAccountForActionMode - .getXmppConnection(); - long connectionAge = (SystemClock - .elapsedRealtime() - xmpp.lastConnect) / 60000; - long sessionAge = (SystemClock - .elapsedRealtime() - xmpp.lastSessionStarted) / 60000; - long connectionAgeHours = connectionAge / 60; - long sessionAgeHours = sessionAge / 60; - View view = (View) getLayoutInflater() - .inflate( - R.layout.server_info, - null); - TextView connection = (TextView) view - .findViewById(R.id.connection); - TextView session = (TextView) view - .findViewById(R.id.session); - TextView pcks_sent = (TextView) view - .findViewById(R.id.pcks_sent); - TextView pcks_received = (TextView) view - .findViewById(R.id.pcks_received); - TextView carbon = (TextView) view - .findViewById(R.id.carbon); - TextView stream = (TextView) view - .findViewById(R.id.stream); - TextView roster = (TextView) view - .findViewById(R.id.roster); - TextView presences = (TextView) view - .findViewById(R.id.number_presences); - presences.setText(selectedAccountForActionMode - .countPresences() - + ""); - pcks_received.setText("" - + xmpp.getReceivedStanzas()); - pcks_sent.setText("" - + xmpp.getSentStanzas()); - if (connectionAgeHours >= 2) { - connection - .setText(connectionAgeHours - + " " - + getString(R.string.hours)); - } else { - connection - .setText(connectionAge - + " " - + getString(R.string.mins)); - } - if (xmpp.hasFeatureStreamManagment()) { - if (sessionAgeHours >= 2) { - session.setText(sessionAgeHours - + " " - + getString(R.string.hours)); - } else { - session.setText(sessionAge - + " " - + getString(R.string.mins)); - } - stream.setText(getString(R.string.yes)); - } else { - stream.setText(getString(R.string.no)); - session.setText(connection - .getText()); - } - if (xmpp.hasFeaturesCarbon()) { - carbon.setText(getString(R.string.yes)); - } else { - carbon.setText(getString(R.string.no)); - } - if (xmpp.hasFeatureRosterManagment()) { - roster.setText(getString(R.string.yes)); - } else { - roster.setText(getString(R.string.no)); - } - builder.setView(view); - } else { - builder.setMessage(getString(R.string.mgmt_account_account_offline)); - } - builder.setPositiveButton( - getString(R.string.hide), - null); - builder.create().show(); - } - return true; - } - - })); + .startActionMode(mActionModeCallback); return true; } else { return false; @@ -422,11 +276,10 @@ public class ManageAccountActivity extends XmppActivity { xmppConnectionService.setOnAccountListChangedListener(accountChanged); this.accountList.clear(); this.accountList.addAll(xmppConnectionService.getAccounts()); - accountListViewAdapter.notifyDataSetChanged(); + mAccountAdapter.notifyDataSetChanged(); if ((this.accountList.size() == 0) && (this.firstrun)) { getActionBar().setDisplayHomeAsUpEnabled(false); getActionBar().setHomeButtonEnabled(false); - addAccount(); this.firstrun = false; } } @@ -441,7 +294,7 @@ public class ManageAccountActivity extends XmppActivity { public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_add_account: - addAccount(); + startActivity(new Intent(getApplicationContext(), EditAccountActivity.class)); break; default: break; @@ -452,7 +305,8 @@ public class ManageAccountActivity extends XmppActivity { @Override public boolean onNavigateUp() { if (xmppConnectionService.getConversations().size() == 0) { - Intent contactsIntent = new Intent(this, StartConversationActivity.class); + Intent contactsIntent = new Intent(this, + StartConversationActivity.class); contactsIntent.setFlags( // if activity exists in stack, pop the stack and go back to it Intent.FLAG_ACTIVITY_CLEAR_TOP | @@ -470,37 +324,9 @@ public class ManageAccountActivity extends XmppActivity { } private void editAccount(Account account) { - EditAccountDialog dialog = new EditAccountDialog(); - dialog.setAccount(account); - dialog.setEditAccountListener(new EditAccountListener() { - - @Override - public void onAccountEdited(Account account) { - xmppConnectionService.updateAccount(account); - if (actionMode != null) { - actionMode.finish(); - } - } - }); - dialog.show(getFragmentManager(), "edit_account"); - dialog.setKnownHosts(xmppConnectionService.getKnownHosts(), this); - - } - - protected void addAccount() { - final Activity activity = this; - EditAccountDialog dialog = new EditAccountDialog(); - dialog.setEditAccountListener(new EditAccountListener() { - - @Override - public void onAccountEdited(Account account) { - xmppConnectionService.createAccount(account); - activity.getActionBar().setDisplayHomeAsUpEnabled(true); - activity.getActionBar().setHomeButtonEnabled(true); - } - }); - dialog.show(getFragmentManager(), "add_account"); - dialog.setKnownHosts(xmppConnectionService.getKnownHosts(), this); + Intent intent = new Intent(this, EditAccountActivity.class); + intent.putExtra("jid", account.getJid()); + startActivity(intent); } @Override diff --git a/src/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/eu/siacs/conversations/ui/PublishProfilePictureActivity.java new file mode 100644 index 00000000..e874eeb7 --- /dev/null +++ b/src/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -0,0 +1,215 @@ +package eu.siacs.conversations.ui; + +import android.app.PendingIntent; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.PhoneHelper; +import eu.siacs.conversations.xmpp.pep.Avatar; + +public class PublishProfilePictureActivity extends XmppActivity { + + private static final int REQUEST_CHOOSE_FILE = 0xac23; + + private ImageView avatar; + private TextView accountTextView; + private TextView hintOrWarning; + private TextView secondaryHint; + private Button cancelButton; + private Button publishButton; + + private Uri avatarUri; + private Uri defaultUri; + + private Account account; + + private boolean support = false; + + private UiCallback<Avatar> avatarPublication = new UiCallback<Avatar>() { + + @Override + public void success(Avatar object) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + finish(); + } + }); + } + + @Override + public void error(final int errorCode, Avatar object) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + hintOrWarning.setText(errorCode); + hintOrWarning.setTextColor(getWarningTextColor()); + publishButton.setText(R.string.publish_avatar); + enablePublishButton(); + } + }); + + } + + @Override + public void userInputRequried(PendingIntent pi, Avatar object) { + } + }; + + private OnLongClickListener backToDefaultListener = new OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + avatarUri = defaultUri; + loadImageIntoPreview(defaultUri); + return true; + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_publish_profile_picture); + this.avatar = (ImageView) findViewById(R.id.account_image); + this.cancelButton = (Button) findViewById(R.id.cancel_button); + this.publishButton = (Button) findViewById(R.id.publish_button); + this.accountTextView = (TextView) findViewById(R.id.account); + this.hintOrWarning = (TextView) findViewById(R.id.hint_or_warning); + this.secondaryHint = (TextView) findViewById(R.id.secondary_hint); + this.publishButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (avatarUri != null) { + publishButton.setText(R.string.publishing); + disablePublishButton(); + xmppConnectionService.publishAvatar(account, avatarUri, + avatarPublication); + } + } + }); + this.cancelButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + finish(); + } + }); + this.avatar.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + Intent attachFileIntent = new Intent(); + attachFileIntent.setType("image/*"); + attachFileIntent.setAction(Intent.ACTION_GET_CONTENT); + Intent chooser = Intent.createChooser(attachFileIntent, + getString(R.string.attach_file)); + startActivityForResult(chooser, REQUEST_CHOOSE_FILE); + } + }); + this.defaultUri = PhoneHelper.getSefliUri(getApplicationContext()); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, + final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + if (requestCode == REQUEST_CHOOSE_FILE) { + this.avatarUri = data.getData(); + if (xmppConnectionServiceBound) { + loadImageIntoPreview(this.avatarUri); + } + } + } + } + + @Override + protected void onBackendConnected() { + if (getIntent() != null) { + String jid = getIntent().getStringExtra("account"); + if (jid != null) { + this.account = xmppConnectionService.findAccountByJid(jid); + if (this.account.getXmppConnection() != null) { + this.support = this.account.getXmppConnection().getFeatures().pubsub(); + } + if (this.avatarUri == null) { + if (this.account.getAvatar() != null || this.defaultUri == null) { + this.avatar.setImageBitmap(this.account.getImage( + getApplicationContext(), 384)); + if (this.defaultUri != null) { + this.avatar + .setOnLongClickListener(this.backToDefaultListener); + } else { + this.secondaryHint.setVisibility(View.INVISIBLE); + } + if (!support) { + this.hintOrWarning.setTextColor(getWarningTextColor()); + this.hintOrWarning.setText(R.string.error_publish_avatar_no_server_support); + } + } else { + this.avatarUri = this.defaultUri; + loadImageIntoPreview(this.defaultUri); + this.secondaryHint.setVisibility(View.INVISIBLE); + } + } else { + loadImageIntoPreview(avatarUri); + } + this.accountTextView.setText(this.account.getJid()); + } + } + + } + + protected void loadImageIntoPreview(Uri uri) { + Bitmap bm = xmppConnectionService.getFileBackend().cropCenterSquare( + uri, 384); + if (bm==null) { + disablePublishButton(); + this.hintOrWarning.setTextColor(getWarningTextColor()); + this.hintOrWarning.setText(R.string.error_publish_avatar_converting); + return; + } + this.avatar.setImageBitmap(bm); + if (support) { + enablePublishButton(); + this.publishButton.setText(R.string.publish_avatar); + this.hintOrWarning.setText(R.string.publish_avatar_explanation); + this.hintOrWarning.setTextColor(getPrimaryTextColor()); + } else { + disablePublishButton(); + this.hintOrWarning.setTextColor(getWarningTextColor()); + this.hintOrWarning.setText(R.string.error_publish_avatar_no_server_support); + } + if (this.defaultUri != null && uri.equals(this.defaultUri)) { + this.secondaryHint.setVisibility(View.INVISIBLE); + this.avatar.setOnLongClickListener(null); + } else if (this.defaultUri != null ) { + this.secondaryHint.setVisibility(View.VISIBLE); + this.avatar.setOnLongClickListener(this.backToDefaultListener); + } + } + + protected void enablePublishButton() { + this.publishButton.setEnabled(true); + this.publishButton.setTextColor(getPrimaryTextColor()); + } + + protected void disablePublishButton() { + this.publishButton.setEnabled(false); + this.publishButton.setTextColor(getSecondaryTextColor()); + } + +} diff --git a/src/eu/siacs/conversations/ui/StartConversationActivity.java b/src/eu/siacs/conversations/ui/StartConversationActivity.java index d12d2878..9dad5c16 100644 --- a/src/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/eu/siacs/conversations/ui/StartConversationActivity.java @@ -21,6 +21,7 @@ import android.text.Editable; import android.text.TextWatcher; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; +import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -63,6 +64,7 @@ public class StartConversationActivity extends XmppActivity { private List<String> mKnownHosts; private List<String> mKnownConferenceHosts; + private Menu mOptionsMenu; private EditText mSearchEditText; public int conference_context_id; @@ -148,7 +150,9 @@ public class StartConversationActivity extends XmppActivity { @Override public void run() { - filter(mSearchEditText.getText().toString()); + if (mSearchEditText != null) { + filter(mSearchEditText.getText().toString()); + } } }); } @@ -430,6 +434,7 @@ public class StartConversationActivity extends XmppActivity { @Override public boolean onCreateOptionsMenu(Menu menu) { + this.mOptionsMenu = menu; getMenuInflater().inflate(R.menu.start_conversation, menu); MenuItem menuCreateContact = (MenuItem) menu .findItem(R.id.action_create_contact); @@ -461,6 +466,17 @@ public class StartConversationActivity extends XmppActivity { } return super.onOptionsItemSelected(item); } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) + { + if(keyCode == KeyEvent.KEYCODE_SEARCH && !event.isLongPress()) + { + mOptionsMenu.findItem(R.id.action_search).expandActionView(); + return true; + } + return super.onKeyUp(keyCode, event); + } @Override void onBackendConnected() { @@ -482,8 +498,10 @@ public class StartConversationActivity extends XmppActivity { } protected void filter(String needle) { - this.filterContacts(needle); - this.filterConferences(needle); + if (xmppConnectionServiceBound) { + this.filterContacts(needle); + this.filterConferences(needle); + } } protected void filterContacts(String needle) { diff --git a/src/eu/siacs/conversations/ui/XmppActivity.java b/src/eu/siacs/conversations/ui/XmppActivity.java index fad4d026..44043a79 100644 --- a/src/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/eu/siacs/conversations/ui/XmppActivity.java @@ -31,22 +31,24 @@ import android.widget.EditText; public abstract class XmppActivity extends Activity { - public static final int REQUEST_ANNOUNCE_PGP = 0x73731; - protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x341830; + protected static final int REQUEST_ANNOUNCE_PGP = 0x0101; + protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102; protected final static String LOGTAG = "xmppService"; public XmppConnectionService xmppConnectionService; public boolean xmppConnectionServiceBound = false; protected boolean handledViewIntent = false; - + protected int mPrimaryTextColor; protected int mSecondaryTextColor; - + protected int mWarningTextColor; + protected int mPrimaryColor; + protected interface OnValueEdited { public void onValueEdited(String value); } - + public interface OnPresenceSelected { public void onPresenceSelected(); } @@ -152,6 +154,9 @@ public abstract class XmppActivity extends Activity { case R.id.action_accounts: startActivity(new Intent(this, ManageAccountActivity.class)); break; + case android.R.id.home: + finish(); + break; } return super.onOptionsItemSelected(item); } @@ -162,6 +167,8 @@ public abstract class XmppActivity extends Activity { ExceptionHelper.init(getApplicationContext()); mPrimaryTextColor = getResources().getColor(R.color.primarytext); mSecondaryTextColor = getResources().getColor(R.color.secondarytext); + mWarningTextColor = getResources().getColor(R.color.warningtext); + mPrimaryColor = getResources().getColor(R.color.primary); } public void switchToConversation(Conversation conversation) { @@ -199,11 +206,12 @@ public abstract class XmppActivity extends Activity { } protected void inviteToConversation(Conversation conversation) { - Intent intent = new Intent(getApplicationContext(), ChooseContactActivity.class); - intent.putExtra("conversation",conversation.getUuid()); + Intent intent = new Intent(getApplicationContext(), + ChooseContactActivity.class); + intent.putExtra("conversation", conversation.getUuid()); startActivityForResult(intent, REQUEST_INVITE_TO_CONVERSATION); } - + protected void announcePgp(Account account, final Conversation conversation) { xmppConnectionService.getPgpEngine().generateSignature(account, "online", new UiCallback<Account>() { @@ -214,7 +222,8 @@ public abstract class XmppActivity extends Activity { try { startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); - } catch (SendIntentException e) {} + } catch (SendIntentException e) { + } } @Override @@ -275,15 +284,17 @@ public abstract class XmppActivity extends Activity { builder.create().show(); } - protected void quickEdit(final String previousValue, final OnValueEdited callback) { + protected void quickEdit(final String previousValue, + final OnValueEdited callback) { AlertDialog.Builder builder = new AlertDialog.Builder(this); - View view = (View) getLayoutInflater().inflate(R.layout.quickedit, null); + View view = (View) getLayoutInflater() + .inflate(R.layout.quickedit, null); final EditText editor = (EditText) view.findViewById(R.id.editor); editor.setText(previousValue); builder.setView(view); builder.setNegativeButton(R.string.cancel, null); builder.setPositiveButton(R.string.edit, new OnClickListener() { - + @Override public void onClick(DialogInterface dialog, int which) { String value = editor.getText().toString(); @@ -294,11 +305,11 @@ public abstract class XmppActivity extends Activity { }); builder.create().show(); } - + public void selectPresence(final Conversation conversation, final OnPresenceSelected listener) { Contact contact = conversation.getContact(); - if (contact == null) { + if (!contact.showInRoster()) { showAddToRosterDialog(conversation); } else { Presences presences = contact.getPresences(); @@ -346,26 +357,37 @@ public abstract class XmppActivity extends Activity { } } } - + protected void onActivityResult(int requestCode, int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) { + if (requestCode == REQUEST_INVITE_TO_CONVERSATION + && resultCode == RESULT_OK) { String contactJid = data.getStringExtra("contact"); String conversationUuid = data.getStringExtra("conversation"); - Conversation conversation = xmppConnectionService.findConversationByUuid(conversationUuid); + Conversation conversation = xmppConnectionService + .findConversationByUuid(conversationUuid); if (conversation.getMode() == Conversation.MODE_MULTI) { xmppConnectionService.invite(conversation, contactJid); } - Log.d("xmppService","inviting "+contactJid+" to "+conversation.getName(true)); + Log.d("xmppService", "inviting " + contactJid + " to " + + conversation.getName(true)); } } - + public int getSecondaryTextColor() { return this.mSecondaryTextColor; } - + public int getPrimaryTextColor() { return this.mPrimaryTextColor; } + + public int getWarningTextColor() { + return this.mWarningTextColor; + } + + public int getPrimaryColor() { + return this.mPrimaryColor; + } } diff --git a/src/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/eu/siacs/conversations/ui/adapter/AccountAdapter.java new file mode 100644 index 00000000..cd8b376f --- /dev/null +++ b/src/eu/siacs/conversations/ui/adapter/AccountAdapter.java @@ -0,0 +1,101 @@ +package eu.siacs.conversations.ui.adapter; + +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.ui.XmppActivity; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +public class AccountAdapter extends ArrayAdapter<Account> { + + private XmppActivity activity; + + public AccountAdapter(XmppActivity activity, List<Account> objects) { + super(activity, 0, objects); + this.activity = activity; + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + Account account = getItem(position); + if (view == null) { + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + view = (View) inflater.inflate(R.layout.account_row, parent, false); + } + TextView jid = (TextView) view.findViewById(R.id.account_jid); + jid.setText(account.getJid()); + TextView statusView = (TextView) view.findViewById(R.id.account_status); + ImageView imageView = (ImageView) view.findViewById(R.id.account_image); + imageView.setImageBitmap(account.getImage(activity,48)); + switch (account.getStatus()) { + case Account.STATUS_DISABLED: + statusView.setText(getContext().getString( + R.string.account_status_disabled)); + statusView.setTextColor(activity.getSecondaryTextColor()); + break; + case Account.STATUS_ONLINE: + statusView.setText(getContext().getString( + R.string.account_status_online)); + statusView.setTextColor(activity.getPrimaryColor()); + break; + case Account.STATUS_CONNECTING: + statusView.setText(getContext().getString( + R.string.account_status_connecting)); + statusView.setTextColor(activity.getSecondaryTextColor()); + break; + case Account.STATUS_OFFLINE: + statusView.setText(getContext().getString( + R.string.account_status_offline)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_UNAUTHORIZED: + statusView.setText(getContext().getString( + R.string.account_status_unauthorized)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_SERVER_NOT_FOUND: + statusView.setText(getContext().getString( + R.string.account_status_not_found)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_NO_INTERNET: + statusView.setText(getContext().getString( + R.string.account_status_no_internet)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_REGISTRATION_FAILED: + statusView.setText(getContext().getString( + R.string.account_status_regis_fail)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_REGISTRATION_CONFLICT: + statusView.setText(getContext().getString( + R.string.account_status_regis_conflict)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_REGISTRATION_SUCCESSFULL: + statusView.setText(getContext().getString( + R.string.account_status_regis_success)); + statusView.setTextColor(activity.getSecondaryTextColor()); + break; + case Account.STATUS_REGISTRATION_NOT_SUPPORTED: + statusView.setText(getContext().getString( + R.string.account_status_regis_not_sup)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + default: + statusView.setText(""); + break; + } + + return view; + } +} diff --git a/src/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/eu/siacs/conversations/ui/adapter/ConversationAdapter.java new file mode 100644 index 00000000..66403804 --- /dev/null +++ b/src/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -0,0 +1,107 @@ +package eu.siacs.conversations.ui.adapter; + +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.ui.ConversationActivity; +import eu.siacs.conversations.utils.UIHelper; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +public class ConversationAdapter extends ArrayAdapter<Conversation> { + + ConversationActivity activity; + + public ConversationAdapter(ConversationActivity activity, + List<Conversation> conversations) { + super(activity, 0, conversations); + this.activity = activity; + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + if (view == null) { + LayoutInflater inflater = (LayoutInflater) activity + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + view = (View) inflater.inflate(R.layout.conversation_list_row, + parent, false); + } + Conversation conv = getItem(position); + if (!activity.getSlidingPaneLayout().isSlideable()) { + if (conv == activity.getSelectedConversation()) { + view.setBackgroundColor(0xffdddddd); + } else { + view.setBackgroundColor(Color.TRANSPARENT); + } + } else { + view.setBackgroundColor(Color.TRANSPARENT); + } + TextView convName = (TextView) view + .findViewById(R.id.conversation_name); + convName.setText(conv.getName(true)); + TextView convLastMsg = (TextView) view + .findViewById(R.id.conversation_lastmsg); + ImageView imagePreview = (ImageView) view + .findViewById(R.id.conversation_lastimage); + + Message latestMessage = conv.getLatestMessage(); + + if (latestMessage.getType() == Message.TYPE_TEXT + || latestMessage.getType() == Message.TYPE_PRIVATE) { + if ((latestMessage.getEncryption() != Message.ENCRYPTION_PGP) + && (latestMessage.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)) { + convLastMsg.setText(conv.getLatestMessage().getBody()); + } else { + convLastMsg.setText(activity + .getText(R.string.encrypted_message_received)); + } + convLastMsg.setVisibility(View.VISIBLE); + imagePreview.setVisibility(View.GONE); + } else if (latestMessage.getType() == Message.TYPE_IMAGE) { + if (latestMessage.getStatus() >= Message.STATUS_RECIEVED) { + convLastMsg.setVisibility(View.GONE); + imagePreview.setVisibility(View.VISIBLE); + activity.loadBitmap(latestMessage, imagePreview); + } else { + convLastMsg.setVisibility(View.VISIBLE); + imagePreview.setVisibility(View.GONE); + if (latestMessage.getStatus() == Message.STATUS_RECEIVED_OFFER) { + convLastMsg.setText(activity + .getText(R.string.image_offered_for_download)); + } else if (latestMessage.getStatus() == Message.STATUS_RECIEVING) { + convLastMsg.setText(activity + .getText(R.string.receiving_image)); + } else { + convLastMsg.setText(""); + } + } + } + + if (!conv.isRead()) { + convName.setTypeface(null, Typeface.BOLD); + convLastMsg.setTypeface(null, Typeface.BOLD); + } else { + convName.setTypeface(null, Typeface.NORMAL); + convLastMsg.setTypeface(null, Typeface.NORMAL); + } + + ((TextView) view.findViewById(R.id.conversation_lastupdate)) + .setText(UIHelper.readableTimeDifference(getContext(), conv + .getLatestMessage().getTimeSent())); + + ImageView profilePicture = (ImageView) view + .findViewById(R.id.conversation_image); + profilePicture.setImageBitmap(conv.getImage(activity, 56)); + + return view; + } +} diff --git a/src/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 0a2857d2..34fea41f 100644 --- a/src/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -7,16 +7,14 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.services.ImageProvider; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.jingle.JingleConnection; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.Typeface; -import android.preference.PreferenceManager; +import android.text.Html; import android.util.DisplayMetrics; import android.view.View; import android.view.ViewGroup; @@ -37,44 +35,38 @@ public class MessageAdapter extends ArrayAdapter<Message> { private ConversationActivity activity; - private Bitmap selfBitmap2; + private Bitmap accountBitmap; private BitmapCache mBitmapCache = new BitmapCache(); private DisplayMetrics metrics; - private boolean useSubject = true; - private OnContactPictureClicked mOnContactPictureClickedListener; + private OnContactPictureLongClicked mOnContactPictureLongClickedListener; public MessageAdapter(ConversationActivity activity, List<Message> messages) { super(activity, 0, messages); this.activity = activity; metrics = getContext().getResources().getDisplayMetrics(); - SharedPreferences preferences = PreferenceManager - .getDefaultSharedPreferences(getContext()); - useSubject = preferences.getBoolean("use_subject_in_muc", true); } private Bitmap getSelfBitmap() { - if (this.selfBitmap2 == null) { + if (this.accountBitmap == null) { if (getCount() > 0) { - SharedPreferences preferences = PreferenceManager - .getDefaultSharedPreferences(getContext()); - boolean showPhoneSelfContactPicture = preferences.getBoolean( - "show_phone_selfcontact_picture", true); - - this.selfBitmap2 = UIHelper.getSelfContactPicture(getItem(0) - .getConversation().getAccount(), 48, - showPhoneSelfContactPicture, getContext()); + this.accountBitmap = getItem(0).getConversation().getAccount() + .getImage(getContext(), 48); } } - return this.selfBitmap2; + return this.accountBitmap; } public void setOnContactPictureClicked(OnContactPictureClicked listener) { this.mOnContactPictureClickedListener = listener; } + + public void setOnContactPictureLongClicked(OnContactPictureLongClicked listener) { + this.mOnContactPictureLongClickedListener = listener; + } @Override public int getViewTypeCount() { @@ -130,7 +122,16 @@ public class MessageAdapter extends ArrayAdapter<Message> { error = true; default: if (multiReceived) { - info = message.getCounterpart(); + Contact contact = message.getContact(); + if (contact != null) { + info = contact.getDisplayName(); + } else { + if (message.getPresence() != null) { + info = message.getPresence(); + } else { + info = message.getCounterpart(); + } + } } break; } @@ -199,14 +200,33 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.messageBody.setTextIsSelectable(false); } - private void displayTextMessage(ViewHolder viewHolder, String text) { + private void displayTextMessage(ViewHolder viewHolder, Message message) { if (viewHolder.download_button != null) { viewHolder.download_button.setVisibility(View.GONE); } viewHolder.image.setVisibility(View.GONE); viewHolder.messageBody.setVisibility(View.VISIBLE); - if (text != null) { - viewHolder.messageBody.setText(text.trim()); + if (message.getBody() != null) { + if (message.getType() != Message.TYPE_PRIVATE) { + viewHolder.messageBody.setText(message.getBody().trim()); + } else { + StringBuilder builder = new StringBuilder(); + builder.append(message.getBody().trim()); + builder.append(" <b><i>("); + if (message.getStatus() <= Message.STATUS_RECIEVED) { + builder.append(activity.getString(R.string.private_message)); + } else { + String to; + if (message.getPresence() != null) { + to = message.getPresence(); + } else { + to = message.getCounterpart(); + } + builder.append(activity.getString(R.string.private_message_to, to)); + } + builder.append(")</i></b>"); + viewHolder.messageBody.setText(Html.fromHtml(builder.toString())); + } } else { viewHolder.messageBody.setText(""); } @@ -245,8 +265,8 @@ public class MessageAdapter extends ArrayAdapter<Message> { @Override public void onClick(View v) { Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(ImageProvider.getContentUri(message), - "image/*"); + intent.setDataAndType(activity.xmppConnectionService + .getFileBackend().getJingleFileUri(message), "image/*"); getContext().startActivity(intent); } }); @@ -257,7 +277,9 @@ public class MessageAdapter extends ArrayAdapter<Message> { Intent shareIntent = new Intent(); shareIntent.setAction(Intent.ACTION_SEND); shareIntent.putExtra(Intent.EXTRA_STREAM, - ImageProvider.getContentUri(message)); + activity.xmppConnectionService.getFileBackend() + .getJingleFileUri(message)); + shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); shareIntent.setType("image/webp"); getContext().startActivity( Intent.createChooser(shareIntent, @@ -277,7 +299,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { switch (type) { case SENT: view = (View) activity.getLayoutInflater().inflate( - R.layout.message_sent, null); + R.layout.message_sent, parent, false); viewHolder.message_box = (LinearLayout) view .findViewById(R.id.message_box); viewHolder.contact_picture = (ImageView) view @@ -295,7 +317,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { break; case RECIEVED: view = (View) activity.getLayoutInflater().inflate( - R.layout.message_recieved, null); + R.layout.message_recieved, parent, false); viewHolder.message_box = (LinearLayout) view .findViewById(R.id.message_box); viewHolder.contact_picture = (ImageView) view @@ -307,9 +329,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { if (item.getConversation().getMode() == Conversation.MODE_SINGLE) { viewHolder.contact_picture.setImageBitmap(mBitmapCache.get( - item.getConversation().getName(useSubject), item - .getConversation().getContact(), - getContext())); + item.getConversation().getContact(), getContext())); } viewHolder.indicator = (ImageView) view @@ -324,15 +344,13 @@ public class MessageAdapter extends ArrayAdapter<Message> { break; case STATUS: view = (View) activity.getLayoutInflater().inflate( - R.layout.message_status, null); + R.layout.message_status, parent, false); viewHolder.contact_picture = (ImageView) view .findViewById(R.id.message_photo); if (item.getConversation().getMode() == Conversation.MODE_SINGLE) { viewHolder.contact_picture.setImageBitmap(mBitmapCache.get( - item.getConversation().getName(useSubject), item - .getConversation().getContact(), - getContext())); + item.getConversation().getContact(), getContext())); viewHolder.contact_picture.setAlpha(128); viewHolder.contact_picture .setOnClickListener(new OnClickListener() { @@ -366,8 +384,17 @@ public class MessageAdapter extends ArrayAdapter<Message> { if (type == RECIEVED) { if (item.getConversation().getMode() == Conversation.MODE_MULTI) { - viewHolder.contact_picture.setImageBitmap(mBitmapCache.get( - item.getCounterpart(), null, getContext())); + Contact contact = item.getContact(); + if (contact != null) { + viewHolder.contact_picture.setImageBitmap(mBitmapCache.get( + contact, getContext())); + } else { + String name = item.getPresence(); + if (name==null) { + name = item.getCounterpart(); + } + viewHolder.contact_picture.setImageBitmap(mBitmapCache.get(name, getContext())); + } viewHolder.contact_picture .setOnClickListener(new OnClickListener() { @@ -381,6 +408,18 @@ public class MessageAdapter extends ArrayAdapter<Message> { } }); + viewHolder.contact_picture.setOnLongClickListener(new OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) { + MessageAdapter.this.mOnContactPictureLongClickedListener.onContactPictureLongClicked(item); + return true; + } else { + return false; + } + } + }); } } @@ -431,7 +470,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { } else if (item.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { displayDecryptionFailed(viewHolder); } else { - displayTextMessage(viewHolder, item.getBody()); + displayTextMessage(viewHolder, item); } } @@ -453,20 +492,24 @@ public class MessageAdapter extends ArrayAdapter<Message> { } private class BitmapCache { - private HashMap<String, Bitmap> bitmaps = new HashMap<String, Bitmap>(); + private HashMap<String, Bitmap> contactBitmaps = new HashMap<String, Bitmap>(); + private HashMap<String, Bitmap> unknownBitmaps = new HashMap<String, Bitmap>(); + + public Bitmap get(Contact contact, Context context) { + if (!contactBitmaps.containsKey(contact.getJid())) { + contactBitmaps.put(contact.getJid(), + contact.getImage(48, context)); + } + return contactBitmaps.get(contact.getJid()); + } - public Bitmap get(String name, Contact contact, Context context) { - if (bitmaps.containsKey(name)) { - return bitmaps.get(name); + public Bitmap get(String name, Context context) { + if (unknownBitmaps.containsKey(name)) { + return unknownBitmaps.get(name); } else { - Bitmap bm; - if (contact != null) { - bm = UIHelper - .getContactPicture(contact, 48, context, false); - } else { - bm = UIHelper.getContactPicture(name, 48, context, false); - } - bitmaps.put(name, bm); + Bitmap bm = UIHelper + .getContactPicture(name, 48, context, false); + unknownBitmaps.put(name, bm); return bm; } } @@ -475,4 +518,8 @@ public class MessageAdapter extends ArrayAdapter<Message> { public interface OnContactPictureClicked { public void onContactPictureClicked(Message message); } + + public interface OnContactPictureLongClicked { + public void onContactPictureLongClicked(Message message); + } } diff --git a/src/eu/siacs/conversations/utils/PhoneHelper.java b/src/eu/siacs/conversations/utils/PhoneHelper.java index bccd7210..63c17761 100644 --- a/src/eu/siacs/conversations/utils/PhoneHelper.java +++ b/src/eu/siacs/conversations/utils/PhoneHelper.java @@ -70,11 +70,11 @@ public class PhoneHelper { public static Uri getSefliUri(Context context) { String[] mProjection = new String[] { Profile._ID, - Profile.PHOTO_THUMBNAIL_URI }; + Profile.PHOTO_URI }; Cursor mProfileCursor = context.getContentResolver().query( Profile.CONTENT_URI, mProjection, null, null, null); - if (mProfileCursor.getCount() == 0) { + if (mProfileCursor == null || mProfileCursor.getCount() == 0) { return null; } else { mProfileCursor.moveToFirst(); diff --git a/src/eu/siacs/conversations/utils/UIHelper.java b/src/eu/siacs/conversations/utils/UIHelper.java index 1cd3403c..335b471a 100644 --- a/src/eu/siacs/conversations/utils/UIHelper.java +++ b/src/eu/siacs/conversations/utils/UIHelper.java @@ -15,6 +15,7 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions.User; +import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.ManageAccountActivity; import android.app.Activity; @@ -214,13 +215,17 @@ public class UIHelper { new String[] { conversation.getName(false) }, size, bgColor, fgColor); } - String[] names = new String[members.size() + 1]; - names[0] = conversation.getMucOptions().getActualNick(); - for (int i = 0; i < members.size(); ++i) { - names[i + 1] = members.get(i).getName(); + ArrayList<String> names = new ArrayList<String>(); + names.add(conversation.getMucOptions().getActualNick()); + for(User user : members) { + names.add(user.getName()); + if (names.size() > 4 ) { + break; + } } - - return getUnknownContactPicture(names, size, bgColor, fgColor); + String[] mArrayNames = new String[names.size()]; + names.toArray(mArrayNames); + return getUnknownContactPicture(mArrayNames, size, bgColor, fgColor); } public static Bitmap getContactPicture(Conversation conversation, @@ -341,7 +346,7 @@ public class UIHelper { if ((currentCon != null) && (currentCon.getMode() == Conversation.MODE_MULTI) - && (!alwaysNotify)) { + && (!alwaysNotify) && notify) { String nick = currentCon.getMucOptions().getActualNick(); Pattern highlight = generateNickHighlightPattern(nick); Matcher m = highlight.matcher(currentCon.getLatestMessage() @@ -372,8 +377,7 @@ public class UIHelper { } else if (unread.size() == 1) { Conversation conversation = unread.get(0); targetUuid = conversation.getUuid(); - mBuilder.setLargeIcon(UIHelper.getContactPicture(conversation, 64, - context, true)); + mBuilder.setLargeIcon(conversation.getImage(context, 64)); mBuilder.setContentTitle(conversation.getName(useSubject)); if (notify) { mBuilder.setTicker(conversation.getLatestMessage() @@ -484,8 +488,7 @@ public class UIHelper { long id = Long.parseLong(systemAccount[0]); badge.assignContactUri(Contacts.getLookupUri(id, systemAccount[1])); } - badge.setImageBitmap(UIHelper.getContactPicture(contact, 72, context, - false)); + badge.setImageBitmap(contact.getImage(72, context)); } public static AlertDialog getVerifyFingerprintDialog( diff --git a/src/eu/siacs/conversations/utils/Validator.java b/src/eu/siacs/conversations/utils/Validator.java index 9ca1e34f..a1a119e7 100644 --- a/src/eu/siacs/conversations/utils/Validator.java +++ b/src/eu/siacs/conversations/utils/Validator.java @@ -5,7 +5,7 @@ import java.util.regex.Pattern; public class Validator { public static final Pattern VALID_JID = - Pattern.compile("\\b^[^@/<>'\"\\s]+@[^@/<>'\"\\s]+$", Pattern.CASE_INSENSITIVE); + Pattern.compile("^[^@/<>'\"\\s]+@[^@/<>'\"\\s]+$", Pattern.CASE_INSENSITIVE); public static boolean isValidJid(String jid) { Matcher matcher = VALID_JID.matcher(jid); diff --git a/src/eu/siacs/conversations/xml/Element.java b/src/eu/siacs/conversations/xml/Element.java index f8e070f7..3d22ba6a 100644 --- a/src/eu/siacs/conversations/xml/Element.java +++ b/src/eu/siacs/conversations/xml/Element.java @@ -144,4 +144,12 @@ public class Element { public void clearChildren() { this.children.clear(); } + + public void setAttribute(String name, long value) { + this.setAttribute(name, ""+value); + } + + public void setAttribute(String name, int value) { + this.setAttribute(name, ""+value); + } } diff --git a/src/eu/siacs/conversations/xml/TagWriter.java b/src/eu/siacs/conversations/xml/TagWriter.java index 23a260f2..4828d5d9 100644 --- a/src/eu/siacs/conversations/xml/TagWriter.java +++ b/src/eu/siacs/conversations/xml/TagWriter.java @@ -41,12 +41,18 @@ public class TagWriter { public TagWriter() { } - public void setOutputStream(OutputStream out) { + public void setOutputStream(OutputStream out) throws IOException { + if (out==null) { + throw new IOException(); + } this.plainOutputStream = out; this.outputStream = new OutputStreamWriter(out); } - public OutputStream getOutputStream() { + public OutputStream getOutputStream() throws IOException { + if (this.plainOutputStream==null) { + throw new IOException(); + } return this.plainOutputStream; } diff --git a/src/eu/siacs/conversations/xml/XmlReader.java b/src/eu/siacs/conversations/xml/XmlReader.java index 10b2fb06..1c7e94e6 100644 --- a/src/eu/siacs/conversations/xml/XmlReader.java +++ b/src/eu/siacs/conversations/xml/XmlReader.java @@ -28,24 +28,33 @@ public class XmlReader { this.wakeLock = wakeLock; } - public void setInputStream(InputStream inputStream) { + public void setInputStream(InputStream inputStream) throws IOException { + if (inputStream==null) { + throw new IOException(); + } this.is = inputStream; try { parser.setInput(new InputStreamReader(this.is)); } catch (XmlPullParserException e) { - Log.d(LOGTAG,"error setting input stream"); + throw new IOException("error resetting parser"); } } - public InputStream getInputStream() { + public InputStream getInputStream() throws IOException { + if (this.is==null) { + throw new IOException(); + } return is; } - public void reset() { + public void reset() throws IOException { + if (this.is==null) { + throw new IOException(); + } try { parser.setInput(new InputStreamReader(this.is)); } catch (XmlPullParserException e) { - Log.d(LOGTAG,"error resetting input stream"); + throw new IOException("error resetting parser"); } } @@ -54,7 +63,7 @@ public class XmlReader { try { wakeLock.release();} catch (RuntimeException re) {} } try { - while(parser.next() != XmlPullParser.END_DOCUMENT) { + while(this.is != null && parser.next() != XmlPullParser.END_DOCUMENT) { wakeLock.acquire(); if (parser.getEventType() == XmlPullParser.START_TAG) { Tag tag = Tag.start(parser.getName()); @@ -81,8 +90,6 @@ public class XmlReader { throw new IOException("xml parser mishandled ArrayIndexOufOfBounds", e); } catch (StringIndexOutOfBoundsException e) { throw new IOException("xml parser mishandled StringIndexOufOfBounds", e); - } catch (NullPointerException e) { - throw new IOException("null pointer in xml parser"); } return null; } diff --git a/src/eu/siacs/conversations/xmpp/XmppConnection.java b/src/eu/siacs/conversations/xmpp/XmppConnection.java index 45bac2f6..8e93a91d 100644 --- a/src/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/eu/siacs/conversations/xmpp/XmppConnection.java @@ -7,14 +7,8 @@ import java.math.BigInteger; import java.net.Socket; import java.net.UnknownHostException; import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -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.Hashtable; @@ -25,16 +19,13 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; + import javax.net.ssl.X509TrustManager; -import org.bouncycastle.pqc.math.linearalgebra.GoppaCode.MaMaPe; import org.xmlpull.v1.XmlPullParserException; import de.duenndns.ssl.MemorizingTrustManager; -import android.content.Context; import android.os.Bundle; import android.os.PowerManager; import android.os.PowerManager.WakeLock; @@ -73,6 +64,8 @@ public class XmppConnection implements Runnable { private Socket socket; private XmlReader tagReader; private TagWriter tagWriter; + + private Features features = new Features(this); private boolean shouldBind = true; private boolean shouldAuthenticate = true; @@ -228,11 +221,6 @@ public class XmppConnection implements Runnable { processStreamError(nextTag); } else if (nextTag.isStart("features")) { processStreamFeatures(nextTag); - if ((streamFeatures.getChildren().size() == 1) - && (streamFeatures.hasChild("starttls")) - && (!account.isOptionSet(Account.OPTION_USETLS))) { - changeStatus(Account.STATUS_SERVER_REQUIRES_TLS); - } } else if (nextTag.isStart("proceed")) { switchOverToTls(nextTag); } else if (nextTag.isStart("compressed")) { @@ -422,7 +410,6 @@ public class XmppConnection implements Runnable { throws XmlPullParserException, IOException, NoSuchAlgorithmException { tagReader.readTag(); // read tag close - tagWriter.setOutputStream(new ZLibOutputStream(tagWriter .getOutputStream())); tagReader @@ -609,25 +596,32 @@ public class XmppConnection implements Runnable { this.sendUnboundIqPacket(iq, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - String resource = packet.findChild("bind").findChild("jid") - .getContent().split("/")[1]; - account.setResource(resource); - if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) { - smVersion = 3; - EnablePacket enable = new EnablePacket(smVersion); - tagWriter.writeStanzaAsync(enable); - } else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) { - smVersion = 2; - EnablePacket enable = new EnablePacket(smVersion); - tagWriter.writeStanzaAsync(enable); - } - sendServiceDiscoveryInfo(account.getServer()); - sendServiceDiscoveryItems(account.getServer()); - if (bindListener != null) { - bindListener.onBind(account); + Element bind = packet.findChild("bind"); + if (bind!=null) { + Element jid = bind.findChild("jid"); + if (jid!=null) { + account.setResource(jid.getContent().split("/")[1]); + if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) { + smVersion = 3; + EnablePacket enable = new EnablePacket(smVersion); + tagWriter.writeStanzaAsync(enable); + } else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) { + smVersion = 2; + EnablePacket enable = new EnablePacket(smVersion); + tagWriter.writeStanzaAsync(enable); + } + sendServiceDiscoveryInfo(account.getServer()); + sendServiceDiscoveryItems(account.getServer()); + if (bindListener != null) { + bindListener.onBind(account); + } + changeStatus(Account.STATUS_ONLINE); + } else { + disconnect(true); + } + } else { + disconnect(true); } - - changeStatus(Account.STATUS_ONLINE); } }); if (this.streamFeatures.hasChild("session")) { @@ -664,7 +658,7 @@ public class XmppConnection implements Runnable { } private void enableAdvancedStreamFeatures() { - if (hasFeaturesCarbon()) { + if (getFeatures().carbons()) { sendEnableCarbons(); } } @@ -835,33 +829,6 @@ public class XmppConnection implements Runnable { } } - public boolean hasFeatureRosterManagment() { - if (this.streamFeatures == null) { - return false; - } else { - return this.streamFeatures.hasChild("ver"); - } - } - - public boolean hasFeatureStreamManagment() { - if (this.streamFeatures == null) { - return false; - } else { - return this.streamFeatures.hasChild("sm"); - } - } - - public boolean hasFeaturesCarbon() { - return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2"); - } - - public boolean hasDiscoFeature(String server, String feature) { - if (!disco.containsKey(server)) { - return false; - } - return disco.get(server).contains(feature); - } - public List<String> findDiscoItemsByFeature(String feature) { List<String> items = new ArrayList<String>(); for (Entry<String, List<String>> cursor : disco.entrySet()) { @@ -905,4 +872,46 @@ public class XmppConnection implements Runnable { public int getAttempt() { return this.attempt; } + + public Features getFeatures() { + return this.features; + } + + public class Features { + XmppConnection connection; + public Features(XmppConnection connection) { + this.connection = connection; + } + + private boolean hasDiscoFeature(String server, String feature) { + if (!connection.disco.containsKey(server)) { + return false; + } + return connection.disco.get(server).contains(feature); + } + + public boolean carbons() { + return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2"); + } + + public boolean sm() { + if (connection.streamFeatures == null) { + return false; + } else { + return connection.streamFeatures.hasChild("sm"); + } + } + + public boolean pubsub() { + return hasDiscoFeature(account.getServer(), "http://jabber.org/protocol/pubsub#publish"); + } + + public boolean rosterVersioning() { + if (connection.streamFeatures == null) { + return false; + } else { + return connection.streamFeatures.hasChild("ver"); + } + } + } } diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java index f1a0373c..dafb2639 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -8,7 +8,9 @@ import java.util.List; import java.util.Locale; import java.util.Map.Entry; +import android.content.Intent; import android.graphics.BitmapFactory; +import android.net.Uri; import android.util.Log; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; @@ -23,12 +25,12 @@ import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class JingleConnection { - private final String[] extensions = {"webp","jpeg","jpg","png"}; - private final String[] cryptoExtensions = {"pgp","gpg","otr"}; - + private final String[] extensions = { "webp", "jpeg", "jpg", "png" }; + private final String[] cryptoExtensions = { "pgp", "gpg", "otr" }; + 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; @@ -36,9 +38,9 @@ public class JingleConnection { public static final int STATUS_FINISHED = 4; public static final int STATUS_TRANSMITTING = 5; public static final int STATUS_FAILED = 99; - + private int ibbBlockSize = 4096; - + private int status = -1; private Message message; private String sessionId; @@ -47,54 +49,64 @@ public class JingleConnection { private String responder; private List<JingleCandidate> candidates = new ArrayList<JingleCandidate>(); private HashMap<String, JingleSocks5Transport> connections = new HashMap<String, JingleSocks5Transport>(); - + private String transportId; private Element fileOffer; private JingleFile file = null; - + private String contentName; private String contentCreator; - + private boolean receivedCandidate = false; private boolean sentCandidate = false; - + private boolean acceptedAutomatically = false; - + private JingleTransport transport = null; - + private OnIqPacketReceived responseListener = new OnIqPacketReceived() { - + @Override public void onIqPacketReceived(Account account, IqPacket packet) { if (packet.getType() == IqPacket.TYPE_ERROR) { if (initiator.equals(account.getFullJid())) { - mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); + mXmppConnectionService.markMessage(message, + Message.STATUS_SEND_FAILED); } status = STATUS_FAILED; } } }; - + final OnFileTransmissionStatusChanged onFileTransmissionSatusChanged = new OnFileTransmissionStatusChanged() { - + @Override public void onFileTransmitted(JingleFile file) { if (responder.equals(account.getFullJid())) { sendSuccess(); if (acceptedAutomatically) { message.markUnread(); - JingleConnection.this.mXmppConnectionService.notifyUi(message.getConversation(), true); + JingleConnection.this.mXmppConnectionService.notifyUi( + message.getConversation(), true); } BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(file.getAbsolutePath(),options); + BitmapFactory.decodeFile(file.getAbsolutePath(), options); int imageHeight = options.outHeight; int imageWidth = options.outWidth; - message.setBody(""+file.getSize()+","+imageWidth+","+imageHeight); + message.setBody("" + file.getSize() + "," + imageWidth + "," + + imageHeight); mXmppConnectionService.databaseBackend.createMessage(message); - mXmppConnectionService.markMessage(message, Message.STATUS_RECIEVED); + mXmppConnectionService.markMessage(message, + Message.STATUS_RECIEVED); + } + Log.d("xmppService", + "sucessfully transmitted file:" + file.getAbsolutePath()); + if (message.getEncryption()!=Message.ENCRYPTION_PGP) { + Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(Uri.fromFile(file)); + mXmppConnectionService.sendBroadcast(intent); } - Log.d("xmppService","sucessfully transmitted file:"+file.getAbsolutePath()); } @Override @@ -103,48 +115,49 @@ public class JingleConnection { JingleConnection.this.cancel(); } }; - + private OnProxyActivated onProxyActivated = new OnProxyActivated() { - + @Override public void success() { if (initiator.equals(account.getFullJid())) { - Log.d("xmppService","we were initiating. sending file"); - transport.send(file,onFileTransmissionSatusChanged); + Log.d("xmppService", "we were initiating. sending file"); + transport.send(file, onFileTransmissionSatusChanged); } else { - transport.receive(file,onFileTransmissionSatusChanged); - Log.d("xmppService","we were responding. receiving file"); + transport.receive(file, onFileTransmissionSatusChanged); + Log.d("xmppService", "we were responding. receiving file"); } } - + @Override public void failed() { - Log.d("xmppService","proxy activation failed"); + Log.d("xmppService", "proxy activation failed"); } }; - + public JingleConnection(JingleConnectionManager mJingleConnectionManager) { this.mJingleConnectionManager = mJingleConnectionManager; - this.mXmppConnectionService = mJingleConnectionManager.getXmppConnectionService(); + 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) { boolean returnResult = true; if (packet.isAction("session-terminate")) { Reason reason = packet.getReason(); - if (reason!=null) { + if (reason != null) { if (reason.hasChild("cancel")) { this.cancel(); } else if (reason.hasChild("success")) { @@ -164,24 +177,26 @@ public class JingleConnection { returnResult = this.receiveFallbackToIbb(packet); } else { returnResult = false; - Log.d("xmppService","trying to fallback to something unknown"+packet.toString()); + Log.d("xmppService", "trying to fallback to something unknown" + + packet.toString()); } } else if (packet.isAction("transport-accept")) { returnResult = this.receiveTransportAccept(packet); } else { - Log.d("xmppService","packet arrived in connection. action was "+packet.getAction()); + Log.d("xmppService", "packet arrived in connection. action was " + + packet.getAction()); returnResult = false; } IqPacket response; if (returnResult) { response = packet.generateRespone(IqPacket.TYPE_RESULT); - + } else { response = packet.generateRespone(IqPacket.TYPE_ERROR); } account.getXmppConnection().sendIqPacket(response, null); } - + public void init(Message message) { this.contentCreator = "initiator"; this.contentName = this.mJingleConnectionManager.nextRandomId(); @@ -193,42 +208,52 @@ public class JingleConnection { if (this.candidates.size() > 0) { this.sendInitRequest(); } else { - this.mJingleConnectionManager.getPrimaryCandidate(account, new OnPrimaryCandidateFound() { - - @Override - public void onPrimaryCandidateFound(boolean success, final JingleCandidate candidate) { - if (success) { - final JingleSocks5Transport socksConnection = new JingleSocks5Transport(JingleConnection.this, candidate); - connections.put(candidate.getCid(), socksConnection); - socksConnection.connect(new OnTransportConnected() { - - @Override - public void failed() { - Log.d("xmppService","connection to our own primary candidete failed"); - sendInitRequest(); - } - - @Override - public void established() { - Log.d("xmppService","succesfully connected to our own primary candidate"); + this.mJingleConnectionManager.getPrimaryCandidate(account, + new OnPrimaryCandidateFound() { + + @Override + public void onPrimaryCandidateFound(boolean success, + final JingleCandidate candidate) { + if (success) { + final JingleSocks5Transport socksConnection = new JingleSocks5Transport( + JingleConnection.this, candidate); + connections.put(candidate.getCid(), + socksConnection); + socksConnection + .connect(new OnTransportConnected() { + + @Override + public void failed() { + Log.d("xmppService", + "connection to our own primary candidete failed"); + sendInitRequest(); + } + + @Override + public void established() { + Log.d("xmppService", + "succesfully connected to our own primary candidate"); + mergeCandidate(candidate); + sendInitRequest(); + } + }); mergeCandidate(candidate); + } else { + Log.d("xmppService", + "no primary candidate of our own was found"); sendInitRequest(); } - }); - mergeCandidate(candidate); - } else { - Log.d("xmppService","no primary candidate of our own was found"); - sendInitRequest(); - } - } - }); + } + }); } - + } - + public void init(Account account, JinglePacket packet) { this.status = STATUS_INITIATED; - Conversation conversation = this.mXmppConnectionService.findOrCreateConversation(account, packet.getFrom().split("/")[0], false); + Conversation conversation = this.mXmppConnectionService + .findOrCreateConversation(account, + packet.getFrom().split("/")[0], false); this.message = new Message(conversation, "", Message.ENCRYPTION_NONE); this.message.setType(Message.TYPE_IMAGE); this.message.setStatus(Message.STATUS_RECEIVED_OFFER); @@ -243,46 +268,62 @@ public class JingleConnection { this.contentCreator = content.getAttribute("creator"); this.contentName = content.getAttribute("name"); this.transportId = content.getTransportId(); - this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren())); + this.mergeCandidates(JingleCandidate.parse(content.socks5transport() + .getChildren())); this.fileOffer = packet.getJingleContent().getFileOffer(); - if (fileOffer!=null) { + if (fileOffer != null) { Element fileSize = fileOffer.findChild("size"); Element fileNameElement = fileOffer.findChild("name"); - if (fileNameElement!=null) { + if (fileNameElement != null) { boolean supportedFile = false; - String[] filename = fileNameElement.getContent().toLowerCase(Locale.US).split("\\."); - if (Arrays.asList(this.extensions).contains(filename[filename.length - 1])) { + String[] filename = fileNameElement.getContent() + .toLowerCase(Locale.US).split("\\."); + if (Arrays.asList(this.extensions).contains( + filename[filename.length - 1])) { supportedFile = true; - } else if (Arrays.asList(this.cryptoExtensions).contains(filename[filename.length - 1])) { + } else if (Arrays.asList(this.cryptoExtensions).contains( + filename[filename.length - 1])) { if (filename.length == 3) { - if (Arrays.asList(this.extensions).contains(filename[filename.length -2])) { + if (Arrays.asList(this.extensions).contains( + filename[filename.length - 2])) { supportedFile = true; if (filename[filename.length - 1].equals("otr")) { - Log.d("xmppService","receiving otr file"); - this.message.setEncryption(Message.ENCRYPTION_OTR); + Log.d("xmppService", "receiving otr file"); + this.message + .setEncryption(Message.ENCRYPTION_OTR); } else { - this.message.setEncryption(Message.ENCRYPTION_PGP); + this.message + .setEncryption(Message.ENCRYPTION_PGP); } } } } if (supportedFile) { long size = Long.parseLong(fileSize.getContent()); - message.setBody(""+size); + message.setBody("" + size); conversation.getMessages().add(message); - if (size<=this.mJingleConnectionManager.getAutoAcceptFileSize()) { - Log.d("xmppService","auto accepting file from "+packet.getFrom()); + if (size <= this.mJingleConnectionManager + .getAutoAcceptFileSize()) { + Log.d("xmppService", "auto accepting file from " + + packet.getFrom()); this.acceptedAutomatically = true; this.sendAccept(); } else { message.markUnread(); - Log.d("xmppService","not auto accepting new file offer with size: "+size+" allowed size:"+this.mJingleConnectionManager.getAutoAcceptFileSize()); - this.mXmppConnectionService.notifyUi(conversation, true); + Log.d("xmppService", + "not auto accepting new file offer with size: " + + size + + " allowed size:" + + this.mJingleConnectionManager + .getAutoAcceptFileSize()); + this.mXmppConnectionService + .notifyUi(conversation, true); } - this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message,false); + this.file = this.mXmppConnectionService.getFileBackend() + .getJingleFile(message, false); if (message.getEncryption() == Message.ENCRYPTION_OTR) { byte[] key = conversation.getSymmetricKey(); - if (key==null) { + if (key == null) { this.sendCancel(); this.cancel(); return; @@ -304,20 +345,21 @@ public class JingleConnection { this.cancel(); } } - + private void sendInitRequest() { JinglePacket packet = this.bootstrapPacket("session-initiate"); - Content content = new Content(this.contentCreator,this.contentName); + Content content = new Content(this.contentCreator, this.contentName); if (message.getType() == Message.TYPE_IMAGE) { content.setTransportId(this.transportId); - this.file = this.mXmppConnectionService.getFileBackend().getJingleFile(message,false); + this.file = this.mXmppConnectionService.getFileBackend() + .getJingleFile(message, false); if (message.getEncryption() == Message.ENCRYPTION_OTR) { Conversation conversation = this.message.getConversation(); this.mXmppConnectionService.renewSymmetricKey(conversation); content.setFileOffer(this.file, true); this.file.setKey(conversation.getSymmetricKey()); } else { - content.setFileOffer(this.file,false); + content.setFileOffer(this.file, false); } this.transportId = this.mJingleConnectionManager.nextRandomId(); content.setTransportId(this.transportId); @@ -327,62 +369,72 @@ public class JingleConnection { this.status = STATUS_INITIATED; } } - + private List<Element> getCandidatesAsElements() { List<Element> elements = new ArrayList<Element>(); - for(JingleCandidate c : this.candidates) { + for (JingleCandidate c : this.candidates) { elements.add(c.toElement()); } return elements; } - + private void sendAccept() { status = STATUS_ACCEPTED; mXmppConnectionService.markMessage(message, Message.STATUS_RECIEVING); - this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() { - - @Override - public void onPrimaryCandidateFound(boolean success,final JingleCandidate candidate) { - final JinglePacket packet = bootstrapPacket("session-accept"); - final Content content = new Content(contentCreator,contentName); - content.setFileOffer(fileOffer); - content.setTransportId(transportId); - if ((success)&&(!equalCandidateExists(candidate))) { - final JingleSocks5Transport socksConnection = new JingleSocks5Transport(JingleConnection.this, candidate); - connections.put(candidate.getCid(), socksConnection); - socksConnection.connect(new OnTransportConnected() { - - @Override - public void failed() { - Log.d("xmppService","connection to our own primary candidate failed"); - content.socks5transport().setChildren(getCandidatesAsElements()); - packet.setContent(content); - sendJinglePacket(packet); - connectNextCandidate(); - } - - @Override - public void established() { - Log.d("xmppService","connected to primary candidate"); - mergeCandidate(candidate); - content.socks5transport().setChildren(getCandidatesAsElements()); + this.mJingleConnectionManager.getPrimaryCandidate(this.account, + new OnPrimaryCandidateFound() { + + @Override + public void onPrimaryCandidateFound(boolean success, + final JingleCandidate candidate) { + final JinglePacket packet = bootstrapPacket("session-accept"); + final Content content = new Content(contentCreator, + contentName); + content.setFileOffer(fileOffer); + content.setTransportId(transportId); + if ((success) && (!equalCandidateExists(candidate))) { + final JingleSocks5Transport socksConnection = new JingleSocks5Transport( + JingleConnection.this, candidate); + connections.put(candidate.getCid(), socksConnection); + socksConnection.connect(new OnTransportConnected() { + + @Override + public void failed() { + Log.d("xmppService", + "connection to our own primary candidate failed"); + content.socks5transport().setChildren( + getCandidatesAsElements()); + packet.setContent(content); + sendJinglePacket(packet); + connectNextCandidate(); + } + + @Override + public void established() { + Log.d("xmppService", + "connected to primary candidate"); + mergeCandidate(candidate); + content.socks5transport().setChildren( + getCandidatesAsElements()); + packet.setContent(content); + sendJinglePacket(packet); + connectNextCandidate(); + } + }); + } else { + Log.d("xmppService", + "did not find a primary candidate for ourself"); + content.socks5transport().setChildren( + getCandidatesAsElements()); packet.setContent(content); sendJinglePacket(packet); connectNextCandidate(); } - }); - } else { - Log.d("xmppService","did not find a primary candidate for ourself"); - content.socks5transport().setChildren(getCandidatesAsElements()); - packet.setContent(content); - sendJinglePacket(packet); - connectNextCandidate(); - } - } - }); - + } + }); + } - + private JinglePacket bootstrapPacket(String action) { JinglePacket packet = new JinglePacket(); packet.setAction(action); @@ -392,15 +444,16 @@ public class JingleConnection { packet.setInitiator(this.initiator); return packet; } - + private void sendJinglePacket(JinglePacket packet) { - //Log.d("xmppService",packet.toString()); - account.getXmppConnection().sendIqPacket(packet,responseListener); + // Log.d("xmppService",packet.toString()); + account.getXmppConnection().sendIqPacket(packet, responseListener); } - + private boolean receiveAccept(JinglePacket packet) { Content content = packet.getJingleContent(); - mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren())); + mergeCandidates(JingleCandidate.parse(content.socks5transport() + .getChildren())); this.status = STATUS_ACCEPTED; mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); this.connectNextCandidate(); @@ -411,16 +464,20 @@ public class JingleConnection { Content content = packet.getJingleContent(); if (content.hasSocks5Transport()) { if (content.socks5transport().hasChild("activated")) { - if ((this.transport!=null)&&(this.transport instanceof JingleSocks5Transport)) { + if ((this.transport != null) + && (this.transport instanceof JingleSocks5Transport)) { onProxyActivated.success(); } else { - String cid = content.socks5transport().findChild("activated").getAttribute("cid"); - Log.d("xmppService","received proxy activated ("+cid+")prior to choosing our own transport"); - JingleSocks5Transport connection = this.connections.get(cid); - if (connection!=null) { + String cid = content.socks5transport() + .findChild("activated").getAttribute("cid"); + Log.d("xmppService", "received proxy activated (" + cid + + ")prior to choosing our own transport"); + JingleSocks5Transport connection = this.connections + .get(cid); + if (connection != null) { connection.setActivated(true); } else { - Log.d("xmppService","activated connection not found"); + Log.d("xmppService", "activated connection not found"); this.sendCancel(); this.cancel(); } @@ -430,23 +487,25 @@ public class JingleConnection { onProxyActivated.failed(); return true; } else if (content.socks5transport().hasChild("candidate-error")) { - Log.d("xmppService","received candidate error"); + Log.d("xmppService", "received candidate error"); this.receivedCandidate = true; - if ((status == STATUS_ACCEPTED)&&(this.sentCandidate)) { + if ((status == STATUS_ACCEPTED) && (this.sentCandidate)) { this.connect(); } return true; - } else if (content.socks5transport().hasChild("candidate-used")){ - String cid = content.socks5transport().findChild("candidate-used").getAttribute("cid"); - if (cid!=null) { - Log.d("xmppService","candidate used by counterpart:"+cid); + } else if (content.socks5transport().hasChild("candidate-used")) { + String cid = content.socks5transport() + .findChild("candidate-used").getAttribute("cid"); + if (cid != null) { + Log.d("xmppService", "candidate used by counterpart:" + cid); JingleCandidate candidate = getCandidate(cid); candidate.flagAsUsedByCounterpart(); this.receivedCandidate = true; - if ((status == STATUS_ACCEPTED)&&(this.sentCandidate)) { + if ((status == STATUS_ACCEPTED) && (this.sentCandidate)) { this.connect(); } else { - Log.d("xmppService","ignoring because file is already in transmission or we havent sent our candidate yet"); + Log.d("xmppService", + "ignoring because file is already in transmission or we havent sent our candidate yet"); } return true; } else { @@ -463,8 +522,8 @@ public class JingleConnection { private void connect() { final JingleSocks5Transport connection = chooseConnection(); this.transport = connection; - if (connection==null) { - Log.d("xmppService","could not find suitable candidate"); + if (connection == null) { + Log.d("xmppService", "could not find suitable candidate"); this.disconnect(); if (this.initiator.equals(account.getFullJid())) { this.sendFallbackToIbb(); @@ -473,65 +532,80 @@ public class JingleConnection { this.status = STATUS_TRANSMITTING; if (connection.needsActivation()) { if (connection.getCandidate().isOurs()) { - Log.d("xmppService","candidate "+connection.getCandidate().getCid()+" was our proxy. going to activate"); + Log.d("xmppService", "candidate " + + connection.getCandidate().getCid() + + " was our proxy. going to activate"); IqPacket activation = new IqPacket(IqPacket.TYPE_SET); activation.setTo(connection.getCandidate().getJid()); - activation.query("http://jabber.org/protocol/bytestreams").setAttribute("sid", this.getSessionId()); - activation.query().addChild("activate").setContent(this.getCounterPart()); - this.account.getXmppConnection().sendIqPacket(activation, new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType()==IqPacket.TYPE_ERROR) { - onProxyActivated.failed(); - } else { - onProxyActivated.success(); - sendProxyActivated(connection.getCandidate().getCid()); - } - } - }); + activation.query("http://jabber.org/protocol/bytestreams") + .setAttribute("sid", this.getSessionId()); + activation.query().addChild("activate") + .setContent(this.getCounterPart()); + this.account.getXmppConnection().sendIqPacket(activation, + new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, + IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_ERROR) { + onProxyActivated.failed(); + } else { + onProxyActivated.success(); + sendProxyActivated(connection + .getCandidate().getCid()); + } + } + }); } else { - Log.d("xmppService","candidate "+connection.getCandidate().getCid()+" was a proxy. waiting for other party to activate"); + Log.d("xmppService", + "candidate " + + connection.getCandidate().getCid() + + " was a proxy. waiting for other party to activate"); } } else { if (initiator.equals(account.getFullJid())) { - Log.d("xmppService","we were initiating. sending file"); - connection.send(file,onFileTransmissionSatusChanged); + Log.d("xmppService", "we were initiating. sending file"); + connection.send(file, onFileTransmissionSatusChanged); } else { - Log.d("xmppService","we were responding. receiving file"); - connection.receive(file,onFileTransmissionSatusChanged); + Log.d("xmppService", "we were responding. receiving file"); + connection.receive(file, onFileTransmissionSatusChanged); } } } } - + private JingleSocks5Transport chooseConnection() { JingleSocks5Transport connection = null; - for (Entry<String, JingleSocks5Transport> cursor : connections.entrySet()) { - JingleSocks5Transport currentConnection = cursor.getValue(); - //Log.d("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; - } - } - } - } - } - } + for (Entry<String, JingleSocks5Transport> cursor : connections + .entrySet()) { + JingleSocks5Transport currentConnection = cursor.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; + } + } + } + } + } + } return connection; } @@ -543,60 +617,68 @@ public class JingleConnection { this.sendJinglePacket(packet); this.disconnect(); this.status = STATUS_FINISHED; - this.mXmppConnectionService.markMessage(this.message, Message.STATUS_RECIEVED); + this.mXmppConnectionService.markMessage(this.message, + Message.STATUS_RECIEVED); this.mJingleConnectionManager.finishConnection(this); } - + private void sendFallbackToIbb() { JinglePacket packet = this.bootstrapPacket("transport-replace"); - Content content = new Content(this.contentCreator,this.contentName); + Content content = new Content(this.contentCreator, this.contentName); this.transportId = this.mJingleConnectionManager.nextRandomId(); content.setTransportId(this.transportId); - content.ibbTransport().setAttribute("block-size",""+this.ibbBlockSize); + content.ibbTransport().setAttribute("block-size", + "" + this.ibbBlockSize); packet.setContent(content); this.sendJinglePacket(packet); } - + private boolean receiveFallbackToIbb(JinglePacket packet) { - String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size"); - if (receivedBlockSize!=null) { + String receivedBlockSize = packet.getJingleContent().ibbTransport() + .getAttribute("block-size"); + if (receivedBlockSize != null) { int bs = Integer.parseInt(receivedBlockSize); - if (bs>this.ibbBlockSize) { + if (bs > this.ibbBlockSize) { this.ibbBlockSize = bs; } } this.transportId = packet.getJingleContent().getTransportId(); - this.transport = new JingleInbandTransport(this.account,this.responder,this.transportId,this.ibbBlockSize); + this.transport = new JingleInbandTransport(this.account, + this.responder, this.transportId, this.ibbBlockSize); this.transport.receive(file, onFileTransmissionSatusChanged); JinglePacket answer = bootstrapPacket("transport-accept"); Content content = new Content("initiator", "a-file-offer"); content.setTransportId(this.transportId); - content.ibbTransport().setAttribute("block-size", ""+this.ibbBlockSize); + content.ibbTransport().setAttribute("block-size", + "" + this.ibbBlockSize); answer.setContent(content); this.sendJinglePacket(answer); return true; } - + private boolean receiveTransportAccept(JinglePacket packet) { if (packet.getJingleContent().hasIbbTransport()) { - String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size"); - if (receivedBlockSize!=null) { + String receivedBlockSize = packet.getJingleContent().ibbTransport() + .getAttribute("block-size"); + if (receivedBlockSize != null) { int bs = Integer.parseInt(receivedBlockSize); - if (bs>this.ibbBlockSize) { + if (bs > this.ibbBlockSize) { this.ibbBlockSize = bs; } } - this.transport = new JingleInbandTransport(this.account,this.responder,this.transportId,this.ibbBlockSize); + this.transport = new JingleInbandTransport(this.account, + this.responder, this.transportId, this.ibbBlockSize); this.transport.connect(new OnTransportConnected() { - + @Override public void failed() { - Log.d("xmppService","ibb open failed"); + Log.d("xmppService", "ibb open failed"); } - + @Override public void established() { - JingleConnection.this.transport.send(file, onFileTransmissionSatusChanged); + JingleConnection.this.transport.send(file, + onFileTransmissionSatusChanged); } }); return true; @@ -604,31 +686,35 @@ public class JingleConnection { return false; } } - + private void receiveSuccess() { this.status = STATUS_FINISHED; - this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND); + this.mXmppConnectionService.markMessage(this.message, + Message.STATUS_SEND); this.disconnect(); this.mJingleConnectionManager.finishConnection(this); } - + public void cancel() { this.disconnect(); - if (this.message!=null) { + if (this.message != null) { if (this.responder.equals(account.getFullJid())) { - this.mXmppConnectionService.markMessage(this.message, Message.STATUS_RECEPTION_FAILED); + this.mXmppConnectionService.markMessage(this.message, + Message.STATUS_RECEPTION_FAILED); } else { if (this.status == STATUS_INITIATED) { - this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_REJECTED); + this.mXmppConnectionService.markMessage(this.message, + Message.STATUS_SEND_REJECTED); } else { - this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_FAILED); + this.mXmppConnectionService.markMessage(this.message, + Message.STATUS_SEND_FAILED); } } } this.status = STATUS_CANCELED; this.mJingleConnectionManager.finishConnection(this); } - + private void sendCancel() { JinglePacket packet = bootstrapPacket("session-terminate"); Reason reason = new Reason(); @@ -638,73 +724,82 @@ public class JingleConnection { } private void connectNextCandidate() { - for(JingleCandidate candidate : this.candidates) { - if ((!connections.containsKey(candidate.getCid())&&(!candidate.isOurs()))) { + for (JingleCandidate candidate : this.candidates) { + if ((!connections.containsKey(candidate.getCid()) && (!candidate + .isOurs()))) { this.connectWithCandidate(candidate); return; } } this.sendCandidateError(); } - + private void connectWithCandidate(final JingleCandidate candidate) { - final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this,candidate); + final JingleSocks5Transport socksConnection = new JingleSocks5Transport( + this, candidate); connections.put(candidate.getCid(), socksConnection); socksConnection.connect(new OnTransportConnected() { - + @Override public void failed() { - Log.d("xmppService", "connection failed with "+candidate.getHost()+":"+candidate.getPort()); + Log.d("xmppService", + "connection failed with " + candidate.getHost() + ":" + + candidate.getPort()); connectNextCandidate(); } - + @Override public void established() { - Log.d("xmppService", "established connection with "+candidate.getHost()+":"+candidate.getPort()); + Log.d("xmppService", + "established connection with " + candidate.getHost() + + ":" + candidate.getPort()); sendCandidateUsed(candidate.getCid()); } }); } private void disconnect() { - Iterator<Entry<String, JingleSocks5Transport>> it = this.connections.entrySet().iterator(); - while (it.hasNext()) { - Entry<String, JingleSocks5Transport> pairs = it.next(); - pairs.getValue().disconnect(); - it.remove(); - } - } - + Iterator<Entry<String, JingleSocks5Transport>> it = this.connections + .entrySet().iterator(); + while (it.hasNext()) { + Entry<String, JingleSocks5Transport> pairs = it.next(); + pairs.getValue().disconnect(); + it.remove(); + } + } + private void sendProxyActivated(String cid) { JinglePacket packet = bootstrapPacket("transport-info"); - Content content = new Content(this.contentCreator,this.contentName); + Content content = new Content(this.contentCreator, this.contentName); content.setTransportId(this.transportId); - content.socks5transport().addChild("activated").setAttribute("cid", cid); + content.socks5transport().addChild("activated") + .setAttribute("cid", cid); packet.setContent(content); this.sendJinglePacket(packet); } - + private void sendCandidateUsed(final String cid) { JinglePacket packet = bootstrapPacket("transport-info"); - Content content = new Content(this.contentCreator,this.contentName); + Content content = new Content(this.contentCreator, this.contentName); content.setTransportId(this.transportId); - content.socks5transport().addChild("candidate-used").setAttribute("cid", cid); + content.socks5transport().addChild("candidate-used") + .setAttribute("cid", cid); packet.setContent(content); this.sentCandidate = true; - if ((receivedCandidate)&&(status == STATUS_ACCEPTED)) { + if ((receivedCandidate) && (status == STATUS_ACCEPTED)) { connect(); } this.sendJinglePacket(packet); } - + private void sendCandidateError() { JinglePacket packet = bootstrapPacket("transport-info"); - Content content = new Content(this.contentCreator,this.contentName); + Content content = new Content(this.contentCreator, this.contentName); content.setTransportId(this.transportId); content.socks5transport().addChild("candidate-error"); packet.setContent(content); this.sentCandidate = true; - if ((receivedCandidate)&&(status == STATUS_ACCEPTED)) { + if ((receivedCandidate) && (status == STATUS_ACCEPTED)) { connect(); } this.sendJinglePacket(packet); @@ -713,72 +808,73 @@ public class JingleConnection { 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) { + for (JingleCandidate c : this.candidates) { if (c.equalValues(candidate)) { return true; } } return false; } - + private void mergeCandidate(JingleCandidate candidate) { - for(JingleCandidate c : this.candidates) { + for (JingleCandidate c : this.candidates) { if (c.equals(candidate)) { return; } } this.candidates.add(candidate); } - + private void mergeCandidates(List<JingleCandidate> candidates) { - for(JingleCandidate c : candidates) { + for (JingleCandidate c : candidates) { mergeCandidate(c); } } - + private JingleCandidate getCandidate(String cid) { - for(JingleCandidate c : this.candidates) { + for (JingleCandidate c : this.candidates) { if (c.getCid().equals(cid)) { return c; } } return null; } - + interface OnProxyActivated { public void success(); + public void failed(); } public boolean hasTransportId(String sid) { return sid.equals(this.transportId); } - + public JingleTransport getTransport() { return this.transport; } public void accept() { - if (status==STATUS_INITIATED) { + if (status == STATUS_INITIATED) { new Thread(new Runnable() { - + @Override public void run() { sendAccept(); } }).start(); } else { - Log.d("xmppService","status ("+status+") was not ok"); + Log.d("xmppService", "status (" + status + ") was not ok"); } } } diff --git a/src/eu/siacs/conversations/xmpp/pep/Avatar.java b/src/eu/siacs/conversations/xmpp/pep/Avatar.java new file mode 100644 index 00000000..6d5c1431 --- /dev/null +++ b/src/eu/siacs/conversations/xmpp/pep/Avatar.java @@ -0,0 +1,68 @@ +package eu.siacs.conversations.xmpp.pep; + +import eu.siacs.conversations.xml.Element; +import android.util.Base64; + +public class Avatar { + public String type; + public String sha1sum; + public String image; + public int height; + public int width; + public long size; + public String owner; + public byte[] getImageAsBytes() { + return Base64.decode(image, Base64.DEFAULT); + } + public String getFilename() { + if (type==null) { + return sha1sum; + } else if (type.equalsIgnoreCase("image/webp")) { + return sha1sum+".webp"; + } else if (type.equalsIgnoreCase("image/png")) { + return sha1sum+".png"; + } else { + return sha1sum; + } + } + + public static Avatar parseMetadata(Element items) { + Element item = items.findChild("item"); + if (item==null) { + return null; + } + Element metadata = item.findChild("metadata"); + if (metadata==null) { + return null; + } + String primaryId = item.getAttribute("id"); + if (primaryId==null) { + return null; + } + for(Element child : metadata.getChildren()) { + if (child.getName().equals("info") && primaryId.equals(child.getAttribute("id"))) { + Avatar avatar = new Avatar(); + String height = child.getAttribute("height"); + String width = child.getAttribute("width"); + String size = child.getAttribute("bytes"); + try { + if (height!=null) { + avatar.height = Integer.parseInt(height); + } + if (width!=null) { + avatar.width = Integer.parseInt(width); + } + if (size!=null) { + avatar.size = Long.parseLong(size); + } + } catch (NumberFormatException e) { + return null; + } + avatar.type = child.getAttribute("type"); + avatar.sha1sum = child.getAttribute("id"); + return avatar; + } + } + return null; + } +} diff --git a/src/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/src/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java index 64a9edc3..433b08c9 100644 --- a/src/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java +++ b/src/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java @@ -8,6 +8,7 @@ public class MessagePacket extends AbstractStanza { public static final int TYPE_NORMAL = 2; public static final int TYPE_GROUPCHAT = 3; public static final int TYPE_ERROR = 4; + public static final int TYPE_HEADLINE = 5; public MessagePacket() { super("message"); @@ -59,6 +60,8 @@ public class MessagePacket extends AbstractStanza { return TYPE_GROUPCHAT; } else if (type.equals("error")) { return TYPE_ERROR; + } else if (type.equals("headline")) { + return TYPE_HEADLINE; } else { return TYPE_UNKNOWN; } |