diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d5d92537..1d1e8cc13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Version 2.8.1 +* Audible feedback (dialing, call started, call ended) for voice calls. +* Fixed issue with retrying failed video call + ### Version 2.8.0 * Audio/Video calls (Requires server support in form of STUN and TURN servers discoverable via XEP-0215) diff --git a/build.gradle b/build.gradle index 2aaddce04..892c46743 100644 --- a/build.gradle +++ b/build.gradle @@ -96,8 +96,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 25 - versionCode 379 - versionName "2.8.0" + versionCode 381 + versionName "2.8.1" archivesBaseName += "-$versionName" applicationId "eu.sum7.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt deleted file mode 100644 index d03cefd88..000000000 --- a/fastlane/metadata/android/en-US/full_description.txt +++ /dev/null @@ -1,39 +0,0 @@ -Easy to use, reliable, battery friendly. With built-in support for images, group chats and e2e encryption. - -Design principles: - -* Be as beautiful and easy to use as possible without sacrificing security or privacy -* Rely on existing, well established protocols -* Do not require a Google Account or specifically Google Cloud Messaging (GCM) -* Require as few permissions as possible - -Features: - -* End-to-end encryption with either OMEMO or OpenPGP -* Sending and receiving images -* Make audio and video calls -* Intuitive UI that follows Android Design guidelines -* Pictures / Avatars for your Contacts -* Syncs with desktop client -* Conferences (with support for bookmarks) -* Address book integration -* Multiple accounts / unified inbox -* Very low impact on battery life - -Conversations makes it very easy to create an account on the conversations.im server. Using that server comes with an annual fee of 8 Euro after a 6 month trial period. However Conversations will work with any other XMPP server as well. A lot of XMPP servers are run by volunteers and are free of charge. - -XMPP Features: - -Conversations works with every XMPP server out there. However XMPP is an extensible protocol. These extensions are standardized as well in so called XEP’s. Conversations supports a couple of those to make the overall user experience better. There is a chance that your current XMPP server does not support these extensions. Therefore to get the most out of Conversations you should consider either switching to an XMPP server that does or - even better - run your own XMPP server for you and your friends. - -These XEPs are - as of now: - -* XEP-0065: SOCKS5 Bytestreams (or mod_proxy65). Will be used to transfer files if both parties are behind a firewall (NAT). -* XEP-0163: Personal Eventing Protocol for avatars -* XEP-0191: Blocking command lets you blacklist spammers or block contacts without removing them from your roster. -* XEP-0198: Stream Management allows XMPP to survive small network outages and changes of the underlying TCP connection. -* XEP-0280: Message Carbons which automatically syncs the messages you send to your desktop client and thus allows you to switch seamlessly from your mobile client to your desktop client and back within one conversation. -* XEP-0237: Roster Versioning mainly to save bandwidth on poor mobile connections -* XEP-0313: Message Archive Management synchronize message history with the server. Catch up with messages that were sent while Conversations was offline. -* XEP-0352: Client State Indication lets the server know whether or not Conversations is in the background. Allows the server to save bandwidth by withholding unimportant packages. -* XEP-0363: HTTP File Upload allows you to share files in conferences and with offline contacts. Requires an additional component on your server. diff --git a/metadata/en-US/changelogs/381.txt b/metadata/en-US/changelogs/381.txt new file mode 100644 index 000000000..0312a9d3b --- /dev/null +++ b/metadata/en-US/changelogs/381.txt @@ -0,0 +1,2 @@ +• Audible feedback (dialing, call started, call ended) for voice calls. +• Fixed issue with retrying failed video call diff --git a/src/conversations/res/values-ca/strings.xml b/src/conversations/res/values-ca/strings.xml new file mode 100644 index 000000000..71a32bc6b --- /dev/null +++ b/src/conversations/res/values-ca/strings.xml @@ -0,0 +1,5 @@ + + + Triï el seu proveïdor de XMPP + + \ No newline at end of file diff --git a/src/free/java/eu/siacs/conversations/services/PushManagementService.java b/src/free/java/eu/siacs/conversations/services/PushManagementService.java index 9fac3655e..f436da434 100644 --- a/src/free/java/eu/siacs/conversations/services/PushManagementService.java +++ b/src/free/java/eu/siacs/conversations/services/PushManagementService.java @@ -15,18 +15,10 @@ public class PushManagementService { //stub implementation. only affects playstore flavor } - void registerPushTokenOnServer(Conversation conversation) { - //stub implementation. only affects playstore flavor - } - void unregisterChannel(Account account, String hash) { //stub implementation. only affects playstore flavor } - void disablePushOnServer(Conversation conversation) { - //stub implementation. only affects playstore flavor - } - public boolean available(Account account) { return false; } diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 53d2d74b3..94efb8fcf 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -33,1011 +33,1023 @@ import static eu.siacs.conversations.entities.Bookmark.printableValue; public class Conversation extends AbstractEntity implements Blockable, Comparable, Conversational, AvatarService.Avatarable { - public static final String TABLENAME = "conversations"; - - public static final int STATUS_AVAILABLE = 0; - public static final int STATUS_ARCHIVED = 1; - - public static final String NAME = "name"; - public static final String ACCOUNT = "accountUuid"; - public static final String CONTACT = "contactUuid"; - public static final String CONTACTJID = "contactJid"; - public static final String STATUS = "status"; - public static final String CREATED = "created"; - public static final String MODE = "mode"; - public static final String ATTRIBUTES = "attributes"; - - public static final String ATTRIBUTE_MUTED_TILL = "muted_till"; - public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify"; - public static final String ATTRIBUTE_PUSH_NODE = "push_node"; - public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history"; - static final String ATTRIBUTE_MUC_PASSWORD = "muc_password"; - private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message"; - private static final String ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP = "next_message_timestamp"; - private static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets"; - private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption"; - private static final String ATTRIBUTE_CORRECTING_MESSAGE = "correcting_message"; - static final String ATTRIBUTE_MEMBERS_ONLY = "members_only"; - static final String ATTRIBUTE_MODERATED = "moderated"; - static final String ATTRIBUTE_NON_ANONYMOUS = "non_anonymous"; - public static final String ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS = "formerly_private_non_anonymous"; - protected final ArrayList messages = new ArrayList<>(); - public AtomicBoolean messagesLoaded = new AtomicBoolean(true); - protected Account account = null; - private String draftMessage; - private String name; - private String contactUuid; - private String accountUuid; - private Jid contactJid; - private int status; - private long created; - private int mode; - private JSONObject attributes; - private Jid nextCounterpart; - private transient MucOptions mucOptions = null; - private boolean messagesLeftOnServer = true; - private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE; - private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE; - private String mFirstMamReference = null; - - public Conversation(final String name, final Account account, final Jid contactJid, - final int mode) { - this(java.util.UUID.randomUUID().toString(), name, null, account - .getUuid(), contactJid, System.currentTimeMillis(), - STATUS_AVAILABLE, mode, ""); - this.account = account; - } - - public Conversation(final String uuid, final String name, final String contactUuid, - final String accountUuid, final Jid contactJid, final long created, final int status, - final int mode, final String attributes) { - this.uuid = uuid; - this.name = name; - this.contactUuid = contactUuid; - this.accountUuid = accountUuid; - this.contactJid = contactJid; - this.created = created; - this.status = status; - this.mode = mode; - try { - this.attributes = new JSONObject(attributes == null ? "" : attributes); - } catch (JSONException e) { - this.attributes = new JSONObject(); - } - } - - public static Conversation fromCursor(Cursor cursor) { - return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)), - cursor.getString(cursor.getColumnIndex(NAME)), - cursor.getString(cursor.getColumnIndex(CONTACT)), - cursor.getString(cursor.getColumnIndex(ACCOUNT)), - JidHelper.parseOrFallbackToInvalid(cursor.getString(cursor.getColumnIndex(CONTACTJID))), - cursor.getLong(cursor.getColumnIndex(CREATED)), - cursor.getInt(cursor.getColumnIndex(STATUS)), - cursor.getInt(cursor.getColumnIndex(MODE)), - cursor.getString(cursor.getColumnIndex(ATTRIBUTES))); - } - - public boolean hasMessagesLeftOnServer() { - return messagesLeftOnServer; - } - - public void setHasMessagesLeftOnServer(boolean value) { - this.messagesLeftOnServer = value; - } - - public Message getFirstUnreadMessage() { - Message first = null; - synchronized (this.messages) { - for (int i = messages.size() - 1; i >= 0; --i) { - if (messages.get(i).isRead()) { - return first; - } else { - first = messages.get(i); - } - } - } - return first; - } - - public Message findUnsentMessageWithUuid(String uuid) { - synchronized (this.messages) { - for (final Message message : this.messages) { - final int s = message.getStatus(); - if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) { - return message; - } - } - } - return null; - } - - public void findWaitingMessages(OnMessageFound onMessageFound) { - final ArrayList results = new ArrayList<>(); - synchronized (this.messages) { - for (Message message : this.messages) { - if (message.getStatus() == Message.STATUS_WAITING) { - results.add(message); - } - } - } - for(Message result : results) { - onMessageFound.onMessageFound(result); - } - } - - public void findUnreadMessages(OnMessageFound onMessageFound) { - final ArrayList results = new ArrayList<>(); - synchronized (this.messages) { - for (Message message : this.messages) { - if (!message.isRead()) { - results.add(message); - } - } - } - for(Message result : results) { - onMessageFound.onMessageFound(result); - } - } - - public Message findMessageWithFileAndUuid(final String uuid) { - synchronized (this.messages) { - for (final Message message : this.messages) { - if (message.getUuid().equals(uuid) - && message.getEncryption() != Message.ENCRYPTION_PGP - && (message.isFileOrImage() || message.treatAsDownloadable())) { - return message; - } - } - } - return null; - } - - public boolean markAsDeleted(final List uuids) { - boolean deleted = false; - final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService(); - synchronized (this.messages) { - for(Message message : this.messages) { - if (uuids.contains(message.getUuid())) { - message.setDeleted(true); - deleted = true; - if (message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) { - pgpDecryptionService.discard(message); - } - } - } - } - return deleted; - } - - public boolean markAsChanged(final List files) { - boolean changed = false; - final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService(); - synchronized (this.messages) { - for(Message message : this.messages) { - for(final DatabaseBackend.FilePathInfo file : files) - if (file.uuid.toString().equals(message.getUuid())) { - message.setDeleted(file.deleted); - changed = true; - if (file.deleted && message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) { - pgpDecryptionService.discard(message); - } - } - } - } - return changed; - } - - public void clearMessages() { - synchronized (this.messages) { - this.messages.clear(); - } - } - - public boolean setIncomingChatState(ChatState state) { - if (this.mIncomingChatState == state) { - return false; - } - this.mIncomingChatState = state; - return true; - } - - public ChatState getIncomingChatState() { - return this.mIncomingChatState; - } - - public boolean setOutgoingChatState(ChatState state) { - if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) { - if (this.mOutgoingChatState != state) { - this.mOutgoingChatState = state; - return true; - } - } - return false; - } - - public ChatState getOutgoingChatState() { - return this.mOutgoingChatState; - } - - public void trim() { - synchronized (this.messages) { - final int size = messages.size(); - final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES; - if (size > maxsize) { - List discards = this.messages.subList(0, size - maxsize); - final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService(); - if (pgpDecryptionService != null) { - pgpDecryptionService.discard(discards); - } - discards.clear(); - untieMessages(); - } - } - } - - public void findUnsentTextMessages(OnMessageFound onMessageFound) { - final ArrayList results = new ArrayList<>(); - synchronized (this.messages) { - for (Message message : this.messages) { - if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) && message.getStatus() == Message.STATUS_UNSEND) { - results.add(message); - } - } - } - for(Message result : results) { - onMessageFound.onMessageFound(result); - } - } - - public Message findSentMessageWithUuidOrRemoteId(String id) { - synchronized (this.messages) { - for (Message message : this.messages) { - if (id.equals(message.getUuid()) - || (message.getStatus() >= Message.STATUS_SEND - && id.equals(message.getRemoteMsgId()))) { - return message; - } - } - } - return null; - } - - public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) { - synchronized (this.messages) { - for (int i = this.messages.size() - 1; i >= 0; --i) { - final Message message = messages.get(i); - final Jid mcp = message.getCounterpart(); - if (mcp == null) { - continue; - } - final boolean counterpartMatch = mode == MODE_SINGLE ? - counterpart.asBareJid().equals(mcp.asBareJid()) : - counterpart.equals(mcp); - if (counterpartMatch && ((message.getStatus() == Message.STATUS_RECEIVED) == received) - && (carbon == message.isCarbon() || received)) { - final boolean idMatch = id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id); - if (idMatch && !message.isFileOrImage() && !message.treatAsDownloadable()) { - return message; - } else { - return null; - } - } - } - } - return null; - } - - public Message findSentMessageWithUuid(String id) { - synchronized (this.messages) { - for (Message message : this.messages) { - if (id.equals(message.getUuid())) { - return message; - } - } - } - return null; - } - - public Message findMessageWithRemoteId(String id, Jid counterpart) { - synchronized (this.messages) { - for (Message message : this.messages) { - if (counterpart.equals(message.getCounterpart()) - && (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) { - return message; - } - } - } - return null; - } - - public boolean hasMessageWithCounterpart(Jid counterpart) { - synchronized (this.messages) { - for (Message message : this.messages) { - if (counterpart.equals(message.getCounterpart())) { - return true; - } - } - } - return false; - } - - public void populateWithMessages(final List messages) { - synchronized (this.messages) { - messages.clear(); - messages.addAll(this.messages); - } - for (Iterator iterator = messages.iterator(); iterator.hasNext(); ) { - if (iterator.next().wasMergedIntoPrevious()) { - iterator.remove(); - } - } - } - - @Override - public boolean isBlocked() { - return getContact().isBlocked(); - } - - @Override - public boolean isDomainBlocked() { - return getContact().isDomainBlocked(); - } - - @Override - public Jid getBlockedJid() { - return getContact().getBlockedJid(); - } - - public int countMessages() { - synchronized (this.messages) { - return this.messages.size(); - } - } - - public String getFirstMamReference() { - return this.mFirstMamReference; - } - - public void setFirstMamReference(String reference) { - this.mFirstMamReference = reference; - } - - public void setLastClearHistory(long time, String reference) { - if (reference != null) { - setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, String.valueOf(time) + ":" + reference); - } else { - setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time); - } - } - - public MamReference getLastClearHistory() { - return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY)); - } - - public List getAcceptedCryptoTargets() { - if (mode == MODE_SINGLE) { - return Collections.singletonList(getJid().asBareJid()); - } else { - return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS); - } - } - - public void setAcceptedCryptoTargets(List acceptedTargets) { - setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets); - } - - public boolean setCorrectingMessage(Message correctingMessage) { - setAttribute(ATTRIBUTE_CORRECTING_MESSAGE,correctingMessage == null ? null : correctingMessage.getUuid()); - return correctingMessage == null && draftMessage != null; - } - - public Message getCorrectingMessage() { - final String uuid = getAttribute(ATTRIBUTE_CORRECTING_MESSAGE); - return uuid == null ? null : findSentMessageWithUuid(uuid); - } - - public boolean withSelf() { - return getContact().isSelf(); - } - - @Override - public int compareTo(@NonNull Conversation another) { - return Long.compare(another.getSortableTime(), getSortableTime()); - } - - private long getSortableTime() { - Draft draft = getDraft(); - long messageTime = getLatestMessage().getTimeSent(); - if (draft == null) { - return messageTime; - } else { - return Math.max(messageTime, draft.getTimestamp()); - } - } - - public String getDraftMessage() { - return draftMessage; - } - - public void setDraftMessage(String draftMessage) { - this.draftMessage = draftMessage; - } - - public boolean isRead() { - return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead(); - } - - public List markRead(String upToUuid) { - final List unread = new ArrayList<>(); - synchronized (this.messages) { - for (Message message : this.messages) { - if (!message.isRead()) { - message.markRead(); - unread.add(message); - } - if (message.getUuid().equals(upToUuid)) { - return unread; - } - } - } - return unread; - } - - public static Message getLatestMarkableMessage(final List messages, boolean isPrivateAndNonAnonymousMuc) { - for (int i = messages.size() - 1; i >= 0; --i) { - final Message message = messages.get(i); - if (message.getStatus() <= Message.STATUS_RECEIVED - && (message.markable || isPrivateAndNonAnonymousMuc) - && !message.isPrivateMessage()) { - return message; - } - } - return null; - } - - public Message getLatestMessage() { - synchronized (this.messages) { - if (this.messages.size() == 0) { - Message message = new Message(this, "", Message.ENCRYPTION_NONE); - message.setType(Message.TYPE_STATUS); - message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp())); - return message; - } else { - return this.messages.get(this.messages.size() - 1); - } - } - } - - public @NonNull CharSequence getName() { - if (getMode() == MODE_MULTI) { - final String roomName = getMucOptions().getName(); - final String subject = getMucOptions().getSubject(); - final Bookmark bookmark = getBookmark(); - final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null; - if (printableValue(roomName)) { - return roomName; - } else if (printableValue(subject)) { - return subject; - } else if (printableValue(bookmarkName, false)) { - return bookmarkName; - } else { - final String generatedName = getMucOptions().createNameFromParticipants(); - if (printableValue(generatedName)) { - return generatedName; - } else { - return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid; - } - } - } else if ((QuickConversationsService.isConversations() || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) && isWithStranger()) { - return contactJid; - } else { - return this.getContact().getDisplayName(); - } - } - - public String getAccountUuid() { - return this.accountUuid; - } - - public Account getAccount() { - return this.account; - } - - public void setAccount(final Account account) { - this.account = account; - } - - public Contact getContact() { - return this.account.getRoster().getContact(this.contactJid); - } - - @Override - public Jid getJid() { - return this.contactJid; - } - - public int getStatus() { - return this.status; - } - - public void setStatus(int status) { - this.status = status; - } - - public long getCreated() { - return this.created; - } - - public ContentValues getContentValues() { - ContentValues values = new ContentValues(); - values.put(UUID, uuid); - values.put(NAME, name); - values.put(CONTACT, contactUuid); - values.put(ACCOUNT, accountUuid); - values.put(CONTACTJID, contactJid.toString()); - values.put(CREATED, created); - values.put(STATUS, status); - values.put(MODE, mode); - synchronized (this.attributes) { - values.put(ATTRIBUTES, attributes.toString()); - } - return values; - } - - public int getMode() { - return this.mode; - } - - public void setMode(int mode) { - this.mode = mode; - } - - /** - * short for is Private and Non-anonymous - */ - public boolean isSingleOrPrivateAndNonAnonymous() { - return mode == MODE_SINGLE || isPrivateAndNonAnonymous(); - } - - public boolean isPrivateAndNonAnonymous() { - return getMucOptions().isPrivateAndNonAnonymous(); - } - - public synchronized MucOptions getMucOptions() { - if (this.mucOptions == null) { - this.mucOptions = new MucOptions(this); - } - return this.mucOptions; - } - - public void resetMucOptions() { - this.mucOptions = null; - } - - public void setContactJid(final Jid jid) { - this.contactJid = jid; - } - - public Jid getNextCounterpart() { - return this.nextCounterpart; - } - - public void setNextCounterpart(Jid jid) { - this.nextCounterpart = jid; - } - - public int getNextEncryption() { - if (!Config.supportOmemo() && !Config.supportOpenPgp()) { - return Message.ENCRYPTION_NONE; - } - if (OmemoSetting.isAlways()) { - return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE; - } - final int defaultEncryption; - if (suitableForOmemoByDefault(this)) { - defaultEncryption = OmemoSetting.getEncryption(); - } else { - defaultEncryption = Message.ENCRYPTION_NONE; - } - int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption); - if (encryption == Message.ENCRYPTION_OTR || encryption < 0) { - return defaultEncryption; - } else { - return encryption; - } - } - - private static boolean suitableForOmemoByDefault(final Conversation conversation) { - if (conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)) { - return false; - } - if (conversation.getContact().isOwnServer()) { - return false; - } - final String contact = conversation.getJid().getDomain(); - final String account = conversation.getAccount().getServer(); - if (Config.OMEMO_EXCEPTIONS.CONTACT_DOMAINS.contains(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) { - return false; - } - return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false); - } - - public boolean setNextEncryption(int encryption) { - return this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, encryption); - } - - public String getNextMessage() { - final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE); - return nextMessage == null ? "" : nextMessage; - } - - public @Nullable - Draft getDraft() { - long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0); - if (timestamp > getLatestMessage().getTimeSent()) { - String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE); - if (!TextUtils.isEmpty(message) && timestamp != 0) { - return new Draft(message, timestamp); - } - } - return null; - } - - public boolean setNextMessage(final String input) { - final String message = input == null || input.trim().isEmpty() ? null : input; - boolean changed = !getNextMessage().equals(message); - this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message); - if (changed) { - this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis()); - } - return changed; - } - - public Bookmark getBookmark() { - return this.account.getBookmark(this.contactJid); - } - - public Message findDuplicateMessage(Message message) { - synchronized (this.messages) { - for (int i = this.messages.size() - 1; i >= 0; --i) { - if (this.messages.get(i).similar(message)) { - return this.messages.get(i); - } - } - } - return null; - } - - public boolean hasDuplicateMessage(Message message) { - return findDuplicateMessage(message) != null; - } - - public Message findSentMessageWithBody(String body) { - synchronized (this.messages) { - for (int i = this.messages.size() - 1; i >= 0; --i) { - Message message = this.messages.get(i); - if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) { - String otherBody; - if (message.hasFileOnRemoteHost()) { - otherBody = message.getFileParams().url.toString(); - } else { - otherBody = message.body; - } - if (otherBody != null && otherBody.equals(body)) { - return message; - } - } - } - return null; - } - } - - public Message findRtpSession(final String sessionId, final int s) { - synchronized (this.messages) { - for (int i = this.messages.size() - 1; i >= 0; --i) { - final Message message = this.messages.get(i); - if ((message.getStatus() == s) && (message.getType() == Message.TYPE_RTP_SESSION) && sessionId.equals(message.getRemoteMsgId())) { - return message; - } - } - } - return null; - } - - public boolean possibleDuplicate(final String serverMsgId, final String remoteMsgId) { - if (serverMsgId == null || remoteMsgId == null) { - return false; - } - synchronized (this.messages) { - for(Message message : this.messages) { - if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) { - return true; - } - } - } - return false; - } - - public MamReference getLastMessageTransmitted() { - final MamReference lastClear = getLastClearHistory(); - MamReference lastReceived = new MamReference(0); - synchronized (this.messages) { - for (int i = this.messages.size() - 1; i >= 0; --i) { - final Message message = this.messages.get(i); - if (message.isPrivateMessage()) { - continue; //it's unsafe to use private messages as anchor. They could be coming from user archive - } - if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) { - lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId()); - break; - } - } - } - return MamReference.max(lastClear, lastReceived); - } - - public void setMutedTill(long value) { - this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value)); - } - - public boolean isMuted() { - return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0); - } - - public boolean alwaysNotify() { - return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous()); - } - - public boolean setAttribute(String key, boolean value) { - return setAttribute(key, String.valueOf(value)); - } - - private boolean setAttribute(String key, long value) { - return setAttribute(key, Long.toString(value)); - } - - private boolean setAttribute(String key, int value) { - return setAttribute(key, String.valueOf(value)); - } - - public boolean setAttribute(String key, String value) { - synchronized (this.attributes) { - try { - if (value == null) { - if (this.attributes.has(key)) { - this.attributes.remove(key); - return true; - } else { - return false; - } - } else { - final String prev = this.attributes.optString(key, null); - this.attributes.put(key, value); - return !value.equals(prev); - } - } catch (JSONException e) { - throw new AssertionError(e); - } - } - } - - public boolean setAttribute(String key, List jids) { - JSONArray array = new JSONArray(); - for (Jid jid : jids) { - array.put(jid.asBareJid().toString()); - } - synchronized (this.attributes) { - try { - this.attributes.put(key, array); - return true; - } catch (JSONException e) { - return false; - } - } - } - - public String getAttribute(String key) { - synchronized (this.attributes) { - return this.attributes.optString(key, null); - } - } - - private List getJidListAttribute(String key) { - ArrayList list = new ArrayList<>(); - synchronized (this.attributes) { - try { - JSONArray array = this.attributes.getJSONArray(key); - for (int i = 0; i < array.length(); ++i) { - try { - list.add(Jid.of(array.getString(i))); - } catch (IllegalArgumentException e) { - //ignored - } - } - } catch (JSONException e) { - //ignored - } - } - return list; - } - - private int getIntAttribute(String key, int defaultValue) { - String value = this.getAttribute(key); - if (value == null) { - return defaultValue; - } else { - try { - return Integer.parseInt(value); - } catch (NumberFormatException e) { - return defaultValue; - } - } - } - - public long getLongAttribute(String key, long defaultValue) { - String value = this.getAttribute(key); - if (value == null) { - return defaultValue; - } else { - try { - return Long.parseLong(value); - } catch (NumberFormatException e) { - return defaultValue; - } - } - } - - public boolean getBooleanAttribute(String key, boolean defaultValue) { - String value = this.getAttribute(key); - if (value == null) { - return defaultValue; - } else { - return Boolean.parseBoolean(value); - } - } - - public void add(Message message) { - synchronized (this.messages) { - this.messages.add(message); - } - } - - public void prepend(int offset, Message message) { - synchronized (this.messages) { - this.messages.add(Math.min(offset, this.messages.size()), message); - } - } - - public void addAll(int index, List messages) { - synchronized (this.messages) { - this.messages.addAll(index, messages); - } - account.getPgpDecryptionService().decrypt(messages); - } - - public void expireOldMessages(long timestamp) { - synchronized (this.messages) { - for (ListIterator iterator = this.messages.listIterator(); iterator.hasNext(); ) { - if (iterator.next().getTimeSent() < timestamp) { - iterator.remove(); - } - } - untieMessages(); - } - } - - public void sort() { - synchronized (this.messages) { - Collections.sort(this.messages, (left, right) -> { - if (left.getTimeSent() < right.getTimeSent()) { - return -1; - } else if (left.getTimeSent() > right.getTimeSent()) { - return 1; - } else { - return 0; - } - }); - untieMessages(); - } - } - - private void untieMessages() { - for (Message message : this.messages) { - message.untie(); - } - } - - public int unreadCount() { - synchronized (this.messages) { - int count = 0; - for (int i = this.messages.size() - 1; i >= 0; --i) { - if (this.messages.get(i).isRead()) { - return count; - } - ++count; - } - return count; - } - } - - public int receivedMessagesCount() { - int count = 0; - synchronized (this.messages) { - for (Message message : messages) { - if (message.getStatus() == Message.STATUS_RECEIVED) { - ++count; - } - } - } - return count; - } - - public int sentMessagesCount() { - int count = 0; - synchronized (this.messages) { - for (Message message : messages) { - if (message.getStatus() != Message.STATUS_RECEIVED) { - ++count; - } - } - } - return count; - } - - public boolean isWithStranger() { - final Contact contact = getContact(); - return mode == MODE_SINGLE - && !contact.isOwnServer() - && !contact.showInContactList() - && !contact.isSelf() - && !Config.QUICKSY_DOMAIN.equals(contact.getJid().toEscapedString()) - && sentMessagesCount() == 0; - } - - public int getReceivedMessagesCountSinceUuid(String uuid) { - if (uuid == null) { - return 0; - } - int count = 0; - synchronized (this.messages) { - for (int i = messages.size() - 1; i >= 0; i--) { - final Message message = messages.get(i); - if (uuid.equals(message.getUuid())) { - return count; - } - if (message.getStatus() <= Message.STATUS_RECEIVED) { - ++count; - } - } - } - return 0; - } - - @Override - public int getAvatarBackgroundColor() { - return UIHelper.getColorForName(getName().toString()); - } + public static final String TABLENAME = "conversations"; + + public static final int STATUS_AVAILABLE = 0; + public static final int STATUS_ARCHIVED = 1; + + public static final String NAME = "name"; + public static final String ACCOUNT = "accountUuid"; + public static final String CONTACT = "contactUuid"; + public static final String CONTACTJID = "contactJid"; + public static final String STATUS = "status"; + public static final String CREATED = "created"; + public static final String MODE = "mode"; + public static final String ATTRIBUTES = "attributes"; + + public static final String ATTRIBUTE_MUTED_TILL = "muted_till"; + public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify"; + public static final String ATTRIBUTE_PUSH_NODE = "push_node"; + public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history"; + public static final String ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS = "formerly_private_non_anonymous"; + static final String ATTRIBUTE_MUC_PASSWORD = "muc_password"; + static final String ATTRIBUTE_MEMBERS_ONLY = "members_only"; + static final String ATTRIBUTE_MODERATED = "moderated"; + static final String ATTRIBUTE_NON_ANONYMOUS = "non_anonymous"; + private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message"; + private static final String ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP = "next_message_timestamp"; + private static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets"; + private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption"; + private static final String ATTRIBUTE_CORRECTING_MESSAGE = "correcting_message"; + protected final ArrayList messages = new ArrayList<>(); + public AtomicBoolean messagesLoaded = new AtomicBoolean(true); + protected Account account = null; + private String draftMessage; + private String name; + private String contactUuid; + private String accountUuid; + private Jid contactJid; + private int status; + private long created; + private int mode; + private JSONObject attributes; + private Jid nextCounterpart; + private transient MucOptions mucOptions = null; + private boolean messagesLeftOnServer = true; + private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE; + private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE; + private String mFirstMamReference = null; + + public Conversation(final String name, final Account account, final Jid contactJid, + final int mode) { + this(java.util.UUID.randomUUID().toString(), name, null, account + .getUuid(), contactJid, System.currentTimeMillis(), + STATUS_AVAILABLE, mode, ""); + this.account = account; + } + + public Conversation(final String uuid, final String name, final String contactUuid, + final String accountUuid, final Jid contactJid, final long created, final int status, + final int mode, final String attributes) { + this.uuid = uuid; + this.name = name; + this.contactUuid = contactUuid; + this.accountUuid = accountUuid; + this.contactJid = contactJid; + this.created = created; + this.status = status; + this.mode = mode; + try { + this.attributes = new JSONObject(attributes == null ? "" : attributes); + } catch (JSONException e) { + this.attributes = new JSONObject(); + } + } + + public static Conversation fromCursor(Cursor cursor) { + return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(NAME)), + cursor.getString(cursor.getColumnIndex(CONTACT)), + cursor.getString(cursor.getColumnIndex(ACCOUNT)), + JidHelper.parseOrFallbackToInvalid(cursor.getString(cursor.getColumnIndex(CONTACTJID))), + cursor.getLong(cursor.getColumnIndex(CREATED)), + cursor.getInt(cursor.getColumnIndex(STATUS)), + cursor.getInt(cursor.getColumnIndex(MODE)), + cursor.getString(cursor.getColumnIndex(ATTRIBUTES))); + } + + public static Message getLatestMarkableMessage(final List messages, boolean isPrivateAndNonAnonymousMuc) { + for (int i = messages.size() - 1; i >= 0; --i) { + final Message message = messages.get(i); + if (message.getStatus() <= Message.STATUS_RECEIVED + && (message.markable || isPrivateAndNonAnonymousMuc) + && !message.isPrivateMessage()) { + return message; + } + } + return null; + } + + private static boolean suitableForOmemoByDefault(final Conversation conversation) { + if (conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)) { + return false; + } + if (conversation.getContact().isOwnServer()) { + return false; + } + final String contact = conversation.getJid().getDomain(); + final String account = conversation.getAccount().getServer(); + if (Config.OMEMO_EXCEPTIONS.CONTACT_DOMAINS.contains(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) { + return false; + } + return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false); + } + + public boolean hasMessagesLeftOnServer() { + return messagesLeftOnServer; + } + + public void setHasMessagesLeftOnServer(boolean value) { + this.messagesLeftOnServer = value; + } + + public Message getFirstUnreadMessage() { + Message first = null; + synchronized (this.messages) { + for (int i = messages.size() - 1; i >= 0; --i) { + if (messages.get(i).isRead()) { + return first; + } else { + first = messages.get(i); + } + } + } + return first; + } + + public Message findUnsentMessageWithUuid(String uuid) { + synchronized (this.messages) { + for (final Message message : this.messages) { + final int s = message.getStatus(); + if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) { + return message; + } + } + } + return null; + } + + public void findWaitingMessages(OnMessageFound onMessageFound) { + final ArrayList results = new ArrayList<>(); + synchronized (this.messages) { + for (Message message : this.messages) { + if (message.getStatus() == Message.STATUS_WAITING) { + results.add(message); + } + } + } + for (Message result : results) { + onMessageFound.onMessageFound(result); + } + } + + public void findUnreadMessages(OnMessageFound onMessageFound) { + final ArrayList results = new ArrayList<>(); + synchronized (this.messages) { + for (Message message : this.messages) { + if (!message.isRead()) { + results.add(message); + } + } + } + for (Message result : results) { + onMessageFound.onMessageFound(result); + } + } + + public Message findMessageWithFileAndUuid(final String uuid) { + synchronized (this.messages) { + for (final Message message : this.messages) { + if (message.getUuid().equals(uuid) + && message.getEncryption() != Message.ENCRYPTION_PGP + && (message.isFileOrImage() || message.treatAsDownloadable())) { + return message; + } + } + } + return null; + } + + public boolean markAsDeleted(final List uuids) { + boolean deleted = false; + final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService(); + synchronized (this.messages) { + for (Message message : this.messages) { + if (uuids.contains(message.getUuid())) { + message.setDeleted(true); + deleted = true; + if (message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) { + pgpDecryptionService.discard(message); + } + } + } + } + return deleted; + } + + public boolean markAsChanged(final List files) { + boolean changed = false; + final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService(); + synchronized (this.messages) { + for (Message message : this.messages) { + for (final DatabaseBackend.FilePathInfo file : files) + if (file.uuid.toString().equals(message.getUuid())) { + message.setDeleted(file.deleted); + changed = true; + if (file.deleted && message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) { + pgpDecryptionService.discard(message); + } + } + } + } + return changed; + } + + public void clearMessages() { + synchronized (this.messages) { + this.messages.clear(); + } + } + + public boolean setIncomingChatState(ChatState state) { + if (this.mIncomingChatState == state) { + return false; + } + this.mIncomingChatState = state; + return true; + } + + public ChatState getIncomingChatState() { + return this.mIncomingChatState; + } + + public boolean setOutgoingChatState(ChatState state) { + if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) { + if (this.mOutgoingChatState != state) { + this.mOutgoingChatState = state; + return true; + } + } + return false; + } + + public ChatState getOutgoingChatState() { + return this.mOutgoingChatState; + } + + public void trim() { + synchronized (this.messages) { + final int size = messages.size(); + final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES; + if (size > maxsize) { + List discards = this.messages.subList(0, size - maxsize); + final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService(); + if (pgpDecryptionService != null) { + pgpDecryptionService.discard(discards); + } + discards.clear(); + untieMessages(); + } + } + } + + public void findUnsentTextMessages(OnMessageFound onMessageFound) { + final ArrayList results = new ArrayList<>(); + synchronized (this.messages) { + for (Message message : this.messages) { + if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) && message.getStatus() == Message.STATUS_UNSEND) { + results.add(message); + } + } + } + for (Message result : results) { + onMessageFound.onMessageFound(result); + } + } + + public Message findSentMessageWithUuidOrRemoteId(String id) { + synchronized (this.messages) { + for (Message message : this.messages) { + if (id.equals(message.getUuid()) + || (message.getStatus() >= Message.STATUS_SEND + && id.equals(message.getRemoteMsgId()))) { + return message; + } + } + } + return null; + } + + public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) { + synchronized (this.messages) { + for (int i = this.messages.size() - 1; i >= 0; --i) { + final Message message = messages.get(i); + final Jid mcp = message.getCounterpart(); + if (mcp == null) { + continue; + } + final boolean counterpartMatch = mode == MODE_SINGLE ? + counterpart.asBareJid().equals(mcp.asBareJid()) : + counterpart.equals(mcp); + if (counterpartMatch && ((message.getStatus() == Message.STATUS_RECEIVED) == received) + && (carbon == message.isCarbon() || received)) { + final boolean idMatch = id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id); + if (idMatch && !message.isFileOrImage() && !message.treatAsDownloadable()) { + return message; + } else { + return null; + } + } + } + } + return null; + } + + public Message findSentMessageWithUuid(String id) { + synchronized (this.messages) { + for (Message message : this.messages) { + if (id.equals(message.getUuid())) { + return message; + } + } + } + return null; + } + + public Message findMessageWithRemoteId(String id, Jid counterpart) { + synchronized (this.messages) { + for (Message message : this.messages) { + if (counterpart.equals(message.getCounterpart()) + && (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) { + return message; + } + } + } + return null; + } + + public Message findMessageWithServerMsgId(String id) { + synchronized (this.messages) { + for (Message message : this.messages) { + if (id != null && id.equals(message.getServerMsgId())) { + return message; + } + } + } + return null; + } + + public boolean hasMessageWithCounterpart(Jid counterpart) { + synchronized (this.messages) { + for (Message message : this.messages) { + if (counterpart.equals(message.getCounterpart())) { + return true; + } + } + } + return false; + } + + public void populateWithMessages(final List messages) { + synchronized (this.messages) { + messages.clear(); + messages.addAll(this.messages); + } + for (Iterator iterator = messages.iterator(); iterator.hasNext(); ) { + if (iterator.next().wasMergedIntoPrevious()) { + iterator.remove(); + } + } + } + + @Override + public boolean isBlocked() { + return getContact().isBlocked(); + } + + @Override + public boolean isDomainBlocked() { + return getContact().isDomainBlocked(); + } + + @Override + public Jid getBlockedJid() { + return getContact().getBlockedJid(); + } + + public int countMessages() { + synchronized (this.messages) { + return this.messages.size(); + } + } + + public String getFirstMamReference() { + return this.mFirstMamReference; + } + + public void setFirstMamReference(String reference) { + this.mFirstMamReference = reference; + } + + public void setLastClearHistory(long time, String reference) { + if (reference != null) { + setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, String.valueOf(time) + ":" + reference); + } else { + setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time); + } + } + + public MamReference getLastClearHistory() { + return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY)); + } + + public List getAcceptedCryptoTargets() { + if (mode == MODE_SINGLE) { + return Collections.singletonList(getJid().asBareJid()); + } else { + return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS); + } + } + + public void setAcceptedCryptoTargets(List acceptedTargets) { + setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets); + } + + public boolean setCorrectingMessage(Message correctingMessage) { + setAttribute(ATTRIBUTE_CORRECTING_MESSAGE, correctingMessage == null ? null : correctingMessage.getUuid()); + return correctingMessage == null && draftMessage != null; + } + + public Message getCorrectingMessage() { + final String uuid = getAttribute(ATTRIBUTE_CORRECTING_MESSAGE); + return uuid == null ? null : findSentMessageWithUuid(uuid); + } + + public boolean withSelf() { + return getContact().isSelf(); + } + + @Override + public int compareTo(@NonNull Conversation another) { + return Long.compare(another.getSortableTime(), getSortableTime()); + } + + private long getSortableTime() { + Draft draft = getDraft(); + long messageTime = getLatestMessage().getTimeSent(); + if (draft == null) { + return messageTime; + } else { + return Math.max(messageTime, draft.getTimestamp()); + } + } + + public String getDraftMessage() { + return draftMessage; + } + + public void setDraftMessage(String draftMessage) { + this.draftMessage = draftMessage; + } + + public boolean isRead() { + return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead(); + } + + public List markRead(String upToUuid) { + final List unread = new ArrayList<>(); + synchronized (this.messages) { + for (Message message : this.messages) { + if (!message.isRead()) { + message.markRead(); + unread.add(message); + } + if (message.getUuid().equals(upToUuid)) { + return unread; + } + } + } + return unread; + } + + public Message getLatestMessage() { + synchronized (this.messages) { + if (this.messages.size() == 0) { + Message message = new Message(this, "", Message.ENCRYPTION_NONE); + message.setType(Message.TYPE_STATUS); + message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp())); + return message; + } else { + return this.messages.get(this.messages.size() - 1); + } + } + } + + public @NonNull + CharSequence getName() { + if (getMode() == MODE_MULTI) { + final String roomName = getMucOptions().getName(); + final String subject = getMucOptions().getSubject(); + final Bookmark bookmark = getBookmark(); + final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null; + if (printableValue(roomName)) { + return roomName; + } else if (printableValue(subject)) { + return subject; + } else if (printableValue(bookmarkName, false)) { + return bookmarkName; + } else { + final String generatedName = getMucOptions().createNameFromParticipants(); + if (printableValue(generatedName)) { + return generatedName; + } else { + return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid; + } + } + } else if ((QuickConversationsService.isConversations() || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) && isWithStranger()) { + return contactJid; + } else { + return this.getContact().getDisplayName(); + } + } + + public String getAccountUuid() { + return this.accountUuid; + } + + public Account getAccount() { + return this.account; + } + + public void setAccount(final Account account) { + this.account = account; + } + + public Contact getContact() { + return this.account.getRoster().getContact(this.contactJid); + } + + @Override + public Jid getJid() { + return this.contactJid; + } + + public int getStatus() { + return this.status; + } + + public void setStatus(int status) { + this.status = status; + } + + public long getCreated() { + return this.created; + } + + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(UUID, uuid); + values.put(NAME, name); + values.put(CONTACT, contactUuid); + values.put(ACCOUNT, accountUuid); + values.put(CONTACTJID, contactJid.toString()); + values.put(CREATED, created); + values.put(STATUS, status); + values.put(MODE, mode); + synchronized (this.attributes) { + values.put(ATTRIBUTES, attributes.toString()); + } + return values; + } + + public int getMode() { + return this.mode; + } + + public void setMode(int mode) { + this.mode = mode; + } + + /** + * short for is Private and Non-anonymous + */ + public boolean isSingleOrPrivateAndNonAnonymous() { + return mode == MODE_SINGLE || isPrivateAndNonAnonymous(); + } + + public boolean isPrivateAndNonAnonymous() { + return getMucOptions().isPrivateAndNonAnonymous(); + } + + public synchronized MucOptions getMucOptions() { + if (this.mucOptions == null) { + this.mucOptions = new MucOptions(this); + } + return this.mucOptions; + } + + public void resetMucOptions() { + this.mucOptions = null; + } + + public void setContactJid(final Jid jid) { + this.contactJid = jid; + } + + public Jid getNextCounterpart() { + return this.nextCounterpart; + } + + public void setNextCounterpart(Jid jid) { + this.nextCounterpart = jid; + } + + public int getNextEncryption() { + if (!Config.supportOmemo() && !Config.supportOpenPgp()) { + return Message.ENCRYPTION_NONE; + } + if (OmemoSetting.isAlways()) { + return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE; + } + final int defaultEncryption; + if (suitableForOmemoByDefault(this)) { + defaultEncryption = OmemoSetting.getEncryption(); + } else { + defaultEncryption = Message.ENCRYPTION_NONE; + } + int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption); + if (encryption == Message.ENCRYPTION_OTR || encryption < 0) { + return defaultEncryption; + } else { + return encryption; + } + } + + public boolean setNextEncryption(int encryption) { + return this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, encryption); + } + + public String getNextMessage() { + final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE); + return nextMessage == null ? "" : nextMessage; + } + + public @Nullable + Draft getDraft() { + long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0); + if (timestamp > getLatestMessage().getTimeSent()) { + String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE); + if (!TextUtils.isEmpty(message) && timestamp != 0) { + return new Draft(message, timestamp); + } + } + return null; + } + + public boolean setNextMessage(final String input) { + final String message = input == null || input.trim().isEmpty() ? null : input; + boolean changed = !getNextMessage().equals(message); + this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message); + if (changed) { + this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis()); + } + return changed; + } + + public Bookmark getBookmark() { + return this.account.getBookmark(this.contactJid); + } + + public Message findDuplicateMessage(Message message) { + synchronized (this.messages) { + for (int i = this.messages.size() - 1; i >= 0; --i) { + if (this.messages.get(i).similar(message)) { + return this.messages.get(i); + } + } + } + return null; + } + + public boolean hasDuplicateMessage(Message message) { + return findDuplicateMessage(message) != null; + } + + public Message findSentMessageWithBody(String body) { + synchronized (this.messages) { + for (int i = this.messages.size() - 1; i >= 0; --i) { + Message message = this.messages.get(i); + if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) { + String otherBody; + if (message.hasFileOnRemoteHost()) { + otherBody = message.getFileParams().url.toString(); + } else { + otherBody = message.body; + } + if (otherBody != null && otherBody.equals(body)) { + return message; + } + } + } + return null; + } + } + + public Message findRtpSession(final String sessionId, final int s) { + synchronized (this.messages) { + for (int i = this.messages.size() - 1; i >= 0; --i) { + final Message message = this.messages.get(i); + if ((message.getStatus() == s) && (message.getType() == Message.TYPE_RTP_SESSION) && sessionId.equals(message.getRemoteMsgId())) { + return message; + } + } + } + return null; + } + + public boolean possibleDuplicate(final String serverMsgId, final String remoteMsgId) { + if (serverMsgId == null || remoteMsgId == null) { + return false; + } + synchronized (this.messages) { + for (Message message : this.messages) { + if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) { + return true; + } + } + } + return false; + } + + public MamReference getLastMessageTransmitted() { + final MamReference lastClear = getLastClearHistory(); + MamReference lastReceived = new MamReference(0); + synchronized (this.messages) { + for (int i = this.messages.size() - 1; i >= 0; --i) { + final Message message = this.messages.get(i); + if (message.isPrivateMessage()) { + continue; //it's unsafe to use private messages as anchor. They could be coming from user archive + } + if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) { + lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId()); + break; + } + } + } + return MamReference.max(lastClear, lastReceived); + } + + public void setMutedTill(long value) { + this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value)); + } + + public boolean isMuted() { + return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0); + } + + public boolean alwaysNotify() { + return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous()); + } + + public boolean setAttribute(String key, boolean value) { + return setAttribute(key, String.valueOf(value)); + } + + private boolean setAttribute(String key, long value) { + return setAttribute(key, Long.toString(value)); + } + + private boolean setAttribute(String key, int value) { + return setAttribute(key, String.valueOf(value)); + } + + public boolean setAttribute(String key, String value) { + synchronized (this.attributes) { + try { + if (value == null) { + if (this.attributes.has(key)) { + this.attributes.remove(key); + return true; + } else { + return false; + } + } else { + final String prev = this.attributes.optString(key, null); + this.attributes.put(key, value); + return !value.equals(prev); + } + } catch (JSONException e) { + throw new AssertionError(e); + } + } + } + + public boolean setAttribute(String key, List jids) { + JSONArray array = new JSONArray(); + for (Jid jid : jids) { + array.put(jid.asBareJid().toString()); + } + synchronized (this.attributes) { + try { + this.attributes.put(key, array); + return true; + } catch (JSONException e) { + return false; + } + } + } + + public String getAttribute(String key) { + synchronized (this.attributes) { + return this.attributes.optString(key, null); + } + } + + private List getJidListAttribute(String key) { + ArrayList list = new ArrayList<>(); + synchronized (this.attributes) { + try { + JSONArray array = this.attributes.getJSONArray(key); + for (int i = 0; i < array.length(); ++i) { + try { + list.add(Jid.of(array.getString(i))); + } catch (IllegalArgumentException e) { + //ignored + } + } + } catch (JSONException e) { + //ignored + } + } + return list; + } + + private int getIntAttribute(String key, int defaultValue) { + String value = this.getAttribute(key); + if (value == null) { + return defaultValue; + } else { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + } + + public long getLongAttribute(String key, long defaultValue) { + String value = this.getAttribute(key); + if (value == null) { + return defaultValue; + } else { + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + } + + public boolean getBooleanAttribute(String key, boolean defaultValue) { + String value = this.getAttribute(key); + if (value == null) { + return defaultValue; + } else { + return Boolean.parseBoolean(value); + } + } + + public void add(Message message) { + synchronized (this.messages) { + this.messages.add(message); + } + } + + public void prepend(int offset, Message message) { + synchronized (this.messages) { + this.messages.add(Math.min(offset, this.messages.size()), message); + } + } + + public void addAll(int index, List messages) { + synchronized (this.messages) { + this.messages.addAll(index, messages); + } + account.getPgpDecryptionService().decrypt(messages); + } + + public void expireOldMessages(long timestamp) { + synchronized (this.messages) { + for (ListIterator iterator = this.messages.listIterator(); iterator.hasNext(); ) { + if (iterator.next().getTimeSent() < timestamp) { + iterator.remove(); + } + } + untieMessages(); + } + } + + public void sort() { + synchronized (this.messages) { + Collections.sort(this.messages, (left, right) -> { + if (left.getTimeSent() < right.getTimeSent()) { + return -1; + } else if (left.getTimeSent() > right.getTimeSent()) { + return 1; + } else { + return 0; + } + }); + untieMessages(); + } + } + + private void untieMessages() { + for (Message message : this.messages) { + message.untie(); + } + } + + public int unreadCount() { + synchronized (this.messages) { + int count = 0; + for (int i = this.messages.size() - 1; i >= 0; --i) { + if (this.messages.get(i).isRead()) { + return count; + } + ++count; + } + return count; + } + } + + public int receivedMessagesCount() { + int count = 0; + synchronized (this.messages) { + for (Message message : messages) { + if (message.getStatus() == Message.STATUS_RECEIVED) { + ++count; + } + } + } + return count; + } + + public int sentMessagesCount() { + int count = 0; + synchronized (this.messages) { + for (Message message : messages) { + if (message.getStatus() != Message.STATUS_RECEIVED) { + ++count; + } + } + } + return count; + } + + public boolean isWithStranger() { + final Contact contact = getContact(); + return mode == MODE_SINGLE + && !contact.isOwnServer() + && !contact.showInContactList() + && !contact.isSelf() + && !Config.QUICKSY_DOMAIN.equals(contact.getJid().toEscapedString()) + && sentMessagesCount() == 0; + } + + public int getReceivedMessagesCountSinceUuid(String uuid) { + if (uuid == null) { + return 0; + } + int count = 0; + synchronized (this.messages) { + for (int i = messages.size() - 1; i >= 0; i--) { + final Message message = messages.get(i); + if (uuid.equals(message.getUuid())) { + return count; + } + if (message.getStatus() <= Message.STATUS_RECEIVED) { + ++count; + } + } + } + return 0; + } + + @Override + public int getAvatarBackgroundColor() { + return UIHelper.getColorForName(getName().toString()); + } public interface OnMessageFound { - void onMessageFound(final Message message); - } + void onMessageFound(final Message message); + } - public static class Draft { - private final String message; - private final long timestamp; + public static class Draft { + private final String message; + private final long timestamp; - private Draft(String message, long timestamp) { - this.message = message; - this.timestamp = timestamp; - } + private Draft(String message, long timestamp) { + this.message = message; + this.timestamp = timestamp; + } - public long getTimestamp() { - return timestamp; - } + public long getTimestamp() { + return timestamp; + } - public String getMessage() { - return message; - } - } + public String getMessage() { + return message; + } + } } diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 74a3c538d..f45d93330 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -114,10 +114,6 @@ public class MucOptions { return MessageArchiveService.Version.has(getFeatures()); } - public boolean push() { - return getFeatures().contains(Namespace.PUSH); - } - public boolean updateConfiguration(ServiceDiscoveryResult serviceDiscoveryResult) { this.serviceDiscoveryResult = serviceDiscoveryResult; String name; diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index a0cb0ca1f..6969e4a5e 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -12,6 +12,7 @@ import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.http.P1S3UrlStreamHandler; import eu.siacs.conversations.services.XmppConnectionService; @@ -160,15 +161,23 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket confirm(final Account account, final Jid to, final String id, final Jid counterpart, final boolean groupChat) { - MessagePacket packet = new MessagePacket(); + public MessagePacket confirm(final Message message) { + final boolean groupChat = message.getConversation().getMode() == Conversational.MODE_MULTI; + final Jid to = message.getCounterpart(); + final MessagePacket packet = new MessagePacket(); packet.setType(groupChat ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT); packet.setTo(groupChat ? to.asBareJid() : to); - packet.setFrom(account.getJid()); - Element displayed = packet.addChild("displayed", "urn:xmpp:chat-markers:0"); - displayed.setAttribute("id", id); - if (groupChat && counterpart != null) { - displayed.setAttribute("sender", counterpart.toString()); + final Element displayed = packet.addChild("displayed", "urn:xmpp:chat-markers:0"); + if (groupChat) { + final String stanzaId = message.getServerMsgId(); + if (stanzaId != null) { + displayed.setAttribute("id", stanzaId); + } else { + displayed.setAttribute("sender", to.toString()); + displayed.setAttribute("id", message.getRemoteMsgId()); + } + } else { + displayed.setAttribute("id", message.getRemoteMsgId()); } packet.addChild("store", "urn:xmpp:hints"); return packet; diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index e5ef662bb..f825e464d 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -387,29 +387,6 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet); } mXmppConnectionService.sendIqPacket(account, response, null); - } else if (packet.hasChild("pubsub", Namespace.PUBSUB) && packet.getType() == IqPacket.TYPE.SET) { - final Jid server = packet.getFrom(); - final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB); - final Element publish = pubsub == null ? null : pubsub.findChild("publish"); - final String node = publish == null ? null : publish.getAttribute("node"); - final Element item = publish == null ? null : publish.findChild("item"); - final Element notification = item == null ? null : item.findChild("notification", Namespace.PUSH); - if (notification != null && node != null && server != null) { - final Conversation conversation = mXmppConnectionService.findConversationByUuid(node); - if (conversation != null && conversation.getAccount() == account && conversation.getJid().getDomain().equals(server.getDomain())) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received muc push event for "+conversation.getJid().asBareJid()); - mXmppConnectionService.sendIqPacket(account, packet.generateResponse(IqPacket.TYPE.RESULT), null); - mXmppConnectionService.mucSelfPingAndRejoin(conversation); - } else { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received push event for unknown conference from "+server); - final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); - final Element error = response.addChild("error"); - error.setAttribute("type", "cancel"); - error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas"); - mXmppConnectionService.sendIqPacket(account, response, null); - } - } - } else { if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) { final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 9cb6e7bbd..8d11499d2 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -927,22 +927,29 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece activateGracePeriod(account); } } else if (isTypeGroupChat) { - Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid()); - if (conversation != null && id != null && sender != null) { - Message message = conversation.findMessageWithRemoteId(id, sender); - if (message != null) { - final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart); - final Jid trueJid = getTrueCounterpart((query != null && query.safeToExtractTrueCounterpart()) ? mucUserElement : null, fallback); - final boolean trueJidMatchesAccount = account.getJid().asBareJid().equals(trueJid == null ? null : trueJid.asBareJid()); - if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) { - if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections - mXmppConnectionService.markRead(conversation); - } - } else if (!counterpart.isBareJid() && trueJid != null) { - final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid); - if (message.addReadByMarker(readByMarker)) { - mXmppConnectionService.updateMessage(message, false); - } + final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid()); + final Message message; + if (conversation != null && id != null) { + if (sender != null) { + message = conversation.findMessageWithRemoteId(id, sender); + } else { + message = conversation.findMessageWithServerMsgId(id); + } + } else { + message = null; + } + if (message != null) { + final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart); + final Jid trueJid = getTrueCounterpart((query != null && query.safeToExtractTrueCounterpart()) ? mucUserElement : null, fallback); + final boolean trueJidMatchesAccount = account.getJid().asBareJid().equals(trueJid == null ? null : trueJid.asBareJid()); + if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) { + if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections + mXmppConnectionService.markRead(conversation); + } + } else if (!counterpart.isBareJid() && trueJid != null) { + final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid); + if (message.addReadByMarker(readByMarker)) { + mXmppConnectionService.updateMessage(message, false); } } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 0efa27618..4b1c5c2ff 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -610,7 +610,6 @@ public class XmppConnectionService extends Service { toggleForegroundService(true); } String pushedAccountHash = null; - String pushedChannelHash = null; boolean interactive = false; if (action != null) { final String uuid = intent.getStringExtra("uuid"); @@ -735,7 +734,6 @@ public class XmppConnectionService extends Service { break; case ACTION_FCM_MESSAGE_RECEIVED: pushedAccountHash = intent.getStringExtra("account"); - pushedChannelHash = intent.getStringExtra("channel"); Log.d(Config.LOGTAG, "push message arrived in service. account=" + pushedAccountHash); break; case Intent.ACTION_SEND: @@ -758,9 +756,6 @@ public class XmppConnectionService extends Service { "ui".equals(action), pushWasMeantForThisAccount, pingCandidates); - if (pushWasMeantForThisAccount && pushedChannelHash != null) { - checkMucStillJoined(account, pushedAccountHash, androidId); - } } if (pingNow) { for (Account account : pingCandidates) { @@ -853,20 +848,6 @@ public class XmppConnectionService extends Service { return pingNow; } - private void checkMucStillJoined(final Account account, final String hash, final String androidId) { - for (final Conversation conversation : this.conversations) { - if (conversation.getAccount() == account && conversation.getMode() == Conversational.MODE_MULTI) { - Jid jid = conversation.getJid().asBareJid(); - final String currentHash = CryptoHelper.getFingerprint(jid, androidId); - if (currentHash.equals(hash)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received cloud push notification for MUC " + jid); - return; - } - } - } - mPushManagementService.unregisterChannel(account, hash); - } - public void reinitializeMuclumbusService() { mChannelDiscoveryService.initializeMuclumbusService(); } @@ -2156,10 +2137,6 @@ public class XmppConnectionService extends Service { } } } - if (conversation.getMucOptions().push()) { - disableDirectMucPush(conversation); - mPushManagementService.disablePushOnServer(conversation); - } leaveMuc(conversation); } else { if (conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { @@ -2758,9 +2735,6 @@ public class XmppConnectionService extends Service { } } } - if (mucOptions.push()) { - enableMucPush(conversation); - } synchronized (account.inProgressConferenceJoins) { account.inProgressConferenceJoins.remove(conversation); sendUnsentMessages(conversation); @@ -2805,40 +2779,6 @@ public class XmppConnectionService extends Service { updateConversationUi(); } } - - private void enableDirectMucPush(final Conversation conversation) { - final Account account = conversation.getAccount(); - final Jid room = conversation.getJid().asBareJid(); - final IqPacket enable = mIqGenerator.enablePush(conversation.getAccount().getJid(), conversation.getUuid(), null); - enable.setTo(room); - sendIqPacket(account, enable, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": enabled direct push for muc " + room); - } else if (response.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": unable to enable direct push for muc " + room + " " + response.getError()); - } - }); - } - - private void enableMucPush(final Conversation conversation) { - enableDirectMucPush(conversation); - mPushManagementService.registerPushTokenOnServer(conversation); - } - - private void disableDirectMucPush(final Conversation conversation) { - final Account account = conversation.getAccount(); - final Jid room = conversation.getJid().asBareJid(); - final IqPacket disable = mIqGenerator.disablePush(conversation.getAccount().getJid(), conversation.getUuid()); - disable.setTo(room); - sendIqPacket(account, disable, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": disabled direct push for muc " + room); - } else if (response.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": unable to disable direct push for muc " + room + " " + response.getError()); - } - }); - } - private void fetchConferenceMembers(final Conversation conversation) { final Account account = conversation.getAccount(); final AxolotlService axolotlService = account.getAxolotlService(); @@ -4122,6 +4062,7 @@ public class XmppConnectionService extends Service { } }; mDatabaseWriterExecutor.execute(runnable); + updateConversationUi(); updateUnreadCountBadge(); return readMessages; } else { @@ -4154,11 +4095,9 @@ public class XmppConnectionService extends Service { && (markable.trusted() || isPrivateAndNonAnonymousMuc) && markable.getRemoteMsgId() != null) { Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": sending read marker to " + markable.getCounterpart().toString()); - Account account = conversation.getAccount(); - final Jid to = markable.getCounterpart(); - final boolean groupChat = conversation.getMode() == Conversation.MODE_MULTI; - MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId(), markable.getCounterpart(), groupChat); - this.sendMessagePacket(conversation.getAccount(), packet); + final Account account = conversation.getAccount(); + final MessagePacket packet = mMessageGenerator.confirm(markable); + this.sendMessagePacket(account, packet); } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 0e33cde2b..3bb3650cb 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -52,6 +52,8 @@ import android.widget.PopupMenu; import android.widget.TextView.OnEditorActionListener; import android.widget.Toast; +import com.google.common.base.Optional; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -117,6 +119,7 @@ import eu.siacs.conversations.utils.TimeframeUtils; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.chatstate.ChatState; +import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import eu.siacs.conversations.xmpp.jingle.JingleFileTransferConnection; import eu.siacs.conversations.xmpp.jingle.RtpCapability; import rocks.xmpp.addr.Jid; @@ -956,6 +959,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke final MenuItem menuMute = menu.findItem(R.id.action_mute); final MenuItem menuUnmute = menu.findItem(R.id.action_unmute); final MenuItem menuCall = menu.findItem(R.id.action_call); + final MenuItem menuOngoingCall = menu.findItem(R.id.action_ongoing_call); final MenuItem menuVideoCall = menu.findItem(R.id.action_video_call); @@ -965,10 +969,18 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke menuInviteContact.setVisible(conversation.getMucOptions().canInvite()); menuMucDetails.setTitle(conversation.getMucOptions().isPrivateAndNonAnonymous() ? R.string.action_muc_details : R.string.channel_details); menuCall.setVisible(false); + menuOngoingCall.setVisible(false); } else { - final RtpCapability.Capability rtpCapability = RtpCapability.check(conversation.getContact()); - menuCall.setVisible(rtpCapability != RtpCapability.Capability.NONE); - menuVideoCall.setVisible(rtpCapability == RtpCapability.Capability.VIDEO); + final Optional ongoingRtpSession = activity.xmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); + if (ongoingRtpSession.isPresent()) { + menuOngoingCall.setVisible(true); + menuCall.setVisible(false); + } else { + menuOngoingCall.setVisible(false); + final RtpCapability.Capability rtpCapability = RtpCapability.check(conversation.getContact()); + menuCall.setVisible(rtpCapability != RtpCapability.Capability.NONE); + menuVideoCall.setVisible(rtpCapability == RtpCapability.Capability.VIDEO); + } menuContactDetails.setVisible(!this.conversation.withSelf()); menuMucDetails.setVisible(false); final XmppConnectionService service = activity.xmppConnectionService; @@ -1245,12 +1257,28 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke case R.id.action_video_call: checkPermissionAndTriggerVideoCall(); break; + case R.id.action_ongoing_call: + returnToOngoingCall(); + break; default: break; } return super.onOptionsItemSelected(item); } + private void returnToOngoingCall() { + final Optional ongoingRtpSession = activity.xmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); + if (ongoingRtpSession.isPresent()) { + final AbstractJingleConnection.Id id = ongoingRtpSession.get(); + final Intent intent = new Intent(getActivity(), RtpSessionActivity.class); + intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString()); + intent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString()); + intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId); + startActivity(intent); + } + + } + private void checkPermissionAndTriggerAudioCall() { if (activity.mUseTor || conversation.getAccount().isOnion()) { Toast.makeText(activity, R.string.disable_tor_to_make_call, Toast.LENGTH_SHORT).show(); diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index acd9d151e..335063b2c 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -21,6 +21,7 @@ import android.view.WindowManager; import android.widget.Toast; import com.google.common.base.Optional; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -29,6 +30,7 @@ import org.webrtc.VideoTrack; import java.lang.ref.WeakReference; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Set; @@ -315,7 +317,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe if (remoteVideo.isPresent()) { remoteVideo.get().removeSink(binding.remoteVideo); } - final Optional localVideo = jingleRtpConnection.geLocalVideoTrack(); + final Optional localVideo = jingleRtpConnection.getLocalVideoTrack(); if (localVideo.isPresent()) { localVideo.get().removeSink(binding.localVideo); } @@ -385,6 +387,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe finish(); return true; } + final Set media = getMedia(); if (currentState == RtpEndUserState.INCOMING_CALL) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } @@ -393,16 +396,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } binding.with.setText(getWith().getDisplayName()); updateVideoViews(currentState); - updateStateDisplay(currentState); - updateButtonConfiguration(currentState); + updateStateDisplay(currentState, media); + updateButtonConfiguration(currentState, media); updateProfilePicture(currentState); return false; } private void reInitializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) { - runOnUiThread(() -> { - initializeActivityWithRunningRtpSession(account, with, sessionId); - }); + runOnUiThread(() -> initializeActivityWithRunningRtpSession(account, with, sessionId)); final Intent intent = new Intent(Intent.ACTION_VIEW); intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString()); intent.putExtra(EXTRA_WITH, with.toEscapedString()); @@ -421,9 +422,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void updateStateDisplay(final RtpEndUserState state) { + updateStateDisplay(state, Collections.emptySet()); + } + + private void updateStateDisplay(final RtpEndUserState state, final Set media) { switch (state) { case INCOMING_CALL: - if (getMedia().contains(Media.VIDEO)) { + Preconditions.checkArgument(media.size() > 0, "Media must not be empty"); + if (media.contains(Media.VIDEO)) { setTitle(R.string.rtp_state_incoming_video_call); } else { setTitle(R.string.rtp_state_incoming_call); @@ -467,11 +473,19 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void updateProfilePicture(final RtpEndUserState state) { + updateProfilePicture(state, null); + } + + private void updateProfilePicture(final RtpEndUserState state, final Contact contact) { if (state == RtpEndUserState.INCOMING_CALL || state == RtpEndUserState.ACCEPTING_CALL) { final boolean show = getResources().getBoolean(R.bool.show_avatar_incoming_call); if (show) { binding.contactPhoto.setVisibility(View.VISIBLE); - AvatarWorkerTask.loadAvatar(getWith(), binding.contactPhoto, R.dimen.publish_avatar_size); + if (contact == null) { + AvatarWorkerTask.loadAvatar(getWith(), binding.contactPhoto, R.dimen.publish_avatar_size); + } else { + AvatarWorkerTask.loadAvatar(contact, binding.contactPhoto, R.dimen.publish_avatar_size); + } } else { binding.contactPhoto.setVisibility(View.GONE); } @@ -484,8 +498,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe return requireRtpConnection().getMedia(); } - @SuppressLint("RestrictedApi") private void updateButtonConfiguration(final RtpEndUserState state) { + updateButtonConfiguration(state, Collections.emptySet()); + } + + @SuppressLint("RestrictedApi") + private void updateButtonConfiguration(final RtpEndUserState state, final Set media) { if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) { this.binding.rejectCall.setVisibility(View.INVISIBLE); this.binding.endCall.setVisibility(View.INVISIBLE); @@ -519,7 +537,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.binding.endCall.setVisibility(View.VISIBLE); this.binding.acceptCall.setVisibility(View.INVISIBLE); } - updateInCallButtonConfiguration(state); + updateInCallButtonConfiguration(state, media); } private boolean isPictureInPicture() { @@ -531,13 +549,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void updateInCallButtonConfiguration() { - updateInCallButtonConfiguration(requireRtpConnection().getEndUserState()); + updateInCallButtonConfiguration(requireRtpConnection().getEndUserState(), requireRtpConnection().getMedia()); } @SuppressLint("RestrictedApi") - private void updateInCallButtonConfiguration(final RtpEndUserState state) { + private void updateInCallButtonConfiguration(final RtpEndUserState state, final Set media) { if (state == RtpEndUserState.CONNECTED && !isPictureInPicture()) { - if (getMedia().contains(Media.VIDEO)) { + Preconditions.checkArgument(media.size() > 0, "Media must not be empty"); + if (media.contains(Media.VIDEO)) { updateInCallButtonConfigurationVideo(requireRtpConnection().isVideoEnabled()); } else { final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager(); @@ -626,7 +645,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void updateVideoViews(final RtpEndUserState state) { if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) { binding.localVideo.setVisibility(View.GONE); + binding.localVideo.release(); binding.remoteVideo.setVisibility(View.GONE); + binding.remoteVideo.release(); binding.pipLocalMicOffIndicator.setVisibility(View.GONE); if (isPictureInPicture()) { binding.appBarLayout.setVisibility(View.GONE); @@ -655,7 +676,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe binding.pipLocalMicOffIndicator.setVisibility(View.GONE); return; } - final Optional localVideoTrack = requireRtpConnection().geLocalVideoTrack(); + final Optional localVideoTrack = getLocalVideoTrack(); if (localVideoTrack.isPresent() && !isPictureInPicture()) { ensureSurfaceViewRendererIsSetup(binding.localVideo); //paint local view over remote view @@ -665,7 +686,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } else { binding.localVideo.setVisibility(View.GONE); } - final Optional remoteVideoTrack = requireRtpConnection().getRemoteVideoTrack(); + final Optional remoteVideoTrack = getRemoteVideoTrack(); if (remoteVideoTrack.isPresent()) { ensureSurfaceViewRendererIsSetup(binding.remoteVideo); remoteVideoTrack.get().addSink(binding.remoteVideo); @@ -688,6 +709,22 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } + private Optional getLocalVideoTrack() { + final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + if (connection == null) { + return Optional.absent(); + } + return connection.getLocalVideoTrack(); + } + + private Optional getRemoteVideoTrack() { + final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + if (connection == null) { + return Optional.absent(); + } + return connection.getRemoteVideoTrack(); + } + private void disableMicrophone(View view) { JingleRtpConnection rtpConnection = requireRtpConnection(); rtpConnection.setMicrophoneEnabled(false); @@ -762,19 +799,23 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe return; } final AbstractJingleConnection.Id id = requireRtpConnection().getId(); + final Set media = getMedia(); + final Contact contact = getWith(); if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) { if (state == RtpEndUserState.ENDED) { finish(); return; } runOnUiThread(() -> { - updateStateDisplay(state); - updateButtonConfiguration(state); + updateStateDisplay(state, media); + updateButtonConfiguration(state, media); updateVideoViews(state); - updateProfilePicture(state); + updateProfilePicture(state, contact); }); if (END_CARD.contains(state)) { - resetIntent(account, with, state, requireRtpConnection().getMedia()); + final JingleRtpConnection rtpConnection = requireRtpConnection(); + resetIntent(account, with, state, rtpConnection.getMedia()); + releaseVideoTracks(rtpConnection); this.rtpConnectionReference = null; } } else { diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java index 3ea451653..dacfc8df0 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -9,11 +9,14 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.google.common.base.Optional; + import java.util.List; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ConversationListRowBinding; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.ui.ConversationFragment; import eu.siacs.conversations.ui.XmppActivity; @@ -22,6 +25,7 @@ import eu.siacs.conversations.ui.util.StyledAttributes; import eu.siacs.conversations.utils.EmojiWrapper; import eu.siacs.conversations.utils.IrregularUnicodeDetector; import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import rocks.xmpp.addr.Jid; public class ConversationAdapter extends RecyclerView.Adapter { @@ -160,21 +164,35 @@ public class ConversationAdapter extends RecyclerView.Adapter= System.currentTimeMillis()) { - viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); - int ic_notifications_paused = activity.getThemeResource(R.attr.icon_notifications_paused, R.drawable.ic_notifications_paused_black_24dp); - viewHolder.binding.notificationStatus.setImageResource(ic_notifications_paused); - } else if (conversation.alwaysNotify()) { - viewHolder.binding.notificationStatus.setVisibility(View.GONE); + + final Optional ongoingCall; + if (conversation.getMode() == Conversational.MODE_MULTI) { + ongoingCall = Optional.absent(); } else { + ongoingCall = activity.xmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); + } + + if (ongoingCall.isPresent()) { viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); - int ic_notifications_none = activity.getThemeResource(R.attr.icon_notifications_none, R.drawable.ic_notifications_none_black_24dp); - viewHolder.binding.notificationStatus.setImageResource(ic_notifications_none); + final int ic_ongoing_call = activity.getThemeResource(R.attr.ic_ongoing_call_hint, R.drawable.ic_phone_in_talk_black_18dp); + viewHolder.binding.notificationStatus.setImageResource(ic_ongoing_call); + } else { + final long muted_till = conversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0); + if (muted_till == Long.MAX_VALUE) { + viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); + int ic_notifications_off = activity.getThemeResource(R.attr.icon_notifications_off, R.drawable.ic_notifications_off_black_24dp); + viewHolder.binding.notificationStatus.setImageResource(ic_notifications_off); + } else if (muted_till >= System.currentTimeMillis()) { + viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); + int ic_notifications_paused = activity.getThemeResource(R.attr.icon_notifications_paused, R.drawable.ic_notifications_paused_black_24dp); + viewHolder.binding.notificationStatus.setImageResource(ic_notifications_paused); + } else if (conversation.alwaysNotify()) { + viewHolder.binding.notificationStatus.setVisibility(View.GONE); + } else { + viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); + int ic_notifications_none = activity.getThemeResource(R.attr.icon_notifications_none, R.drawable.ic_notifications_none_black_24dp); + viewHolder.binding.notificationStatus.setImageResource(ic_notifications_none); + } } long timestamp; diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java index 8defc9976..e9f3bcb24 100644 --- a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java @@ -29,82 +29,85 @@ import eu.siacs.conversations.ui.XmppActivity; public class ExceptionHelper { - private static final String FILENAME = "stacktrace.txt"; - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); + private static final String FILENAME = "stacktrace.txt"; + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); - public static void init(Context context) { - if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler)) { - Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler( - context)); - } - } + public static void init(Context context) { + if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler)) { + Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler( + context)); + } + } - public static boolean checkForCrash(XmppActivity activity) { - try { - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; - if (service == null) { - return false; - } - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); - boolean neverSend = preferences.getBoolean("never_send", false); - if (neverSend || Config.BUG_REPORTS == null) { - return false; - } - final Account account = AccountUtils.getFirstEnabled(service); - if (account == null) { - return false; - } - FileInputStream file = activity.openFileInput(FILENAME); - InputStreamReader inputStreamReader = new InputStreamReader(file); - BufferedReader stacktrace = new BufferedReader(inputStreamReader); - final StringBuilder report = new StringBuilder(); - PackageManager pm = activity.getPackageManager(); - PackageInfo packageInfo; - try { - packageInfo = pm.getPackageInfo(activity.getPackageName(), PackageManager.GET_SIGNATURES); - report.append("Version: ").append(packageInfo.versionName).append('\n'); - report.append("Last Update: ").append(DATE_FORMAT.format(new Date(packageInfo.lastUpdateTime))).append('\n'); - Signature[] signatures = packageInfo.signatures; - if (signatures != null && signatures.length >= 1) { - report.append("SHA-1: ").append(CryptoHelper.getFingerprintCert(packageInfo.signatures[0].toByteArray())).append('\n'); - } - report.append('\n'); - } catch (Exception e) { - e.printStackTrace(); - return false; - } - String line; - while ((line = stacktrace.readLine()) != null) { - report.append(line); - report.append('\n'); - } - file.close(); - activity.deleteFile(FILENAME); - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(activity.getString(R.string.crash_report_title)); - builder.setMessage(activity.getText(R.string.crash_report_message)); - builder.setPositiveButton(activity.getText(R.string.send_now), (dialog, which) -> { + public static boolean checkForCrash(XmppActivity activity) { + try { + final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; + if (service == null) { + return false; + } + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); + boolean neverSend = preferences.getBoolean("never_send", false); + if (neverSend || Config.BUG_REPORTS == null) { + return false; + } + final Account account = AccountUtils.getFirstEnabled(service); + if (account == null) { + return false; + } + FileInputStream file = activity.openFileInput(FILENAME); + InputStreamReader inputStreamReader = new InputStreamReader(file); + BufferedReader stacktrace = new BufferedReader(inputStreamReader); + final StringBuilder report = new StringBuilder(); + PackageManager pm = activity.getPackageManager(); + PackageInfo packageInfo; + try { + packageInfo = pm.getPackageInfo(activity.getPackageName(), PackageManager.GET_SIGNATURES); + final String versionName = packageInfo.versionName; + final int versionCode = packageInfo.versionCode; + final int version = versionCode > 10000 ? (versionCode / 100) : versionCode; + report.append(String.format(Locale.ROOT, "Version: %s(%d)", versionName, version)).append('\n'); + report.append("Last Update: ").append(DATE_FORMAT.format(new Date(packageInfo.lastUpdateTime))).append('\n'); + Signature[] signatures = packageInfo.signatures; + if (signatures != null && signatures.length >= 1) { + report.append("SHA-1: ").append(CryptoHelper.getFingerprintCert(packageInfo.signatures[0].toByteArray())).append('\n'); + } + report.append('\n'); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + String line; + while ((line = stacktrace.readLine()) != null) { + report.append(line); + report.append('\n'); + } + file.close(); + activity.deleteFile(FILENAME); + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(activity.getString(R.string.crash_report_title)); + builder.setMessage(activity.getText(R.string.crash_report_message)); + builder.setPositiveButton(activity.getText(R.string.send_now), (dialog, which) -> { - Log.d(Config.LOGTAG, "using account=" + account.getJid().asBareJid() + " to send in stack trace"); - Conversation conversation = service.findOrCreateConversation(account, Config.BUG_REPORTS, false, true); - Message message = new Message(conversation, report.toString(), Message.ENCRYPTION_NONE); - service.sendMessage(message); - }); - builder.setNegativeButton(activity.getText(R.string.send_never), (dialog, which) -> preferences.edit().putBoolean("never_send", true).apply()); - builder.create().show(); - return true; - } catch (final IOException ignored) { - return false; - } - } + Log.d(Config.LOGTAG, "using account=" + account.getJid().asBareJid() + " to send in stack trace"); + Conversation conversation = service.findOrCreateConversation(account, Config.BUG_REPORTS, false, true); + Message message = new Message(conversation, report.toString(), Message.ENCRYPTION_NONE); + service.sendMessage(message); + }); + builder.setNegativeButton(activity.getText(R.string.send_never), (dialog, which) -> preferences.edit().putBoolean("never_send", true).apply()); + builder.create().show(); + return true; + } catch (final IOException ignored) { + return false; + } + } - static void writeToStacktraceFile(Context context, String msg) { - try { - OutputStream os = context.openFileOutput(FILENAME, Context.MODE_PRIVATE); - os.write(msg.getBytes()); - os.flush(); - os.close(); - } catch (IOException ignored) { - } - } + static void writeToStacktraceFile(Context context, String msg) { + try { + OutputStream os = context.openFileOutput(FILENAME, Context.MODE_PRIVATE); + os.write(msg.getBytes()); + os.flush(); + os.close(); + } catch (IOException ignored) { + } + } } 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 4c410a5be..975f25114 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -1,19 +1,16 @@ package eu.siacs.conversations.xmpp.jingle; -import android.os.SystemClock; import android.util.Base64; import android.util.Log; -import com.google.common.base.Function; import com.google.common.base.Objects; +import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableSet; -import org.checkerframework.checker.nullness.compatqual.NullableDecl; - import java.lang.ref.WeakReference; import java.security.SecureRandom; import java.util.Collection; @@ -53,9 +50,10 @@ import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; public class JingleConnectionManager extends AbstractConnectionManager { - private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + public static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor(); + public final ToneManager toneManager = new ToneManager(); private final HashMap rtpSessionProposals = new HashMap<>(); - private final Map connections = new ConcurrentHashMap<>(); + private final ConcurrentHashMap connections = new ConcurrentHashMap<>(); private final Cache endedSessions = CacheBuilder.newBuilder() .expireAfterWrite(30, TimeUnit.MINUTES) @@ -108,6 +106,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { return; } connections.put(id, connection); + mXmppConnectionService.updateConversationUi(); connection.deliverPacket(packet); } else { Log.d(Config.LOGTAG, "unable to route jingle packet: " + packet); @@ -143,7 +142,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { } ScheduledFuture schedule(final Runnable runnable, final long delay, final TimeUnit timeUnit) { - return this.scheduledExecutorService.schedule(runnable, delay, timeUnit); + return this.SCHEDULED_EXECUTOR_SERVICE.schedule(runnable, delay, timeUnit); } void respondWithJingleError(final Account account, final IqPacket original, String jingleCondition, String condition, String conditionType) { @@ -270,6 +269,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { synchronized (rtpSessionProposals) { if (rtpSessionProposals.remove(proposal) != null) { writeLogMissedOutgoing(account, proposal.with, proposal.sessionId, serverMsgId, timestamp); + toneManager.transition(true, RtpEndUserState.DECLINED_OR_BUSY); mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, proposal.with, proposal.sessionId, RtpEndUserState.DECLINED_OR_BUSY); } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver reject"); @@ -353,6 +353,18 @@ public class JingleConnectionManager extends AbstractConnectionManager { connection.init(message); } + public Optional getOngoingRtpConnection(final Contact contact) { + for (final Map.Entry entry : this.connections.entrySet()) { + if (entry.getValue() instanceof JingleRtpConnection) { + final AbstractJingleConnection.Id id = entry.getKey(); + if (id.account == contact.getAccount() && id.with.asBareJid().equals(contact.getJid().asBareJid())) { + return Optional.of(id); + } + } + } + return Optional.absent(); + } + void finishConnection(final AbstractJingleConnection connection) { this.connections.remove(connection.getId()); } @@ -413,6 +425,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } if (matchingProposal != null) { + toneManager.transition(true, RtpEndUserState.ENDED); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retracting rtp session proposal with " + with); this.rtpSessionProposals.remove(matchingProposal); final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(matchingProposal); @@ -429,11 +442,13 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (proposal.account == account && with.asBareJid().equals(proposal.with)) { final DeviceDiscoveryState preexistingState = entry.getValue(); if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) { + final RtpEndUserState endUserState = preexistingState.toEndUserState(); + toneManager.transition(true, endUserState); mXmppConnectionService.notifyJingleRtpConnectionUpdate( account, with, proposal.sessionId, - preexistingState.toEndUserState() + endUserState ); return; } @@ -519,7 +534,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { return; } this.rtpSessionProposals.put(sessionProposal, target); - mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, sessionProposal.with, sessionProposal.sessionId, target.toEndUserState()); + final RtpEndUserState endUserState = target.toEndUserState(); + toneManager.transition(true, endUserState); + mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, sessionProposal.with, sessionProposal.sessionId, endUserState); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": flagging session " + sessionId + " as " + target); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 0052228b1..e46ce78ec 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -6,6 +6,7 @@ import android.util.Log; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import com.google.common.base.Throwables; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -48,15 +49,12 @@ import rocks.xmpp.addr.Jid; public class JingleRtpConnection extends AbstractJingleConnection implements WebRTCWrapper.EventCallback { - private static final long BUSY_TIME_OUT = 30; - public static final List STATES_SHOWING_ONGOING_CALL = Arrays.asList( State.PROCEED, - State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED ); - + private static final long BUSY_TIME_OUT = 30; private static final List TERMINATED = Arrays.asList( State.TERMINATED_SUCCESS, State.TERMINATED_DECLINED_OR_BUSY, @@ -236,31 +234,40 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (identificationTags.size() == 0) { Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices"); } - for (final Map.Entry content : contentMap.contents.entrySet()) { - final String ufrag = content.getValue().transport.getAttribute("ufrag"); - for (final IceUdpTransportInfo.Candidate candidate : content.getValue().transport.getCandidates()) { - final String sdp; - try { - sdp = candidate.toSdpAttribute(ufrag); - } catch (IllegalArgumentException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage()); - continue; - } - final String sdpMid = content.getKey(); - final int mLineIndex = identificationTags.indexOf(sdpMid); - final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp); - if (isInState(State.SESSION_ACCEPTED)) { - Log.d(Config.LOGTAG, "received candidate: " + iceCandidate); - this.webRTCWrapper.addIceCandidate(iceCandidate); - } else { - this.pendingIceCandidates.offer(iceCandidate); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": put ICE candidate on backlog"); - } + receiveCandidates(identificationTags, contentMap.contents.entrySet()); + } else { + if (isTerminated()) { + respondOk(jinglePacket); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring out-of-order transport info; we where already terminated"); + } else { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received transport info while in state=" + this.state); + terminateWithOutOfOrder(jinglePacket); + } + } + } + + private void receiveCandidates(final List identificationTags, final Set> contents) { + for (final Map.Entry content : contents) { + final String ufrag = content.getValue().transport.getAttribute("ufrag"); + for (final IceUdpTransportInfo.Candidate candidate : content.getValue().transport.getCandidates()) { + final String sdp; + try { + sdp = candidate.toSdpAttribute(ufrag); + } catch (IllegalArgumentException e) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage()); + continue; + } + final String sdpMid = content.getKey(); + final int mLineIndex = identificationTags.indexOf(sdpMid); + final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp); + if (isInState(State.SESSION_ACCEPTED)) { + Log.d(Config.LOGTAG, "received candidate: " + iceCandidate); + this.webRTCWrapper.addIceCandidate(iceCandidate); + } else { + this.pendingIceCandidates.offer(iceCandidate); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": put ICE candidate on backlog"); } } - } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received transport info while in state=" + this.state); - terminateWithOutOfOrder(jinglePacket); } } @@ -276,9 +283,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web contentMap.requireContentDescriptions(); contentMap.requireDTLSFingerprint(); } catch (final RuntimeException e) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e)); respondOk(jinglePacket); sendSessionTerminate(Reason.of(e), e.getMessage()); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); return; } Log.d(Config.LOGTAG, "processing session-init with " + contentMap.contents.size() + " contents"); @@ -302,6 +309,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) { respondOk(jinglePacket); + final List identificationTags = contentMap.group == null ? Collections.emptyList() : contentMap.group.getIdentificationTags(); + receiveCandidates(identificationTags, contentMap.contents.entrySet()); if (target == State.SESSION_INITIALIZED_PRE_APPROVED) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate"); sendSessionAccept(); @@ -346,6 +355,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (transition(State.SESSION_ACCEPTED)) { respondOk(jinglePacket); receiveSessionAccept(contentMap); + final List identificationTags = contentMap.group == null ? Collections.emptyList() : contentMap.group.getIdentificationTags(); + receiveCandidates(identificationTags, contentMap.contents.entrySet()); } else { Log.d(Config.LOGTAG, String.format("%s: received session-accept while in state %s", id.account.getJid().asBareJid(), state)); respondOk(jinglePacket); @@ -369,8 +380,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web ); try { this.webRTCWrapper.setRemoteDescription(answer).get(); - } catch (Exception e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", e); + } catch (final Exception e) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", Throwables.getRootCause(e)); webRTCWrapper.close(); sendSessionTerminate(Reason.FAILED_APPLICATION); } @@ -419,10 +430,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); sendSessionAccept(respondingRtpContentMap); - this.webRTCWrapper.setLocalDescription(webRTCSessionDescription); - } catch (Exception e) { - Log.d(Config.LOGTAG, "unable to send session accept", e); - + this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); + } catch (final Exception e) { + Log.d(Config.LOGTAG, "unable to send session accept", Throwables.getRootCause(e)); + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION); } } @@ -635,7 +647,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web sendSessionInitiate(rtpContentMap, targetState); this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); } catch (final Exception e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", e); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", Throwables.getRootCause(e)); webRTCWrapper.close(); if (isInState(targetState)) { sendSessionTerminate(Reason.FAILED_APPLICATION); @@ -657,9 +669,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void sendSessionTerminate(final Reason reason, final String text) { + final State previous = this.state; final State target = reasonToState(reason); transitionOrThrow(target); - writeLogMessage(target); + if (previous != State.NULL) { + writeLogMessage(target); + } final JinglePacket jinglePacket = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); jinglePacket.setReason(reason, text); Log.d(Config.LOGTAG, jinglePacket.toString()); @@ -672,7 +687,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; transportInfo = rtpContentMap.transportInfo(contentName, candidate); - } catch (Exception e) { + } catch (final Exception e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to prepare transport-info from candidate for content=" + contentName); return; } @@ -788,15 +803,18 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } public Set getMedia() { - if (isInState(State.NULL)) { + final State current = getState(); + if (current == State.NULL) { throw new IllegalStateException("RTP connection has not been initialized yet"); } - if (isInState(State.PROPOSED, State.PROCEED)) { + if (Arrays.asList(State.PROPOSED, State.PROCEED).contains(current)) { return Preconditions.checkNotNull(this.proposedMedia, "RTP connection has not been initialized properly"); } final RtpContentMap initiatorContentMap = initiatorRtpContentMap; if (initiatorContentMap != null) { return initiatorContentMap.getMedia(); + } else if (isTerminated()) { + return Collections.emptySet(); //we might fail before we ever got a chance to set media } else { return Preconditions.checkNotNull(this.proposedMedia, "RTP connection has not been initialized properly"); } @@ -1024,7 +1042,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void updateEndUserState() { - xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, getEndUserState()); + final RtpEndUserState endUserState = getEndUserState(); + jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, getMedia()); + xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, endUserState); } private void updateOngoingCallNotification() { @@ -1140,7 +1160,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return TERMINATED.contains(this.state); } - public Optional geLocalVideoTrack() { + public Optional getLocalVideoTrack() { return webRTCWrapper.getLocalVideoTrack(); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index da48d017f..9150d393e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -59,7 +59,7 @@ public class RtpContentMap { })); } - public void requireContentDescriptions() { + void requireContentDescriptions() { if (this.contents.size() == 0) { throw new IllegalStateException("No contents available"); } @@ -70,7 +70,7 @@ public class RtpContentMap { } } - public void requireDTLSFingerprint() { + void requireDTLSFingerprint() { if (this.contents.size() == 0) { throw new IllegalStateException("No contents available"); } @@ -80,10 +80,13 @@ public class RtpContentMap { if (fingerprint == null || Strings.isNullOrEmpty(fingerprint.getContent()) || Strings.isNullOrEmpty(fingerprint.getHash())) { throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s", entry.getKey())); } + if (Strings.isNullOrEmpty(fingerprint.getSetup())) { + throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute", entry.getKey())); + } } } - public JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) { + JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) { final JinglePacket jinglePacket = new JinglePacket(action, sessionId); if (this.group != null) { jinglePacket.addGroup(this.group); @@ -99,7 +102,7 @@ public class RtpContentMap { return jinglePacket; } - public RtpContentMap transportInfo(final String contentName, final IceUdpTransportInfo.Candidate candidate) { + RtpContentMap transportInfo(final String contentName, final IceUdpTransportInfo.Candidate candidate) { final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName); final IceUdpTransportInfo transportInfo = descriptionTransport == null ? null : descriptionTransport.transport; if (transportInfo == null) { @@ -115,7 +118,7 @@ public class RtpContentMap { public final RtpDescription description; public final IceUdpTransportInfo transport; - public DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) { + DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) { this.description = description; this.transport = transport; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java new file mode 100644 index 000000000..3274452e9 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java @@ -0,0 +1,121 @@ +package eu.siacs.conversations.xmpp.jingle; + +import android.media.AudioManager; +import android.media.ToneGenerator; +import android.util.Log; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import eu.siacs.conversations.Config; + +import static java.util.Arrays.asList; + +public class ToneManager { + + private final ToneGenerator toneGenerator; + + private ToneState state = null; + private ScheduledFuture currentTone; + + public ToneManager() { + this.toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 35); + } + + public void transition(final boolean isInitiator, final RtpEndUserState state) { + transition(of(isInitiator, state, Collections.emptySet())); + } + + public void transition(final boolean isInitiator, final RtpEndUserState state, final Set media) { + transition(of(isInitiator, state, media)); + } + + private static ToneState of(final boolean isInitiator, final RtpEndUserState state, final Set media) { + if (isInitiator) { + if (asList(RtpEndUserState.RINGING, RtpEndUserState.CONNECTING).contains(state)) { + return ToneState.RINGING; + } + if (state == RtpEndUserState.DECLINED_OR_BUSY) { + return ToneState.BUSY; + } + } + if (state == RtpEndUserState.ENDING_CALL) { + if (media.contains(Media.VIDEO)) { + return ToneState.NULL; + } else { + return ToneState.ENDING_CALL; + } + } + if (state == RtpEndUserState.CONNECTED) { + if (media.contains(Media.VIDEO)) { + return ToneState.NULL; + } else { + return ToneState.CONNECTED; + } + } + return ToneState.NULL; + } + + private synchronized void transition(ToneState state) { + if (this.state == state) { + return; + } + if (state == ToneState.NULL && this.state == ToneState.ENDING_CALL) { + return; + } + cancelCurrentTone(); + Log.d(Config.LOGTAG, getClass().getName() + ".transition(" + state + ")"); + switch (state) { + case RINGING: + scheduleWaitingTone(); + break; + case CONNECTED: + scheduleConnected(); + break; + case BUSY: + scheduleBusy(); + break; + case ENDING_CALL: + scheduleEnding(); + break; + } + this.state = state; + } + + private void scheduleConnected() { + this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> { + this.toneGenerator.startTone(ToneGenerator.TONE_PROP_PROMPT, 200); + }, 0, TimeUnit.SECONDS); + } + + private void scheduleEnding() { + this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> { + this.toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); + }, 0, TimeUnit.SECONDS); + } + + private void scheduleBusy() { + this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> { + this.toneGenerator.startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500); + }, 0, TimeUnit.SECONDS); + } + + private void scheduleWaitingTone() { + this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(() -> { + this.toneGenerator.startTone(ToneGenerator.TONE_CDMA_DIAL_TONE_LITE, 750); + }, 0, 3, TimeUnit.SECONDS); + } + + private void cancelCurrentTone() { + if (currentTone != null) { + currentTone.cancel(true); + } + toneGenerator.stopTone(); + } + + private enum ToneState { + NULL, RINGING, CONNECTED, BUSY, ENDING_CALL + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index a5fc8429a..17c1b77fe 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -158,7 +158,7 @@ public class WebRTCWrapper { private EglBase eglBase = null; private CapturerChoice capturerChoice; - public WebRTCWrapper(final EventCallback eventCallback) { + WebRTCWrapper(final EventCallback eventCallback) { this.eventCallback = eventCallback; } @@ -175,7 +175,7 @@ public class WebRTCWrapper { }); } - public void initializePeerConnection(final Set media, final List iceServers) throws InitializationException { + synchronized void initializePeerConnection(final Set media, final List iceServers) throws InitializationException { Preconditions.checkState(this.eglBase != null); Preconditions.checkNotNull(media); Preconditions.checkArgument(media.size() > 0, "media can not be empty when initializing peer connection"); @@ -224,13 +224,13 @@ public class WebRTCWrapper { this.peerConnection = peerConnection; } - public void close() { + synchronized void close() { final PeerConnection peerConnection = this.peerConnection; final CapturerChoice capturerChoice = this.capturerChoice; final AppRTCAudioManager audioManager = this.appRTCAudioManager; final EglBase eglBase = this.eglBase; if (peerConnection != null) { - peerConnection.dispose(); + dispose(peerConnection); this.peerConnection = null; } if (audioManager != null) { @@ -251,7 +251,15 @@ public class WebRTCWrapper { } } - void verifyClosed() { + private static void dispose(final PeerConnection peerConnection) { + try { + peerConnection.dispose(); + } catch (final IllegalStateException e) { + Log.e(Config.LOGTAG,"unable to dispose of peer connection", e); + } + } + + synchronized void verifyClosed() { if (this.peerConnection != null || this.eglBase != null || this.localVideoTrack != null @@ -278,7 +286,7 @@ public class WebRTCWrapper { audioTrack.setEnabled(enabled); } - public boolean isVideoEnabled() { + boolean isVideoEnabled() { final VideoTrack videoTrack = this.localVideoTrack; if (videoTrack == null) { throw new IllegalStateException("Local video track does not exist"); @@ -286,7 +294,7 @@ public class WebRTCWrapper { return videoTrack.enabled(); } - public void setVideoEnabled(final boolean enabled) { + void setVideoEnabled(final boolean enabled) { final VideoTrack videoTrack = this.localVideoTrack; if (videoTrack == null) { throw new IllegalStateException("Local video track does not exist"); @@ -294,7 +302,7 @@ public class WebRTCWrapper { videoTrack.setEnabled(enabled); } - public ListenableFuture createOffer() { + ListenableFuture createOffer() { return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { final SettableFuture future = SettableFuture.create(); peerConnection.createOffer(new CreateSdpObserver() { @@ -305,7 +313,6 @@ public class WebRTCWrapper { @Override public void onCreateFailure(String s) { - Log.d(Config.LOGTAG, "create failure" + s); future.setException(new IllegalStateException("Unable to create offer: " + s)); } }, new MediaConstraints()); @@ -313,7 +320,7 @@ public class WebRTCWrapper { }, MoreExecutors.directExecutor()); } - public ListenableFuture createAnswer() { + ListenableFuture createAnswer() { return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { final SettableFuture future = SettableFuture.create(); peerConnection.createAnswer(new CreateSdpObserver() { @@ -331,7 +338,7 @@ public class WebRTCWrapper { }, MoreExecutors.directExecutor()); } - public ListenableFuture setLocalDescription(final SessionDescription sessionDescription) { + ListenableFuture setLocalDescription(final SessionDescription sessionDescription) { Log.d(EXTENDED_LOGGING_TAG, "setting local description:"); for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { Log.d(EXTENDED_LOGGING_TAG, line); @@ -345,8 +352,7 @@ public class WebRTCWrapper { } @Override - public void onSetFailure(String s) { - Log.d(Config.LOGTAG, "unable to set local " + s); + public void onSetFailure(final String s) { future.setException(new IllegalArgumentException("unable to set local session description: " + s)); } @@ -355,7 +361,7 @@ public class WebRTCWrapper { }, MoreExecutors.directExecutor()); } - public ListenableFuture setRemoteDescription(final SessionDescription sessionDescription) { + ListenableFuture setRemoteDescription(final SessionDescription sessionDescription) { Log.d(EXTENDED_LOGGING_TAG, "setting remote description:"); for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { Log.d(EXTENDED_LOGGING_TAG, line); @@ -388,7 +394,7 @@ public class WebRTCWrapper { } } - public void addIceCandidate(IceCandidate iceCandidate) { + void addIceCandidate(IceCandidate iceCandidate) { requirePeerConnection().addIceCandidate(iceCandidate); } @@ -439,11 +445,11 @@ public class WebRTCWrapper { return this.eglBase.getEglBaseContext(); } - public Optional getLocalVideoTrack() { + Optional getLocalVideoTrack() { return Optional.fromNullable(this.localVideoTrack); } - public Optional getRemoteVideoTrack() { + Optional getRemoteVideoTrack() { return Optional.fromNullable(this.remoteVideoTrack); } @@ -463,7 +469,7 @@ public class WebRTCWrapper { return context; } - public AppRTCAudioManager getAudioManager() { + AppRTCAudioManager getAudioManager() { return appRTCAudioManager; } @@ -504,7 +510,7 @@ public class WebRTCWrapper { } } - public static class InitializationException extends Exception { + static class InitializationException extends Exception { private InitializationException(String message) { super(message); @@ -515,12 +521,12 @@ public class WebRTCWrapper { private final CameraVideoCapturer cameraVideoCapturer; private final CameraEnumerationAndroid.CaptureFormat captureFormat; - public CapturerChoice(CameraVideoCapturer cameraVideoCapturer, CameraEnumerationAndroid.CaptureFormat captureFormat) { + CapturerChoice(CameraVideoCapturer cameraVideoCapturer, CameraEnumerationAndroid.CaptureFormat captureFormat) { this.cameraVideoCapturer = cameraVideoCapturer; this.captureFormat = captureFormat; } - public int getFrameRate() { + int getFrameRate() { return Math.max(captureFormat.framerate.min, Math.min(CAPTURING_MAX_FRAME_RATE, captureFormat.framerate.max)); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index 1e7ada424..a56f60d93 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -194,6 +194,9 @@ public class IceUdpTransportInfo extends GenericTransportInfo { checkNotNullNoWhitespace(component, "component"); final String transport = this.getAttribute("protocol"); checkNotNullNoWhitespace(transport, "protocol"); + if (!"udp".equals(transport)) { + throw new IllegalArgumentException(String.format("'%s' is not a supported protocol", transport)); + } final String priority = this.getAttribute("priority"); checkNotNullNoWhitespace(priority, "priority"); final String connectionAddress = this.getAttribute("ip"); diff --git a/src/main/res/drawable-hdpi/ic_phone_in_talk_black_18dp.png b/src/main/res/drawable-hdpi/ic_phone_in_talk_black_18dp.png new file mode 100644 index 000000000..37bf2abc3 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_phone_in_talk_black_18dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_phone_in_talk_white_18dp.png b/src/main/res/drawable-hdpi/ic_phone_in_talk_white_18dp.png new file mode 100644 index 000000000..ecb24eb76 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_phone_in_talk_white_18dp.png differ diff --git a/src/main/res/drawable-hdpi/ic_phone_in_talk_white_24dp.png b/src/main/res/drawable-hdpi/ic_phone_in_talk_white_24dp.png new file mode 100644 index 000000000..f2c89424e Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_phone_in_talk_white_24dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_phone_in_talk_black_18dp.png b/src/main/res/drawable-mdpi/ic_phone_in_talk_black_18dp.png new file mode 100644 index 000000000..ee4526853 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_phone_in_talk_black_18dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_phone_in_talk_white_18dp.png b/src/main/res/drawable-mdpi/ic_phone_in_talk_white_18dp.png new file mode 100644 index 000000000..833386bbc Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_phone_in_talk_white_18dp.png differ diff --git a/src/main/res/drawable-mdpi/ic_phone_in_talk_white_24dp.png b/src/main/res/drawable-mdpi/ic_phone_in_talk_white_24dp.png new file mode 100644 index 000000000..e6f98af95 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_phone_in_talk_white_24dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_phone_in_talk_black_18dp.png b/src/main/res/drawable-xhdpi/ic_phone_in_talk_black_18dp.png new file mode 100644 index 000000000..0eea8bcb7 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_phone_in_talk_black_18dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_phone_in_talk_white_18dp.png b/src/main/res/drawable-xhdpi/ic_phone_in_talk_white_18dp.png new file mode 100644 index 000000000..f2c89424e Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_phone_in_talk_white_18dp.png differ diff --git a/src/main/res/drawable-xhdpi/ic_phone_in_talk_white_24dp.png b/src/main/res/drawable-xhdpi/ic_phone_in_talk_white_24dp.png new file mode 100644 index 000000000..a2d78b221 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_phone_in_talk_white_24dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_phone_in_talk_black_18dp.png b/src/main/res/drawable-xxhdpi/ic_phone_in_talk_black_18dp.png new file mode 100644 index 000000000..ed36763d8 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_phone_in_talk_black_18dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_phone_in_talk_white_18dp.png b/src/main/res/drawable-xxhdpi/ic_phone_in_talk_white_18dp.png new file mode 100644 index 000000000..ec67c6ef9 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_phone_in_talk_white_18dp.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_phone_in_talk_white_24dp.png b/src/main/res/drawable-xxhdpi/ic_phone_in_talk_white_24dp.png new file mode 100644 index 000000000..9c002da0a Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_phone_in_talk_white_24dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_phone_in_talk_black_18dp.png b/src/main/res/drawable-xxxhdpi/ic_phone_in_talk_black_18dp.png new file mode 100644 index 000000000..64b912bca Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_phone_in_talk_black_18dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_phone_in_talk_white_18dp.png b/src/main/res/drawable-xxxhdpi/ic_phone_in_talk_white_18dp.png new file mode 100644 index 000000000..9c002da0a Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_phone_in_talk_white_18dp.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_phone_in_talk_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_phone_in_talk_white_24dp.png new file mode 100644 index 000000000..36be1da99 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_phone_in_talk_white_24dp.png differ diff --git a/src/main/res/menu/fragment_conversation.xml b/src/main/res/menu/fragment_conversation.xml index b01e8cfa5..77306486a 100644 --- a/src/main/res/menu/fragment_conversation.xml +++ b/src/main/res/menu/fragment_conversation.xml @@ -60,6 +60,12 @@ android:title="@string/send_location" /> + 关闭聊天 联系人详情 群聊详情 - 频道详情 + 群聊详情 安全聊天 添加账号 编辑姓名 @@ -31,10 +31,10 @@ 1分钟前 %d分钟前 %d条未读消息 - 正在发送… - 解密信息中. 请稍候… - OpenPGP 加密的信息 - 该名称已存在 + 发送中… + 解密中. 请稍候… + OpenPGP加密的信息 + 该用户名已存在 无效的用户名 管理员 所有者 @@ -47,11 +47,11 @@ 封禁 %s 中的所有联系人? 解封%s 中所有联系人? 联系人已封禁 - 已屏蔽 + 已封禁 从书签中移除 %s ?相关会话消息不会被清除 . 在服务器上注册新账户 在服务器上修改密码 - 分享…… + 分享… 开始会话 邀请联系人 邀请 @@ -62,8 +62,8 @@ 添加 编辑 删除 - 屏蔽 - 解除屏蔽 + 封禁 + 解封 保存 完成 畅聊已崩溃 @@ -73,7 +73,7 @@ 无法连接至账户 无法连接至多个账户 点击此处管理账户 - 附加文件 + 添加文件 该联系人不在您的列表,需要加为联系人吗 ? 添加联系人 传递失败 @@ -109,13 +109,17 @@ 因您的联系人未公布公钥,畅聊未能成功加密您的信息.\n\n请通知联系人设置OpenPGP. 常规 接收文件 - 自动接收小于 … 的文件 + 自动接收小于此大小的文件 附件 通知 震动 收到新消息时震动 LED 灯提示 收到新消息时闪烁通知灯 + 铃声 + 通知铃声 + 新消息通知铃声 + 来电铃声 静默期限 在您的其他设备之一上检测到活动之后,时间通知的长度将被静音。 高级 @@ -134,7 +138,7 @@ 接收在线联系人列表更新 请求在线联系人列表更新 选择图片 - 照相 + 拍摄图片 预先同意订阅请求 您选择的文件不是图像文件 转换图像出错 @@ -188,6 +192,7 @@ XEP-0191:屏蔽指令 XEP-0237:名单版本 XEP-0198:流管理 + XEP-0215:发现外部服务 XEP-0163:PEP(头像/OMEMO) XEP-0363:HTTP文件上传 XEP-0357:推送 @@ -229,23 +234,23 @@ 保存为书签 删除书签 解散群聊 - 解散频道 + 解散群聊 您确定要解散此群聊吗?\n\n警告:此群聊将在服务器上完全删除。 - 您确定要解散此公共频道吗?\n\n警告:该频道将在服务器上完全删除。 + 您确定要解散此公共群聊吗?\n\n警告:该群聊将在服务器上完全删除。 无法解散群聊 - 无法解散频道 + 无法解散群聊 编辑群聊主题 主题 正在加入群聊… 离开 联系人已添加你到联系人列表 反向添加 - %s 已读此句 - %s 已读此句 - %1$s +%2$d 都已读此句 - 全部已读此句 + %s 读到这里了 + %s 读到这里了 + %1$s 和另外%2$d人读到这里了 + 所有人都读到这里了 发布 - 点击头像可从相册中选择头像 + 点击头像以选择图片 正在发布… 服务器拒绝了您的发布请求 转换头像图片出错 @@ -283,7 +288,7 @@ 同步书签 根据书签中的自动加入标记加入并离开群聊。 OMEMO 指纹已拷贝到剪贴板! - 你已经被禁言了 + 你已经被封禁了 这个群组只允许群组成员聊天 资源限制 您已被移出该群组了 @@ -374,10 +379,10 @@ 授予所有者权限 撤销所有者权限 从群聊中移除 - 从频道中移除 + 从群聊中移除 不能修改 %s 的从属关系 屏蔽群聊 - 从频道中屏蔽 + 从群聊中屏蔽 %s将被从公共群聊中移除。只有将此用户封禁才能将他从群聊永远移除。 现在屏蔽 不能修改 %s 的角色 @@ -465,7 +470,7 @@ xmpp.example.com 使用证书添加账户 无法解析证书 - 留空以认证 w/ 证书 + 留空以使用证书认证 压缩设置 服务端压缩设置 正在获取压缩设置。请稍候…… @@ -483,7 +488,7 @@ 所有连接使用 Tor 网络传输,需要 Orbot 主机名 端口 - 服务器 - 或者 .orion 地址 + 服务器 - 或者 .onion 地址 该端口号无效 该主机名无效 %2$d 个中的 %1$d 个账户已连接 @@ -550,7 +555,7 @@ - 广播最后打开该应用的时间 + 广播最后使用应用的时间 让你的所有联系人知道你使用畅聊的时间 隐私 主题 @@ -560,8 +565,8 @@ 黑暗主题 无法连接到 OpenKeychain 此设备不再使用 - 计算机 - 移动电话 + 电脑 + 手机 平板 浏览器 控制台 @@ -637,6 +642,7 @@ 相应的对话已关闭。 联系人已屏蔽 陌生人也通知 + 提醒来自陌生人的消息与通话 已收到陌生人的信息 屏蔽陌生人 屏蔽整个域名 @@ -735,9 +741,14 @@ 连接问题 此通知类别用于显示一旦帐户连接出现问题的通知。 消息 + 通话 消息 + 来电 + 正在进行的通话 无声消息 此通知组用于显示不应触发任何声音的通知。 例如,当在另一个设备上激活时(宽限期)。 + 消息通知设置 + 来电通知设置 重要性,声音,振动 视频压缩 查看媒体文件 @@ -773,7 +784,7 @@ 确定放弃注册? - 正在验证...... + 正在验证..... 请求短信... 验证码错误。 验证码已失效 @@ -830,7 +841,7 @@ 允许任何成员修改主题 允许任何成员邀请其他人 允许任何成员修改主题 - 拥有者可修改话题 + 拥有者可修改主题 管理员可修改主题 所有者可以邀请其他人 允许任何成员邀请其他人 @@ -845,7 +856,7 @@ 发现群聊 搜索群聊 可能侵犯隐私! - search.jabber.network。

