diff --git a/.travis.yml b/.travis.yml index 4f147a61d..a2959e0a2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,8 @@ android: components: - platform-tools - tools + - build-tools-22.0.1 - build-tools-21.1.2 - build-tools-19.1.0 - - android-21 + - android-22 - extra-android-m2repository diff --git a/CHANGELOG.md b/CHANGELOG.md index ae267086e..61980b618 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ ###Changelog +####Version 1.4.0 +* send button turns into quick action button to offer faster access to take photo, send location or record audio +* visually seperate merged messages +* faster reconnects of failed accounts after network switches +* r/o vcard avatars for contacts +* various bug fixes + ####Version 1.3.0 * swipe conversations to end them * quickly enable / disable account via slider diff --git a/README.md b/README.md index 5519e47f6..e7221eab5 100644 --- a/README.md +++ b/README.md @@ -67,12 +67,13 @@ run your own XMPP server for you and your friends. These XEP's are: (In order of appearance) -* [Rene Treffer](https://github.com/rtreffer) -* [Andreas Straub](https://github.com/strb) -* [Alethea Butler](https://github.com/alethea) -* [M. Dietrich](https://github.com/emdete) -* [betheg](https://github.com/betheg) -* [Sam Whited](https://github.com/SamWhited) +* [Rene Treffer](https://github.com/rtreffer) ([PRs](https://github.com/siacs/Conversations/pulls?utf8=%E2%9C%93&q=is%3Apr+author%3Artreffer+is%3Amerged)) +* [Andreas Straub](https://github.com/strb) ([PRs](https://github.com/siacs/Conversations/pulls?utf8=%E2%9C%93&q=is%3Apr+author%3Astrb+is%3Amerged)) +* [Alethea Butler](https://github.com/alethea) ([PRs](https://github.com/siacs/Conversations/pulls?utf8=%E2%9C%93&q=is%3Apr+author%3Aalethea+is%3Amerged)) +* [M. Dietrich](https://github.com/emdete) ([PRs](https://github.com/siacs/Conversations/pulls?utf8=%E2%9C%93&q=is%3Apr+author%3Aemdete+is%3Amerged)) +* [betheg](https://github.com/betheg) ([PRs](https://github.com/siacs/Conversations/pulls?utf8=%E2%9C%93&q=is%3Apr+author%3Abetheg+is%3Amerged)) +* [Sam Whited](https://github.com/SamWhited) ([PRs](https://github.com/siacs/Conversations/pulls?utf8=%E2%9C%93&q=is%3Apr+author%3ASamWhited+is%3Amerged)) +* [BrianBlade](https://github.com/BrianBlade) ([PRs](https://github.com/siacs/Conversations/pulls?utf8=%E2%9C%93&q=is%3Apr+author%3ABrianBlade+is%3Amerged)) #### Logo * [Ilia Rostovtsev](https://github.com/qooob) (Progress) diff --git a/art/ic_received_indicator.svg b/art/ic_received_indicator.svg index d9378c60d..43689c267 100644 --- a/art/ic_received_indicator.svg +++ b/art/ic_received_indicator.svg @@ -13,7 +13,7 @@ width="95" height="95" id="Yes_check" - inkscape:version="0.48.5 r10040" + inkscape:version="0.91 r13725" sodipodi:docname="ic_received_indicator.svg"> @@ -23,7 +23,7 @@ image/svg+xml - + @@ -36,17 +36,17 @@ guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" - inkscape:window-width="1233" - inkscape:window-height="828" + inkscape:window-width="956" + inkscape:window-height="1156" id="namedview8" showgrid="false" showguides="true" inkscape:guide-bbox="true" inkscape:zoom="5.04" - inkscape:cx="26.829268" + inkscape:cx="-4.3215257" inkscape:cy="37.489149" - inkscape:window-x="0" - inkscape:window-y="0" + inkscape:window-x="2880" + inkscape:window-y="20" inkscape:window-maximized="0" inkscape:current-layer="Yes_check" fit-margin-top="0" @@ -69,7 +69,7 @@ diff --git a/art/ic_send_cancel_away.svg b/art/ic_send_cancel_away.svg new file mode 100644 index 000000000..1ee9c40f6 --- /dev/null +++ b/art/ic_send_cancel_away.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/art/ic_send_cancel_dnd.svg b/art/ic_send_cancel_dnd.svg new file mode 100644 index 000000000..67a562b2f --- /dev/null +++ b/art/ic_send_cancel_dnd.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/art/ic_send_cancel_offline.svg b/art/ic_send_cancel_offline.svg new file mode 100644 index 000000000..b88ade09d --- /dev/null +++ b/art/ic_send_cancel_offline.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/art/ic_send_cancel_online.svg b/art/ic_send_cancel_online.svg new file mode 100644 index 000000000..40946af67 --- /dev/null +++ b/art/ic_send_cancel_online.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/art/ic_send_location_away.svg b/art/ic_send_location_away.svg new file mode 100644 index 000000000..fcd50b521 --- /dev/null +++ b/art/ic_send_location_away.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/art/ic_send_location_dnd.svg b/art/ic_send_location_dnd.svg new file mode 100644 index 000000000..705cdb6f4 --- /dev/null +++ b/art/ic_send_location_dnd.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/art/ic_send_location_offline.svg b/art/ic_send_location_offline.svg new file mode 100644 index 000000000..56529b723 --- /dev/null +++ b/art/ic_send_location_offline.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/art/ic_send_location_online.svg b/art/ic_send_location_online.svg new file mode 100644 index 000000000..275a7a789 --- /dev/null +++ b/art/ic_send_location_online.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/art/ic_send_photo_away.svg b/art/ic_send_photo_away.svg new file mode 100644 index 000000000..31a20e09f --- /dev/null +++ b/art/ic_send_photo_away.svg @@ -0,0 +1,60 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/art/ic_send_photo_dnd.svg b/art/ic_send_photo_dnd.svg new file mode 100644 index 000000000..9ef8b7821 --- /dev/null +++ b/art/ic_send_photo_dnd.svg @@ -0,0 +1,60 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/art/ic_send_photo_offline.svg b/art/ic_send_photo_offline.svg new file mode 100644 index 000000000..b2ca20a6f --- /dev/null +++ b/art/ic_send_photo_offline.svg @@ -0,0 +1,60 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/art/ic_send_photo_online.svg b/art/ic_send_photo_online.svg new file mode 100644 index 000000000..79f713475 --- /dev/null +++ b/art/ic_send_photo_online.svg @@ -0,0 +1,60 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/art/ic_send_text_away.svg b/art/ic_send_text_away.svg new file mode 100644 index 000000000..ea83086ae --- /dev/null +++ b/art/ic_send_text_away.svg @@ -0,0 +1,69 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/art/ic_send_text_dnd.svg b/art/ic_send_text_dnd.svg new file mode 100644 index 000000000..1b7ad51f4 --- /dev/null +++ b/art/ic_send_text_dnd.svg @@ -0,0 +1,69 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/art/ic_send_text_offline.svg b/art/ic_send_text_offline.svg new file mode 100644 index 000000000..c87bfaac6 --- /dev/null +++ b/art/ic_send_text_offline.svg @@ -0,0 +1,69 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/art/ic_action_send_now.svg b/art/ic_send_text_online.svg similarity index 80% rename from art/ic_action_send_now.svg rename to art/ic_send_text_online.svg index 6bde9158f..39e3d1e8a 100644 --- a/art/ic_action_send_now.svg +++ b/art/ic_send_text_online.svg @@ -11,7 +11,7 @@ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" id="svg3621" version="1.1" - inkscape:version="0.48.4 r9939" + inkscape:version="0.91 r13725" width="96" height="96" sodipodi:docname="ic_action_send_now.svg" @@ -26,7 +26,7 @@ image/svg+xml - + @@ -41,16 +41,16 @@ guidetolerance="10" inkscape:pageopacity="0" inkscape:pageshadow="2" - inkscape:window-width="1916" - inkscape:window-height="1161" + inkscape:window-width="1920" + inkscape:window-height="1200" id="namedview3623" showgrid="true" showguides="true" - inkscape:zoom="1" - inkscape:cx="47.28873" - inkscape:cy="43.262706" - inkscape:window-x="0" - inkscape:window-y="18" + inkscape:zoom="8" + inkscape:cx="69.783303" + inkscape:cy="56.761328" + inkscape:window-x="1920" + inkscape:window-y="0" inkscape:window-maximized="0" inkscape:current-layer="svg3621"> + + + + + image/svg+xml + + + + + + + + diff --git a/art/ic_send_voice_dnd.svg b/art/ic_send_voice_dnd.svg new file mode 100644 index 000000000..b1b7a7a97 --- /dev/null +++ b/art/ic_send_voice_dnd.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/art/ic_send_voice_offline.svg b/art/ic_send_voice_offline.svg new file mode 100644 index 000000000..64ea44731 --- /dev/null +++ b/art/ic_send_voice_offline.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/art/ic_send_voice_online.svg b/art/ic_send_voice_online.svg new file mode 100644 index 000000000..97284b021 --- /dev/null +++ b/art/ic_send_voice_online.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/art/render.rb b/art/render.rb index 2ab3e94fc..ad53b1f4f 100755 --- a/art/render.rb +++ b/art/render.rb @@ -10,6 +10,26 @@ images = { 'conversations_baloon.svg' => ['ic_launcher', 48], 'conversations_mono.svg' => ['ic_notification', 24], 'ic_received_indicator.svg' => ['ic_received_indicator', 12], + 'ic_send_text_offline.svg' => ['ic_send_text_offline', 36], + 'ic_send_text_online.svg' => ['ic_send_text_online', 36], + 'ic_send_text_away.svg' => ['ic_send_text_away', 36], + 'ic_send_text_dnd.svg' => ['ic_send_text_dnd', 36], + 'ic_send_photo_online.svg' => ['ic_send_photo_online', 36], + 'ic_send_photo_offline.svg' => ['ic_send_photo_offline', 36], + 'ic_send_photo_away.svg' => ['ic_send_photo_away', 36], + 'ic_send_photo_dnd.svg' => ['ic_send_photo_dnd', 36], + 'ic_send_location_online.svg' => ['ic_send_location_online', 36], + 'ic_send_location_offline.svg' => ['ic_send_location_offline', 36], + 'ic_send_location_away.svg' => ['ic_send_location_away', 36], + 'ic_send_location_dnd.svg' => ['ic_send_location_dnd', 36], + 'ic_send_voice_online.svg' => ['ic_send_voice_online', 36], + 'ic_send_voice_offline.svg' => ['ic_send_voice_offline', 36], + 'ic_send_voice_away.svg' => ['ic_send_voice_away', 36], + 'ic_send_voice_dnd.svg' => ['ic_send_voice_dnd', 36], + 'ic_send_cancel_online.svg' => ['ic_send_cancel_online', 36], + 'ic_send_cancel_offline.svg' => ['ic_send_cancel_offline', 36], + 'ic_send_cancel_away.svg' => ['ic_send_cancel_away', 36], + 'ic_send_cancel_dnd.svg' => ['ic_send_cancel_dnd', 36], } images.each do |source, result| resolutions.each do |name, factor| diff --git a/build.gradle b/build.gradle index afc7bb4bc..98b341d52 100644 --- a/build.gradle +++ b/build.gradle @@ -38,14 +38,14 @@ dependencies { } android { - compileSdkVersion 21 - buildToolsVersion "21.1.2" + compileSdkVersion 22 + buildToolsVersion "22.0.1" defaultConfig { minSdkVersion 14 targetSdkVersion 21 - versionCode 61 - versionName "1.3.1" + versionCode 66 + versionName "1.4.0" } compileOptions { diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 5cca6c0b7..779cbbe86 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -11,6 +11,7 @@ public final class Config { public static final int PING_MAX_INTERVAL = 300; public static final int PING_MIN_INTERVAL = 30; public static final int PING_TIMEOUT = 10; + public static final int SOCKET_TIMEOUT = 15; public static final int CONNECT_TIMEOUT = 90; public static final int CARBON_GRACE_PERIOD = 60; public static final int MINI_GRACE_PERIOD = 750; @@ -28,7 +29,8 @@ public final class Config { public static final boolean NO_PROXY_LOOKUP = false; //useful to debug ibb public static final boolean DISABLE_STRING_PREP = false; // setting to true might increase startup performance - public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts + public static final boolean EXTENDED_SM_LOGGING = true; // log stanza counts + public static final boolean RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE = true; //setting to true might increase power consumption public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY / 2; diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index cef03ebe4..9dbca59a1 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -15,6 +15,7 @@ import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; +import eu.siacs.conversations.xmpp.pep.Avatar; public class Contact implements ListItem, Blockable { public static final String TABLENAME = "contacts"; @@ -40,11 +41,11 @@ public class Contact implements ListItem, Blockable { protected int subscription = 0; protected String systemAccount; protected String photoUri; - protected String avatar; protected JSONObject keys = new JSONObject(); protected JSONArray groups = new JSONArray(); protected Presences presences = new Presences(); protected Account account; + protected Avatar avatar; public Contact(final String account, final String systemName, final String serverName, final Jid jid, final int subscription, final String photoUri, @@ -61,7 +62,11 @@ public class Contact implements ListItem, Blockable { } catch (JSONException e) { this.keys = new JSONObject(); } - this.avatar = avatar; + if (avatar != null) { + this.avatar = new Avatar(); + this.avatar.sha1sum = avatar; + this.avatar.origin = Avatar.Origin.VCARD; //always assume worst + } try { this.groups = (groups == null ? new JSONArray() : new JSONArray(groups)); } catch (JSONException e) { @@ -135,10 +140,10 @@ public class Contact implements ListItem, Blockable { tags.add(new Tag("away", 0xffff9800)); break; case Presences.XA: - tags.add(new Tag("not available", 0xffe51c23)); + tags.add(new Tag("not available", 0xfff44336)); break; case Presences.DND: - tags.add(new Tag("dnd", 0xffe51c23)); + tags.add(new Tag("dnd", 0xfff44336)); break; } if (isBlocked()) { @@ -187,7 +192,7 @@ public class Contact implements ListItem, Blockable { values.put(SYSTEMACCOUNT, systemAccount); values.put(PHOTOURI, photoUri); values.put(KEYS, keys.toString()); - values.put(AVATAR, avatar); + values.put(AVATAR, avatar == null ? null : avatar.getFilename()); values.put(LAST_PRESENCE, lastseen.presence); values.put(LAST_TIME, lastseen.time); values.put(GROUPS, groups.toString()); @@ -411,17 +416,20 @@ public class Contact implements ListItem, Blockable { return getJid().toDomainJid(); } - public boolean setAvatar(String filename) { - if (this.avatar != null && this.avatar.equals(filename)) { + public boolean setAvatar(Avatar avatar) { + if (this.avatar != null && this.avatar.equals(avatar)) { return false; } else { - this.avatar = filename; + if (this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) { + return false; + } + this.avatar = avatar; return true; } } public String getAvatar() { - return this.avatar; + return avatar == null ? null : avatar.getFilename(); } public boolean deleteOtrFingerprint(String fingerprint) { @@ -478,6 +486,10 @@ public class Contact implements ListItem, Blockable { } } + public boolean isSelf() { + return account.getJid().toBareJid().equals(getJid().toBareJid()); + } + public static class Lastseen { public long time; public String presence; diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index bfee50075..95a8c957d 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -16,6 +16,7 @@ import java.security.interfaces.DSAPublicKey; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.Iterator; import java.util.List; import eu.siacs.conversations.Config; @@ -218,6 +219,11 @@ public class Conversation extends AbstractEntity implements Blockable { messages.clear(); messages.addAll(this.messages); } + for(Iterator iterator = messages.iterator(); iterator.hasNext();) { + if (iterator.next().wasMergedIntoPrevious()) { + iterator.remove(); + } + } } @Override diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 7ea3d60b0..38152edba 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -9,6 +9,7 @@ import java.util.Arrays; import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.GeoHelper; +import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; @@ -16,6 +17,8 @@ public class Message extends AbstractEntity { public static final String TABLENAME = "messages"; + public static final String MERGE_SEPARATOR = "\u200B\n\n"; + public static final int STATUS_RECEIVED = 0; public static final int STATUS_UNSEND = 1; public static final int STATUS_SEND = 2; @@ -95,9 +98,9 @@ public class Message extends AbstractEntity { } private Message(final String uuid, final String conversationUUid, final Jid counterpart, - final Jid trueCounterpart, final String body, final long timeSent, - final int encryption, final int status, final int type, final String remoteMsgId, - final String relativeFilePath, final String serverMsgId) { + final Jid trueCounterpart, final String body, final long timeSent, + final int encryption, final int status, final int type, final String remoteMsgId, + final String relativeFilePath, final String serverMsgId) { this.uuid = uuid; this.conversationUuid = conversationUUid; this.counterpart = counterpart; @@ -179,7 +182,7 @@ public class Message extends AbstractEntity { values.put(TYPE, type); values.put(REMOTE_MSG_ID, remoteMsgId); values.put(RELATIVE_FILE_PATH, relativeFilePath); - values.put(SERVER_MSG_ID,serverMsgId); + values.put(SERVER_MSG_ID, serverMsgId); return values; } @@ -211,7 +214,7 @@ public class Message extends AbstractEntity { return null; } else { return this.conversation.getAccount().getRoster() - .getContactFromRoster(this.trueCounterpart); + .getContactFromRoster(this.trueCounterpart); } } } @@ -359,41 +362,43 @@ public class Message extends AbstractEntity { public boolean mergeable(final Message message) { return message != null && - (message.getType() == Message.TYPE_TEXT && - this.getDownloadable() == null && - message.getDownloadable() == null && - message.getEncryption() != Message.ENCRYPTION_PGP && - this.getType() == message.getType() && - //this.getStatus() == message.getStatus() && - isStatusMergeable(this.getStatus(),message.getStatus()) && - this.getEncryption() == message.getEncryption() && - this.getCounterpart() != null && - this.getCounterpart().equals(message.getCounterpart()) && - (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && - !GeoHelper.isGeoUri(message.getBody()) && - !GeoHelper.isGeoUri(this.body) && - !message.bodyContainsDownloadable() && - !this.bodyContainsDownloadable() && - !message.getBody().startsWith(ME_COMMAND) && - !this.getBody().startsWith(ME_COMMAND) - ); + (message.getType() == Message.TYPE_TEXT && + this.getDownloadable() == null && + message.getDownloadable() == null && + message.getEncryption() != Message.ENCRYPTION_PGP && + this.getType() == message.getType() && + //this.getStatus() == message.getStatus() && + isStatusMergeable(this.getStatus(), message.getStatus()) && + this.getEncryption() == message.getEncryption() && + this.getCounterpart() != null && + this.getCounterpart().equals(message.getCounterpart()) && + (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && + !GeoHelper.isGeoUri(message.getBody()) && + !GeoHelper.isGeoUri(this.body) && + !message.bodyContainsDownloadable() && + !this.bodyContainsDownloadable() && + !message.getBody().startsWith(ME_COMMAND) && + !this.getBody().startsWith(ME_COMMAND) && + !this.bodyIsHeart() && + !message.bodyIsHeart() + ); } private static boolean isStatusMergeable(int a, int b) { return a == b || ( - ( a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND) - || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND) - || (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND) - || (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND_RECEIVED) - || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND) - || (a == Message.STATUS_SEND && b == Message.STATUS_SEND_RECEIVED) + (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND) + || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND) + || (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND) + || (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND_RECEIVED) + || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND) + || (a == Message.STATUS_SEND && b == Message.STATUS_SEND_RECEIVED) ); } public String getMergedBody() { final Message next = this.next(); if (this.mergeable(next)) { - return getBody().trim() + '\n' + next.getMergedBody(); + return getBody().trim() + MERGE_SEPARATOR + next.getMergedBody(); } return getBody().trim(); } @@ -435,7 +440,7 @@ public class Message extends AbstractEntity { * "http://example.com/image.jpg text that will not be shown /abc.png" * or more than one image link in one message. */ - if (body.contains(" ")) { + if (body.trim().contains(" ")) { return false; } try { @@ -443,7 +448,7 @@ public class Message extends AbstractEntity { if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) { return false; - } + } String sUrlPath = url.getPath(); if (sUrlPath == null || sUrlPath.isEmpty()) { @@ -457,14 +462,14 @@ public class Message extends AbstractEntity { String[] extensionParts = sLastUrlPath.split("\\."); if (extensionParts.length == 2 && Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains( - extensionParts[extensionParts.length - 1])) { + extensionParts[extensionParts.length - 1])) { return true; } else if (extensionParts.length == 3 && Arrays .asList(Downloadable.VALID_CRYPTO_EXTENSIONS) .contains(extensionParts[extensionParts.length - 1]) && Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains( - extensionParts[extensionParts.length - 2])) { + extensionParts[extensionParts.length - 2])) { return true; } else { return false; @@ -474,6 +479,10 @@ public class Message extends AbstractEntity { } } + public boolean bodyIsHeart() { + return body != null && UIHelper.HEARTS.contains(body.trim()); + } + public ImageParams getImageParams() { ImageParams params = getLegacyImageParams(); if (params != null) { diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index addee8db2..d867a3708 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -343,8 +343,6 @@ public class MucOptions { setError(ERROR_BANNED); } else if (error != null && error.hasChild("registration-required")) { setError(ERROR_MEMBERS_ONLY); - } else { - setError(ERROR_UNKNOWN); } } } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 6bc629b58..d7366daa8 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -91,7 +91,7 @@ public class IqGenerator extends AbstractGenerator { return publish("urn:xmpp:avatar:metadata", item); } - public IqPacket retrieveAvatar(final Avatar avatar) { + public IqPacket retrievePepAvatar(final Avatar avatar) { final Element item = new Element("item"); item.setAttribute("id", avatar.sha1sum); final IqPacket packet = retrieve("urn:xmpp:avatar:data", item); @@ -99,6 +99,13 @@ public class IqGenerator extends AbstractGenerator { return packet; } + public IqPacket retrieveVcardAvatar(final Avatar avatar) { + final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + packet.setTo(avatar.owner); + packet.addChild("vCard","vcard-temp"); + return packet; + } + public IqPacket retrieveAvatarMetaData(final Jid to) { final IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null); if (to != null) { diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java index 526005f33..c40c6d051 100644 --- a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java @@ -49,7 +49,7 @@ public class PresenceGenerator extends AbstractGenerator { Element cap = packet.addChild("c", "http://jabber.org/protocol/caps"); cap.setAttribute("hash", "sha-1"); - cap.setAttribute("node", "http://conversions.im"); + cap.setAttribute("node", "http://conversations.im"); cap.setAttribute("ver", capHash); } return packet; @@ -61,4 +61,4 @@ public class PresenceGenerator extends AbstractGenerator { packet.setAttribute("type","unavailable"); return packet; } -} \ No newline at end of file +} diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 76d014688..7870fdbff 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -494,7 +494,7 @@ public class MessageParser extends AbstractParser implements } else { Contact contact = account.getRoster().getContact( from); - contact.setAvatar(avatar.getFilename()); + contact.setAvatar(avatar); mXmppConnectionService.getAvatarService().clear( contact); mXmppConnectionService.updateConversationUi(); diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 7505b091b..f7872210d 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -13,6 +13,7 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnPresencePacketReceived; import eu.siacs.conversations.xmpp.jid.Jid; +import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; public class PresenceParser extends AbstractParser implements @@ -101,6 +102,20 @@ public class PresenceParser extends AbstractParser implements if (nick != null) { contact.setPresenceName(nick.getContent()); } + Element x = packet.findChild("x","vcard-temp:x:update"); + Avatar avatar = Avatar.parsePresence(x); + if (avatar != null && !contact.isSelf()) { + avatar.owner = from.toBareJid(); + if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { + if (contact.setAvatar(avatar)) { + mXmppConnectionService.getAvatarService().clear(contact); + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.updateRosterUi(); + } + } else { + mXmppConnectionService.fetchAvatar(account,avatar); + } + } mXmppConnectionService.updateRosterUi(); } diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 28e1c47e7..ed88e434e 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -4,11 +4,13 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Roster; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; import android.content.Context; @@ -16,13 +18,14 @@ import android.database.Cursor; import android.database.sqlite.SQLiteCantOpenDatabaseException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; public class DatabaseBackend extends SQLiteOpenHelper { private static DatabaseBackend instance = null; private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 13; + private static final int DATABASE_VERSION = 14; private static String CREATE_CONTATCS_STATEMENT = "create table " + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " @@ -130,6 +133,88 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL("delete from "+Contact.TABLENAME); db.execSQL("update "+Account.TABLENAME+" set "+Account.ROSTERVERSION+" = NULL"); } + if (oldVersion < 14 && newVersion >= 14) { + // migrate db to new, canonicalized JID domainpart representation + + // Conversation table + Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME, new String[0]); + while(cursor.moveToNext()) { + String newJid; + try { + newJid = Jid.fromString( + cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID)) + ).toString(); + } catch (InvalidJidException ignored) { + Log.e(Config.LOGTAG, "Failed to migrate Conversation CONTACTJID " + +cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID)) + +": " + ignored +". Skipping..."); + continue; + } + + String updateArgs[] = { + newJid, + cursor.getString(cursor.getColumnIndex(Conversation.UUID)), + }; + db.execSQL("update " + Conversation.TABLENAME + + " set " + Conversation.CONTACTJID + " = ? " + + " where " + Conversation.UUID + " = ?", updateArgs); + } + cursor.close(); + + // Contact table + cursor = db.rawQuery("select * from " + Contact.TABLENAME, new String[0]); + while(cursor.moveToNext()) { + String newJid; + try { + newJid = Jid.fromString( + cursor.getString(cursor.getColumnIndex(Contact.JID)) + ).toString(); + } catch (InvalidJidException ignored) { + Log.e(Config.LOGTAG, "Failed to migrate Contact JID " + +cursor.getString(cursor.getColumnIndex(Contact.JID)) + +": " + ignored +". Skipping..."); + continue; + } + + String updateArgs[] = { + newJid, + cursor.getString(cursor.getColumnIndex(Contact.ACCOUNT)), + cursor.getString(cursor.getColumnIndex(Contact.JID)), + }; + db.execSQL("update " + Contact.TABLENAME + + " set " + Contact.JID + " = ? " + + " where " + Contact.ACCOUNT + " = ? " + + " AND " + Contact.JID + " = ?", updateArgs); + } + cursor.close(); + + // Account table + cursor = db.rawQuery("select * from " + Account.TABLENAME, new String[0]); + while(cursor.moveToNext()) { + String newServer; + try { + newServer = Jid.fromParts( + cursor.getString(cursor.getColumnIndex(Account.USERNAME)), + cursor.getString(cursor.getColumnIndex(Account.SERVER)), + "mobile" + ).getDomainpart(); + } catch (InvalidJidException ignored) { + Log.e(Config.LOGTAG, "Failed to migrate Account SERVER " + +cursor.getString(cursor.getColumnIndex(Account.SERVER)) + +": " + ignored +". Skipping..."); + continue; + } + + String updateArgs[] = { + newServer, + cursor.getString(cursor.getColumnIndex(Account.UUID)), + }; + db.execSQL("update " + Account.TABLENAME + + " set " + Account.SERVER + " = ? " + + " where " + Account.UUID + " = ?", updateArgs); + } + cursor.close(); + } } public static synchronized DatabaseBackend getInstance(Context context) { diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index fc40ce756..e111da955 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -85,7 +85,11 @@ public class NotificationService { i.putExtra("messageType", "PEBBLE_ALERT"); i.putExtra("sender", "Conversations"); /* XXX: Shouldn't be hardcoded, e.g., AbstractGenerator.APP_NAME); */ i.putExtra("notificationData", notificationData); - + // notify Pebble App + i.setPackage("com.getpebble.android"); + mXmppConnectionService.sendBroadcast(i); + // notify Gadgetbridge + i.setPackage("nodomain.freeyourgadget.gadgetbridge"); mXmppConnectionService.sendBroadcast(i); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index ec0b3f928..63d9ba7a8 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -41,6 +41,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Hashtable; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -210,6 +211,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager( this); private AvatarService mAvatarService = new AvatarService(this); + private final List mInProgressAvatarFetches = new ArrayList<>(); private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this); private OnConversationUpdate mOnConversationUpdate = null; private Integer convChangedListenerCount = 0; @@ -328,7 +330,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa message.setCounterpart(conversation.getNextCounterpart()); } if (encryption == Message.ENCRYPTION_DECRYPTED) { - getPgpEngine().encrypt(message,callback); + getPgpEngine().encrypt(message, callback); } else { callback.success(message); } @@ -347,7 +349,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } message.setCounterpart(conversation.getNextCounterpart()); message.setType(Message.TYPE_FILE); - message.setStatus(Message.STATUS_OFFERED); String path = getFileBackend().getOriginalPath(uri); if (path!=null) { message.setRelativeFilePath(path); @@ -390,7 +391,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } message.setCounterpart(conversation.getNextCounterpart()); message.setType(Message.TYPE_IMAGE); - message.setStatus(Message.STATUS_OFFERED); new Thread(new Runnable() { @Override @@ -422,6 +422,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa final String action = intent == null ? null : intent.getAction(); if (action != null) { switch (action) { + case ConnectivityManager.CONNECTIVITY_ACTION: + if (hasInternetConnection() && Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) { + resetAllAttemptCounts(true); + } + break; case ACTION_MERGE_PHONE_CONTACTS: if (mRestoredFromDatabase) { PhoneHelper.loadPhoneContacts(getApplicationContext(), @@ -440,14 +445,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa toggleForegroundService(); break; case ACTION_TRY_AGAIN: - for(Account account : accounts) { - if (account.hasErrorStatus()) { - final XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - connection.resetAttemptCount(); - } - } - } + resetAllAttemptCounts(false); break; case ACTION_DISABLE_ACCOUNT: try { @@ -529,6 +527,18 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return START_STICKY; } + private void resetAllAttemptCounts(boolean reallyAll) { + Log.d(Config.LOGTAG,"resetting all attepmt counts"); + for(Account account : accounts) { + if (account.hasErrorStatus() || reallyAll) { + final XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.resetAttemptCount(); + } + } + } + } + public boolean hasInternetConnection() { ConnectivityManager cm = (ConnectivityManager) getApplicationContext() .getSystemService(Context.CONNECTIVITY_SERVICE); @@ -801,7 +811,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa Presences presences = contact.getPresences(); if ((message.getCounterpart() != null) && (presences.has(message.getCounterpart().getResourcepart()))) { - markMessage(message, Message.STATUS_OFFERED); mJingleConnectionManager.createNewConnection(message); } else { if (presences.size() == 1) { @@ -811,7 +820,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } catch (InvalidJidException e) { return; } - markMessage(message, Message.STATUS_OFFERED); mJingleConnectionManager.createNewConnection(message); } } @@ -1867,6 +1875,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa IqPacket result) { if (result.getType() == IqPacket.TYPE.RESULT) { if (account.setAvatar(avatar.getFilename())) { + getAvatarService().clear(account); databaseBackend.updateAccount(account); } callback.success(avatar); @@ -1893,13 +1902,39 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa fetchAvatar(account, avatar, null); } - public void fetchAvatar(Account account, final Avatar avatar, - final UiCallback callback) { - IqPacket packet = this.mIqGenerator.retrieveAvatar(avatar); + private static String generateFetchKey(Account account, final Avatar avatar) { + return account.getJid().toBareJid()+"_"+avatar.owner+"_"+avatar.sha1sum; + } + + public void fetchAvatar(Account account, final Avatar avatar, final UiCallback callback) { + final String KEY = generateFetchKey(account, avatar); + synchronized(this.mInProgressAvatarFetches) { + if (this.mInProgressAvatarFetches.contains(KEY)) { + return; + } else { + switch (avatar.origin) { + case PEP: + this.mInProgressAvatarFetches.add(KEY); + fetchAvatarPep(account, avatar, callback); + break; + case VCARD: + this.mInProgressAvatarFetches.add(KEY); + fetchAvatarVcard(account, avatar, callback); + break; + } + } + } + } + + private void fetchAvatarPep(Account account, final Avatar avatar, final UiCallback callback) { + IqPacket packet = this.mIqGenerator.retrievePepAvatar(avatar); sendIqPacket(account, packet, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket result) { + synchronized (mInProgressAvatarFetches) { + mInProgressAvatarFetches.remove(generateFetchKey(account, avatar)); + } final String ERROR = account.getJid().toBareJid() + ": fetching avatar for " + avatar.owner + " failed "; if (result.getType() == IqPacket.TYPE.RESULT) { @@ -1916,7 +1951,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } else { Contact contact = account.getRoster() .getContact(avatar.owner); - contact.setAvatar(avatar.getFilename()); + contact.setAvatar(avatar); getAvatarService().clear(contact); updateConversationUi(); updateRosterUi(); @@ -1925,8 +1960,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa callback.success(avatar); } Log.d(Config.LOGTAG, account.getJid().toBareJid() - + ": succesfully fetched avatar for " - + avatar.owner); + + ": succesfuly fetched pep avatar for " + avatar.owner); return; } } else { @@ -1949,8 +1983,38 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa }); } - public void checkForAvatar(Account account, - final UiCallback callback) { + private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback callback) { + IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar); + this.sendIqPacket(account,packet,new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + synchronized(mInProgressAvatarFetches) { + mInProgressAvatarFetches.remove(generateFetchKey(account,avatar)); + } + if (packet.getType() == IqPacket.TYPE.RESULT) { + Element vCard = packet.findChild("vCard","vcard-temp"); + Element photo = vCard != null ? vCard.findChild("PHOTO") : null; + Element binval = photo != null ? photo.findChild("BINVAL") : null; + String image = binval != null ? binval.getContent() : null; + if (image != null) { + avatar.image = image; + if (getFileBackend().save(avatar)) { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + + ": successfully fetched vCard avatar for " + avatar.owner); + Contact contact = account.getRoster() + .getContact(avatar.owner); + contact.setAvatar(avatar); + getAvatarService().clear(contact); + updateConversationUi(); + updateRosterUi(); + } + } + } + } + }); + } + + public void checkForAvatar(Account account, final UiCallback callback) { IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null); this.sendIqPacket(account, packet, new OnIqPacketReceived() { @@ -1972,7 +2036,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa getAvatarService().clear(account); callback.success(avatar); } else { - fetchAvatar(account, avatar, callback); + fetchAvatarPep(account, avatar, callback); } return; } @@ -2008,6 +2072,16 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa disconnect(account, force); } if (!account.isOptionSet(Account.OPTION_DISABLED)) { + + synchronized (this.mInProgressAvatarFetches) { + for(Iterator iterator = this.mInProgressAvatarFetches.iterator(); iterator.hasNext();) { + final String KEY = iterator.next(); + if (KEY.startsWith(account.getJid().toBareJid()+"_")) { + iterator.remove(); + } + } + } + if (account.getXmppConnection() == null) { account.setXmppConnection(createConnection(account)); } @@ -2031,6 +2105,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public void invite(Conversation conversation, Jid contact) { + Log.d(Config.LOGTAG,conversation.getAccount().getJid().toBareJid()+": inviting "+contact+" to "+conversation.getJid().toBareJid()); MessagePacket packet = mMessageGenerator.invite(conversation, contact); sendMessagePacket(conversation.getAccount(), packet); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 8c4f6eafe..07b8819d9 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -385,6 +385,10 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers @Override void onBackendConnected() { + if (mPendingConferenceInvite != null) { + mPendingConferenceInvite.execute(this); + mPendingConferenceInvite = null; + } if (getIntent().getAction().equals(ACTION_VIEW_MUC)) { this.uuid = getIntent().getExtras().getString("uuid"); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index aec755fce..1b5e51781 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -60,11 +60,11 @@ public class ConversationActivity extends XmppActivity public static final int REQUEST_SEND_MESSAGE = 0x0201; public static final int REQUEST_DECRYPT_PGP = 0x0202; public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207; - private static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301; - private static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302; - private static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303; - private static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304; - private static final int ATTACHMENT_CHOICE_LOCATION = 0x0305; + public static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301; + public static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302; + public static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303; + public static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304; + public static final int ATTACHMENT_CHOICE_LOCATION = 0x0305; private static final String STATE_OPEN_CONVERSATION = "state_open_conversation"; private static final String STATE_PANEL_OPEN = "state_panel_open"; private static final String STATE_PENDING_URI = "state_pending_uri"; @@ -398,61 +398,88 @@ public class ConversationActivity extends XmppActivity } private void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) { + final OnPresenceSelected callback = new OnPresenceSelected() { + + @Override + public void onPresenceSelected() { + Intent intent = new Intent(); + boolean chooser = false; + String fallbackPackageId = null; + switch (attachmentChoice) { + case ATTACHMENT_CHOICE_CHOOSE_IMAGE: + intent.setAction(Intent.ACTION_GET_CONTENT); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE,true); + } + intent.setType("image/*"); + chooser = true; + break; + case ATTACHMENT_CHOICE_TAKE_PHOTO: + Uri uri = xmppConnectionService.getFileBackend().getTakePhotoUri(); + intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); + intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); + mPendingImageUris.clear(); + mPendingImageUris.add(uri); + break; + case ATTACHMENT_CHOICE_CHOOSE_FILE: + chooser = true; + intent.setType("*/*"); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setAction(Intent.ACTION_GET_CONTENT); + break; + case ATTACHMENT_CHOICE_RECORD_VOICE: + intent.setAction(MediaStore.Audio.Media.RECORD_SOUND_ACTION); + fallbackPackageId = "eu.siacs.conversations.voicerecorder"; + break; + case ATTACHMENT_CHOICE_LOCATION: + intent.setAction("eu.siacs.conversations.location.request"); + fallbackPackageId = "eu.siacs.conversations.sharelocation"; + break; + } + if (intent.resolveActivity(getPackageManager()) != null) { + if (chooser) { + startActivityForResult( + Intent.createChooser(intent, getString(R.string.perform_action_with)), + attachmentChoice); + } else { + startActivityForResult(intent, attachmentChoice); + } + } else if (fallbackPackageId != null) { + startActivity(getInstallApkIntent(fallbackPackageId)); + } + } + }; if (attachmentChoice == ATTACHMENT_CHOICE_LOCATION && encryption != Message.ENCRYPTION_OTR) { getSelectedConversation().setNextCounterpart(null); - Intent intent = new Intent("eu.siacs.conversations.location.request"); - startActivityForResult(intent,attachmentChoice); + callback.onPresenceSelected(); } else { - selectPresence(getSelectedConversation(), new OnPresenceSelected() { - - @Override - public void onPresenceSelected() { - Intent intent = new Intent(); - boolean chooser = false; - switch (attachmentChoice) { - case ATTACHMENT_CHOICE_CHOOSE_IMAGE: - intent.setAction(Intent.ACTION_GET_CONTENT); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE,true); - } - intent.setType("image/*"); - chooser = true; - break; - case ATTACHMENT_CHOICE_TAKE_PHOTO: - Uri uri = xmppConnectionService.getFileBackend().getTakePhotoUri(); - intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); - intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); - mPendingImageUris.clear(); - mPendingImageUris.add(uri); - break; - case ATTACHMENT_CHOICE_CHOOSE_FILE: - chooser = true; - intent.setType("*/*"); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setAction(Intent.ACTION_GET_CONTENT); - break; - case ATTACHMENT_CHOICE_RECORD_VOICE: - intent.setAction(MediaStore.Audio.Media.RECORD_SOUND_ACTION); - break; - case ATTACHMENT_CHOICE_LOCATION: - intent.setAction("eu.siacs.conversations.location.request"); - break; - } - if (intent.resolveActivity(getPackageManager()) != null) { - if (chooser) { - startActivityForResult( - Intent.createChooser(intent, getString(R.string.perform_action_with)), - attachmentChoice); - } else { - startActivityForResult(intent, attachmentChoice); - } - } - } - }); + selectPresence(getSelectedConversation(),callback); } } - private void attachFile(final int attachmentChoice) { + private Intent getInstallApkIntent(final String packageId) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("market://details?id="+packageId)); + if (intent.resolveActivity(getPackageManager()) != null) { + return intent; + } else { + intent.setData(Uri.parse("http://play.google.com/store/apps/details?id="+packageId)); + return intent; + } + } + + public void attachFile(final int attachmentChoice) { + switch (attachmentChoice) { + case ATTACHMENT_CHOICE_LOCATION: + getPreferences().edit().putString("recently_used_quick_action","location").apply(); + break; + case ATTACHMENT_CHOICE_RECORD_VOICE: + getPreferences().edit().putString("recently_used_quick_action","voice").apply(); + break; + case ATTACHMENT_CHOICE_TAKE_PHOTO: + getPreferences().edit().putString("recently_used_quick_action","photo").apply(); + break; + } final Conversation conversation = getSelectedConversation(); final int encryption = conversation.getNextEncryption(forceEncryption()); if (encryption == Message.ENCRYPTION_PGP) { @@ -875,6 +902,12 @@ public class ConversationActivity extends XmppActivity void onBackendConnected() { this.xmppConnectionService.getNotificationService().setIsInForeground(true); updateConversationList(); + + if (mPendingConferenceInvite != null) { + mPendingConferenceInvite.execute(this); + mPendingConferenceInvite = null; + } + if (xmppConnectionService.getAccounts().size() == 0) { if (!mRedirected) { this.mRedirected = true; @@ -901,9 +934,7 @@ public class ConversationActivity extends XmppActivity } this.mConversationFragment.reInit(getSelectedConversation()); mOpenConverstaion = null; - } else if (getSelectedConversation() != null) { - this.mConversationFragment.reInit(getSelectedConversation()); - } else { + } else if (getSelectedConversation() == null) { showConversationsOverview(); mPendingImageUris.clear(); mPendingFileUris.clear(); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 5b1e9b4d6..20fc17506 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -59,6 +59,7 @@ 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.GeoHelper; +import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jid.Jid; @@ -117,16 +118,37 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } + private int getIndexOf(String uuid, List messages) { + if (uuid == null) { + return 0; + } + for(int i = 0; i < messages.size(); ++i) { + if (uuid.equals(messages.get(i).getUuid())) { + return i; + } else { + Message next = messages.get(i); + while(next != null && next.wasMergedIntoPrevious()) { + if (uuid.equals(next.getUuid())) { + return i; + } + next = next.next(); + } + + } + } + return 0; + } + @Override public void onScroll(AbsListView view, int firstVisibleItem, - int visibleItemCount, int totalItemCount) { + int visibleItemCount, int totalItemCount) { synchronized (ConversationFragment.this.messageList) { if (firstVisibleItem < 5 && messagesLoaded && messageList.size() > 0) { long timestamp = ConversationFragment.this.messageList.get(0).getTimeSent(); messagesLoaded = false; activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() { @Override - public void onMoreMessagesLoaded(final int count, Conversation conversation) { + public void onMoreMessagesLoaded(final int c, Conversation conversation) { if (ConversationFragment.this.conversation != conversation) { return; } @@ -134,29 +156,18 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa @Override public void run() { final int oldPosition = messagesView.getFirstVisiblePosition(); + Message message = messageList.get(oldPosition); + String uuid = message != null ? message.getUuid() : null; View v = messagesView.getChildAt(0); final int pxOffset = (v == null) ? 0 : v.getTop(); ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList); updateStatusMessages(); messageListAdapter.notifyDataSetChanged(); - if (count != 0) { - final int newPosition = oldPosition + count; - int offset = 0; - try { - Message tmpMessage = messageList.get(newPosition); - - while(tmpMessage.wasMergedIntoPrevious()) { - offset++; - tmpMessage = tmpMessage.prev(); - } - } catch (final IndexOutOfBoundsException ignored) { - - } - messagesView.setSelectionFromTop(newPosition - offset, pxOffset); - messagesLoaded = true; - if (messageLoaderToast != null) { - messageLoaderToast.cancel(); - } + int pos = getIndexOf(uuid,messageList); + messagesView.setSelectionFromTop(pos, pxOffset); + messagesLoaded = true; + if (messageLoaderToast != null) { + messageLoaderToast.cancel(); } } }); @@ -174,7 +185,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (ConversationFragment.this.conversation != conversation) { return; } - messageLoaderToast = Toast.makeText(activity,resId,Toast.LENGTH_LONG); + messageLoaderToast = Toast.makeText(activity, resId, Toast.LENGTH_LONG); messageLoaderToast.show(); } }); @@ -208,7 +219,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa @Override public void onClick(View v) { - activity.verifyOtrSessionDialog(conversation,v); + activity.verifyOtrSessionDialog(conversation, v); } }; private ConcurrentLinkedQueue mEncryptedMessages = new ConcurrentLinkedQueue<>(); @@ -219,7 +230,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (actionId == EditorInfo.IME_ACTION_SEND) { InputMethodManager imm = (InputMethodManager) v.getContext() - .getSystemService(Context.INPUT_METHOD_SERVICE); + .getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(v.getWindowToken(), 0); sendMessage(); return true; @@ -232,15 +243,39 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa @Override public void onClick(View v) { - sendMessage(); + Object tag = v.getTag(); + if (tag instanceof SendButtonAction) { + SendButtonAction action = (SendButtonAction) tag; + switch (action) { + case TAKE_PHOTO: + activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_TAKE_PHOTO); + break; + case SEND_LOCATION: + activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_LOCATION); + break; + case RECORD_VOICE: + activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_RECORD_VOICE); + break; + case CANCEL: + if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { + conversation.setNextCounterpart(null); + updateChatMsgHint(); + updateSendButton(); + } + break; + default: + sendMessage(); + } + } else { + sendMessage(); + } } }; private OnClickListener clickToMuc = new OnClickListener() { @Override public void onClick(View v) { - Intent intent = new Intent(getActivity(), - ConferenceDetailsActivity.class); + Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class); intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); intent.putExtra("uuid", conversation.getUuid()); startActivity(intent); @@ -253,16 +288,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (this.conversation == null) { return; } - if (mEditMessage.getText().length() < 1) { - if (this.conversation.getMode() == Conversation.MODE_MULTI) { - conversation.setNextCounterpart(null); - updateChatMsgHint(); - } - return; - } Message message = new Message(conversation, mEditMessage.getText() .toString(), conversation.getNextEncryption(activity - .forceEncryption())); + .forceEncryption())); if (conversation.getMode() == Conversation.MODE_MULTI) { if (conversation.getNextCounterpart() != null) { message.setCounterpart(conversation.getNextCounterpart()); @@ -282,13 +310,13 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getNextCounterpart() != null) { this.mEditMessage.setHint(getString( - R.string.send_private_message_to, - conversation.getNextCounterpart().getResourcepart())); + R.string.send_private_message_to, + conversation.getNextCounterpart().getResourcepart())); } else { switch (conversation.getNextEncryption(activity.forceEncryption())) { case Message.ENCRYPTION_NONE: mEditMessage - .setHint(getString(R.string.send_plain_text_message)); + .setHint(getString(R.string.send_plain_text_message)); break; case Message.ENCRYPTION_OTR: mEditMessage.setHint(getString(R.string.send_otr_message)); @@ -304,7 +332,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } private void setupIme() { - if (((ConversationActivity)getActivity()).usingEnterKey()) { + if (((ConversationActivity) getActivity()).usingEnterKey()) { mEditMessage.setInputType(mEditMessage.getInputType() & (~InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE)); } else { mEditMessage.setInputType(mEditMessage.getInputType() | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE); @@ -313,8 +341,8 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa @Override public View onCreateView(final LayoutInflater inflater, - ViewGroup container, Bundle savedInstanceState) { - final View view = inflater.inflate(R.layout.fragment_conversation,container, false); + ViewGroup container, Bundle savedInstanceState) { + final View view = inflater.inflate(R.layout.fragment_conversation, container, false); view.setOnClickListener(null); mEditMessage = (EditMessage) view.findViewById(R.id.textinput); setupIme(); @@ -365,21 +393,21 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } }); messageListAdapter - .setOnContactPictureLongClicked(new OnContactPictureLongClicked() { + .setOnContactPictureLongClicked(new OnContactPictureLongClicked() { - @Override - public void onContactPictureLongClicked(Message message) { - if (message.getStatus() <= Message.STATUS_RECEIVED) { - if (message.getConversation().getMode() == Conversation.MODE_MULTI) { - if (message.getCounterpart() != null) { - privateMessageWith(message.getCounterpart()); + @Override + public void onContactPictureLongClicked(Message message) { + if (message.getStatus() <= Message.STATUS_RECEIVED) { + if (message.getConversation().getMode() == Conversation.MODE_MULTI) { + if (message.getCounterpart() != null) { + privateMessageWith(message.getCounterpart()); + } } + } else { + activity.showQrCode(); } - } else { - activity.showQrCode(); } - } - }); + }); messagesView.setAdapter(messageListAdapter); registerForContextMenu(messagesView); @@ -389,7 +417,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa @Override public void onCreateContextMenu(ContextMenu menu, View v, - ContextMenuInfo menuInfo) { + ContextMenuInfo menuInfo) { synchronized (this.messageList) { super.onCreateContextMenu(menu, v, menuInfo); AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; @@ -416,7 +444,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if ((m.getType() == Message.TYPE_TEXT || m.getType() == Message.TYPE_PRIVATE || m.getDownloadable() != null) - && (!GeoHelper.isGeoUri(m.getBody()))) { + && (!GeoHelper.isGeoUri(m.getBody()))) { shareWith.setVisible(false); } if (m.getStatus() != Message.STATUS_SEND_FAILED) { @@ -425,17 +453,17 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (((m.getType() != Message.TYPE_IMAGE && m.getDownloadable() == null) || m.getImageParams().url == null) && !GeoHelper.isGeoUri(m.getBody())) { copyUrl.setVisible(false); - } + } if (m.getType() != Message.TYPE_TEXT || m.getDownloadable() != null || !m.bodyContainsDownloadable()) { downloadImage.setVisible(false); - } + } if (!((m.getDownloadable() != null && !(m.getDownloadable() instanceof DownloadablePlaceholder)) - || (m.isFileOrImage() && (m.getStatus() == Message.STATUS_WAITING - || m.getStatus() == Message.STATUS_OFFERED)))) { + || (m.isFileOrImage() && (m.getStatus() == Message.STATUS_WAITING + || m.getStatus() == Message.STATUS_OFFERED)))) { cancelTransmission.setVisible(false); - } + } } } @@ -483,12 +511,12 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } shareIntent.setType(mime); } - activity.startActivity(Intent.createChooser(shareIntent,getText(R.string.share_with))); + activity.startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with))); } private void copyText(Message message) { if (activity.copyTextToClipboard(message.getMergedBody(), - R.string.message_text)) { + R.string.message_text)) { Toast.makeText(activity, R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT).show(); } @@ -498,7 +526,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) { DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); if (!file.exists()) { - Toast.makeText(activity,R.string.file_deleted,Toast.LENGTH_SHORT).show(); + Toast.makeText(activity, R.string.file_deleted, Toast.LENGTH_SHORT).show(); message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED)); return; } @@ -519,20 +547,20 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (activity.copyTextToClipboard(url, resId)) { Toast.makeText(activity, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show(); - } + } } private void downloadImage(Message message) { activity.xmppConnectionService.getHttpConnectionManager() - .createNewConnection(message); + .createNewConnection(message); } private void cancelTransmission(Message message) { Downloadable downloadable = message.getDownloadable(); - if (downloadable!=null) { + if (downloadable != null) { downloadable.cancel(); } else { - activity.xmppConnectionService.markMessage(message,Message.STATUS_SEND_FAILED); + activity.xmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); } } @@ -540,6 +568,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa this.mEditMessage.setText(""); this.conversation.setNextCounterpart(counterpart); updateChatMsgHint(); + updateSendButton(); } protected void highlightInConference(String nick) { @@ -548,9 +577,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa mEditMessage.getText().insert(0, nick + ": "); } else { if (mEditMessage.getText().charAt( - mEditMessage.getSelectionStart() - 1) != ' ') { + mEditMessage.getSelectionStart() - 1) != ' ') { nick = " " + nick; - } + } mEditMessage.getText().insert(mEditMessage.getSelectionStart(), nick + " "); } @@ -563,7 +592,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (this.conversation != null) { final String msg = mEditMessage.getText().toString(); this.conversation.setNextMessage(msg); - updateChatState(this.conversation,msg); + updateChatState(this.conversation, msg); } } @@ -586,7 +615,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa final String msg = mEditMessage.getText().toString(); this.conversation.setNextMessage(msg); if (this.conversation != conversation) { - updateChatState(this.conversation,msg); + updateChatState(this.conversation, msg); } this.conversation.trim(); } @@ -632,7 +661,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa @Override public void onClick(View v) { - final Contact contact = conversation == null ? null :conversation.getContact(); + final Contact contact = conversation == null ? null : conversation.getContact(); if (contact != null) { activity.xmppConnectionService.createContact(contact); activity.switchToContactDetails(contact); @@ -655,7 +684,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT); intent.putExtra("contact", conversation.getContact().getJid().toBareJid().toString()); intent.putExtra("account", conversation.getAccount().getJid().toBareJid().toString()); - intent.putExtra("mode",VerifyOTRActivity.MODE_ANSWER_QUESTION); + intent.putExtra("mode", VerifyOTRActivity.MODE_ANSWER_QUESTION); startActivity(intent); } }; @@ -665,11 +694,11 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa final Contact contact = conversation.getContact(); final int mode = conversation.getMode(); if (conversation.isBlocked()) { - showSnackbar(R.string.contact_blocked, R.string.unblock,this.mUnblockClickListener); + showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener); } else if (!contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { - showSnackbar(R.string.contact_added_you, R.string.add_back,this.mAddBackClickListener); + showSnackbar(R.string.contact_added_you, R.string.add_back, this.mAddBackClickListener); } else if (mode == Conversation.MODE_MULTI - &&!conversation.getMucOptions().online() + && !conversation.getMucOptions().online() && account.getStatus() == Account.State.ONLINE) { switch (conversation.getMucOptions().getError()) { case MucOptions.ERROR_NICK_IN_USE: @@ -693,18 +722,18 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa default: break; } - } else if (askForPassphraseIntent != null ) { - showSnackbar(R.string.openpgp_messages_found,R.string.decrypt, clickToDecryptListener); + } else if (askForPassphraseIntent != null) { + showSnackbar(R.string.openpgp_messages_found, R.string.decrypt, clickToDecryptListener); } else if (mode == Conversation.MODE_SINGLE && conversation.smpRequested()) { - showSnackbar(R.string.smp_requested, R.string.verify,this.mAnswerSmpClickListener); + showSnackbar(R.string.smp_requested, R.string.verify, this.mAnswerSmpClickListener); } else if (mode == Conversation.MODE_SINGLE - &&conversation.hasValidOtrSession() + && conversation.hasValidOtrSession() && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!conversation.isOtrFingerprintVerified())) { showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, clickToVerify); } else if (conversation.isMuted()) { - showSnackbar(R.string.notifications_disabled, R.string.enable,this.mUnmuteClickListener); + showSnackbar(R.string.notifications_disabled, R.string.enable, this.mUnmuteClickListener); } else { hideSnackbar(); } @@ -722,12 +751,12 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa for (final Message message : this.messageList) { if (message.getEncryption() == Message.ENCRYPTION_PGP && (message.getStatus() == Message.STATUS_RECEIVED || message - .getStatus() >= Message.STATUS_SEND) + .getStatus() >= Message.STATUS_SEND) && message.getDownloadable() == null) { if (!mEncryptedMessages.contains(message)) { mEncryptedMessages.add(message); } - } + } } decryptNext(); updateStatusMessages(); @@ -790,53 +819,128 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa updateChatMsgHint(); } + enum SendButtonAction {TEXT, TAKE_PHOTO, SEND_LOCATION, RECORD_VOICE, CANCEL} + + private int getSendButtonImageResource(SendButtonAction action, int status) { + switch (action) { + case TEXT: + switch (status) { + case Presences.CHAT: + case Presences.ONLINE: + return R.drawable.ic_send_text_online; + case Presences.AWAY: + return R.drawable.ic_send_text_away; + case Presences.XA: + case Presences.DND: + return R.drawable.ic_send_text_dnd; + default: + return R.drawable.ic_send_text_offline; + } + case TAKE_PHOTO: + switch (status) { + case Presences.CHAT: + case Presences.ONLINE: + return R.drawable.ic_send_photo_online; + case Presences.AWAY: + return R.drawable.ic_send_photo_away; + case Presences.XA: + case Presences.DND: + return R.drawable.ic_send_photo_dnd; + default: + return R.drawable.ic_send_photo_offline; + } + case RECORD_VOICE: + switch (status) { + case Presences.CHAT: + case Presences.ONLINE: + return R.drawable.ic_send_voice_online; + case Presences.AWAY: + return R.drawable.ic_send_voice_away; + case Presences.XA: + case Presences.DND: + return R.drawable.ic_send_voice_dnd; + default: + return R.drawable.ic_send_voice_offline; + } + case SEND_LOCATION: + switch (status) { + case Presences.CHAT: + case Presences.ONLINE: + return R.drawable.ic_send_location_online; + case Presences.AWAY: + return R.drawable.ic_send_location_away; + case Presences.XA: + case Presences.DND: + return R.drawable.ic_send_location_dnd; + default: + return R.drawable.ic_send_location_offline; + } + case CANCEL: + switch (status) { + case Presences.CHAT: + case Presences.ONLINE: + return R.drawable.ic_send_cancel_online; + case Presences.AWAY: + return R.drawable.ic_send_cancel_away; + case Presences.XA: + case Presences.DND: + return R.drawable.ic_send_cancel_dnd; + default: + return R.drawable.ic_send_cancel_offline; + } + } + return R.drawable.ic_send_text_offline; + } + public void updateSendButton() { - Conversation c = this.conversation; + final Conversation c = this.conversation; + final SendButtonAction action; + final int status; + final boolean empty = this.mEditMessage == null || this.mEditMessage.getText().length() == 0; + if (c.getMode() == Conversation.MODE_MULTI) { + if (empty && c.getNextCounterpart() != null) { + action = SendButtonAction.CANCEL; + } else { + action = SendButtonAction.TEXT; + } + } else { + if (empty) { + String setting = activity.getPreferences().getString("quick_action","recent"); + if (!setting.equals("none") && UIHelper.receivedLocationQuestion(conversation.getLatestMessage())) { + setting = "location"; + } else if (setting.equals("recent")) { + setting = activity.getPreferences().getString("recently_used_quick_action","text"); + } + switch (setting) { + case "photo": + action = SendButtonAction.TAKE_PHOTO; + break; + case "location": + action = SendButtonAction.SEND_LOCATION; + break; + case "voice": + action = SendButtonAction.RECORD_VOICE; + break; + default: + action = SendButtonAction.TEXT; + break; + } + } else { + action = SendButtonAction.TEXT; + } + } if (activity.useSendButtonToIndicateStatus() && c != null && c.getAccount().getStatus() == Account.State.ONLINE) { if (c.getMode() == Conversation.MODE_SINGLE) { - switch (c.getContact().getMostAvailableStatus()) { - case Presences.CHAT: - this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_online); - break; - case Presences.ONLINE: - this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_online); - break; - case Presences.AWAY: - this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_away); - break; - case Presences.XA: - this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_away); - break; - case Presences.DND: - this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_dnd); - break; - default: - this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_offline); - break; - } - } else if (c.getMode() == Conversation.MODE_MULTI) { - if (c.getMucOptions().online()) { - this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_online); - } else { - this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_offline); - } + status = c.getContact().getMostAvailableStatus(); } else { - this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_offline); + status = c.getMucOptions().online() ? Presences.ONLINE : Presences.OFFLINE; } } else { - this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_offline); + status = Presences.OFFLINE; } + this.mSendButton.setTag(action); + this.mSendButton.setImageResource(getSendButtonImageResource(action, status)); } protected void updateStatusMessages() { @@ -865,7 +969,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } protected void showSnackbar(final int message, final int action, - final OnClickListener clickListener) { + final OnClickListener clickListener) { snackbar.setVisibility(View.VISIBLE); snackbar.setOnClickListener(null); snackbarMessage.setText(message); @@ -897,7 +1001,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa @Override public void userInputRequried(PendingIntent pi, - Contact contact) { + Contact contact) { activity.runIntent( pi, ConversationActivity.REQUEST_ENCRYPT_MESSAGE); @@ -921,11 +1025,11 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa @Override public void onClick(DialogInterface dialog, - int which) { + int which) { conversation - .setNextEncryption(Message.ENCRYPTION_NONE); + .setNextEncryption(Message.ENCRYPTION_NONE); xmppService.databaseBackend - .updateConversation(conversation); + .updateConversation(conversation); message.setEncryption(Message.ENCRYPTION_NONE); xmppService.sendMessage(message); messageSent(); @@ -936,9 +1040,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (conversation.getMucOptions().pgpKeysInUse()) { if (!conversation.getMucOptions().everybodyHasKeys()) { Toast warning = Toast - .makeText(getActivity(), - R.string.missing_public_keys, - Toast.LENGTH_LONG); + .makeText(getActivity(), + R.string.missing_public_keys, + Toast.LENGTH_LONG); warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0); warning.show(); } @@ -950,12 +1054,12 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa @Override public void onClick(DialogInterface dialog, - int which) { + int which) { conversation - .setNextEncryption(Message.ENCRYPTION_NONE); + .setNextEncryption(Message.ENCRYPTION_NONE); message.setEncryption(Message.ENCRYPTION_NONE); xmppService.databaseBackend - .updateConversation(conversation); + .updateConversation(conversation); xmppService.sendMessage(message); messageSent(); } @@ -968,7 +1072,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } public void showNoPGPKeyDialog(boolean plural, - DialogInterface.OnClickListener listener) { + DialogInterface.OnClickListener listener) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setIconAttribute(android.R.attr.alertDialogIcon); if (plural) { @@ -1026,6 +1130,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.COMPOSING)) { activity.xmppConnectionService.sendChatState(conversation); } + updateSendButton(); } @Override @@ -1042,6 +1147,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { activity.xmppConnectionService.sendChatState(conversation); } + updateSendButton(); } } diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 7aa7b1c2e..931a1a2fa 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -223,7 +223,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate if (avatar != null) { intent = new Intent(getApplicationContext(), StartConversationActivity.class); - intent.putExtra("init",true); + if (xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 1) { + intent.putExtra("init", true); + } } else { intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class); diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java index e8ab8dae7..4333dbdb0 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -116,7 +116,9 @@ public class PublishProfilePictureActivity extends XmppActivity { if (mInitialAccountSetup) { Intent intent = new Intent(getApplicationContext(), StartConversationActivity.class); - intent.putExtra("init",true); + if (xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 1) { + intent.putExtra("init", true); + } startActivity(intent); } finish(); diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index a556b8b7f..7863ff945 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -65,6 +65,7 @@ import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; import eu.siacs.conversations.ui.adapter.ListItemAdapter; import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; +import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; @@ -757,14 +758,16 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU } else { activity.contact_context_id = acmi.position; final Blockable contact = (Contact) activity.contacts.get(acmi.position); - final MenuItem blockUnblockItem = menu.findItem(R.id.context_contact_block_unblock); - if (blockUnblockItem != null) { + XmppConnection xmpp = contact.getAccount().getXmppConnection(); + if (xmpp != null && xmpp.getFeatures().blocking()) { if (contact.isBlocked()) { blockUnblockItem.setTitle(R.string.unblock_contact); } else { blockUnblockItem.setTitle(R.string.block_contact); } + } else { + blockUnblockItem.setVisible(false); } } } diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 392e57a70..934c696f0 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -113,6 +113,8 @@ public abstract class XmppActivity extends Activity { } }; + protected ConferenceInvite mPendingConferenceInvite = null; + protected void refreshUi() { final long diff = SystemClock.elapsedRealtime() - mLastUiRefresh; @@ -367,7 +369,7 @@ public abstract class XmppActivity extends Activity { } public void highlightInMuc(Conversation conversation, String nick) { - switchToConversation(conversation,null,nick,false); + switchToConversation(conversation, null, nick, false); } private void switchToConversation(Conversation conversation, String text, String nick, boolean newTask) { @@ -435,7 +437,7 @@ public abstract class XmppActivity extends Activity { @Override public void userInputRequried(PendingIntent pi, - Account account) { + Account account) { try { startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); @@ -446,13 +448,13 @@ public abstract class XmppActivity extends Activity { @Override public void success(Account account) { xmppConnectionService.databaseBackend - .updateAccount(account); + .updateAccount(account); xmppConnectionService.sendPresence(account); if (conversation != null) { conversation - .setNextEncryption(Message.ENCRYPTION_PGP); + .setNextEncryption(Message.ENCRYPTION_PGP); xmppConnectionService.databaseBackend - .updateConversation(conversation); + .updateConversation(conversation); } } @@ -665,32 +667,11 @@ 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) { - try { - String conversationUuid = data.getStringExtra("conversation"); - Conversation conversation = xmppConnectionService - .findConversationByUuid(conversationUuid); - List jids = new ArrayList(); - if (data.getBooleanExtra("multiple", false)) { - String[] toAdd = data.getStringArrayExtra("contacts"); - for (String item : toAdd) { - jids.add(Jid.fromString(item)); - } - } else { - jids.add(Jid.fromString(data.getStringExtra("contact"))); - } - - if (conversation.getMode() == Conversation.MODE_MULTI) { - for (Jid jid : jids) { - xmppConnectionService.invite(conversation, jid); - } - } else { - jids.add(conversation.getJid().toBareJid()); - xmppConnectionService.createAdhocConference(conversation.getAccount(), jids, adhocCallback); - } - } catch (final InvalidJidException ignored) { - + if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) { + mPendingConferenceInvite = ConferenceInvite.parse(data); + if (xmppConnectionServiceBound && mPendingConferenceInvite != null) { + mPendingConferenceInvite.execute(this); + mPendingConferenceInvite = null; } } } @@ -855,6 +836,48 @@ public abstract class XmppActivity extends Activity { } } + public static class ConferenceInvite { + private String uuid; + private List jids = new ArrayList<>(); + + public static ConferenceInvite parse(Intent data) { + ConferenceInvite invite = new ConferenceInvite(); + invite.uuid = data.getStringExtra("conversation"); + if (invite.uuid == null) { + return null; + } + try { + if (data.getBooleanExtra("multiple", false)) { + String[] toAdd = data.getStringArrayExtra("contacts"); + for (String item : toAdd) { + invite.jids.add(Jid.fromString(item)); + } + } else { + invite.jids.add(Jid.fromString(data.getStringExtra("contact"))); + } + } catch (final InvalidJidException ignored) { + return null; + } + return invite; + } + + public void execute(XmppActivity activity) { + XmppConnectionService service = activity.xmppConnectionService; + Conversation conversation = service.findConversationByUuid(this.uuid); + if (conversation == null) { + return; + } + if (conversation.getMode() == Conversation.MODE_MULTI) { + for (Jid jid : jids) { + service.invite(conversation, jid); + } + } else { + jids.add(conversation.getJid().toBareJid()); + service.createAdhocConference(conversation.getAccount(), jids, activity.adhocCallback); + } + } + } + public AvatarService avatarService() { return xmppConnectionService.getAvatarService(); } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index da92fb183..29dfced2e 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -7,10 +7,11 @@ import android.graphics.Typeface; import android.net.Uri; import android.text.Spannable; import android.text.SpannableString; +import android.text.Spanned; import android.text.style.ForegroundColorSpan; +import android.text.style.RelativeSizeSpan; import android.text.style.StyleSpan; import android.util.DisplayMetrics; -import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; @@ -24,7 +25,6 @@ import android.widget.Toast; import java.util.List; -import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; @@ -42,7 +42,6 @@ public class MessageAdapter extends ArrayAdapter { private static final int SENT = 0; private static final int RECEIVED = 1; private static final int STATUS = 2; - private static final int NULL = 3; private ConversationActivity activity; @@ -77,14 +76,12 @@ public class MessageAdapter extends ArrayAdapter { @Override public int getViewTypeCount() { - return 4; + return 3; } @Override public int getItemViewType(int position) { - if (getItem(position).wasMergedIntoPrevious()) { - return NULL; - } else if (getItem(position).getType() == Message.TYPE_STATUS) { + if (getItem(position).getType() == Message.TYPE_STATUS) { return STATUS; } else if (getItem(position).getStatus() <= Message.STATUS_RECEIVED) { return RECEIVED; @@ -207,22 +204,42 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.image.setVisibility(View.GONE); viewHolder.messageBody.setVisibility(View.VISIBLE); viewHolder.messageBody.setText(getContext().getString( - R.string.decryption_failed)); + R.string.decryption_failed)); viewHolder.messageBody.setTextColor(activity.getWarningTextColor()); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); viewHolder.messageBody.setTextIsSelectable(false); } + private void displayHeartMessage(final ViewHolder viewHolder, final String body) { + if (viewHolder.download_button != null) { + viewHolder.download_button.setVisibility(View.GONE); + } + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + viewHolder.messageBody.setIncludeFontPadding(false); + Spannable span = new SpannableString(body); + span.setSpan(new RelativeSizeSpan(4.0f), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + span.setSpan(new ForegroundColorSpan(activity.getWarningTextColor()), 0, body.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + viewHolder.messageBody.setText(span); + } + private void displayTextMessage(final ViewHolder viewHolder, final Message message) { if (viewHolder.download_button != null) { viewHolder.download_button.setVisibility(View.GONE); } viewHolder.image.setVisibility(View.GONE); viewHolder.messageBody.setVisibility(View.VISIBLE); + viewHolder.messageBody.setIncludeFontPadding(true); if (message.getBody() != null) { final String nick = UIHelper.getMessageDisplayName(message); - final String formattedBody = message.getMergedBody().replaceAll("^" + Message.ME_COMMAND, - nick + " "); + final String body = message.getMergedBody().replaceAll("^" + Message.ME_COMMAND,nick + " "); + final SpannableString formattedBody = new SpannableString(body); + int i = body.indexOf(Message.MERGE_SEPARATOR); + while(i >= 0) { + final int end = i + Message.MERGE_SEPARATOR.length(); + formattedBody.setSpan(new RelativeSizeSpan(0.3f),i,end,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + i = body.indexOf(Message.MERGE_SEPARATOR,end); + } if (message.getType() != Message.TYPE_PRIVATE) { if (message.hasMeCommand()) { final Spannable span = new SpannableString(formattedBody); @@ -230,7 +247,7 @@ public class MessageAdapter extends ArrayAdapter { Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); viewHolder.messageBody.setText(span); } else { - viewHolder.messageBody.setText(message.getMergedBody()); + viewHolder.messageBody.setText(formattedBody); } } else { String privateMarker; @@ -289,7 +306,7 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.image.setVisibility(View.GONE); viewHolder.messageBody.setVisibility(View.GONE); viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity,message))); + viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity, message))); viewHolder.download_button.setOnClickListener(new OnClickListener() { @Override @@ -334,7 +351,7 @@ public class MessageAdapter extends ArrayAdapter { scalledH = (int) (params.height / ((double) params.width / target)); } viewHolder.image.setLayoutParams(new LinearLayout.LayoutParams( - scalledW, scalledH)); + scalledW, scalledH)); activity.loadBitmap(message, viewHolder.image); viewHolder.image.setOnClickListener(new OnClickListener() { @@ -359,10 +376,6 @@ public class MessageAdapter extends ArrayAdapter { if (view == null) { viewHolder = new ViewHolder(); switch (type) { - case NULL: - view = activity.getLayoutInflater().inflate( - R.layout.message_null, parent, false); - break; case SENT: view = activity.getLayoutInflater().inflate( R.layout.message_sent, parent, false); @@ -429,25 +442,6 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.status_message.setText(message.getBody()); } return view; - } else if (type == NULL) { - if (viewHolder.message_box != null) { - Log.e(Config.LOGTAG, "detected type=NULL but with wrong cached view"); - view = activity.getLayoutInflater().inflate(R.layout.message_null, parent, false); - view.setTag(new ViewHolder()); - } - if (position == getCount() - 1) { - view.getLayoutParams().height = 1; - } else { - view.getLayoutParams().height = 0; - - } - view.setLayoutParams(view.getLayoutParams()); - return view; - } else if (message.wasMergedIntoPrevious()) { - Log.e(Config.LOGTAG,"detected wasMergedIntoPrevious with wrong type"); - return view; - } else if (viewHolder.messageBody == null || viewHolder.image == null) { - return view; //avoiding weird platform bugs } else if (type == RECEIVED) { Contact contact = message.getContact(); if (contact != null) { @@ -528,7 +522,11 @@ public class MessageAdapter extends ArrayAdapter { if (GeoHelper.isGeoUri(message.getBody())) { displayLocationMessage(viewHolder,message); } else { - displayTextMessage(viewHolder, message); + if (message.bodyIsHeart()) { + displayHeartMessage(viewHolder, message.getBody().trim()); + } else { + displayTextMessage(viewHolder, message); + } } } diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index c3195d868..2f96a83a9 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -1,8 +1,11 @@ package eu.siacs.conversations.utils; import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.Date; +import java.util.Locale; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Contact; @@ -17,6 +20,33 @@ import android.text.format.DateUtils; import android.util.Pair; public class UIHelper { + + private static String BLACK_HEART_SUIT = "\u2665"; + private static String HEAVY_BLACK_HEART_SUIT = "\u2764"; + private static String WHITE_HEART_SUIT = "\u2661"; + + public static final ArrayList HEARTS = new ArrayList<>(Arrays.asList(BLACK_HEART_SUIT,HEAVY_BLACK_HEART_SUIT,WHITE_HEART_SUIT)); + + private static final ArrayList LOCATION_QUESTIONS = new ArrayList<>(Arrays.asList( + "where are you", //en + "where are you now", //en + "where are you right now", //en + "whats your 20", //en + "what is your 20", //en + "what's your 20", //en + "whats your twenty", //en + "what is your twenty", //en + "what's your twenty", //en + "wo bist du", //de + "wo bist du jetzt", //de + "wo bist du gerade", //de + "wo seid ihr", //de + "wo seid ihr jetzt", //de + "wo seid ihr gerade", //de + "dónde estás", //es + "donde estas" //es + )); + private static final int SHORT_DATE_FLAGS = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL; private static final int FULL_DATE_FLAGS = DateUtils.FORMAT_SHOW_TIME @@ -225,4 +255,15 @@ public class UIHelper { return counterpart.toString().trim(); } } + + public static boolean receivedLocationQuestion(Message message) { + if (message == null + || message.getStatus() != Message.STATUS_RECEIVED + || message.getType() != Message.TYPE_TEXT) { + return false; + } + String body = message.getBody() == null ? null : message.getBody().trim().toLowerCase(Locale.getDefault()); + body = body.replace("?","").replace("¿",""); + return LOCATION_QUESTIONS.contains(body); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index a2b58a14f..9bda3c096 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -167,7 +167,7 @@ public class XmppConnection implements Runnable { try { String srvRecordServer; try { - srvRecordServer=IDN.toASCII(namePort.getString("name")); + srvRecordServer = IDN.toASCII(namePort.getString("name")); } catch (final IllegalArgumentException e) { // TODO: Handle me?` srvRecordServer = ""; @@ -187,7 +187,7 @@ public class XmppConnection implements Runnable { + srvRecordServer + ":" + srvRecordPort); } socket = new Socket(); - socket.connect(addr, 20000); + socket.connect(addr, Config.SOCKET_TIMEOUT * 1000); socketError = false; } catch (final UnknownHostException e) { Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); @@ -224,6 +224,12 @@ public class XmppConnection implements Runnable { if (socket.isConnected()) { socket.close(); } + } catch (final IncompatibleServerException e) { + this.changeStatus(Account.State.INCOMPATIBLE_SERVER); + } catch (final SecurityException e) { + this.changeStatus(Account.State.SECURITY_ERROR); + } catch (final UnauthorizedException e) { + this.changeStatus(Account.State.UNAUTHORIZED); } catch (final UnknownHostException | ConnectException e) { this.changeStatus(Account.State.SERVER_NOT_FOUND); } catch (final IOException | XmlPullParserException | NoSuchAlgorithmException e) { @@ -231,6 +237,13 @@ public class XmppConnection implements Runnable { this.changeStatus(Account.State.OFFLINE); this.attempt--; //don't count attempt when reconnecting instantly anyway } finally { + if (socket != null) { + try { + socket.close(); + } catch (IOException e) { + + } + } if (wakeLock.isHeld()) { try { wakeLock.release(); @@ -279,8 +292,7 @@ public class XmppConnection implements Runnable { processStream(tagReader.readTag()); break; } else if (nextTag.isStart("failure")) { - tagReader.readElement(nextTag); - changeStatus(Account.State.UNAUTHORIZED); + throw new UnauthorizedException(); } else if (nextTag.isStart("challenge")) { final String challenge = tagReader.readElement(nextTag).getContent(); final Element response = new Element("response"); @@ -437,6 +449,10 @@ public class XmppConnection implements Runnable { throw new IOException("interrupted mid tag"); } } + if (stanzasReceived == Integer.MAX_VALUE) { + resetStreamId(); + throw new IOException("time to restart the session. cant handle >2 billion pcks"); + } ++stanzasReceived; lastPacketReceived = SystemClock.elapsedRealtime(); return element; @@ -538,8 +554,7 @@ public class XmppConnection implements Runnable { if (!verifier.verify(account.getServer().getDomainpart(),sslSocket.getSession())) { Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed"); - disconnect(true); - changeStatus(Account.State.SECURITY_ERROR); + throw new SecurityException(); } tagReader.setInputStream(sslSocket.getInputStream()); tagWriter.setOutputStream(sslSocket.getOutputStream()); @@ -550,8 +565,7 @@ public class XmppConnection implements Runnable { sslSocket.close(); } catch (final NoSuchAlgorithmException | KeyManagementException e1) { Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed"); - disconnect(true); - changeStatus(Account.State.SECURITY_ERROR); + throw new SecurityException(); } } @@ -590,8 +604,7 @@ public class XmppConnection implements Runnable { " has lower priority (" + String.valueOf(saslMechanism.getPriority()) + ") than pinned priority (" + keys.getInt(Account.PINNED_MECHANISM_KEY) + "). Possible downgrade attack?"); - disconnect(true); - changeStatus(Account.State.SECURITY_ERROR); + throw new SecurityException(); } } catch (final JSONException e) { Log.d(Config.LOGTAG, "Parse error while checking pinned auth mechanism"); @@ -603,8 +616,7 @@ public class XmppConnection implements Runnable { } tagWriter.writeElement(auth); } else { - disconnect(true); - changeStatus(Account.State.INCOMPATIBLE_SERVER); + throw new IncompatibleServerException(); } } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + smVersion) @@ -643,10 +655,8 @@ public class XmppConnection implements Runnable { if (packet.query().hasChild("username") && (packet.query().hasChild("password"))) { final IqPacket register = new IqPacket(IqPacket.TYPE.SET); - final Element username = new Element("username") - .setContent(account.getUsername()); - final Element password = new Element("password") - .setContent(account.getPassword()); + final Element username = new Element("username").setContent(account.getUsername()); + final Element password = new Element("password").setContent(account.getPassword()); register.query("jabber:iq:register").addChild(username); register.query().addChild(password); sendIqPacket(register, new OnIqPacketReceived() { @@ -659,7 +669,7 @@ public class XmppConnection implements Runnable { changeStatus(Account.State.REGISTRATION_SUCCESSFUL); } else if (packet.hasChild("error") && (packet.findChild("error") - .hasChild("conflict"))) { + .hasChild("conflict"))) { changeStatus(Account.State.REGISTRATION_CONFLICT); } else { changeStatus(Account.State.REGISTRATION_FAILED); @@ -673,7 +683,7 @@ public class XmppConnection implements Runnable { disconnect(true); Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not register. instructions are" - + instructions.getContent()); + + (instructions != null ? instructions.getContent() : "")); } } }); @@ -688,7 +698,7 @@ public class XmppConnection implements Runnable { } final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind") - .addChild("resource").setContent(account.getResource()); + .addChild("resource").setContent(account.getResource()); this.sendUnmodifiedIqPacket(iq, new OnIqPacketReceived() { @Override public void onIqPacketReceived(final Account account, final IqPacket packet) { @@ -901,6 +911,11 @@ public class XmppConnection implements Runnable { } private synchronized void sendPacket(final AbstractStanza packet) { + if (stanzasSent == Integer.MAX_VALUE) { + resetStreamId(); + disconnect(true); + return; + } final String name = packet.getName(); if (name.equals("iq") || name.equals("message") || name.equals("presence")) { ++stanzasSent; @@ -1091,6 +1106,18 @@ public class XmppConnection implements Runnable { public final ArrayList> identities = new ArrayList<>(); } + private class UnauthorizedException extends IOException { + + } + + private class SecurityException extends IOException { + + } + + private class IncompatibleServerException extends IOException { + + } + public class Features { XmppConnection connection; private boolean carbonsEnabled = false; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java b/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java index 295e067a6..f989c0c25 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java @@ -130,12 +130,19 @@ public final class Jid { if (resourcepart.isEmpty() || resourcepart.length() > 1023) { throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH); } - dp = IDN.toUnicode(jid.substring(domainpartStart, slashLoc), IDN.USE_STD3_ASCII_RULES); + try { + dp = IDN.toUnicode(Stringprep.nameprep(jid.substring(domainpartStart, slashLoc)), IDN.USE_STD3_ASCII_RULES); + } catch (final StringprepException e) { + throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e); + } finaljid = finaljid + dp + "/" + rp; } else { resourcepart = ""; - dp = IDN.toUnicode(jid.substring(domainpartStart, jid.length()), - IDN.USE_STD3_ASCII_RULES); + try{ + dp = IDN.toUnicode(Stringprep.nameprep(jid.substring(domainpartStart, jid.length())), IDN.USE_STD3_ASCII_RULES); + } catch (final StringprepException e) { + throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e); + } finaljid = finaljid + dp; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java index e448f9477..c9bb9c93b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -99,7 +99,7 @@ public class JingleConnection implements Downloadable { file.delete(); } } - Log.d(Config.LOGTAG,"sucessfully transmitted file:" + file.getAbsolutePath()); + Log.d(Config.LOGTAG,"successfully transmitted file:" + file.getAbsolutePath()+" ("+file.getSha1Sum()+")"); if (message.getEncryption() != Message.ENCRYPTION_PGP) { Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); intent.setData(Uri.fromFile(file)); @@ -213,7 +213,7 @@ public class JingleConnection implements Downloadable { @Override public void onPrimaryCandidateFound(boolean success, - final JingleCandidate candidate) { + final JingleCandidate candidate) { if (success) { final JingleSocks5Transport socksConnection = new JingleSocks5Transport( JingleConnection.this, candidate); @@ -271,6 +271,9 @@ public class JingleConnection implements Downloadable { this.mergeCandidates(JingleCandidate.parse(content.socks5transport() .getChildren())); this.fileOffer = packet.getJingleContent().getFileOffer(); + + mXmppConnectionService.sendIqPacket(account,packet.generateResponse(IqPacket.TYPE.RESULT),null); + if (fileOffer != null) { Element fileSize = fileOffer.findChild("size"); Element fileNameElement = fileOffer.findChild("name"); @@ -381,6 +384,7 @@ public class JingleConnection implements Downloadable { @Override public void onIqPacketReceived(Account account, IqPacket packet) { if (packet.getType() != IqPacket.TYPE.ERROR) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": other party received offer"); mJingleStatus = JINGLE_STATUS_INITIATED; mXmppConnectionService.markMessage(message, Message.STATUS_OFFERED); } else { @@ -395,7 +399,9 @@ public class JingleConnection implements Downloadable { private List getCandidatesAsElements() { List elements = new ArrayList<>(); for (JingleCandidate c : this.candidates) { - elements.add(c.toElement()); + if (c.isOurs()) { + elements.add(c.toElement()); + } } return elements; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 5dfa3ff40..c19dd04c7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -9,6 +9,7 @@ import android.annotation.SuppressLint; import android.util.Log; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Downloadable; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; @@ -58,7 +59,12 @@ public class JingleConnectionManager extends AbstractConnectionManager { } public JingleConnection createNewConnection(Message message) { + Downloadable old = message.getDownloadable(); + if (old != null) { + old.cancel(); + } JingleConnection connection = new JingleConnection(this); + mXmppConnectionService.markMessage(message,Message.STATUS_WAITING); connection.init(message); this.connections.add(connection); return connection; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java index 9866af03d..29efcf8f1 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java @@ -8,7 +8,9 @@ import java.security.NoSuchAlgorithmException; import java.util.Arrays; import android.util.Base64; +import android.util.Log; +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.persistance.FileBackend; @@ -95,11 +97,13 @@ public class JingleInbandTransport extends JingleTransport { file.createNewFile(); this.fileOutputStream = file.createOutputStream(); if (this.fileOutputStream == null) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not create output stream"); callback.onFileTransferAborted(); return; } this.remainingSize = this.fileSize = file.getExpectedSize(); } catch (final NoSuchAlgorithmException | IOException e) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+" "+e.getMessage()); callback.onFileTransferAborted(); } } @@ -110,12 +114,17 @@ public class JingleInbandTransport extends JingleTransport { this.onFileTransmissionStatusChanged = callback; this.file = file; try { - this.remainingSize = this.file.getSize(); + if (this.file.getKey() != null) { + this.remainingSize = (this.file.getSize() / 16 + 1) * 16; + } else { + this.remainingSize = this.file.getSize(); + } this.fileSize = this.remainingSize; this.digest = MessageDigest.getInstance("SHA-1"); this.digest.reset(); fileInputStream = this.file.createInputStream(); if (fileInputStream == null) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could no create input stream"); callback.onFileTransferAborted(); return; } @@ -124,6 +133,7 @@ public class JingleInbandTransport extends JingleTransport { } } catch (NoSuchAlgorithmException e) { callback.onFileTransferAborted(); + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage()); } } @@ -150,29 +160,33 @@ public class JingleInbandTransport extends JingleTransport { byte[] buffer = new byte[this.bufferSize]; try { int count = fileInputStream.read(buffer); - if (count == -1) { - file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); - fileInputStream.close(); - this.onFileTransmissionStatusChanged.onFileTransmitted(file); - } else { - this.remainingSize -= count; - this.digest.update(buffer); - String base64 = Base64.encodeToString(buffer, Base64.NO_WRAP); - IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - iq.setTo(this.counterpart); - Element data = iq.addChild("data", - "http://jabber.org/protocol/ibb"); - data.setAttribute("seq", Integer.toString(this.seq)); - data.setAttribute("block-size", - Integer.toString(this.blockSize)); - data.setAttribute("sid", this.sessionId); - data.setContent(base64); - this.account.getXmppConnection().sendIqPacket(iq, - this.onAckReceived); - this.seq++; + this.remainingSize -= count; + if (count != buffer.length && count != -1) { + int rem = fileInputStream.read(buffer,count,buffer.length-count); + if (rem > 0) { + count += rem; + } + } + this.digest.update(buffer,0,count); + String base64 = Base64.encodeToString(buffer,0,count, Base64.NO_WRAP); + IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + iq.setTo(this.counterpart); + Element data = iq.addChild("data", "http://jabber.org/protocol/ibb"); + data.setAttribute("seq", Integer.toString(this.seq)); + data.setAttribute("block-size", Integer.toString(this.blockSize)); + data.setAttribute("sid", this.sessionId); + data.setContent(base64); + this.account.getXmppConnection().sendIqPacket(iq, this.onAckReceived); + this.seq++; + if (this.remainingSize > 0) { connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100)); + } else { + file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); + this.onFileTransmissionStatusChanged.onFileTransmitted(file); + fileInputStream.close(); } } catch (IOException e) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage()); FileBackend.close(fileInputStream); this.onFileTransmissionStatusChanged.onFileTransferAborted(); } @@ -182,14 +196,10 @@ public class JingleInbandTransport extends JingleTransport { try { byte[] buffer = Base64.decode(data, Base64.NO_WRAP); if (this.remainingSize < buffer.length) { - buffer = Arrays - .copyOfRange(buffer, 0, (int) this.remainingSize); + buffer = Arrays.copyOfRange(buffer, 0, (int) this.remainingSize); } this.remainingSize -= buffer.length; - - this.fileOutputStream.write(buffer); - this.digest.update(buffer); if (this.remainingSize <= 0) { file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); @@ -200,6 +210,7 @@ public class JingleInbandTransport extends JingleTransport { connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100)); } } catch (IOException e) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage()); FileBackend.close(fileOutputStream); this.onFileTransmissionStatusChanged.onFileTransferAborted(); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java index 72015a058..8d74f44e3 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -1,15 +1,20 @@ package eu.siacs.conversations.xmpp.jingle; +import android.util.Log; + import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.InetSocketAddress; import java.net.Socket; +import java.net.SocketAddress; import java.net.UnknownHostException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.utils.CryptoHelper; @@ -53,8 +58,9 @@ public class JingleSocks5Transport extends JingleTransport { @Override public void run() { try { - socket = new Socket(candidate.getHost(), - candidate.getPort()); + socket = new Socket(); + SocketAddress address = new InetSocketAddress(candidate.getHost(),candidate.getPort()); + socket.connect(address,Config.SOCKET_TIMEOUT * 1000); inputStream = socket.getInputStream(); outputStream = socket.getOutputStream(); byte[] login = { 0x05, 0x01, 0x00 }; @@ -102,6 +108,7 @@ public class JingleSocks5Transport extends JingleTransport { digest.reset(); fileInputStream = file.createInputStream(); if (fileInputStream == null) { + Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create input stream"); callback.onFileTransferAborted(); return; } @@ -121,10 +128,13 @@ public class JingleSocks5Transport extends JingleTransport { callback.onFileTransmitted(file); } } catch (FileNotFoundException e) { + Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); callback.onFileTransferAborted(); } catch (IOException e) { + Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); callback.onFileTransferAborted(); } catch (NoSuchAlgorithmException e) { + Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); callback.onFileTransferAborted(); } finally { FileBackend.close(fileInputStream); @@ -150,6 +160,7 @@ public class JingleSocks5Transport extends JingleTransport { fileOutputStream = file.createOutputStream(); if (fileOutputStream == null) { callback.onFileTransferAborted(); + Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create output stream"); return; } double size = file.getExpectedSize(); @@ -160,6 +171,7 @@ public class JingleSocks5Transport extends JingleTransport { count = inputStream.read(buffer); if (count == -1) { callback.onFileTransferAborted(); + Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": file ended prematurely with "+remainingSize+" bytes remaining"); return; } else { fileOutputStream.write(buffer, 0, count); @@ -173,10 +185,13 @@ public class JingleSocks5Transport extends JingleTransport { file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); callback.onFileTransmitted(file); } catch (FileNotFoundException e) { + Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); callback.onFileTransferAborted(); } catch (IOException e) { + Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); callback.onFileTransferAborted(); } catch (NoSuchAlgorithmException e) { + Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage()); callback.onFileTransferAborted(); } finally { FileBackend.close(fileOutputStream); diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java index 9f5ac988b..04d55bbe5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java +++ b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java @@ -6,6 +6,9 @@ import eu.siacs.conversations.xmpp.jid.Jid; import android.util.Base64; public class Avatar { + + public enum Origin { PEP, VCARD }; + public String type; public String sha1sum; public String image; @@ -13,21 +16,14 @@ public class Avatar { public int width; public long size; public Jid owner; + public Origin origin = Origin.PEP; //default to maintain compat 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; - } + return sha1sum; } public static Avatar parseMetadata(Element items) { @@ -64,10 +60,44 @@ public class Avatar { return null; } avatar.type = child.getAttribute("type"); - avatar.sha1sum = child.getAttribute("id"); + String hash = child.getAttribute("id"); + if (!isValidSHA1(hash)) { + return null; + } + avatar.sha1sum = hash; + avatar.origin = Origin.PEP; return avatar; } } return null; } + + @Override + public boolean equals(Object object) { + if (object != null && object instanceof Avatar) { + Avatar other = (Avatar) object; + return other.getFilename().equals(this.getFilename()); + } else { + return false; + } + } + + public static Avatar parsePresence(Element x) { + Element photo = x != null ? x.findChild("photo") : null; + String hash = photo != null ? photo.getContent() : null; + if (hash == null) { + return null; + } + if (!isValidSHA1(hash)) { + return null; + } + Avatar avatar = new Avatar(); + avatar.sha1sum = hash; + avatar.origin = Origin.VCARD; + return avatar; + } + + private static boolean isValidSHA1(String s) { + return s != null && s.matches("[a-fA-F0-9]{40}"); + } } diff --git a/src/main/res/drawable-hdpi/ic_action_send_now_away.png b/src/main/res/drawable-hdpi/ic_action_send_now_away.png deleted file mode 100644 index 505cbe63a..000000000 Binary files a/src/main/res/drawable-hdpi/ic_action_send_now_away.png and /dev/null differ diff --git a/src/main/res/drawable-hdpi/ic_action_send_now_dnd.png b/src/main/res/drawable-hdpi/ic_action_send_now_dnd.png deleted file mode 100644 index a376524d7..000000000 Binary files a/src/main/res/drawable-hdpi/ic_action_send_now_dnd.png and /dev/null differ diff --git a/src/main/res/drawable-hdpi/ic_action_send_now_offline.png b/src/main/res/drawable-hdpi/ic_action_send_now_offline.png deleted file mode 100644 index d4d2d5103..000000000 Binary files a/src/main/res/drawable-hdpi/ic_action_send_now_offline.png and /dev/null differ diff --git a/src/main/res/drawable-hdpi/ic_action_send_now_online.png b/src/main/res/drawable-hdpi/ic_action_send_now_online.png deleted file mode 100644 index 48676f7bd..000000000 Binary files a/src/main/res/drawable-hdpi/ic_action_send_now_online.png and /dev/null differ diff --git a/src/main/res/drawable-hdpi/ic_launcher.png b/src/main/res/drawable-hdpi/ic_launcher.png index bffc1c652..25fc8591f 100644 Binary files a/src/main/res/drawable-hdpi/ic_launcher.png and b/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/src/main/res/drawable-hdpi/ic_notification.png b/src/main/res/drawable-hdpi/ic_notification.png index c466a7b1c..31c0ee1ae 100644 Binary files a/src/main/res/drawable-hdpi/ic_notification.png and b/src/main/res/drawable-hdpi/ic_notification.png differ diff --git a/src/main/res/drawable-hdpi/ic_received_indicator.png b/src/main/res/drawable-hdpi/ic_received_indicator.png index b1e3f2748..4d2eab562 100644 Binary files a/src/main/res/drawable-hdpi/ic_received_indicator.png and b/src/main/res/drawable-hdpi/ic_received_indicator.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_cancel_away.png b/src/main/res/drawable-hdpi/ic_send_cancel_away.png new file mode 100644 index 000000000..9525c040f Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_cancel_away.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_cancel_dnd.png b/src/main/res/drawable-hdpi/ic_send_cancel_dnd.png new file mode 100644 index 000000000..83a7b94b2 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_cancel_dnd.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_cancel_offline.png b/src/main/res/drawable-hdpi/ic_send_cancel_offline.png new file mode 100644 index 000000000..2cb4f4f38 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_cancel_offline.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_cancel_online.png b/src/main/res/drawable-hdpi/ic_send_cancel_online.png new file mode 100644 index 000000000..cb3bc3b56 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_cancel_online.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_location_away.png b/src/main/res/drawable-hdpi/ic_send_location_away.png new file mode 100644 index 000000000..d139818b4 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_location_away.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_location_dnd.png b/src/main/res/drawable-hdpi/ic_send_location_dnd.png new file mode 100644 index 000000000..a9c51317e Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_location_dnd.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_location_offline.png b/src/main/res/drawable-hdpi/ic_send_location_offline.png new file mode 100644 index 000000000..4aec18af7 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_location_offline.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_location_online.png b/src/main/res/drawable-hdpi/ic_send_location_online.png new file mode 100644 index 000000000..19ddc51e3 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_location_online.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_photo_away.png b/src/main/res/drawable-hdpi/ic_send_photo_away.png new file mode 100644 index 000000000..f6beb23cb Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_photo_away.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_photo_dnd.png b/src/main/res/drawable-hdpi/ic_send_photo_dnd.png new file mode 100644 index 000000000..15b1eb7a0 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_photo_dnd.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_photo_offline.png b/src/main/res/drawable-hdpi/ic_send_photo_offline.png new file mode 100644 index 000000000..046f989b8 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_photo_offline.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_photo_online.png b/src/main/res/drawable-hdpi/ic_send_photo_online.png new file mode 100644 index 000000000..3fd20d10b Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_photo_online.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_text_away.png b/src/main/res/drawable-hdpi/ic_send_text_away.png new file mode 100644 index 000000000..d9ef99c56 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_text_away.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_text_dnd.png b/src/main/res/drawable-hdpi/ic_send_text_dnd.png new file mode 100644 index 000000000..b43428dec Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_text_dnd.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_text_offline.png b/src/main/res/drawable-hdpi/ic_send_text_offline.png new file mode 100644 index 000000000..0f23fdbb0 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_text_offline.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_text_online.png b/src/main/res/drawable-hdpi/ic_send_text_online.png new file mode 100644 index 000000000..305f1ec2f Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_text_online.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_voice_away.png b/src/main/res/drawable-hdpi/ic_send_voice_away.png new file mode 100644 index 000000000..e87d97510 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_voice_away.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_voice_dnd.png b/src/main/res/drawable-hdpi/ic_send_voice_dnd.png new file mode 100644 index 000000000..26a89e8e3 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_voice_dnd.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_voice_offline.png b/src/main/res/drawable-hdpi/ic_send_voice_offline.png new file mode 100644 index 000000000..68ce48b8a Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_voice_offline.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_voice_online.png b/src/main/res/drawable-hdpi/ic_send_voice_online.png new file mode 100644 index 000000000..bfd7dfa7b Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_voice_online.png differ diff --git a/src/main/res/drawable-mdpi/ic_action_send_now_away.png b/src/main/res/drawable-mdpi/ic_action_send_now_away.png deleted file mode 100644 index 0fdca901a..000000000 Binary files a/src/main/res/drawable-mdpi/ic_action_send_now_away.png and /dev/null differ diff --git a/src/main/res/drawable-mdpi/ic_action_send_now_dnd.png b/src/main/res/drawable-mdpi/ic_action_send_now_dnd.png deleted file mode 100644 index c0aef36cc..000000000 Binary files a/src/main/res/drawable-mdpi/ic_action_send_now_dnd.png and /dev/null differ diff --git a/src/main/res/drawable-mdpi/ic_action_send_now_offline.png b/src/main/res/drawable-mdpi/ic_action_send_now_offline.png deleted file mode 100644 index 7723f4aa9..000000000 Binary files a/src/main/res/drawable-mdpi/ic_action_send_now_offline.png and /dev/null differ diff --git a/src/main/res/drawable-mdpi/ic_action_send_now_online.png b/src/main/res/drawable-mdpi/ic_action_send_now_online.png deleted file mode 100644 index 39d00ee48..000000000 Binary files a/src/main/res/drawable-mdpi/ic_action_send_now_online.png and /dev/null differ diff --git a/src/main/res/drawable-mdpi/ic_launcher.png b/src/main/res/drawable-mdpi/ic_launcher.png index 063ee15d0..733e96158 100644 Binary files a/src/main/res/drawable-mdpi/ic_launcher.png and b/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/src/main/res/drawable-mdpi/ic_notification.png b/src/main/res/drawable-mdpi/ic_notification.png index fa35b7c10..aafc54f54 100644 Binary files a/src/main/res/drawable-mdpi/ic_notification.png and b/src/main/res/drawable-mdpi/ic_notification.png differ diff --git a/src/main/res/drawable-mdpi/ic_received_indicator.png b/src/main/res/drawable-mdpi/ic_received_indicator.png index 88ff1efb9..2ba92b698 100644 Binary files a/src/main/res/drawable-mdpi/ic_received_indicator.png and b/src/main/res/drawable-mdpi/ic_received_indicator.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_cancel_away.png b/src/main/res/drawable-mdpi/ic_send_cancel_away.png new file mode 100644 index 000000000..bbc86b45a Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_cancel_away.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_cancel_dnd.png b/src/main/res/drawable-mdpi/ic_send_cancel_dnd.png new file mode 100644 index 000000000..750064469 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_cancel_dnd.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_cancel_offline.png b/src/main/res/drawable-mdpi/ic_send_cancel_offline.png new file mode 100644 index 000000000..604dfefaf Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_cancel_offline.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_cancel_online.png b/src/main/res/drawable-mdpi/ic_send_cancel_online.png new file mode 100644 index 000000000..1b0d1811f Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_cancel_online.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_location_away.png b/src/main/res/drawable-mdpi/ic_send_location_away.png new file mode 100644 index 000000000..821e80d25 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_location_away.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_location_dnd.png b/src/main/res/drawable-mdpi/ic_send_location_dnd.png new file mode 100644 index 000000000..3f4d6aa45 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_location_dnd.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_location_offline.png b/src/main/res/drawable-mdpi/ic_send_location_offline.png new file mode 100644 index 000000000..ff11a080f Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_location_offline.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_location_online.png b/src/main/res/drawable-mdpi/ic_send_location_online.png new file mode 100644 index 000000000..a0eb40189 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_location_online.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_photo_away.png b/src/main/res/drawable-mdpi/ic_send_photo_away.png new file mode 100644 index 000000000..d9c1f2661 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_photo_away.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_photo_dnd.png b/src/main/res/drawable-mdpi/ic_send_photo_dnd.png new file mode 100644 index 000000000..08033f633 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_photo_dnd.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_photo_offline.png b/src/main/res/drawable-mdpi/ic_send_photo_offline.png new file mode 100644 index 000000000..f3e6e1fa4 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_photo_offline.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_photo_online.png b/src/main/res/drawable-mdpi/ic_send_photo_online.png new file mode 100644 index 000000000..0aaab38d8 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_photo_online.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_text_away.png b/src/main/res/drawable-mdpi/ic_send_text_away.png new file mode 100644 index 000000000..ddd983b56 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_text_away.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_text_dnd.png b/src/main/res/drawable-mdpi/ic_send_text_dnd.png new file mode 100644 index 000000000..c3d9e79a8 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_text_dnd.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_text_offline.png b/src/main/res/drawable-mdpi/ic_send_text_offline.png new file mode 100644 index 000000000..72b9da270 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_text_offline.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_text_online.png b/src/main/res/drawable-mdpi/ic_send_text_online.png new file mode 100644 index 000000000..86d1e3306 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_text_online.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_voice_away.png b/src/main/res/drawable-mdpi/ic_send_voice_away.png new file mode 100644 index 000000000..943f690fe Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_voice_away.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_voice_dnd.png b/src/main/res/drawable-mdpi/ic_send_voice_dnd.png new file mode 100644 index 000000000..aaf8ff32c Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_voice_dnd.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_voice_offline.png b/src/main/res/drawable-mdpi/ic_send_voice_offline.png new file mode 100644 index 000000000..e6b2355fb Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_voice_offline.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_voice_online.png b/src/main/res/drawable-mdpi/ic_send_voice_online.png new file mode 100644 index 000000000..bd0e1f870 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_voice_online.png differ diff --git a/src/main/res/drawable-xhdpi/ic_action_send_now_away.png b/src/main/res/drawable-xhdpi/ic_action_send_now_away.png deleted file mode 100644 index bb999d85d..000000000 Binary files a/src/main/res/drawable-xhdpi/ic_action_send_now_away.png and /dev/null differ diff --git a/src/main/res/drawable-xhdpi/ic_action_send_now_dnd.png b/src/main/res/drawable-xhdpi/ic_action_send_now_dnd.png deleted file mode 100644 index a0bf5561c..000000000 Binary files a/src/main/res/drawable-xhdpi/ic_action_send_now_dnd.png and /dev/null differ diff --git a/src/main/res/drawable-xhdpi/ic_action_send_now_offline.png b/src/main/res/drawable-xhdpi/ic_action_send_now_offline.png deleted file mode 100644 index 6da9ff7bd..000000000 Binary files a/src/main/res/drawable-xhdpi/ic_action_send_now_offline.png and /dev/null differ diff --git a/src/main/res/drawable-xhdpi/ic_action_send_now_online.png b/src/main/res/drawable-xhdpi/ic_action_send_now_online.png deleted file mode 100644 index 348ba657d..000000000 Binary files a/src/main/res/drawable-xhdpi/ic_action_send_now_online.png and /dev/null differ diff --git a/src/main/res/drawable-xhdpi/ic_launcher.png b/src/main/res/drawable-xhdpi/ic_launcher.png index fd9937f1a..c9e48859a 100644 Binary files a/src/main/res/drawable-xhdpi/ic_launcher.png and b/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/src/main/res/drawable-xhdpi/ic_notification.png b/src/main/res/drawable-xhdpi/ic_notification.png index 43daff659..042d2cdaa 100644 Binary files a/src/main/res/drawable-xhdpi/ic_notification.png and b/src/main/res/drawable-xhdpi/ic_notification.png differ diff --git a/src/main/res/drawable-xhdpi/ic_received_indicator.png b/src/main/res/drawable-xhdpi/ic_received_indicator.png index 2c8719337..cf7c2bb85 100644 Binary files a/src/main/res/drawable-xhdpi/ic_received_indicator.png and b/src/main/res/drawable-xhdpi/ic_received_indicator.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_cancel_away.png b/src/main/res/drawable-xhdpi/ic_send_cancel_away.png new file mode 100644 index 000000000..44b5f0963 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_cancel_away.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_cancel_dnd.png b/src/main/res/drawable-xhdpi/ic_send_cancel_dnd.png new file mode 100644 index 000000000..dde1b7076 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_cancel_dnd.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_cancel_offline.png b/src/main/res/drawable-xhdpi/ic_send_cancel_offline.png new file mode 100644 index 000000000..53a9ac909 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_cancel_offline.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_cancel_online.png b/src/main/res/drawable-xhdpi/ic_send_cancel_online.png new file mode 100644 index 000000000..23a5f2632 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_cancel_online.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_location_away.png b/src/main/res/drawable-xhdpi/ic_send_location_away.png new file mode 100644 index 000000000..0a5f3d54e Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_location_away.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_location_dnd.png b/src/main/res/drawable-xhdpi/ic_send_location_dnd.png new file mode 100644 index 000000000..99c8ce36f Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_location_dnd.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_location_offline.png b/src/main/res/drawable-xhdpi/ic_send_location_offline.png new file mode 100644 index 000000000..114ce01be Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_location_offline.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_location_online.png b/src/main/res/drawable-xhdpi/ic_send_location_online.png new file mode 100644 index 000000000..17204eeab Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_location_online.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_photo_away.png b/src/main/res/drawable-xhdpi/ic_send_photo_away.png new file mode 100644 index 000000000..7ac674eac Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_photo_away.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_photo_dnd.png b/src/main/res/drawable-xhdpi/ic_send_photo_dnd.png new file mode 100644 index 000000000..fc69cb416 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_photo_dnd.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_photo_offline.png b/src/main/res/drawable-xhdpi/ic_send_photo_offline.png new file mode 100644 index 000000000..6ef1e16a9 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_photo_offline.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_photo_online.png b/src/main/res/drawable-xhdpi/ic_send_photo_online.png new file mode 100644 index 000000000..f585ef98c Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_photo_online.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_text_away.png b/src/main/res/drawable-xhdpi/ic_send_text_away.png new file mode 100644 index 000000000..41f223f6c Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_text_away.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_text_dnd.png b/src/main/res/drawable-xhdpi/ic_send_text_dnd.png new file mode 100644 index 000000000..8b93ec109 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_text_dnd.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_text_offline.png b/src/main/res/drawable-xhdpi/ic_send_text_offline.png new file mode 100644 index 000000000..d0a98e5d0 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_text_offline.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_text_online.png b/src/main/res/drawable-xhdpi/ic_send_text_online.png new file mode 100644 index 000000000..91d240d24 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_text_online.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_voice_away.png b/src/main/res/drawable-xhdpi/ic_send_voice_away.png new file mode 100644 index 000000000..34f8ea86f Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_voice_away.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_voice_dnd.png b/src/main/res/drawable-xhdpi/ic_send_voice_dnd.png new file mode 100644 index 000000000..66b0c6771 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_voice_dnd.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_voice_offline.png b/src/main/res/drawable-xhdpi/ic_send_voice_offline.png new file mode 100644 index 000000000..fc4cff1f1 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_voice_offline.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_voice_online.png b/src/main/res/drawable-xhdpi/ic_send_voice_online.png new file mode 100644 index 000000000..d2f03dd51 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_voice_online.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_action_send_now_away.png b/src/main/res/drawable-xxhdpi/ic_action_send_now_away.png deleted file mode 100644 index 12ec4d33f..000000000 Binary files a/src/main/res/drawable-xxhdpi/ic_action_send_now_away.png and /dev/null differ diff --git a/src/main/res/drawable-xxhdpi/ic_action_send_now_dnd.png b/src/main/res/drawable-xxhdpi/ic_action_send_now_dnd.png deleted file mode 100644 index 7719f81a9..000000000 Binary files a/src/main/res/drawable-xxhdpi/ic_action_send_now_dnd.png and /dev/null differ diff --git a/src/main/res/drawable-xxhdpi/ic_action_send_now_offline.png b/src/main/res/drawable-xxhdpi/ic_action_send_now_offline.png deleted file mode 100644 index 188958132..000000000 Binary files a/src/main/res/drawable-xxhdpi/ic_action_send_now_offline.png and /dev/null differ diff --git a/src/main/res/drawable-xxhdpi/ic_action_send_now_online.png b/src/main/res/drawable-xxhdpi/ic_action_send_now_online.png deleted file mode 100644 index 29bde36e3..000000000 Binary files a/src/main/res/drawable-xxhdpi/ic_action_send_now_online.png and /dev/null differ diff --git a/src/main/res/drawable-xxhdpi/ic_launcher.png b/src/main/res/drawable-xxhdpi/ic_launcher.png index 0e06656ff..e69b9c8d5 100644 Binary files a/src/main/res/drawable-xxhdpi/ic_launcher.png and b/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_notification.png b/src/main/res/drawable-xxhdpi/ic_notification.png index c2ee5dec8..42c62d327 100644 Binary files a/src/main/res/drawable-xxhdpi/ic_notification.png and b/src/main/res/drawable-xxhdpi/ic_notification.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_received_indicator.png b/src/main/res/drawable-xxhdpi/ic_received_indicator.png index 039a9ef9b..5d1c9b876 100644 Binary files a/src/main/res/drawable-xxhdpi/ic_received_indicator.png and b/src/main/res/drawable-xxhdpi/ic_received_indicator.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_cancel_away.png b/src/main/res/drawable-xxhdpi/ic_send_cancel_away.png new file mode 100644 index 000000000..07113f020 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_cancel_away.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_cancel_dnd.png b/src/main/res/drawable-xxhdpi/ic_send_cancel_dnd.png new file mode 100644 index 000000000..e68a912d7 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_cancel_dnd.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_cancel_offline.png b/src/main/res/drawable-xxhdpi/ic_send_cancel_offline.png new file mode 100644 index 000000000..10412faba Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_cancel_offline.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_cancel_online.png b/src/main/res/drawable-xxhdpi/ic_send_cancel_online.png new file mode 100644 index 000000000..6fa18239a Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_cancel_online.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_location_away.png b/src/main/res/drawable-xxhdpi/ic_send_location_away.png new file mode 100644 index 000000000..4fb370ff9 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_location_away.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_location_dnd.png b/src/main/res/drawable-xxhdpi/ic_send_location_dnd.png new file mode 100644 index 000000000..1773e62d2 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_location_dnd.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_location_offline.png b/src/main/res/drawable-xxhdpi/ic_send_location_offline.png new file mode 100644 index 000000000..b4317aae4 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_location_offline.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_location_online.png b/src/main/res/drawable-xxhdpi/ic_send_location_online.png new file mode 100644 index 000000000..10dfed811 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_location_online.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_photo_away.png b/src/main/res/drawable-xxhdpi/ic_send_photo_away.png new file mode 100644 index 000000000..78eea39e3 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_photo_away.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_photo_dnd.png b/src/main/res/drawable-xxhdpi/ic_send_photo_dnd.png new file mode 100644 index 000000000..fe33a1d0d Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_photo_dnd.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_photo_offline.png b/src/main/res/drawable-xxhdpi/ic_send_photo_offline.png new file mode 100644 index 000000000..6b41c3bd9 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_photo_offline.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_photo_online.png b/src/main/res/drawable-xxhdpi/ic_send_photo_online.png new file mode 100644 index 000000000..1c8992c52 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_photo_online.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_text_away.png b/src/main/res/drawable-xxhdpi/ic_send_text_away.png new file mode 100644 index 000000000..2b2b07935 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_text_away.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_text_dnd.png b/src/main/res/drawable-xxhdpi/ic_send_text_dnd.png new file mode 100644 index 000000000..32f5e29c7 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_text_dnd.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_text_offline.png b/src/main/res/drawable-xxhdpi/ic_send_text_offline.png new file mode 100644 index 000000000..6bd9c414f Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_text_offline.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_text_online.png b/src/main/res/drawable-xxhdpi/ic_send_text_online.png new file mode 100644 index 000000000..cb6a2dba8 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_text_online.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_voice_away.png b/src/main/res/drawable-xxhdpi/ic_send_voice_away.png new file mode 100644 index 000000000..b8b9e8079 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_voice_away.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_voice_dnd.png b/src/main/res/drawable-xxhdpi/ic_send_voice_dnd.png new file mode 100644 index 000000000..4a5b41040 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_voice_dnd.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_voice_offline.png b/src/main/res/drawable-xxhdpi/ic_send_voice_offline.png new file mode 100644 index 000000000..3d58f6994 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_voice_offline.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_voice_online.png b/src/main/res/drawable-xxhdpi/ic_send_voice_online.png new file mode 100644 index 000000000..600371eb2 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_voice_online.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_launcher.png b/src/main/res/drawable-xxxhdpi/ic_launcher.png index b6dcb0b9f..668504dfb 100644 Binary files a/src/main/res/drawable-xxxhdpi/ic_launcher.png and b/src/main/res/drawable-xxxhdpi/ic_launcher.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_notification.png b/src/main/res/drawable-xxxhdpi/ic_notification.png index ee2f3a432..c3439f1a3 100644 Binary files a/src/main/res/drawable-xxxhdpi/ic_notification.png and b/src/main/res/drawable-xxxhdpi/ic_notification.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_received_indicator.png b/src/main/res/drawable-xxxhdpi/ic_received_indicator.png index 86db9890c..f35c8b442 100644 Binary files a/src/main/res/drawable-xxxhdpi/ic_received_indicator.png and b/src/main/res/drawable-xxxhdpi/ic_received_indicator.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_cancel_away.png b/src/main/res/drawable-xxxhdpi/ic_send_cancel_away.png new file mode 100644 index 000000000..a9340a886 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_cancel_away.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_cancel_dnd.png b/src/main/res/drawable-xxxhdpi/ic_send_cancel_dnd.png new file mode 100644 index 000000000..307ca8a0f Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_cancel_dnd.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_cancel_offline.png b/src/main/res/drawable-xxxhdpi/ic_send_cancel_offline.png new file mode 100644 index 000000000..abd3af2c2 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_cancel_offline.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_cancel_online.png b/src/main/res/drawable-xxxhdpi/ic_send_cancel_online.png new file mode 100644 index 000000000..80be8edfc Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_cancel_online.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_location_away.png b/src/main/res/drawable-xxxhdpi/ic_send_location_away.png new file mode 100644 index 000000000..0fab2554a Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_location_away.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_location_dnd.png b/src/main/res/drawable-xxxhdpi/ic_send_location_dnd.png new file mode 100644 index 000000000..e7f6fde74 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_location_dnd.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_location_offline.png b/src/main/res/drawable-xxxhdpi/ic_send_location_offline.png new file mode 100644 index 000000000..2af75cde3 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_location_offline.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_location_online.png b/src/main/res/drawable-xxxhdpi/ic_send_location_online.png new file mode 100644 index 000000000..2e54ef892 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_location_online.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_photo_away.png b/src/main/res/drawable-xxxhdpi/ic_send_photo_away.png new file mode 100644 index 000000000..ba171ce13 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_photo_away.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_photo_dnd.png b/src/main/res/drawable-xxxhdpi/ic_send_photo_dnd.png new file mode 100644 index 000000000..8a9b07005 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_photo_dnd.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_photo_offline.png b/src/main/res/drawable-xxxhdpi/ic_send_photo_offline.png new file mode 100644 index 000000000..e94e930dd Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_photo_offline.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_photo_online.png b/src/main/res/drawable-xxxhdpi/ic_send_photo_online.png new file mode 100644 index 000000000..1bf680ebb Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_photo_online.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_text_away.png b/src/main/res/drawable-xxxhdpi/ic_send_text_away.png new file mode 100644 index 000000000..afcfa89d9 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_text_away.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_text_dnd.png b/src/main/res/drawable-xxxhdpi/ic_send_text_dnd.png new file mode 100644 index 000000000..b11cd6b62 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_text_dnd.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_text_offline.png b/src/main/res/drawable-xxxhdpi/ic_send_text_offline.png new file mode 100644 index 000000000..b9122e454 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_text_offline.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_text_online.png b/src/main/res/drawable-xxxhdpi/ic_send_text_online.png new file mode 100644 index 000000000..abec2e0b7 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_text_online.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_voice_away.png b/src/main/res/drawable-xxxhdpi/ic_send_voice_away.png new file mode 100644 index 000000000..de1375e2f Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_voice_away.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_voice_dnd.png b/src/main/res/drawable-xxxhdpi/ic_send_voice_dnd.png new file mode 100644 index 000000000..4ad9d389e Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_voice_dnd.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_voice_offline.png b/src/main/res/drawable-xxxhdpi/ic_send_voice_offline.png new file mode 100644 index 000000000..eec3d8f2b Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_voice_offline.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_voice_online.png b/src/main/res/drawable-xxxhdpi/ic_send_voice_online.png new file mode 100644 index 000000000..fcdfcb437 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_voice_online.png differ diff --git a/src/main/res/layout/fragment_conversation.xml b/src/main/res/layout/fragment_conversation.xml index f9aae10a0..5aa7dffab 100644 --- a/src/main/res/layout/fragment_conversation.xml +++ b/src/main/res/layout/fragment_conversation.xml @@ -57,7 +57,7 @@ android:layout_alignParentRight="true" android:layout_centerVertical="true" android:background="?android:selectableItemBackground" - android:src="@drawable/ic_action_send_now_offline" /> + android:src="@drawable/ic_send_text_offline" /> - - - \ No newline at end of file diff --git a/src/main/res/values-ca/strings.xml b/src/main/res/values-ca/strings.xml index a0861b167..9583932bb 100644 --- a/src/main/res/values-ca/strings.xml +++ b/src/main/res/values-ca/strings.xml @@ -24,8 +24,8 @@ Escollir un contacte LLista bloqueix Ara - 1 min avans - %de minuts avans + 1 min abans + %de minuts abans Converses sense llegir o no llegides enviant… Desxifrant missatge. Espera si us plau… @@ -359,6 +359,7 @@ No s\'ha pogut canviar la contrasenya Començar a enviar un missatge de conversació xifrat Fer una pregunta + Si vosté i el seu contacte tenen un secret en comú que ningú més sap (com una broma o simplement el que vau dinar l\'última vegada que es van trobar) pot utilitzar aquest secret per comprovar les empremtes de cadascú.\n\nProporcionaràs una pista o una pregunta a la que el seu contacte donarà una resposta, que distingeix entre majúscules i minúscules. El seu contacte l\'hi agradaria verificar la seva empremta digital per un repte amb un secret compartit.El seu contacte proporciona el següent suggeriment o pregunta per aquest secret. El seu suggeriment no pot estar buit El teu secret compartit no pot estar buit @@ -416,4 +417,33 @@ Enviant %s Oferint %s Amaga el fora de línia + Deshabilita el compte + %s està escrivint... + %s ha deixat d\'escriure + Notificacions d\'escriptura + Permet el teu contacte saber quan estàs escrivint un missatge nou + Enviar localització + Mostrar localització + No s\'ha trobat cap aplicació per mostrar la localització + Localització + Localització rebuda + Conversa tancada + S\'ha sortit de la conferència + Opcions de certificats + No confiar en les CAs del sistema + Tots els certificats han de ser aprovats manualment + Eliminar certificats + Esborrar certificats aprovats manualment + No hi ha certificats aprovats manualment + Esborrar certificats + Esborrar selecció + Cancel·lar + + %d certificat esborrat + %d certificats esborrats + + + Seleccionar %d contacte + Seleccionar %d contactes + diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index e18493573..70c3a49df 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -202,6 +202,7 @@ Wiadomość zaszyfrowana. Zainstaluj OpenKeychain, żeby odszyfrować. Nieznany odcisk klucza OTR Znaleziono wiadomości zaszyfrowane przez OpenPGP + Odbiór nieudany Twój odcisk klucza Odcisk klucza OTR Weryfikuj @@ -236,7 +237,9 @@ Serwer odrzucił żądanie publikacji Wystąpił błąd podczas konwersji obrazu Nie udało się zapisać obrazu w pamięci urządzenia + (lub przytrzymaj długo, aby ustawić domyślny) Serwer nie udostępnia możliwości publikacji awatarów + szepcze do %s Wyślij prywatną wiadomość do %s Połącz @@ -252,10 +255,12 @@ Konferencja jest zabezpieczona hasłem Wprowadź hasło Kontakt nie udostępnia powiadomień o obecności + Poproś kontakt o udostępnienie powiadomień o obecności.\n\nPozwoli to na ustalenie klienta, z którego korzysta rozmówca. Zażądaj teraz Usuń odcisk klucza Czy na pewno chcesz usunąć odcisk klucza? Ignoruj + Uwaga: Wysyłanie bez obustronnych powiadomień o obecności może powodować nieoczekiwane problemy.\n\nSprawdź subskrypcję powiadomień w szczegółach kontaktu. Ustawienia szyfrowania Wymuszaj szyfrowanie typu end-to-end Szyfruj wszystkie wiadomości (poza konferencjami) @@ -283,6 +288,7 @@ Zbanowano cię w konferencji To jest zamknięty pokój Wyrzucono cię z konferencji + używając konta %s Sprawdzanie obrazka na hoście HTTP Obraz został usunięty Brak połączenia. Spróbuj ponownie później @@ -302,6 +308,7 @@ Szczegóły konta Weryfikuj OTR Zdalny odcisk klucza + skanuj (lub zetknij telefony) Protokół socialist millionaire Podpowiedź lub pytanie @@ -309,6 +316,7 @@ Potwierdź W toku Odpowiedz + Operacja nieudana Sekrety są niezgodne Spróbuj ponownie Zakończ @@ -382,6 +390,7 @@ Konferencja prywatna, dla zaakceptowanych uczestników Opcje konferencji Prywatna (tylko zaakceptowani) + Nieanonimowa Opcje konferencji zostały zmienione! Nie udało się zmienić opcji konferencji Nigdy @@ -420,7 +429,23 @@ Otrzymano lokalizację Zamknięto konwersację Opuszczono konferencję + Ustawienia certyfikatów Nie ufaj certyfikatom systemowym Wymagaj ręcznego potwierdzania certyfikatów + Usuń certyfikat + Wybierz zaufane certyfikaty do usunięcia + Brak ręcznie zaufanych certyfikatów + Usuń certyfikaty + Usuń zaznaczone Anuluj + + Usunięto %d certyfikat + Usunięto %d certyfikaty + Usunięto %d certyfikatów + + + %d kontakt wybrany + %d kontakty wybrane + %d kontaktów wybranych + diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 2fc257f7c..7b1048209 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -441,11 +441,13 @@ Удалён %d сертификат Удалено %d сертификатов + Удалено %d сертификатов Выбран %d контакт Выбрано %d контактов + Выбрано %d контактов diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml index bcf420ba4..ea5cbdb85 100644 --- a/src/main/res/values-sr/strings.xml +++ b/src/main/res/values-sr/strings.xml @@ -97,8 +97,8 @@ Опште ИксМПП ресурс Име са којим се овај клијент идентификује - Прихваћај фајлове - Аутоматски прихваћај фајлове мање од… + Прихватај фајлове + Аутоматски прихватај фајлове мање од… Поставке обавештења Обавештења Обавести кад стигне нова порука diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 49ca9b974..2f80e064a 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -11,6 +11,10 @@ 编辑姓名 添加到手机通讯录 从列表中删除 + 屏蔽联系人 + 解除联系人屏蔽 + 屏蔽域名 + 解除域名屏蔽 管理账户 设置 讨论组详情 @@ -18,6 +22,7 @@ 分享会话 开始会话 选择联系人 + 屏蔽列表 刚刚 1 分钟前 %d分钟前 @@ -30,56 +35,65 @@ 版主 参与者 访客 - 将 %s从列表中移除? 与该联系人的会话消息不会清除. - 从书签中移除 %s?相关会话消息不会被清除 . + 将 %s 从列表中移除? 与该联系人的会话消息不会清除. + 你想屏蔽 %s 将不能发送信息给你? + 你想解除对 %s 的屏蔽吗,他们将可以发送信息给你? + 屏蔽 %s 中的所有联系人? + 解除对 %s 中所有联系人的屏蔽? + 联系人已屏蔽 + 从书签中移除 %s ?相关会话消息不会被清除 . 在服务器上注册新账户 + 在服务器上改变密码 分享 开始会话 邀请联系人 联系人 取消 + 设置 添加 编辑 删除 + 屏蔽 + 解除屏蔽 保存 完成 - Conversations停止运行 - 发送堆栈跟踪到正在开发Conversations的人员\n警告: 该操作将用您的 XMPP账户发送堆栈跟踪到开发人员. + Conversations 崩溃 + 发送堆栈跟踪信息到 Conversations 的开发人员\n警告: 该操作将用您的 XMPP 账户发送堆栈跟踪给开发人员。 现在发送 不再询问 无法连接至账户 无法连接至多个账户 点击此处管理账户 - 附件 - 该联系人不在您的列表.需要加为联系人吗 ? + 附加文件 + 该联系人不在您的列表。需要加为联系人吗 ? 添加联系人 传递失败 拒绝 准备传输图像 清除历史记录 清除会话记录 - 删除该会话中所有信息?\n\n注: 该操作不会影响其他设备或服务器保存的信息. + 删除该会话中所有信息?\n\n注: 该操作不会影响其他设备或服务器保存的信息。 删除消息 之后结束该会话 添加在线用户至联系人 发送纯文本信息 发送 OTR 加密信息 发送 OpenPGP 加密信息 - 用户名修改成功 + 昵称修改成功 下载图片 不加密发送 - 解密失败,可能是私钥不正确. + 解密失败,可能是私钥不正确。 OpenKeychain - 会话运用了第三方app,名为 OpenKeychain 用来加密、解码信息以及管理您的公钥.\n\nOpenKeychain 遵循 GPLv3 并且在 F-Droid和Google Play上可操作.\n\n(之后请重启conversations.) + 会话运用了第三方app,名为 OpenKeychain 用来加密、解密信息以及管理您的密钥。\n\nOpenKeychain 遵循 GPLv3 并且可以在 F-Droid 和 Google Play 上获取。\n\n(之后请重启 conversations) 重启 安装 输入… 等待… - 未发现OpenPGP 密码 - 会话加密信息失败,因为联系人未告知他/她的公钥.\n\n请通知联系人设置 OpenPGP. - 未找到 OpenPGP 密码 + 未发现 OpenPGP 密钥 + 会话加密信息失败,因为联系人未提供他/她的公钥。\n\n请通知联系人设置 OpenPGP。 + 未找到 OpenPGP 密钥 因您的联系人未公布公钥,Conversations未能成功加密您的信息.\n\n请通知联系人设置OpenPGP. - 加密信息已接收.点击进行解密和查看. + 加密信息已接收。点击解密并查看。 常规 XMPP 资源 客户端标识名称 @@ -91,41 +105,41 @@ 震动 收到新消息时震动 声音 - 收到新消息时播放铃声 + 收到新消息时的铃声 讨论组通知 当有新的消息时总是通知而不是亮屏时才通知 通知限期 接收副本短时间内关闭通知 高级选项 - 总不发送故障报告 - 发送堆栈跟踪帮助Conversations开发人员 + 总不发送崩溃报告 + 发送堆栈跟踪帮助 Conversations 开发人员 确认消息 当你已收到消息并且已阅时通知好友 - UI选项 + UI 选项 OpenKeychain 报告了一个错误 - 解码文件时出现I/O错误 + 解密文件时出现 I/O 错误 接受 产生了一个错误 同意更新在线联系人 - 预先同意并请求您的联系人进行更新 + 预先同意并请求更新您的联系人 关注 你的账号 - Keys - 发送在线联系人更新列表 - 接收在线联系人更新列表 - 请求在线联系人更新列表 + 密钥 + 发送在线联系人列表更新 + 接收在线联系人列表更新 + 请求在线联系人列表更新 选择图片 照相 预先同意订阅请求 您选择的文件不是图像文件 转换图像出错 未找到文件 - 常规的I/O错误.可能是存储空间不足的原因? - 您用来选择图片的app没有给予足够权限支持我们读取文件.\n\n请使用另一文件管理器选择图片 + 常规的 I/O 错误。可能是存储空间不足? + 您用来选择图片的 app 没有给予足够权限支持我们读取文件。\n\n请使用另一文件管理器选择图片 未知 暂时不可用 在线 - Connecting\u2026 + 连接中\u2026 离线 未授权 未找到服务器 @@ -134,6 +148,8 @@ 用户名已存在 注册完成 服务器不支持注册 + 安全错误 + 服务器不兼容 纯文本内容 OTR OpenPGP @@ -141,11 +157,11 @@ 删除账号 暂时不可用 发布头像 - 发布 OpenPGP 公共秘钥 + 发布 OpenPGP 公钥 启用账户 确定? 如果删除用户,所有会话信息将会丢失 - Record voice 录音 + 录音 Jabber ID 密码 username@example.com @@ -153,23 +169,28 @@ 密码 确认密码 密码不一致 - 该Jabber ID 无效 - 空间不足,图片过大 + 该 Jabber ID 无效 + 空间不足。图片过大 您将添加 %s 至手机联系人列表? 在线 - 免费对话 + 自由畅聊 离开 长时间离开 请勿打扰 离线 讨论组 其他成员 + 服务器信息 + XEP-0313: MAM XEP-0280: 消息碳 + XEP-0352: 客户端状态指示 + XEP-0191: 屏蔽指令 + XEP-0237: 花名册版本 XEP-0198: 流管理 XEP-0163: PEP (头像) 有效 无效 - 缺少公共秘钥公告 + 缺少公钥通知 最近一次查看为刚刚 最近一次查看为一分钟前 最近一次查看为 %d 分钟前 @@ -178,9 +199,9 @@ 最近一次查看为一天前 最近一次查看为 %d天前 未曾查看 - 加密信息. 请安装OpenKeychain进行解码. - 未知 OTR指纹 - OpenPGP 发现加密信息 + 加密信息. 请安装 OpenKeychain 以解密。 + 未知 OTR 指纹 + 发现 OpenPGP 加密信息 接收失败 你的指纹 OTR 指纹 @@ -192,6 +213,8 @@ 加入讨论组 删除联系人 查看联系人详细信息 + 屏蔽联系人 + 解除联系人屏蔽 创建 联系人已存在 加入 @@ -200,26 +223,26 @@ 保存为书签 删除书签 该书签已存在 - 你的 + 编辑讨论组主题 讨论组未找到 离开 联系人已添加你到联系人列表 反向添加 - 目前读到%s 处 + 目前读到 %s 处 发布 - 点击头像可选择头像 - 请注意: 所有关注您最新动态的人将看到该图像. - 发布… + 点击头像可从相册中选择头像 + 请注意: 所有关注您最新动态的人将看到该图像。 + 正在发布… 服务器拒绝了您的发布请求 - 转换头像出错 - 不能将头像保存至disk + 转换头像图片出错 + 不能将头像保存至磁盘 (或长按按钮将返回默认头像) 您的服务器不支持发布头像 密谈 至 %s - 发送私密消息到%s - Connect + 发送私密消息到 %s + 连接 该账号已存在 下一步 当前会话已建立 @@ -232,21 +255,193 @@ 讨论组设有密码 输入密码 缺少在线联系人更新 - 请先发送更新在线联系人请求.\n\n这将用来判断您的联系人所用的客户端类型人. + 请先发送更新在线联系人的请求。\n\n以判断您的联系人所用的客户端类型。 现在发送请求 删除指纹 是否确定删除该指纹? 忽略 - 警告:在没有相互更新在线联系人的情况下发送将会出现未知问题.\n\n到联系人详情确认您订阅的在线联系人. + 警告:在没有相互更新在线联系人的情况下发送将会出现未知问题。\n\n前往联系人详情以验证您订阅的在线联系人。 加密设置 - 强制要求 end-to-end 加密 + 强制要求端对端加密 总是发送加密信息(讨论组信息除外) 不保存加密信息 警告:此操作将会导致信息丢失 - Expert 选项 + 导出选项 请谨慎使用 + 关于 Conversations + 构建及许可证信息 + 静默时间段 + 开始时间 + 结束时间 + 启用静默时间段 + 在静默时间段内通知将保持静音 放大字体 - 整个app界面使用更大号的字体 + 整个 app 界面使用较大的字体 发送按钮显示状态 + 请求消息回复 + 如果支持消息将会以绿色对勾标识 发送按钮采用其他颜色以示发送状态的区别 + 其他 + 讨论组名称 + 用讨论组的主题来标示讨论组而不是 JID + OTR 指纹已拷贝到剪贴板! + 你被此讨论组屏蔽 + 此讨论组只允许成员加入 + 你被从此讨论组踢出 + 用账户 %s + 正在 HTTP 托管中检查图片 + 此图片已经被删除 + 你没有连接。请稍后重试 + 检查图片文件尺寸 + 消息选项 + 拷贝文本 + 拷贝原始URL + 再次发送 + 图片 URL + 消息文本 + 已经拷贝 URL 到剪贴板 + 消息已经拷贝到剪贴板 + 图片传送失败 + 扫描二维码 + 显示二维码 + 显示屏蔽列表 + 账户详情 + 验证 OTR + 远程指纹 + 扫描 + (或轻触手机) + Socialist Millionaire Protocol + 提示或问题 + 共知的秘密 + 确认 + 处理中 + 回应 + 失败 + 秘密不符 + 再试一遍 + 完成 + 验证通过! + 联系人请求 SMP 验证 + 没有找到 OTR 会话 + Conversations + 保持前台服务 + 防止操作系统中断你的连接 + 关闭文件 + 接收中 %1$s (已完成 %2$d%%) + 下载 %s + 文件 + 打开 %s + 发送中 (已完成 %1$d%%) + 准备传送文件 + 可以下载 %s + 取消传送 + 文件传送失败 + 文件已经删除 + 没有可以打开此文件的应用 + 不能验证指纹 + 手工验证 + 你确认验证你的联系人的 OTR 指纹? + 现实动态标签 + 在联系人下方显示只读标签 + 启用通知 + 与…创建讨论组 + 无法找到讨论组服务器 + 讨论组创建失败! + 讨论组已创建! + 秘密被接受! + 重置 + 账户头像 + 拷贝 OTR 指纹到剪贴板 + 从服务器获取历史记录 + 服务器上没有更多历史记录 + 更新中… + 密码已修改! + 不能修改密码 + 要启动加密聊天先发送一条消息 + 提出问题 + 如果你和你的联系人有一个共知的秘密(比如一个内部笑话或者仅仅只是上次见面时吃的午餐) 你可以使用这个秘密来验证彼此的指纹。\n\n你的联系人将以大小写敏感的方式给出答案,你可以给出提示或问题。 + 你的联系人可以通过一个你们共知的秘密来验证指纹。你的联系人给出了如下的提示或问题。 + 你的提示不能为空 + 你共知的秘密不能为空 + 请仔细核对下面显示出来的你的联系人的指纹。\n你可以使用任何可信赖的联系方式,比如加密邮件或电话,来交换这些指纹信息。 + 修改密码 + 当前密码 + 新密码 + 密码不能为空 + 启用所有账户 + 禁用所有账户 + 做一个动作和 + 没有从属关系 + 没有角色 + 抛弃 + 成员 + 高级模式 + 已授予的成员 + 吊销的成员 + 授予管理员权限 + 吊销管理员权限 + 从讨论组移出 + 不能修改 %s 的从属关系 + 屏蔽出讨论组 + 你正尝试将 %s 移出一个公共的讨论组。唯一的方式是永远屏蔽这个用户 + 现在屏蔽 + 不能修改 %s 的角色 + 公开访问的讨论组 + 私密,只有成员可以加入的讨论组 + 讨论组选项 + 私密(只对成员开放) + 非匿名 + 讨论组选项已修改! + 不能修改讨论组选项 + 从不 + 30 分钟 + 1 个小时 + 2 个小时 + 8 个小时 + 直到新的通知 + 输入选项 + 回车是发送 + 用回车键来发送消息 + 显示回车键 + 改变表情键为回车键 + 音频 + 视频 + 图像 + PDF 文档 + Android App + 联系人 + 已经收到 %s + 禁用前端服务 + 轻触打开 Conversations + 头像已经发布! + 发送中 %s + 提供中 %s + 隐藏离线联系人 + 禁用账户 + %s 正在输入… + %s 已停止输入 + 键盘输入通知 + 让对方知道你正在输入新消息 + 发送位置 + 显示位置 + 无法找到显示位置的应用 + 位置 + 位置已收到 + Conversation 已关闭 + 离开讨论组 + 证书选项 + 不相信系统 CA + 所有证书必须人工通过 + 移除证书 + 删除人工通过的证书 + 没有人工通过的证书 + 移除证书 + 删除选项 + 取消 + + %d 个证书已被删除 + + + 选择 %d 个联系人 + diff --git a/src/main/res/values/arrays.xml b/src/main/res/values/arrays.xml index c413a3c48..fec077ccc 100644 --- a/src/main/res/values/arrays.xml +++ b/src/main/res/values/arrays.xml @@ -40,4 +40,19 @@ -1 + + @string/none + @string/recently_used + @string/attach_take_picture + @string/attach_record_voice + @string/send_location + + + + none + recent + photo + voice + location + diff --git a/src/main/res/values/colors.xml b/src/main/res/values/colors.xml index c21650a5d..753a56df9 100644 --- a/src/main/res/values/colors.xml +++ b/src/main/res/values/colors.xml @@ -11,7 +11,7 @@ #ffeeeeee #ff323232 #1f000000 - #ffe51c23 + #fff44336 #ffff9800 #ff259b24 diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index cc7727d22..4631bd60c 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -1,397 +1,397 @@ - Conversations - Settings - New conversation - Manage accounts - End this conversation - Contact details - Conference details - Secure conversation - Add account - Edit name - Add to phone book - Delete from roster - Block contact - Unblock contact - Block domain - Unblock domain - Manage Accounts - Settings - Conference Details - Contact Details - Share with Conversation - Start Conversation - Choose contact - Block list - just now - 1 min ago - %d mins ago - unread Conversations - sending… - Decrypting message. Please wait… - Nickname is already in use - Admin - Owner - Moderator - Participant - Visitor - Would you like to remove %s from your roster? The conversation associated with this contact will not be removed. - Would you like to block %s from sending you messages? - Would you like to unblock %s and allow them to send you messages? - Block all contacts from %s? - Unblock all contacts from %s? - Contact blocked - Would you like to remove %s as a bookmark? The conversation associated with this bookmark will not be removed. - Register new account on server - Change password on server - Share with… - Start Conversation - Invite Contact - Contacts - Cancel - Set - Add - Edit - Delete - Block - Unblock - Save - OK - Conversations has crashed - By sending in stack traces you are helping the ongoing development of Conversations\nWarning: This will use your XMPP account to send the stack trace to the developer. - Send now - Never ask again - Unable to connect to account - Unable to connect to multiple accounts - Touch here to manage your accounts - Attach file - The contact is not in your roster. Would you like to add it? - Add contact - delivery failed - rejected - Preparing image for transmission - Clear history - Clear Conversation History - Do you want to delete all messages within this Conversation?\n\nWarning: This will not influence messages stored on other devices or servers. - Delete messages - End this conversations afterwards - Choose presence to contact - Send plain text message - Send OTR encrypted message - Send OpenPGP encrypted message - Your nickname has been changed - Download Image - Send unencrypted - Decryption failed. Maybe you don’t have the proper private key. - OpenKeychain - Conversations utilizes a third party app called OpenKeychain to encrypt and decrypt messages and to manage your public keys.\n\nOpenKeychain is licensed under GPLv3 and available on F-Droid and Google Play.\n\n(Please restart Conversations afterwards.) - Restart - Install - offering… - waiting… - No OpenPGP Key found - Conversations is unable to encrypt your messages because your contact is not announcing his or hers public key.\n\nPlease ask your contact to setup OpenPGP. - No OpenPGP Keys found - Conversations is unable to encrypt your messages because your contacts are not announcing their public key.\n\nPlease ask your contacts to setup OpenPGP. - Encrypted message received. Touch to view and decrypt. - General - XMPP resource - The name this client identifies itself with - Accept files - Automatically accept files smaller than… - Notification Settings - Notifications - Notify when a new message arrives - Vibrate - Also vibrate when a new message arrives - Sound - Play ringtone with notification - Conference notifications - Always notify when a new conference message arrives instead of only when highlighted - Notification grace period - Disable notifications for a short time after a carbon copy was received - Advanced Options - Never send crash reports - By sending in stack traces you are helping the ongoing development of Conversations - Confirm Messages - Let your contact know when you have received and read a message - UI Options - OpenKeychain reported an error - I/O Error decrypting file - Accept - An error has occurred - Grant presence updates - Preemptively grant and ask for presence subscription for contacts you created - Subscriptions - Your account - Keys - Send presence updates - Receive presence updates - Ask for presence updates - Choose picture - Take picture - Preemptively grant subscription request - The file you selected is not an image - Error while converting the image file - File not found - General I/O error. Maybe you ran out of storage space? - The app you used to select this image did not provide us with enough permissions to read the file.\n\nUse a different file manager to choose an image - Unknown - Temporarily disabled - Online - Connecting\u2026 - Offline - Unauthorized - Server not found - No connectivity - Registration failed - Username already in use - Registration completed - Server does not support registration - Security error - Incompatible server - Plain text - OTR - OpenPGP - Edit account - Delete account - Temporarily disable - Publish avatar - Publish OpenPGP public key - Enable account - Are you sure? - If you delete your account your entire conversation history will be lost - Record voice - Jabber ID - Password - username@example.com - Confirm password - Password - Confirm password - Passwords do not match - This is not a valid Jabber ID - Out of memory. Image is too large - Do you want to add %s to your phones contact list? - online - free to chat - away - extended away - do not disturb - offline - Conference - Other Members - Server info - XEP-0313: MAM - XEP-0280: Message Carbons - XEP-0352: Client State Indication - XEP-0191: Blocking Command - XEP-0237: Roster Versioning - XEP-0198: Stream Management - XEP-0163: PEP (Avatars) - available - unavailable - Missing public key announcements - last seen just now - last seen 1 minute ago - last seen %d minutes ago - last seen 1 hour ago - last seen %d hours ago - last seen 1 day ago - last seen %d days ago - never seen - Encrypted message. Please install OpenKeychain to decrypt. - Unknown OTR fingerprint - OpenPGP encrypted messages found - Reception failed - Your fingerprint - OTR fingerprint - Verify - Decrypt - Conferences - Search - Create Contact - Join Conference - Delete Contact - View contact details - Block contact - Unblock contact - Create - The contact already exists - Join - Conference address - room@conference.example.com - Save as bookmark - Delete bookmark - This bookmark already exists - You - Edit conference subject - Conference not found - Leave - Contact added you to contact list - Add back - %s has read up to this point - Publish - Touch avatar to select picture from gallery - Please note: Everyone subscribed to your presence updates will be allowed to see this picture. - Publishing… - The server rejected your publication - Something went wrong while converting your picture - Could not save avatar to disk - (Or long press to bring back default) - Your server does not support the publication of avatars - whispered - to %s - Send private message to %s - Connect - This account already exists - Next - Current session established - Additional Information - Skip - Disable notifications - Disable notifications for this conversation - Notifications are disabled - Enable - Conference requires password - Enter password - Missing presence updates from contact - Please request presence updates from your contact first.\n\nThis will be used to determine what client(s) your contact is using. - Request now - Delete Fingerprint - Are you sure you would like to delete this fingerprint? - Ignore - Warning: Sending this without mutual presence updates could cause unexpected problems.\n\nGo to contact details to verify your presence subscriptions. - Encryption settings - Force end-to-end encryption - Always send messages encrypted (except for conferences) - Don’t save encrypted messages - Warning: This could lead to message loss - Expert options - Please be careful with these - About Conversations - Build and licensing information - - Conversations • the very last word in instant messaging. - \n\nCopyright © 2014 Daniel Gultsch - \n\nThis program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - \n\nThis program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - \n\nYou should have received a copy of the GNU General Public License - along with this program. If not, see https://www.gnu.org/licenses - \n\nDownload the full source code at https://github.com/siacs/Conversations - \n\n\nLibraries - \n\nhttps://www.bouncycastle.org\n(The MIT License (MIT)) - \n\nhttps://www.gnu.org/software/libidn\n(Apache License, Version 2.0) - \n\nhttps://github.com/ge0rg/MemorizingTrustManager\n(The MIT License (MIT)) - \n\nhttps://github.com/rtreffer/minidns\n(WTFPL) - \n\nhttps://github.com/open-keychain/openkeychain-api-lib\n(Apache License, Version 2.0) - \n\nhttps://github.com/jitsi/otr4j\n(LGPL-3.0) - \n\nhttps://developer.android.com/tools/support-library\n(Apache License, Version 2.0) - \n\nhttps://github.com/zxing/zxing\n(Apache License, Version 2.0) - \n\nhttps://github.com/google/material-design-icons\n(CC BY 4.0) - \n\nhttps://github.com/timroes/EnhancedListView\n(Apache License, Version 2.0) - - Quiet Hours - Start time - End time - Enable quiet hours - Notifications will be silenced during quiet hours - Increase font size - Use larger font sizes across the entire app - Send button indicates status - Request message receipts - Received messages will be marked with a green tick if supported - Colorize send button to indicate contact status - Other - Conference name - Use room’s subject instead of JID to identify conferences - OTR fingerprint copied to clipboard! - You are banned from this conference - This conference is members only - You have been kicked from this conference - using account %s - Checking image on HTTP host - The image file has been deleted - You are not connected. Try again later - Check image file size - Message options - Copy text - Copy original URL - Send again - Image URL - Message text - URL copied to clipboard - Message copied to clipboard - Image transmission failed - Scan QR code - Show QR code - Show block list - Account details - Verify OTR - Remote Fingerprint - scan - (or touch phones) - Socialist Millionaire Protocol - Hint or Question - Shared Secret - Confirm - In progress - Respond - Failed - Secrets do not match - Try again - Finish - Verified! - Contact requested SMP verification - No valid OTR session has been found! - Conversations - Keep service in foreground - Prevents the operating system from killing your connection - Choose file - Receiving %1$s (%2$d%% completed) - Download %s - file - Open %s - sending (%1$d%% completed) - Preparing file for transmission - %s offered for download - Cancel transmission - file transmission failed - The file has been deleted - No application found to open file - Could not verify fingerprint - Manually verify - Are you sure that you want to verify your contacts OTR fingerprint? - Show dynamic tags - Display read-only tags underneath contacts - Enable notifications - Create conference with… - No conference server found - Conference creation failed! - Conference created! - Secret accepted! - Reset - Account avatar - Copy OTR fingerprint to clipboard - Fetching history from server - No more history on server - Updating… - Password changed! - Could not change password - Send a message to start an encrypted chat - Ask question - If you and your contact have a secret in common that no one else knows (like an inside joke or simply what you had for lunch the last time you met) you can use that secret to verify each other’s fingerprints.\n\nYou provide a hint or a question for your contact who will respond with a case-sensitive answer. - Your contact would like to verify your fingerprint by challenging you with a shared secret. Your contact provided the following hint or question for that secret. - Your hint should not be empty - Your shared secret can not be empty - Carefully compare the fingerprint shown below with the fingerprint of your contact.\nYou can use any trusted form of communication like an encrypted e-mail or a telephone call to exchange those. + Conversations + Settings + New conversation + Manage accounts + End this conversation + Contact details + Conference details + Secure conversation + Add account + Edit name + Add to phone book + Delete from roster + Block contact + Unblock contact + Block domain + Unblock domain + Manage Accounts + Settings + Conference Details + Contact Details + Share with Conversation + Start Conversation + Choose contact + Block list + just now + 1 min ago + %d mins ago + unread Conversations + sending… + Decrypting message. Please wait… + Nickname is already in use + Admin + Owner + Moderator + Participant + Visitor + Would you like to remove %s from your roster? The conversation associated with this contact will not be removed. + Would you like to block %s from sending you messages? + Would you like to unblock %s and allow them to send you messages? + Block all contacts from %s? + Unblock all contacts from %s? + Contact blocked + Would you like to remove %s as a bookmark? The conversation associated with this bookmark will not be removed. + Register new account on server + Change password on server + Share with… + Start Conversation + Invite Contact + Contacts + Cancel + Set + Add + Edit + Delete + Block + Unblock + Save + OK + Conversations has crashed + By sending in stack traces you are helping the ongoing development of Conversations\nWarning: This will use your XMPP account to send the stack trace to the developer. + Send now + Never ask again + Unable to connect to account + Unable to connect to multiple accounts + Touch here to manage your accounts + Attach file + The contact is not in your roster. Would you like to add it? + Add contact + delivery failed + rejected + Preparing image for transmission + Clear history + Clear Conversation History + Do you want to delete all messages within this Conversation?\n\nWarning: This will not influence messages stored on other devices or servers. + Delete messages + End this conversations afterwards + Choose presence to contact + Send plain text message + Send OTR encrypted message + Send OpenPGP encrypted message + Your nickname has been changed + Download Image + Send unencrypted + Decryption failed. Maybe you don’t have the proper private key. + OpenKeychain + Conversations utilizes a third party app called OpenKeychain to encrypt and decrypt messages and to manage your public keys.\n\nOpenKeychain is licensed under GPLv3 and available on F-Droid and Google Play.\n\n(Please restart Conversations afterwards.) + Restart + Install + offering… + waiting… + No OpenPGP Key found + Conversations is unable to encrypt your messages because your contact is not announcing his or hers public key.\n\nPlease ask your contact to setup OpenPGP. + No OpenPGP Keys found + Conversations is unable to encrypt your messages because your contacts are not announcing their public key.\n\nPlease ask your contacts to setup OpenPGP. + Encrypted message received. Touch to view and decrypt. + General + XMPP resource + The name this client identifies itself with + Accept files + Automatically accept files smaller than… + Notification Settings + Notifications + Notify when a new message arrives + Vibrate + Also vibrate when a new message arrives + Sound + Play ringtone with notification + Conference notifications + Always notify when a new conference message arrives instead of only when highlighted + Notification grace period + Disable notifications for a short time after a carbon copy was received + Advanced Options + Never send crash reports + By sending in stack traces you are helping the ongoing development of Conversations + Confirm Messages + Let your contact know when you have received and read a message + UI Options + OpenKeychain reported an error + I/O Error decrypting file + Accept + An error has occurred + Grant presence updates + Preemptively grant and ask for presence subscription for contacts you created + Subscriptions + Your account + Keys + Send presence updates + Receive presence updates + Ask for presence updates + Choose picture + Take picture + Preemptively grant subscription request + The file you selected is not an image + Error while converting the image file + File not found + General I/O error. Maybe you ran out of storage space? + The app you used to select this image did not provide us with enough permissions to read the file.\n\nUse a different file manager to choose an image + Unknown + Temporarily disabled + Online + Connecting\u2026 + Offline + Unauthorized + Server not found + No connectivity + Registration failed + Username already in use + Registration completed + Server does not support registration + Security error + Incompatible server + Plain text + OTR + OpenPGP + Edit account + Delete account + Temporarily disable + Publish avatar + Publish OpenPGP public key + Enable account + Are you sure? + If you delete your account your entire conversation history will be lost + Record voice + Jabber ID + Password + username@example.com + Confirm password + Password + Confirm password + Passwords do not match + This is not a valid Jabber ID + Out of memory. Image is too large + Do you want to add %s to your phones contact list? + online + free to chat + away + extended away + do not disturb + offline + Conference + Other Members + Server info + XEP-0313: MAM + XEP-0280: Message Carbons + XEP-0352: Client State Indication + XEP-0191: Blocking Command + XEP-0237: Roster Versioning + XEP-0198: Stream Management + XEP-0163: PEP (Avatars) + available + unavailable + Missing public key announcements + last seen just now + last seen 1 minute ago + last seen %d minutes ago + last seen 1 hour ago + last seen %d hours ago + last seen 1 day ago + last seen %d days ago + never seen + Encrypted message. Please install OpenKeychain to decrypt. + Unknown OTR fingerprint + OpenPGP encrypted messages found + Reception failed + Your fingerprint + OTR fingerprint + Verify + Decrypt + Conferences + Search + Create Contact + Join Conference + Delete Contact + View contact details + Block contact + Unblock contact + Create + The contact already exists + Join + Conference address + room@conference.example.com + Save as bookmark + Delete bookmark + This bookmark already exists + You + Edit conference subject + Conference not found + Leave + Contact added you to contact list + Add back + %s has read up to this point + Publish + Touch avatar to select picture from gallery + Please note: Everyone subscribed to your presence updates will be allowed to see this picture. + Publishing… + The server rejected your publication + Something went wrong while converting your picture + Could not save avatar to disk + (Or long press to bring back default) + Your server does not support the publication of avatars + whispered + to %s + Send private message to %s + Connect + This account already exists + Next + Current session established + Additional Information + Skip + Disable notifications + Disable notifications for this conversation + Notifications are disabled + Enable + Conference requires password + Enter password + Missing presence updates from contact + Please request presence updates from your contact first.\n\nThis will be used to determine what client(s) your contact is using. + Request now + Delete Fingerprint + Are you sure you would like to delete this fingerprint? + Ignore + Warning: Sending this without mutual presence updates could cause unexpected problems.\n\nGo to contact details to verify your presence subscriptions. + Encryption settings + Force end-to-end encryption + Always send messages encrypted (except for conferences) + Don’t save encrypted messages + Warning: This could lead to message loss + Expert options + Please be careful with these + About Conversations + Build and licensing information + + Conversations • the very last word in instant messaging. + \n\nCopyright © 2014 Daniel Gultsch + \n\nThis program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + \n\nThis program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + \n\nYou should have received a copy of the GNU General Public License + along with this program. If not, see https://www.gnu.org/licenses + \n\nDownload the full source code at https://github.com/siacs/Conversations + \n\n\nLibraries + \n\nhttps://www.bouncycastle.org\n(The MIT License (MIT)) + \n\nhttps://www.gnu.org/software/libidn\n(Apache License, Version 2.0) + \n\nhttps://github.com/ge0rg/MemorizingTrustManager\n(The MIT License (MIT)) + \n\nhttps://github.com/rtreffer/minidns\n(WTFPL) + \n\nhttps://github.com/open-keychain/openkeychain-api-lib\n(Apache License, Version 2.0) + \n\nhttps://github.com/jitsi/otr4j\n(LGPL-3.0) + \n\nhttps://developer.android.com/tools/support-library\n(Apache License, Version 2.0) + \n\nhttps://github.com/zxing/zxing\n(Apache License, Version 2.0) + \n\nhttps://github.com/google/material-design-icons\n(CC BY 4.0) + \n\nhttps://github.com/timroes/EnhancedListView\n(Apache License, Version 2.0) + + Quiet Hours + Start time + End time + Enable quiet hours + Notifications will be silenced during quiet hours + Increase font size + Use larger font sizes across the entire app + Send button indicates status + Request message receipts + Received messages will be marked with a green tick if supported + Colorize send button to indicate contact status + Other + Conference name + Use room’s subject instead of JID to identify conferences + OTR fingerprint copied to clipboard! + You are banned from this conference + This conference is members only + You have been kicked from this conference + using account %s + Checking image on HTTP host + The image file has been deleted + You are not connected. Try again later + Check image file size + Message options + Copy text + Copy original URL + Send again + Image URL + Message text + URL copied to clipboard + Message copied to clipboard + Image transmission failed + Scan QR code + Show QR code + Show block list + Account details + Verify OTR + Remote Fingerprint + scan + (or touch phones) + Socialist Millionaire Protocol + Hint or Question + Shared Secret + Confirm + In progress + Respond + Failed + Secrets do not match + Try again + Finish + Verified! + Contact requested SMP verification + No valid OTR session has been found! + Conversations + Keep service in foreground + Prevents the operating system from killing your connection + Choose file + Receiving %1$s (%2$d%% completed) + Download %s + file + Open %s + sending (%1$d%% completed) + Preparing file for transmission + %s offered for download + Cancel transmission + file transmission failed + The file has been deleted + No application found to open file + Could not verify fingerprint + Manually verify + Are you sure that you want to verify your contacts OTR fingerprint? + Show dynamic tags + Display read-only tags underneath contacts + Enable notifications + Create conference with… + No conference server found + Conference creation failed! + Conference created! + Secret accepted! + Reset + Account avatar + Copy OTR fingerprint to clipboard + Fetching history from server + No more history on server + Updating… + Password changed! + Could not change password + Send a message to start an encrypted chat + Ask question + If you and your contact have a secret in common that no one else knows (like an inside joke or simply what you had for lunch the last time you met) you can use that secret to verify each other’s fingerprints.\n\nYou provide a hint or a question for your contact who will respond with a case-sensitive answer. + Your contact would like to verify your fingerprint by challenging you with a shared secret. Your contact provided the following hint or question for that secret. + Your hint should not be empty + Your shared secret can not be empty + Carefully compare the fingerprint shown below with the fingerprint of your contact.\nYou can use any trusted form of communication like an encrypted e-mail or a telephone call to exchange those. Change password Current password New password @@ -442,19 +442,19 @@ Disable foreground service Touch to open Conversations Avatar has been published! - Sending %s - Offering %s - Hide offline - Disable Account - %s is typing... - %s has stopped typing - Typing notifications - Let your contact know when you are writing a new message - Send location - Show location - No application found to display location - Location - Received location + Sending %s + Offering %s + Hide offline + Disable Account + %s is typing... + %s has stopped typing + Typing notifications + Let your contact know when you are writing a new message + Send location + Show location + No application found to display location + Location + Received location Conversation closed Left conference Certificate options @@ -474,4 +474,9 @@ Select %d contact Select %d contacts + Replace send button with quick action + Quick Action + None + Most recently used + Choose quick action diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index 417e60a43..5b7d69040 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -105,6 +105,14 @@ android:key="send_button_status" android:summary="@string/pref_use_send_button_to_indicate_status_summary" android:title="@string/pref_use_send_button_to_indicate_status" /> +