的第三方服务。使用此功能会将您的IP地址和搜索字词传输到该服务。 有关更多信息,请参见其Privacy Policy。]]>
+ search.jabber.network。

的第三方服务。使用此功能会将您的IP地址和搜索字词传输到该服务。 有关更多信息,请参见其Privacy Policy。]]>
我已有账户 添加已有账户 注册新账户 @@ -866,10 +877,38 @@ jabber.network 本地服务器 大多数用户应该选择“ jabber.network”以从整个XMPP生态系统中获得更好的建议。 - 频道发现方法 + 群聊发现方法 备份 关于 请启用一个帐户 + 拨打电话 + 来电 + 视频来电 + 正在连接 + 已连接 + 正在接受通话 + 正在结束通话 + 接电话 + 忽略 + 正在确定设备位置 + 正在响铃 + 忙碌 + 无法接通来电 + 撤销的通话 + 程序错误 + 挂断 + 正在进行的通话 + 正在进行的视频通话 + 禁用Tor以拨打电话 + 来电 + 来电%s + 去电 + 去电%s + 未接电话 + 语音通话 + 视频通话 + 麦克风不可用 + 在同一时间只能打一通电话 查看%1$d成员 diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml index 6d812e219..5f5d848f7 100644 --- a/src/main/res/values/attrs.xml +++ b/src/main/res/values/attrs.xml @@ -91,6 +91,8 @@ + + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 769610860..12089cc31 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -917,6 +917,7 @@ Video call Your microphone is unavailable You can only have one call at a time. + Return to ongoing call View %1$d Participant View %1$d Participants diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index 316980c19..9e7a0bf4c 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -99,6 +99,8 @@ @drawable/ic_attach_file_white_24dp @drawable/ic_lock_open_white_24dp @drawable/ic_call_white_24dp + @drawable/ic_phone_in_talk_white_24dp + @drawable/ic_phone_in_talk_black_18dp @drawable/ic_delete_black_24dp @drawable/ic_search_white_24dp @drawable/ic_lock_open_white_24dp @@ -219,6 +221,8 @@ @drawable/ic_attach_file_white_24dp @drawable/ic_lock_open_white_24dp @drawable/ic_call_white_24dp + @drawable/ic_phone_in_talk_white_24dp + @drawable/ic_phone_in_talk_white_18dp @drawable/ic_delete_white_24dp @drawable/ic_search_white_24dp @drawable/ic_lock_open_white_24dp diff --git a/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java b/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java index e99de1b14..8469225b0 100644 --- a/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java +++ b/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java @@ -75,34 +75,6 @@ public class PushManagementService { } }); } - - void registerPushTokenOnServer(final Conversation conversation) { - Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": room "+conversation.getJid().asBareJid()+" has push support"); - retrieveFcmInstanceToken(token -> { - final Jid muc = conversation.getJid().asBareJid(); - final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService); - final IqPacket packet = mXmppConnectionService.getIqGenerator().pushTokenToAppServer(getAppServer(), token, androidId, muc); - packet.setTo(muc); - mXmppConnectionService.sendIqPacket(conversation.getAccount(), packet, (a, response) -> { - final Data data = findResponseData(response); - if (response.getType() == IqPacket.TYPE.RESULT && data != null) { - try { - final String node = data.getValue("node"); - final String secret = data.getValue("secret"); - final Jid jid = Jid.of(data.getValue("jid")); - if (node != null && secret != null) { - enablePushOnServer(conversation, jid, node, secret); - } - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } - } else { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": invalid response from app server"); - } - }); - }); - } - private void enablePushOnServer(final Account account, final Jid appServer, final String node, final String secret) { final IqPacket enable = mXmppConnectionService.getIqGenerator().enablePush(appServer, node, secret); mXmppConnectionService.sendIqPacket(account, enable, (a, p) -> { @@ -114,38 +86,6 @@ public class PushManagementService { }); } - private void enablePushOnServer(final Conversation conversation, final Jid appServer, final String node, final String secret) { - final Jid muc = conversation.getJid().asBareJid(); - final IqPacket enable = mXmppConnectionService.getIqGenerator().enablePush(appServer, node, secret); - enable.setTo(muc); - mXmppConnectionService.sendIqPacket(conversation.getAccount(), enable, (a, p) -> { - if (p.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": successfully enabled push on " + muc); - if (conversation.setAttribute(Conversation.ATTRIBUTE_ALWAYS_NOTIFY, node)) { - mXmppConnectionService.updateConversation(conversation); - } - } else if (p.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": enabling push on " + muc + " failed"); - } - }); - } - - public void disablePushOnServer(final Conversation conversation) { - final Jid muc = conversation.getJid().asBareJid(); - final String node = conversation.getAttribute(Conversation.ATTRIBUTE_PUSH_NODE); - if (node != null) { - final IqPacket disable = mXmppConnectionService.getIqGenerator().disablePush(getAppServer(), node); - disable.setTo(muc); - mXmppConnectionService.sendIqPacket(conversation.getAccount(), disable, (account, response) -> { - if (response.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to disable push for room "+muc); - } - }); - } else { - Log.d(Config.LOGTAG,conversation.getAccount().getJid().asBareJid()+": room "+muc+" has no stored node. unable to disable push"); - } - } - private void retrieveFcmInstanceToken(final OnGcmInstanceTokenRetrieved instanceTokenRetrieved) { final FirebaseInstanceId firebaseInstanceId; try {