Merge tag '2.8.1' into develop

This commit is contained in:
genofire 2020-05-01 11:46:27 +02:00
commit 21da82885e
43 changed files with 1598 additions and 1442 deletions

View File

@ -1,5 +1,9 @@
# Changelog # 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 ### Version 2.8.0
* Audio/Video calls (Requires server support in form of STUN and TURN servers discoverable via XEP-0215) * Audio/Video calls (Requires server support in form of STUN and TURN servers discoverable via XEP-0215)

View File

@ -96,8 +96,8 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 25 targetSdkVersion 25
versionCode 379 versionCode 381
versionName "2.8.0" versionName "2.8.1"
archivesBaseName += "-$versionName" archivesBaseName += "-$versionName"
applicationId "eu.sum7.conversations" applicationId "eu.sum7.conversations"
resValue "string", "applicationId", applicationId resValue "string", "applicationId", applicationId

View File

@ -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 <a href="http://conversations.im/omemo/">OMEMO</a> or <a href="http://openpgp.org/about/">OpenPGP</a>
* 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 XEPs. 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.

View File

@ -0,0 +1,2 @@
• Audible feedback (dialing, call started, call ended) for voice calls.
• Fixed issue with retrying failed video call

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pick_a_server">Triï el seu proveïdor de XMPP
</string>
</resources>

View File

@ -15,18 +15,10 @@ public class PushManagementService {
//stub implementation. only affects playstore flavor //stub implementation. only affects playstore flavor
} }
void registerPushTokenOnServer(Conversation conversation) {
//stub implementation. only affects playstore flavor
}
void unregisterChannel(Account account, String hash) { void unregisterChannel(Account account, String hash) {
//stub implementation. only affects playstore flavor //stub implementation. only affects playstore flavor
} }
void disablePushOnServer(Conversation conversation) {
//stub implementation. only affects playstore flavor
}
public boolean available(Account account) { public boolean available(Account account) {
return false; return false;
} }

View File

@ -51,16 +51,16 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify"; public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
public static final String ATTRIBUTE_PUSH_NODE = "push_node"; 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_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_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 = "next_message";
private static final String ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP = "next_message_timestamp"; 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_CRYPTO_TARGETS = "crypto_targets";
private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption"; private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
private static final String ATTRIBUTE_CORRECTING_MESSAGE = "correcting_message"; 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<Message> messages = new ArrayList<>(); protected final ArrayList<Message> messages = new ArrayList<>();
public AtomicBoolean messagesLoaded = new AtomicBoolean(true); public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
protected Account account = null; protected Account account = null;
@ -118,6 +118,33 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
cursor.getString(cursor.getColumnIndex(ATTRIBUTES))); cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
} }
public static Message getLatestMarkableMessage(final List<Message> 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() { public boolean hasMessagesLeftOnServer() {
return messagesLeftOnServer; return messagesLeftOnServer;
} }
@ -351,6 +378,17 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return null; 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) { public boolean hasMessageWithCounterpart(Jid counterpart) {
synchronized (this.messages) { synchronized (this.messages) {
for (Message message : this.messages) { for (Message message : this.messages) {
@ -484,18 +522,6 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return unread; return unread;
} }
public static Message getLatestMarkableMessage(final List<Message> 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() { public Message getLatestMessage() {
synchronized (this.messages) { synchronized (this.messages) {
if (this.messages.size() == 0) { if (this.messages.size() == 0) {
@ -509,7 +535,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
} }
} }
public @NonNull CharSequence getName() { public @NonNull
CharSequence getName() {
if (getMode() == MODE_MULTI) { if (getMode() == MODE_MULTI) {
final String roomName = getMucOptions().getName(); final String roomName = getMucOptions().getName();
final String subject = getMucOptions().getSubject(); final String subject = getMucOptions().getSubject();
@ -648,21 +675,6 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
} }
} }
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) { public boolean setNextEncryption(int encryption) {
return this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, encryption); return this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, encryption);
} }

View File

@ -114,10 +114,6 @@ public class MucOptions {
return MessageArchiveService.Version.has(getFeatures()); return MessageArchiveService.Version.has(getFeatures());
} }
public boolean push() {
return getFeatures().contains(Namespace.PUSH);
}
public boolean updateConfiguration(ServiceDiscoveryResult serviceDiscoveryResult) { public boolean updateConfiguration(ServiceDiscoveryResult serviceDiscoveryResult) {
this.serviceDiscoveryResult = serviceDiscoveryResult; this.serviceDiscoveryResult = serviceDiscoveryResult;
String name; String name;

View File

@ -12,6 +12,7 @@ import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.http.P1S3UrlStreamHandler; import eu.siacs.conversations.http.P1S3UrlStreamHandler;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
@ -160,15 +161,23 @@ public class MessageGenerator extends AbstractGenerator {
return packet; return packet;
} }
public MessagePacket confirm(final Account account, final Jid to, final String id, final Jid counterpart, final boolean groupChat) { public MessagePacket confirm(final Message message) {
MessagePacket packet = new MessagePacket(); 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.setType(groupChat ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT);
packet.setTo(groupChat ? to.asBareJid() : to); packet.setTo(groupChat ? to.asBareJid() : to);
packet.setFrom(account.getJid()); final Element displayed = packet.addChild("displayed", "urn:xmpp:chat-markers:0");
Element displayed = packet.addChild("displayed", "urn:xmpp:chat-markers:0"); if (groupChat) {
displayed.setAttribute("id", id); final String stanzaId = message.getServerMsgId();
if (groupChat && counterpart != null) { if (stanzaId != null) {
displayed.setAttribute("sender", counterpart.toString()); 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"); packet.addChild("store", "urn:xmpp:hints");
return packet; return packet;

View File

@ -387,29 +387,6 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet); response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet);
} }
mXmppConnectionService.sendIqPacket(account, response, null); 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 { } else {
if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) { if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);

View File

@ -927,9 +927,17 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
activateGracePeriod(account); activateGracePeriod(account);
} }
} else if (isTypeGroupChat) { } else if (isTypeGroupChat) {
Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid()); final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
if (conversation != null && id != null && sender != null) { final Message message;
Message message = conversation.findMessageWithRemoteId(id, sender); if (conversation != null && id != null) {
if (sender != null) {
message = conversation.findMessageWithRemoteId(id, sender);
} else {
message = conversation.findMessageWithServerMsgId(id);
}
} else {
message = null;
}
if (message != null) { if (message != null) {
final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart); final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
final Jid trueJid = getTrueCounterpart((query != null && query.safeToExtractTrueCounterpart()) ? mucUserElement : null, fallback); final Jid trueJid = getTrueCounterpart((query != null && query.safeToExtractTrueCounterpart()) ? mucUserElement : null, fallback);
@ -945,7 +953,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
} }
} }
} }
}
} else { } else {
final Message displayedMessage = mXmppConnectionService.markMessage(account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED); final Message displayedMessage = mXmppConnectionService.markMessage(account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED);
Message message = displayedMessage == null ? null : displayedMessage.prev(); Message message = displayedMessage == null ? null : displayedMessage.prev();

View File

@ -610,7 +610,6 @@ public class XmppConnectionService extends Service {
toggleForegroundService(true); toggleForegroundService(true);
} }
String pushedAccountHash = null; String pushedAccountHash = null;
String pushedChannelHash = null;
boolean interactive = false; boolean interactive = false;
if (action != null) { if (action != null) {
final String uuid = intent.getStringExtra("uuid"); final String uuid = intent.getStringExtra("uuid");
@ -735,7 +734,6 @@ public class XmppConnectionService extends Service {
break; break;
case ACTION_FCM_MESSAGE_RECEIVED: case ACTION_FCM_MESSAGE_RECEIVED:
pushedAccountHash = intent.getStringExtra("account"); pushedAccountHash = intent.getStringExtra("account");
pushedChannelHash = intent.getStringExtra("channel");
Log.d(Config.LOGTAG, "push message arrived in service. account=" + pushedAccountHash); Log.d(Config.LOGTAG, "push message arrived in service. account=" + pushedAccountHash);
break; break;
case Intent.ACTION_SEND: case Intent.ACTION_SEND:
@ -758,9 +756,6 @@ public class XmppConnectionService extends Service {
"ui".equals(action), "ui".equals(action),
pushWasMeantForThisAccount, pushWasMeantForThisAccount,
pingCandidates); pingCandidates);
if (pushWasMeantForThisAccount && pushedChannelHash != null) {
checkMucStillJoined(account, pushedAccountHash, androidId);
}
} }
if (pingNow) { if (pingNow) {
for (Account account : pingCandidates) { for (Account account : pingCandidates) {
@ -853,20 +848,6 @@ public class XmppConnectionService extends Service {
return pingNow; 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() { public void reinitializeMuclumbusService() {
mChannelDiscoveryService.initializeMuclumbusService(); mChannelDiscoveryService.initializeMuclumbusService();
} }
@ -2156,10 +2137,6 @@ public class XmppConnectionService extends Service {
} }
} }
} }
if (conversation.getMucOptions().push()) {
disableDirectMucPush(conversation);
mPushManagementService.disablePushOnServer(conversation);
}
leaveMuc(conversation); leaveMuc(conversation);
} else { } else {
if (conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { 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) { synchronized (account.inProgressConferenceJoins) {
account.inProgressConferenceJoins.remove(conversation); account.inProgressConferenceJoins.remove(conversation);
sendUnsentMessages(conversation); sendUnsentMessages(conversation);
@ -2805,40 +2779,6 @@ public class XmppConnectionService extends Service {
updateConversationUi(); 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) { private void fetchConferenceMembers(final Conversation conversation) {
final Account account = conversation.getAccount(); final Account account = conversation.getAccount();
final AxolotlService axolotlService = account.getAxolotlService(); final AxolotlService axolotlService = account.getAxolotlService();
@ -4122,6 +4062,7 @@ public class XmppConnectionService extends Service {
} }
}; };
mDatabaseWriterExecutor.execute(runnable); mDatabaseWriterExecutor.execute(runnable);
updateConversationUi();
updateUnreadCountBadge(); updateUnreadCountBadge();
return readMessages; return readMessages;
} else { } else {
@ -4154,11 +4095,9 @@ public class XmppConnectionService extends Service {
&& (markable.trusted() || isPrivateAndNonAnonymousMuc) && (markable.trusted() || isPrivateAndNonAnonymousMuc)
&& markable.getRemoteMsgId() != null) { && markable.getRemoteMsgId() != null) {
Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": sending read marker to " + markable.getCounterpart().toString()); Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": sending read marker to " + markable.getCounterpart().toString());
Account account = conversation.getAccount(); final Account account = conversation.getAccount();
final Jid to = markable.getCounterpart(); final MessagePacket packet = mMessageGenerator.confirm(markable);
final boolean groupChat = conversation.getMode() == Conversation.MODE_MULTI; this.sendMessagePacket(account, packet);
MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId(), markable.getCounterpart(), groupChat);
this.sendMessagePacket(conversation.getAccount(), packet);
} }
} }

View File

@ -52,6 +52,8 @@ import android.widget.PopupMenu;
import android.widget.TextView.OnEditorActionListener; import android.widget.TextView.OnEditorActionListener;
import android.widget.Toast; import android.widget.Toast;
import com.google.common.base.Optional;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -117,6 +119,7 @@ import eu.siacs.conversations.utils.TimeframeUtils;
import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.chatstate.ChatState; 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.JingleFileTransferConnection;
import eu.siacs.conversations.xmpp.jingle.RtpCapability; import eu.siacs.conversations.xmpp.jingle.RtpCapability;
import rocks.xmpp.addr.Jid; 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 menuMute = menu.findItem(R.id.action_mute);
final MenuItem menuUnmute = menu.findItem(R.id.action_unmute); final MenuItem menuUnmute = menu.findItem(R.id.action_unmute);
final MenuItem menuCall = menu.findItem(R.id.action_call); 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); 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()); menuInviteContact.setVisible(conversation.getMucOptions().canInvite());
menuMucDetails.setTitle(conversation.getMucOptions().isPrivateAndNonAnonymous() ? R.string.action_muc_details : R.string.channel_details); menuMucDetails.setTitle(conversation.getMucOptions().isPrivateAndNonAnonymous() ? R.string.action_muc_details : R.string.channel_details);
menuCall.setVisible(false); menuCall.setVisible(false);
menuOngoingCall.setVisible(false);
} else { } else {
final Optional<AbstractJingleConnection.Id> 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()); final RtpCapability.Capability rtpCapability = RtpCapability.check(conversation.getContact());
menuCall.setVisible(rtpCapability != RtpCapability.Capability.NONE); menuCall.setVisible(rtpCapability != RtpCapability.Capability.NONE);
menuVideoCall.setVisible(rtpCapability == RtpCapability.Capability.VIDEO); menuVideoCall.setVisible(rtpCapability == RtpCapability.Capability.VIDEO);
}
menuContactDetails.setVisible(!this.conversation.withSelf()); menuContactDetails.setVisible(!this.conversation.withSelf());
menuMucDetails.setVisible(false); menuMucDetails.setVisible(false);
final XmppConnectionService service = activity.xmppConnectionService; final XmppConnectionService service = activity.xmppConnectionService;
@ -1245,12 +1257,28 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
case R.id.action_video_call: case R.id.action_video_call:
checkPermissionAndTriggerVideoCall(); checkPermissionAndTriggerVideoCall();
break; break;
case R.id.action_ongoing_call:
returnToOngoingCall();
break;
default: default:
break; break;
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
private void returnToOngoingCall() {
final Optional<AbstractJingleConnection.Id> 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() { private void checkPermissionAndTriggerAudioCall() {
if (activity.mUseTor || conversation.getAccount().isOnion()) { if (activity.mUseTor || conversation.getAccount().isOnion()) {
Toast.makeText(activity, R.string.disable_tor_to_make_call, Toast.LENGTH_SHORT).show(); Toast.makeText(activity, R.string.disable_tor_to_make_call, Toast.LENGTH_SHORT).show();

View File

@ -21,6 +21,7 @@ import android.view.WindowManager;
import android.widget.Toast; import android.widget.Toast;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
@ -29,6 +30,7 @@ import org.webrtc.VideoTrack;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -315,7 +317,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
if (remoteVideo.isPresent()) { if (remoteVideo.isPresent()) {
remoteVideo.get().removeSink(binding.remoteVideo); remoteVideo.get().removeSink(binding.remoteVideo);
} }
final Optional<VideoTrack> localVideo = jingleRtpConnection.geLocalVideoTrack(); final Optional<VideoTrack> localVideo = jingleRtpConnection.getLocalVideoTrack();
if (localVideo.isPresent()) { if (localVideo.isPresent()) {
localVideo.get().removeSink(binding.localVideo); localVideo.get().removeSink(binding.localVideo);
} }
@ -385,6 +387,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
finish(); finish();
return true; return true;
} }
final Set<Media> media = getMedia();
if (currentState == RtpEndUserState.INCOMING_CALL) { if (currentState == RtpEndUserState.INCOMING_CALL) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} }
@ -393,16 +396,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
} }
binding.with.setText(getWith().getDisplayName()); binding.with.setText(getWith().getDisplayName());
updateVideoViews(currentState); updateVideoViews(currentState);
updateStateDisplay(currentState); updateStateDisplay(currentState, media);
updateButtonConfiguration(currentState); updateButtonConfiguration(currentState, media);
updateProfilePicture(currentState); updateProfilePicture(currentState);
return false; return false;
} }
private void reInitializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) { private void reInitializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) {
runOnUiThread(() -> { runOnUiThread(() -> initializeActivityWithRunningRtpSession(account, with, sessionId));
initializeActivityWithRunningRtpSession(account, with, sessionId);
});
final Intent intent = new Intent(Intent.ACTION_VIEW); final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString()); intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
intent.putExtra(EXTRA_WITH, with.toEscapedString()); intent.putExtra(EXTRA_WITH, with.toEscapedString());
@ -421,9 +422,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
} }
private void updateStateDisplay(final RtpEndUserState state) { private void updateStateDisplay(final RtpEndUserState state) {
updateStateDisplay(state, Collections.emptySet());
}
private void updateStateDisplay(final RtpEndUserState state, final Set<Media> media) {
switch (state) { switch (state) {
case INCOMING_CALL: 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); setTitle(R.string.rtp_state_incoming_video_call);
} else { } else {
setTitle(R.string.rtp_state_incoming_call); setTitle(R.string.rtp_state_incoming_call);
@ -467,11 +473,19 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
} }
private void updateProfilePicture(final RtpEndUserState state) { 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) { if (state == RtpEndUserState.INCOMING_CALL || state == RtpEndUserState.ACCEPTING_CALL) {
final boolean show = getResources().getBoolean(R.bool.show_avatar_incoming_call); final boolean show = getResources().getBoolean(R.bool.show_avatar_incoming_call);
if (show) { if (show) {
binding.contactPhoto.setVisibility(View.VISIBLE); binding.contactPhoto.setVisibility(View.VISIBLE);
if (contact == null) {
AvatarWorkerTask.loadAvatar(getWith(), binding.contactPhoto, R.dimen.publish_avatar_size); AvatarWorkerTask.loadAvatar(getWith(), binding.contactPhoto, R.dimen.publish_avatar_size);
} else {
AvatarWorkerTask.loadAvatar(contact, binding.contactPhoto, R.dimen.publish_avatar_size);
}
} else { } else {
binding.contactPhoto.setVisibility(View.GONE); binding.contactPhoto.setVisibility(View.GONE);
} }
@ -484,8 +498,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
return requireRtpConnection().getMedia(); return requireRtpConnection().getMedia();
} }
@SuppressLint("RestrictedApi")
private void updateButtonConfiguration(final RtpEndUserState state) { private void updateButtonConfiguration(final RtpEndUserState state) {
updateButtonConfiguration(state, Collections.emptySet());
}
@SuppressLint("RestrictedApi")
private void updateButtonConfiguration(final RtpEndUserState state, final Set<Media> media) {
if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) { if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) {
this.binding.rejectCall.setVisibility(View.INVISIBLE); this.binding.rejectCall.setVisibility(View.INVISIBLE);
this.binding.endCall.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.endCall.setVisibility(View.VISIBLE);
this.binding.acceptCall.setVisibility(View.INVISIBLE); this.binding.acceptCall.setVisibility(View.INVISIBLE);
} }
updateInCallButtonConfiguration(state); updateInCallButtonConfiguration(state, media);
} }
private boolean isPictureInPicture() { private boolean isPictureInPicture() {
@ -531,13 +549,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
} }
private void updateInCallButtonConfiguration() { private void updateInCallButtonConfiguration() {
updateInCallButtonConfiguration(requireRtpConnection().getEndUserState()); updateInCallButtonConfiguration(requireRtpConnection().getEndUserState(), requireRtpConnection().getMedia());
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private void updateInCallButtonConfiguration(final RtpEndUserState state) { private void updateInCallButtonConfiguration(final RtpEndUserState state, final Set<Media> media) {
if (state == RtpEndUserState.CONNECTED && !isPictureInPicture()) { 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()); updateInCallButtonConfigurationVideo(requireRtpConnection().isVideoEnabled());
} else { } else {
final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager(); final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
@ -626,7 +645,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
private void updateVideoViews(final RtpEndUserState state) { private void updateVideoViews(final RtpEndUserState state) {
if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) { if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) {
binding.localVideo.setVisibility(View.GONE); binding.localVideo.setVisibility(View.GONE);
binding.localVideo.release();
binding.remoteVideo.setVisibility(View.GONE); binding.remoteVideo.setVisibility(View.GONE);
binding.remoteVideo.release();
binding.pipLocalMicOffIndicator.setVisibility(View.GONE); binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
if (isPictureInPicture()) { if (isPictureInPicture()) {
binding.appBarLayout.setVisibility(View.GONE); binding.appBarLayout.setVisibility(View.GONE);
@ -655,7 +676,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
binding.pipLocalMicOffIndicator.setVisibility(View.GONE); binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
return; return;
} }
final Optional<VideoTrack> localVideoTrack = requireRtpConnection().geLocalVideoTrack(); final Optional<VideoTrack> localVideoTrack = getLocalVideoTrack();
if (localVideoTrack.isPresent() && !isPictureInPicture()) { if (localVideoTrack.isPresent() && !isPictureInPicture()) {
ensureSurfaceViewRendererIsSetup(binding.localVideo); ensureSurfaceViewRendererIsSetup(binding.localVideo);
//paint local view over remote view //paint local view over remote view
@ -665,7 +686,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
} else { } else {
binding.localVideo.setVisibility(View.GONE); binding.localVideo.setVisibility(View.GONE);
} }
final Optional<VideoTrack> remoteVideoTrack = requireRtpConnection().getRemoteVideoTrack(); final Optional<VideoTrack> remoteVideoTrack = getRemoteVideoTrack();
if (remoteVideoTrack.isPresent()) { if (remoteVideoTrack.isPresent()) {
ensureSurfaceViewRendererIsSetup(binding.remoteVideo); ensureSurfaceViewRendererIsSetup(binding.remoteVideo);
remoteVideoTrack.get().addSink(binding.remoteVideo); remoteVideoTrack.get().addSink(binding.remoteVideo);
@ -688,6 +709,22 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
} }
} }
private Optional<VideoTrack> getLocalVideoTrack() {
final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
if (connection == null) {
return Optional.absent();
}
return connection.getLocalVideoTrack();
}
private Optional<VideoTrack> 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) { private void disableMicrophone(View view) {
JingleRtpConnection rtpConnection = requireRtpConnection(); JingleRtpConnection rtpConnection = requireRtpConnection();
rtpConnection.setMicrophoneEnabled(false); rtpConnection.setMicrophoneEnabled(false);
@ -762,19 +799,23 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
return; return;
} }
final AbstractJingleConnection.Id id = requireRtpConnection().getId(); final AbstractJingleConnection.Id id = requireRtpConnection().getId();
final Set<Media> media = getMedia();
final Contact contact = getWith();
if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) { if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
if (state == RtpEndUserState.ENDED) { if (state == RtpEndUserState.ENDED) {
finish(); finish();
return; return;
} }
runOnUiThread(() -> { runOnUiThread(() -> {
updateStateDisplay(state); updateStateDisplay(state, media);
updateButtonConfiguration(state); updateButtonConfiguration(state, media);
updateVideoViews(state); updateVideoViews(state);
updateProfilePicture(state); updateProfilePicture(state, contact);
}); });
if (END_CARD.contains(state)) { 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; this.rtpConnectionReference = null;
} }
} else { } else {

View File

@ -9,11 +9,14 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.google.common.base.Optional;
import java.util.List; import java.util.List;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ConversationListRowBinding; import eu.siacs.conversations.databinding.ConversationListRowBinding;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.ui.ConversationFragment; import eu.siacs.conversations.ui.ConversationFragment;
import eu.siacs.conversations.ui.XmppActivity; 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.EmojiWrapper;
import eu.siacs.conversations.utils.IrregularUnicodeDetector; import eu.siacs.conversations.utils.IrregularUnicodeDetector;
import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
import rocks.xmpp.addr.Jid; import rocks.xmpp.addr.Jid;
public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapter.ConversationViewHolder> { public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapter.ConversationViewHolder> {
@ -160,7 +164,20 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapte
} }
} }
long muted_till = conversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0);
final Optional<AbstractJingleConnection.Id> 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);
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) { if (muted_till == Long.MAX_VALUE) {
viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE);
int ic_notifications_off = activity.getThemeResource(R.attr.icon_notifications_off, R.drawable.ic_notifications_off_black_24dp); int ic_notifications_off = activity.getThemeResource(R.attr.icon_notifications_off, R.drawable.ic_notifications_off_black_24dp);
@ -176,6 +193,7 @@ public class ConversationAdapter extends RecyclerView.Adapter<ConversationAdapte
int ic_notifications_none = activity.getThemeResource(R.attr.icon_notifications_none, R.drawable.ic_notifications_none_black_24dp); 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); viewHolder.binding.notificationStatus.setImageResource(ic_notifications_none);
} }
}
long timestamp; long timestamp;
if (draft != null) { if (draft != null) {

View File

@ -62,7 +62,10 @@ public class ExceptionHelper {
PackageInfo packageInfo; PackageInfo packageInfo;
try { try {
packageInfo = pm.getPackageInfo(activity.getPackageName(), PackageManager.GET_SIGNATURES); packageInfo = pm.getPackageInfo(activity.getPackageName(), PackageManager.GET_SIGNATURES);
report.append("Version: ").append(packageInfo.versionName).append('\n'); 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'); report.append("Last Update: ").append(DATE_FORMAT.format(new Date(packageInfo.lastUpdateTime))).append('\n');
Signature[] signatures = packageInfo.signatures; Signature[] signatures = packageInfo.signatures;
if (signatures != null && signatures.length >= 1) { if (signatures != null && signatures.length >= 1) {

View File

@ -1,19 +1,16 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import android.os.SystemClock;
import android.util.Base64; import android.util.Base64;
import android.util.Log; import android.util.Log;
import com.google.common.base.Function;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.cache.Cache; import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Collections2; import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import org.checkerframework.checker.nullness.compatqual.NullableDecl;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Collection; import java.util.Collection;
@ -53,9 +50,10 @@ import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
import rocks.xmpp.addr.Jid; import rocks.xmpp.addr.Jid;
public class JingleConnectionManager extends AbstractConnectionManager { 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<RtpSessionProposal, DeviceDiscoveryState> rtpSessionProposals = new HashMap<>(); private final HashMap<RtpSessionProposal, DeviceDiscoveryState> rtpSessionProposals = new HashMap<>();
private final Map<AbstractJingleConnection.Id, AbstractJingleConnection> connections = new ConcurrentHashMap<>(); private final ConcurrentHashMap<AbstractJingleConnection.Id, AbstractJingleConnection> connections = new ConcurrentHashMap<>();
private final Cache<PersistableSessionId, JingleRtpConnection.State> endedSessions = CacheBuilder.newBuilder() private final Cache<PersistableSessionId, JingleRtpConnection.State> endedSessions = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES) .expireAfterWrite(30, TimeUnit.MINUTES)
@ -108,6 +106,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return; return;
} }
connections.put(id, connection); connections.put(id, connection);
mXmppConnectionService.updateConversationUi();
connection.deliverPacket(packet); connection.deliverPacket(packet);
} else { } else {
Log.d(Config.LOGTAG, "unable to route jingle packet: " + packet); 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) { 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) { 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) { synchronized (rtpSessionProposals) {
if (rtpSessionProposals.remove(proposal) != null) { if (rtpSessionProposals.remove(proposal) != null) {
writeLogMissedOutgoing(account, proposal.with, proposal.sessionId, serverMsgId, timestamp); 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); mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, proposal.with, proposal.sessionId, RtpEndUserState.DECLINED_OR_BUSY);
} else { } else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver reject"); 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); connection.init(message);
} }
public Optional<AbstractJingleConnection.Id> getOngoingRtpConnection(final Contact contact) {
for (final Map.Entry<AbstractJingleConnection.Id, AbstractJingleConnection> 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) { void finishConnection(final AbstractJingleConnection connection) {
this.connections.remove(connection.getId()); this.connections.remove(connection.getId());
} }
@ -413,6 +425,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
} }
} }
if (matchingProposal != null) { if (matchingProposal != null) {
toneManager.transition(true, RtpEndUserState.ENDED);
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retracting rtp session proposal with " + with); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retracting rtp session proposal with " + with);
this.rtpSessionProposals.remove(matchingProposal); this.rtpSessionProposals.remove(matchingProposal);
final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(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)) { if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
final DeviceDiscoveryState preexistingState = entry.getValue(); final DeviceDiscoveryState preexistingState = entry.getValue();
if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) { if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) {
final RtpEndUserState endUserState = preexistingState.toEndUserState();
toneManager.transition(true, endUserState);
mXmppConnectionService.notifyJingleRtpConnectionUpdate( mXmppConnectionService.notifyJingleRtpConnectionUpdate(
account, account,
with, with,
proposal.sessionId, proposal.sessionId,
preexistingState.toEndUserState() endUserState
); );
return; return;
} }
@ -519,7 +534,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return; return;
} }
this.rtpSessionProposals.put(sessionProposal, target); 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); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": flagging session " + sessionId + " as " + target);
} }
} }

View File

@ -6,6 +6,7 @@ import android.util.Log;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.Collections2; import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
@ -48,15 +49,12 @@ import rocks.xmpp.addr.Jid;
public class JingleRtpConnection extends AbstractJingleConnection implements WebRTCWrapper.EventCallback { public class JingleRtpConnection extends AbstractJingleConnection implements WebRTCWrapper.EventCallback {
private static final long BUSY_TIME_OUT = 30;
public static final List<State> STATES_SHOWING_ONGOING_CALL = Arrays.asList( public static final List<State> STATES_SHOWING_ONGOING_CALL = Arrays.asList(
State.PROCEED, State.PROCEED,
State.SESSION_INITIALIZED,
State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_INITIALIZED_PRE_APPROVED,
State.SESSION_ACCEPTED State.SESSION_ACCEPTED
); );
private static final long BUSY_TIME_OUT = 30;
private static final List<State> TERMINATED = Arrays.asList( private static final List<State> TERMINATED = Arrays.asList(
State.TERMINATED_SUCCESS, State.TERMINATED_SUCCESS,
State.TERMINATED_DECLINED_OR_BUSY, State.TERMINATED_DECLINED_OR_BUSY,
@ -236,7 +234,20 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
if (identificationTags.size() == 0) { 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"); 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<String, RtpContentMap.DescriptionTransport> content : contentMap.contents.entrySet()) { 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<String> identificationTags, final Set<Map.Entry<String, RtpContentMap.DescriptionTransport>> contents) {
for (final Map.Entry<String, RtpContentMap.DescriptionTransport> content : contents) {
final String ufrag = content.getValue().transport.getAttribute("ufrag"); final String ufrag = content.getValue().transport.getAttribute("ufrag");
for (final IceUdpTransportInfo.Candidate candidate : content.getValue().transport.getCandidates()) { for (final IceUdpTransportInfo.Candidate candidate : content.getValue().transport.getCandidates()) {
final String sdp; final String sdp;
@ -258,10 +269,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
} }
} }
} else {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received transport info while in state=" + this.state);
terminateWithOutOfOrder(jinglePacket);
}
} }
private void receiveSessionInitiate(final JinglePacket jinglePacket) { private void receiveSessionInitiate(final JinglePacket jinglePacket) {
@ -276,9 +283,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
contentMap.requireContentDescriptions(); contentMap.requireContentDescriptions();
contentMap.requireDTLSFingerprint(); contentMap.requireDTLSFingerprint();
} catch (final RuntimeException e) { } catch (final RuntimeException e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e));
respondOk(jinglePacket); respondOk(jinglePacket);
sendSessionTerminate(Reason.of(e), e.getMessage()); sendSessionTerminate(Reason.of(e), e.getMessage());
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e);
return; return;
} }
Log.d(Config.LOGTAG, "processing session-init with " + contentMap.contents.size() + " contents"); 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)) { if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) {
respondOk(jinglePacket); respondOk(jinglePacket);
final List<String> identificationTags = contentMap.group == null ? Collections.emptyList() : contentMap.group.getIdentificationTags();
receiveCandidates(identificationTags, contentMap.contents.entrySet());
if (target == State.SESSION_INITIALIZED_PRE_APPROVED) { if (target == State.SESSION_INITIALIZED_PRE_APPROVED) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate"); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate");
sendSessionAccept(); sendSessionAccept();
@ -346,6 +355,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
if (transition(State.SESSION_ACCEPTED)) { if (transition(State.SESSION_ACCEPTED)) {
respondOk(jinglePacket); respondOk(jinglePacket);
receiveSessionAccept(contentMap); receiveSessionAccept(contentMap);
final List<String> identificationTags = contentMap.group == null ? Collections.emptyList() : contentMap.group.getIdentificationTags();
receiveCandidates(identificationTags, contentMap.contents.entrySet());
} else { } else {
Log.d(Config.LOGTAG, String.format("%s: received session-accept while in state %s", id.account.getJid().asBareJid(), state)); Log.d(Config.LOGTAG, String.format("%s: received session-accept while in state %s", id.account.getJid().asBareJid(), state));
respondOk(jinglePacket); respondOk(jinglePacket);
@ -369,8 +380,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
); );
try { try {
this.webRTCWrapper.setRemoteDescription(answer).get(); this.webRTCWrapper.setRemoteDescription(answer).get();
} catch (Exception e) { } catch (final Exception e) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", e); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", Throwables.getRootCause(e));
webRTCWrapper.close(); webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION); sendSessionTerminate(Reason.FAILED_APPLICATION);
} }
@ -419,10 +430,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description);
final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription);
sendSessionAccept(respondingRtpContentMap); sendSessionAccept(respondingRtpContentMap);
this.webRTCWrapper.setLocalDescription(webRTCSessionDescription); this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get();
} catch (Exception e) { } catch (final Exception e) {
Log.d(Config.LOGTAG, "unable to send session accept", 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); sendSessionInitiate(rtpContentMap, targetState);
this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get();
} catch (final Exception e) { } 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(); webRTCWrapper.close();
if (isInState(targetState)) { if (isInState(targetState)) {
sendSessionTerminate(Reason.FAILED_APPLICATION); sendSessionTerminate(Reason.FAILED_APPLICATION);
@ -657,9 +669,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
private void sendSessionTerminate(final Reason reason, final String text) { private void sendSessionTerminate(final Reason reason, final String text) {
final State previous = this.state;
final State target = reasonToState(reason); final State target = reasonToState(reason);
transitionOrThrow(target); transitionOrThrow(target);
if (previous != State.NULL) {
writeLogMessage(target); writeLogMessage(target);
}
final JinglePacket jinglePacket = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); final JinglePacket jinglePacket = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
jinglePacket.setReason(reason, text); jinglePacket.setReason(reason, text);
Log.d(Config.LOGTAG, jinglePacket.toString()); Log.d(Config.LOGTAG, jinglePacket.toString());
@ -672,7 +687,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
try { try {
final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap;
transportInfo = rtpContentMap.transportInfo(contentName, candidate); 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); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to prepare transport-info from candidate for content=" + contentName);
return; return;
} }
@ -788,15 +803,18 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} }
public Set<Media> getMedia() { public Set<Media> getMedia() {
if (isInState(State.NULL)) { final State current = getState();
if (current == State.NULL) {
throw new IllegalStateException("RTP connection has not been initialized yet"); 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"); return Preconditions.checkNotNull(this.proposedMedia, "RTP connection has not been initialized properly");
} }
final RtpContentMap initiatorContentMap = initiatorRtpContentMap; final RtpContentMap initiatorContentMap = initiatorRtpContentMap;
if (initiatorContentMap != null) { if (initiatorContentMap != null) {
return initiatorContentMap.getMedia(); return initiatorContentMap.getMedia();
} else if (isTerminated()) {
return Collections.emptySet(); //we might fail before we ever got a chance to set media
} else { } else {
return Preconditions.checkNotNull(this.proposedMedia, "RTP connection has not been initialized properly"); 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() { 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() { private void updateOngoingCallNotification() {
@ -1140,7 +1160,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
return TERMINATED.contains(this.state); return TERMINATED.contains(this.state);
} }
public Optional<VideoTrack> geLocalVideoTrack() { public Optional<VideoTrack> getLocalVideoTrack() {
return webRTCWrapper.getLocalVideoTrack(); return webRTCWrapper.getLocalVideoTrack();
} }

View File

@ -59,7 +59,7 @@ public class RtpContentMap {
})); }));
} }
public void requireContentDescriptions() { void requireContentDescriptions() {
if (this.contents.size() == 0) { if (this.contents.size() == 0) {
throw new IllegalStateException("No contents available"); throw new IllegalStateException("No contents available");
} }
@ -70,7 +70,7 @@ public class RtpContentMap {
} }
} }
public void requireDTLSFingerprint() { void requireDTLSFingerprint() {
if (this.contents.size() == 0) { if (this.contents.size() == 0) {
throw new IllegalStateException("No contents available"); throw new IllegalStateException("No contents available");
} }
@ -80,10 +80,13 @@ public class RtpContentMap {
if (fingerprint == null || Strings.isNullOrEmpty(fingerprint.getContent()) || Strings.isNullOrEmpty(fingerprint.getHash())) { 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())); 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); final JinglePacket jinglePacket = new JinglePacket(action, sessionId);
if (this.group != null) { if (this.group != null) {
jinglePacket.addGroup(this.group); jinglePacket.addGroup(this.group);
@ -99,7 +102,7 @@ public class RtpContentMap {
return jinglePacket; 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 RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName);
final IceUdpTransportInfo transportInfo = descriptionTransport == null ? null : descriptionTransport.transport; final IceUdpTransportInfo transportInfo = descriptionTransport == null ? null : descriptionTransport.transport;
if (transportInfo == null) { if (transportInfo == null) {
@ -115,7 +118,7 @@ public class RtpContentMap {
public final RtpDescription description; public final RtpDescription description;
public final IceUdpTransportInfo transport; public final IceUdpTransportInfo transport;
public DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) { DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) {
this.description = description; this.description = description;
this.transport = transport; this.transport = transport;
} }

View File

@ -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> media) {
transition(of(isInitiator, state, media));
}
private static ToneState of(final boolean isInitiator, final RtpEndUserState state, final Set<Media> 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
}
}

View File

@ -158,7 +158,7 @@ public class WebRTCWrapper {
private EglBase eglBase = null; private EglBase eglBase = null;
private CapturerChoice capturerChoice; private CapturerChoice capturerChoice;
public WebRTCWrapper(final EventCallback eventCallback) { WebRTCWrapper(final EventCallback eventCallback) {
this.eventCallback = eventCallback; this.eventCallback = eventCallback;
} }
@ -175,7 +175,7 @@ public class WebRTCWrapper {
}); });
} }
public void initializePeerConnection(final Set<Media> media, final List<PeerConnection.IceServer> iceServers) throws InitializationException { synchronized void initializePeerConnection(final Set<Media> media, final List<PeerConnection.IceServer> iceServers) throws InitializationException {
Preconditions.checkState(this.eglBase != null); Preconditions.checkState(this.eglBase != null);
Preconditions.checkNotNull(media); Preconditions.checkNotNull(media);
Preconditions.checkArgument(media.size() > 0, "media can not be empty when initializing peer connection"); Preconditions.checkArgument(media.size() > 0, "media can not be empty when initializing peer connection");
@ -224,13 +224,13 @@ public class WebRTCWrapper {
this.peerConnection = peerConnection; this.peerConnection = peerConnection;
} }
public void close() { synchronized void close() {
final PeerConnection peerConnection = this.peerConnection; final PeerConnection peerConnection = this.peerConnection;
final CapturerChoice capturerChoice = this.capturerChoice; final CapturerChoice capturerChoice = this.capturerChoice;
final AppRTCAudioManager audioManager = this.appRTCAudioManager; final AppRTCAudioManager audioManager = this.appRTCAudioManager;
final EglBase eglBase = this.eglBase; final EglBase eglBase = this.eglBase;
if (peerConnection != null) { if (peerConnection != null) {
peerConnection.dispose(); dispose(peerConnection);
this.peerConnection = null; this.peerConnection = null;
} }
if (audioManager != 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 if (this.peerConnection != null
|| this.eglBase != null || this.eglBase != null
|| this.localVideoTrack != null || this.localVideoTrack != null
@ -278,7 +286,7 @@ public class WebRTCWrapper {
audioTrack.setEnabled(enabled); audioTrack.setEnabled(enabled);
} }
public boolean isVideoEnabled() { boolean isVideoEnabled() {
final VideoTrack videoTrack = this.localVideoTrack; final VideoTrack videoTrack = this.localVideoTrack;
if (videoTrack == null) { if (videoTrack == null) {
throw new IllegalStateException("Local video track does not exist"); throw new IllegalStateException("Local video track does not exist");
@ -286,7 +294,7 @@ public class WebRTCWrapper {
return videoTrack.enabled(); return videoTrack.enabled();
} }
public void setVideoEnabled(final boolean enabled) { void setVideoEnabled(final boolean enabled) {
final VideoTrack videoTrack = this.localVideoTrack; final VideoTrack videoTrack = this.localVideoTrack;
if (videoTrack == null) { if (videoTrack == null) {
throw new IllegalStateException("Local video track does not exist"); throw new IllegalStateException("Local video track does not exist");
@ -294,7 +302,7 @@ public class WebRTCWrapper {
videoTrack.setEnabled(enabled); videoTrack.setEnabled(enabled);
} }
public ListenableFuture<SessionDescription> createOffer() { ListenableFuture<SessionDescription> createOffer() {
return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> {
final SettableFuture<SessionDescription> future = SettableFuture.create(); final SettableFuture<SessionDescription> future = SettableFuture.create();
peerConnection.createOffer(new CreateSdpObserver() { peerConnection.createOffer(new CreateSdpObserver() {
@ -305,7 +313,6 @@ public class WebRTCWrapper {
@Override @Override
public void onCreateFailure(String s) { public void onCreateFailure(String s) {
Log.d(Config.LOGTAG, "create failure" + s);
future.setException(new IllegalStateException("Unable to create offer: " + s)); future.setException(new IllegalStateException("Unable to create offer: " + s));
} }
}, new MediaConstraints()); }, new MediaConstraints());
@ -313,7 +320,7 @@ public class WebRTCWrapper {
}, MoreExecutors.directExecutor()); }, MoreExecutors.directExecutor());
} }
public ListenableFuture<SessionDescription> createAnswer() { ListenableFuture<SessionDescription> createAnswer() {
return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> {
final SettableFuture<SessionDescription> future = SettableFuture.create(); final SettableFuture<SessionDescription> future = SettableFuture.create();
peerConnection.createAnswer(new CreateSdpObserver() { peerConnection.createAnswer(new CreateSdpObserver() {
@ -331,7 +338,7 @@ public class WebRTCWrapper {
}, MoreExecutors.directExecutor()); }, MoreExecutors.directExecutor());
} }
public ListenableFuture<Void> setLocalDescription(final SessionDescription sessionDescription) { ListenableFuture<Void> setLocalDescription(final SessionDescription sessionDescription) {
Log.d(EXTENDED_LOGGING_TAG, "setting local description:"); Log.d(EXTENDED_LOGGING_TAG, "setting local description:");
for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) {
Log.d(EXTENDED_LOGGING_TAG, line); Log.d(EXTENDED_LOGGING_TAG, line);
@ -345,8 +352,7 @@ public class WebRTCWrapper {
} }
@Override @Override
public void onSetFailure(String s) { public void onSetFailure(final String s) {
Log.d(Config.LOGTAG, "unable to set local " + s);
future.setException(new IllegalArgumentException("unable to set local session description: " + s)); future.setException(new IllegalArgumentException("unable to set local session description: " + s));
} }
@ -355,7 +361,7 @@ public class WebRTCWrapper {
}, MoreExecutors.directExecutor()); }, MoreExecutors.directExecutor());
} }
public ListenableFuture<Void> setRemoteDescription(final SessionDescription sessionDescription) { ListenableFuture<Void> setRemoteDescription(final SessionDescription sessionDescription) {
Log.d(EXTENDED_LOGGING_TAG, "setting remote description:"); Log.d(EXTENDED_LOGGING_TAG, "setting remote description:");
for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) {
Log.d(EXTENDED_LOGGING_TAG, line); 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); requirePeerConnection().addIceCandidate(iceCandidate);
} }
@ -439,11 +445,11 @@ public class WebRTCWrapper {
return this.eglBase.getEglBaseContext(); return this.eglBase.getEglBaseContext();
} }
public Optional<VideoTrack> getLocalVideoTrack() { Optional<VideoTrack> getLocalVideoTrack() {
return Optional.fromNullable(this.localVideoTrack); return Optional.fromNullable(this.localVideoTrack);
} }
public Optional<VideoTrack> getRemoteVideoTrack() { Optional<VideoTrack> getRemoteVideoTrack() {
return Optional.fromNullable(this.remoteVideoTrack); return Optional.fromNullable(this.remoteVideoTrack);
} }
@ -463,7 +469,7 @@ public class WebRTCWrapper {
return context; return context;
} }
public AppRTCAudioManager getAudioManager() { AppRTCAudioManager getAudioManager() {
return appRTCAudioManager; return appRTCAudioManager;
} }
@ -504,7 +510,7 @@ public class WebRTCWrapper {
} }
} }
public static class InitializationException extends Exception { static class InitializationException extends Exception {
private InitializationException(String message) { private InitializationException(String message) {
super(message); super(message);
@ -515,12 +521,12 @@ public class WebRTCWrapper {
private final CameraVideoCapturer cameraVideoCapturer; private final CameraVideoCapturer cameraVideoCapturer;
private final CameraEnumerationAndroid.CaptureFormat captureFormat; private final CameraEnumerationAndroid.CaptureFormat captureFormat;
public CapturerChoice(CameraVideoCapturer cameraVideoCapturer, CameraEnumerationAndroid.CaptureFormat captureFormat) { CapturerChoice(CameraVideoCapturer cameraVideoCapturer, CameraEnumerationAndroid.CaptureFormat captureFormat) {
this.cameraVideoCapturer = cameraVideoCapturer; this.cameraVideoCapturer = cameraVideoCapturer;
this.captureFormat = captureFormat; this.captureFormat = captureFormat;
} }
public int getFrameRate() { int getFrameRate() {
return Math.max(captureFormat.framerate.min, Math.min(CAPTURING_MAX_FRAME_RATE, captureFormat.framerate.max)); return Math.max(captureFormat.framerate.min, Math.min(CAPTURING_MAX_FRAME_RATE, captureFormat.framerate.max));
} }
} }

View File

@ -194,6 +194,9 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
checkNotNullNoWhitespace(component, "component"); checkNotNullNoWhitespace(component, "component");
final String transport = this.getAttribute("protocol"); final String transport = this.getAttribute("protocol");
checkNotNullNoWhitespace(transport, "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"); final String priority = this.getAttribute("priority");
checkNotNullNoWhitespace(priority, "priority"); checkNotNullNoWhitespace(priority, "priority");
final String connectionAddress = this.getAttribute("ip"); final String connectionAddress = this.getAttribute("ip");

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -60,6 +60,12 @@
android:title="@string/send_location" /> android:title="@string/send_location" />
</menu> </menu>
</item> </item>
<item
android:id="@+id/action_ongoing_call"
android:icon="?attr/icon_ongoing_call"
android:orderInCategory="34"
android:title="@string/return_to_ongoing_call"
app:showAsAction="always" />
<item <item
android:id="@+id/action_call" android:id="@+id/action_call"
android:icon="?attr/icon_call" android:icon="?attr/icon_call"

View File

@ -7,7 +7,7 @@
<string name="action_end_conversation">关闭聊天</string> <string name="action_end_conversation">关闭聊天</string>
<string name="action_contact_details">联系人详情</string> <string name="action_contact_details">联系人详情</string>
<string name="action_muc_details">群聊详情</string> <string name="action_muc_details">群聊详情</string>
<string name="channel_details">频道详情</string> <string name="channel_details">群聊详情</string>
<string name="action_secure">安全聊天</string> <string name="action_secure">安全聊天</string>
<string name="action_add_account">添加账号</string> <string name="action_add_account">添加账号</string>
<string name="action_edit_contact">编辑姓名</string> <string name="action_edit_contact">编辑姓名</string>
@ -31,10 +31,10 @@
<string name="minute_ago">1分钟前</string> <string name="minute_ago">1分钟前</string>
<string name="minutes_ago">%d分钟前</string> <string name="minutes_ago">%d分钟前</string>
<string name="x_unread_conversations">%d条未读消息</string> <string name="x_unread_conversations">%d条未读消息</string>
<string name="sending">正在发送…</string> <string name="sending">发送</string>
<string name="message_decrypting">解密信息中. 请稍候…</string> <string name="message_decrypting">解密中. 请稍候…</string>
<string name="pgp_message">OpenPGP加密的信息</string> <string name="pgp_message">OpenPGP加密的信息</string>
<string name="nick_in_use">该名已存在</string> <string name="nick_in_use">用户名已存在</string>
<string name="invalid_muc_nick">无效的用户名</string> <string name="invalid_muc_nick">无效的用户名</string>
<string name="admin">管理员</string> <string name="admin">管理员</string>
<string name="owner">所有者</string> <string name="owner">所有者</string>
@ -47,11 +47,11 @@
<string name="block_domain_text">封禁 %s 中的所有联系人?</string> <string name="block_domain_text">封禁 %s 中的所有联系人?</string>
<string name="unblock_domain_text">解封%s 中所有联系人?</string> <string name="unblock_domain_text">解封%s 中所有联系人?</string>
<string name="contact_blocked">联系人已封禁</string> <string name="contact_blocked">联系人已封禁</string>
<string name="blocked">屏蔽</string> <string name="blocked">封禁</string>
<string name="remove_bookmark_text">从书签中移除 %s ?相关会话消息不会被清除 .</string> <string name="remove_bookmark_text">从书签中移除 %s ?相关会话消息不会被清除 .</string>
<string name="register_account">在服务器上注册新账户</string> <string name="register_account">在服务器上注册新账户</string>
<string name="change_password_on_server">在服务器上修改密码</string> <string name="change_password_on_server">在服务器上修改密码</string>
<string name="share_with">分享…</string> <string name="share_with">分享…</string>
<string name="start_conversation">开始会话</string> <string name="start_conversation">开始会话</string>
<string name="invite_contact">邀请联系人</string> <string name="invite_contact">邀请联系人</string>
<string name="invite">邀请</string> <string name="invite">邀请</string>
@ -62,8 +62,8 @@
<string name="add">添加</string> <string name="add">添加</string>
<string name="edit">编辑</string> <string name="edit">编辑</string>
<string name="delete">删除</string> <string name="delete">删除</string>
<string name="block">屏蔽</string> <string name="block">封禁</string>
<string name="unblock">除屏蔽</string> <string name="unblock"></string>
<string name="save">保存</string> <string name="save">保存</string>
<string name="ok">完成</string> <string name="ok">完成</string>
<string name="crash_report_title">畅聊已崩溃</string> <string name="crash_report_title">畅聊已崩溃</string>
@ -73,7 +73,7 @@
<string name="problem_connecting_to_account">无法连接至账户</string> <string name="problem_connecting_to_account">无法连接至账户</string>
<string name="problem_connecting_to_accounts">无法连接至多个账户</string> <string name="problem_connecting_to_accounts">无法连接至多个账户</string>
<string name="touch_to_fix">点击此处管理账户</string> <string name="touch_to_fix">点击此处管理账户</string>
<string name="attach_file">加文件</string> <string name="attach_file">加文件</string>
<string name="not_in_roster">该联系人不在您的列表,需要加为联系人吗 ?</string> <string name="not_in_roster">该联系人不在您的列表,需要加为联系人吗 ?</string>
<string name="add_contact">添加联系人</string> <string name="add_contact">添加联系人</string>
<string name="send_failed">传递失败</string> <string name="send_failed">传递失败</string>
@ -109,13 +109,17 @@
<string name="contacts_have_no_pgp_keys">因您的联系人未公布公钥,畅聊未能成功加密您的信息.\n\n<small>请通知联系人设置OpenPGP.</small></string> <string name="contacts_have_no_pgp_keys">因您的联系人未公布公钥,畅聊未能成功加密您的信息.\n\n<small>请通知联系人设置OpenPGP.</small></string>
<string name="pref_general">常规</string> <string name="pref_general">常规</string>
<string name="pref_accept_files">接收文件</string> <string name="pref_accept_files">接收文件</string>
<string name="pref_accept_files_summary">自动接收小于的文件</string> <string name="pref_accept_files_summary">自动接收小于此大小的文件</string>
<string name="pref_attachments">附件</string> <string name="pref_attachments">附件</string>
<string name="pref_notification_settings">通知</string> <string name="pref_notification_settings">通知</string>
<string name="pref_vibrate">震动</string> <string name="pref_vibrate">震动</string>
<string name="pref_vibrate_summary">收到新消息时震动</string> <string name="pref_vibrate_summary">收到新消息时震动</string>
<string name="pref_led">LED 灯提示</string> <string name="pref_led">LED 灯提示</string>
<string name="pref_led_summary">收到新消息时闪烁通知灯</string> <string name="pref_led_summary">收到新消息时闪烁通知灯</string>
<string name="pref_ringtone">铃声</string>
<string name="pref_notification_sound">通知铃声</string>
<string name="pref_notification_sound_summary">新消息通知铃声</string>
<string name="pref_call_ringtone_summary">来电铃声</string>
<string name="pref_notification_grace_period">静默期限</string> <string name="pref_notification_grace_period">静默期限</string>
<string name="pref_notification_grace_period_summary">在您的其他设备之一上检测到活动之后,时间通知的长度将被静音。</string> <string name="pref_notification_grace_period_summary">在您的其他设备之一上检测到活动之后,时间通知的长度将被静音。</string>
<string name="pref_advanced_options">高级</string> <string name="pref_advanced_options">高级</string>
@ -134,7 +138,7 @@
<string name="receive_presence_updates">接收在线联系人列表更新</string> <string name="receive_presence_updates">接收在线联系人列表更新</string>
<string name="ask_for_presence_updates">请求在线联系人列表更新</string> <string name="ask_for_presence_updates">请求在线联系人列表更新</string>
<string name="attach_choose_picture">选择图片</string> <string name="attach_choose_picture">选择图片</string>
<string name="attach_take_picture">照相</string> <string name="attach_take_picture">拍摄图片</string>
<string name="preemptively_grant">预先同意订阅请求</string> <string name="preemptively_grant">预先同意订阅请求</string>
<string name="error_not_an_image_file">您选择的文件不是图像文件</string> <string name="error_not_an_image_file">您选择的文件不是图像文件</string>
<string name="error_compressing_image">转换图像出错</string> <string name="error_compressing_image">转换图像出错</string>
@ -188,6 +192,7 @@
<string name="server_info_blocking">XEP-0191屏蔽指令</string> <string name="server_info_blocking">XEP-0191屏蔽指令</string>
<string name="server_info_roster_version">XEP-0237名单版本</string> <string name="server_info_roster_version">XEP-0237名单版本</string>
<string name="server_info_stream_management">XEP-0198流管理</string> <string name="server_info_stream_management">XEP-0198流管理</string>
<string name="server_info_external_service_discovery">XEP-0215发现外部服务</string>
<string name="server_info_pep">XEP-0163PEP头像/OMEMO</string> <string name="server_info_pep">XEP-0163PEP头像/OMEMO</string>
<string name="server_info_http_upload">XEP-0363HTTP文件上传</string> <string name="server_info_http_upload">XEP-0363HTTP文件上传</string>
<string name="server_info_push">XEP-0357推送</string> <string name="server_info_push">XEP-0357推送</string>
@ -229,23 +234,23 @@
<string name="save_as_bookmark">保存为书签</string> <string name="save_as_bookmark">保存为书签</string>
<string name="delete_bookmark">删除书签</string> <string name="delete_bookmark">删除书签</string>
<string name="destroy_room">解散群聊</string> <string name="destroy_room">解散群聊</string>
<string name="destroy_channel">解散频道</string> <string name="destroy_channel">解散群聊</string>
<string name="destroy_room_dialog">您确定要解散此群聊吗?\n\n<b>警告:</b>此群聊将在服务器上完全删除。</string> <string name="destroy_room_dialog">您确定要解散此群聊吗?\n\n<b>警告:</b>此群聊将在服务器上完全删除。</string>
<string name="destroy_channel_dialog">您确定要解散此公共频道吗?\n\n<b>警告:</b>该频道将在服务器上完全删除。</string> <string name="destroy_channel_dialog">您确定要解散此公共群聊吗?\n\n<b>警告:</b>该群聊将在服务器上完全删除。</string>
<string name="could_not_destroy_room">无法解散群聊</string> <string name="could_not_destroy_room">无法解散群聊</string>
<string name="could_not_destroy_channel">无法解散频道</string> <string name="could_not_destroy_channel">无法解散群聊</string>
<string name="action_edit_subject">编辑群聊主题</string> <string name="action_edit_subject">编辑群聊主题</string>
<string name="topic">主题</string> <string name="topic">主题</string>
<string name="joining_conference">正在加入群聊…</string> <string name="joining_conference">正在加入群聊…</string>
<string name="leave">离开</string> <string name="leave">离开</string>
<string name="contact_added_you">联系人已添加你到联系人列表</string> <string name="contact_added_you">联系人已添加你到联系人列表</string>
<string name="add_back">反向添加</string> <string name="add_back">反向添加</string>
<string name="contact_has_read_up_to_this_point">%s 已读此句</string> <string name="contact_has_read_up_to_this_point">%s 读到这里了</string>
<string name="contacts_have_read_up_to_this_point">%s 已读此句</string> <string name="contacts_have_read_up_to_this_point">%s 读到这里了</string>
<string name="contacts_and_n_more_have_read_up_to_this_point">%1$s +%2$d 都已读此句</string> <string name="contacts_and_n_more_have_read_up_to_this_point">%1$s 和另外%2$d人读到这里了</string>
<string name="everyone_has_read_up_to_this_point">全部已读此句</string> <string name="everyone_has_read_up_to_this_point">所有人都读到这里了</string>
<string name="publish">发布</string> <string name="publish">发布</string>
<string name="touch_to_choose_picture">点击头像可从相册中选择头像 </string> <string name="touch_to_choose_picture">点击头像以选择图片</string>
<string name="publishing">正在发布…</string> <string name="publishing">正在发布…</string>
<string name="error_publish_avatar_server_reject">服务器拒绝了您的发布请求</string> <string name="error_publish_avatar_server_reject">服务器拒绝了您的发布请求</string>
<string name="error_publish_avatar_converting">转换头像图片出错</string> <string name="error_publish_avatar_converting">转换头像图片出错</string>
@ -283,7 +288,7 @@
<string name="pref_autojoin">同步书签</string> <string name="pref_autojoin">同步书签</string>
<string name="pref_autojoin_summary">根据书签中的自动加入标记加入并离开群聊。</string> <string name="pref_autojoin_summary">根据书签中的自动加入标记加入并离开群聊。</string>
<string name="toast_message_omemo_fingerprint">OMEMO 指纹已拷贝到剪贴板!</string> <string name="toast_message_omemo_fingerprint">OMEMO 指纹已拷贝到剪贴板!</string>
<string name="conference_banned">你已经被禁</string> <string name="conference_banned">你已经被禁了</string>
<string name="conference_members_only">这个群组只允许群组成员聊天</string> <string name="conference_members_only">这个群组只允许群组成员聊天</string>
<string name="conference_resource_constraint">资源限制</string> <string name="conference_resource_constraint">资源限制</string>
<string name="conference_kicked">您已被移出该群组了</string> <string name="conference_kicked">您已被移出该群组了</string>
@ -374,10 +379,10 @@
<string name="grant_owner_privileges">授予所有者权限</string> <string name="grant_owner_privileges">授予所有者权限</string>
<string name="remove_owner_privileges">撤销所有者权限</string> <string name="remove_owner_privileges">撤销所有者权限</string>
<string name="remove_from_room">从群聊中移除</string> <string name="remove_from_room">从群聊中移除</string>
<string name="remove_from_channel">频道中移除</string> <string name="remove_from_channel">群聊中移除</string>
<string name="could_not_change_affiliation">不能修改 %s 的从属关系</string> <string name="could_not_change_affiliation">不能修改 %s 的从属关系</string>
<string name="ban_from_conference">屏蔽群聊</string> <string name="ban_from_conference">屏蔽群聊</string>
<string name="ban_from_channel">频道中屏蔽</string> <string name="ban_from_channel">群聊中屏蔽</string>
<string name="removing_from_public_conference">%s将被从公共群聊中移除。只有将此用户封禁才能将他从群聊永远移除。</string> <string name="removing_from_public_conference">%s将被从公共群聊中移除。只有将此用户封禁才能将他从群聊永远移除。</string>
<string name="ban_now">现在屏蔽</string> <string name="ban_now">现在屏蔽</string>
<string name="could_not_change_role">不能修改 %s 的角色</string> <string name="could_not_change_role">不能修改 %s 的角色</string>
@ -465,7 +470,7 @@
<string name="hostname_example">xmpp.example.com</string> <string name="hostname_example">xmpp.example.com</string>
<string name="action_add_account_with_certificate">使用证书添加账户</string> <string name="action_add_account_with_certificate">使用证书添加账户</string>
<string name="unable_to_parse_certificate">无法解析证书</string> <string name="unable_to_parse_certificate">无法解析证书</string>
<string name="authenticate_with_certificate">留空以认证 w/ 证书</string> <string name="authenticate_with_certificate">留空以使用证书认证</string>
<string name="mam_prefs">压缩设置</string> <string name="mam_prefs">压缩设置</string>
<string name="server_side_mam_prefs">服务端压缩设置</string> <string name="server_side_mam_prefs">服务端压缩设置</string>
<string name="fetching_mam_prefs">正在获取压缩设置。请稍候……</string> <string name="fetching_mam_prefs">正在获取压缩设置。请稍候……</string>
@ -483,7 +488,7 @@
<string name="pref_use_tor_summary">所有连接使用 Tor 网络传输,需要 Orbot</string> <string name="pref_use_tor_summary">所有连接使用 Tor 网络传输,需要 Orbot</string>
<string name="account_settings_hostname">主机名</string> <string name="account_settings_hostname">主机名</string>
<string name="account_settings_port">端口</string> <string name="account_settings_port">端口</string>
<string name="hostname_or_onion">服务器 - 或者 .orion 地址</string> <string name="hostname_or_onion">服务器 - 或者 .onion 地址</string>
<string name="not_a_valid_port">该端口号无效</string> <string name="not_a_valid_port">该端口号无效</string>
<string name="not_valid_hostname">该主机名无效</string> <string name="not_valid_hostname">该主机名无效</string>
<string name="connected_accounts">%2$d 个中的 %1$d 个账户已连接</string> <string name="connected_accounts">%2$d 个中的 %1$d 个账户已连接</string>
@ -550,7 +555,7 @@
<string name="gp_short"></string> <string name="gp_short"></string>
<string name="gp_medium"></string> <string name="gp_medium"></string>
<string name="gp_long"></string> <string name="gp_long"></string>
<string name="pref_broadcast_last_activity">广播最后打开该应用的时间</string> <string name="pref_broadcast_last_activity">广播最后使用应用的时间</string>
<string name="pref_broadcast_last_activity_summary">让你的所有联系人知道你使用畅聊的时间</string> <string name="pref_broadcast_last_activity_summary">让你的所有联系人知道你使用畅聊的时间</string>
<string name="pref_privacy">隐私</string> <string name="pref_privacy">隐私</string>
<string name="pref_theme_options">主题</string> <string name="pref_theme_options">主题</string>
@ -560,8 +565,8 @@
<string name="pref_theme_dark">黑暗主题</string> <string name="pref_theme_dark">黑暗主题</string>
<string name="unable_to_connect_to_keychain">无法连接到 OpenKeychain</string> <string name="unable_to_connect_to_keychain">无法连接到 OpenKeychain</string>
<string name="this_device_is_no_longer_in_use">此设备不再使用</string> <string name="this_device_is_no_longer_in_use">此设备不再使用</string>
<string name="type_pc">计算机</string> <string name="type_pc">电脑</string>
<string name="type_phone">移动电话</string> <string name="type_phone">手机</string>
<string name="type_tablet">平板</string> <string name="type_tablet">平板</string>
<string name="type_web">浏览器</string> <string name="type_web">浏览器</string>
<string name="type_console">控制台</string> <string name="type_console">控制台</string>
@ -637,6 +642,7 @@
<string name="corresponding_conversations_closed">相应的对话已关闭。</string> <string name="corresponding_conversations_closed">相应的对话已关闭。</string>
<string name="contact_blocked_past_tense">联系人已屏蔽</string> <string name="contact_blocked_past_tense">联系人已屏蔽</string>
<string name="pref_notifications_from_strangers">陌生人也通知</string> <string name="pref_notifications_from_strangers">陌生人也通知</string>
<string name="pref_notifications_from_strangers_summary">提醒来自陌生人的消息与通话</string>
<string name="received_message_from_stranger">已收到陌生人的信息</string> <string name="received_message_from_stranger">已收到陌生人的信息</string>
<string name="block_stranger">屏蔽陌生人</string> <string name="block_stranger">屏蔽陌生人</string>
<string name="block_entire_domain">屏蔽整个域名</string> <string name="block_entire_domain">屏蔽整个域名</string>
@ -735,9 +741,14 @@
<string name="error_channel_name">连接问题</string> <string name="error_channel_name">连接问题</string>
<string name="error_channel_description">此通知类别用于显示一旦帐户连接出现问题的通知。</string> <string name="error_channel_description">此通知类别用于显示一旦帐户连接出现问题的通知。</string>
<string name="notification_group_messages">消息</string> <string name="notification_group_messages">消息</string>
<string name="notification_group_calls">通话</string>
<string name="messages_channel_name">消息</string> <string name="messages_channel_name">消息</string>
<string name="incoming_calls_channel_name">来电</string>
<string name="ongoing_calls_channel_name">正在进行的通话</string>
<string name="silent_messages_channel_name">无声消息</string> <string name="silent_messages_channel_name">无声消息</string>
<string name="silent_messages_channel_description">此通知组用于显示不应触发任何声音的通知。 例如,当在另一个设备上激活时(宽限期)。</string> <string name="silent_messages_channel_description">此通知组用于显示不应触发任何声音的通知。 例如,当在另一个设备上激活时(宽限期)。</string>
<string name="pref_message_notification_settings">消息通知设置</string>
<string name="pref_incoming_call_notification_settings">来电通知设置</string>
<string name="pref_more_notification_settings_summary">重要性,声音,振动</string> <string name="pref_more_notification_settings_summary">重要性,声音,振动</string>
<string name="video_compression_channel_name">视频压缩</string> <string name="video_compression_channel_name">视频压缩</string>
<string name="view_media">查看媒体文件</string> <string name="view_media">查看媒体文件</string>
@ -773,7 +784,7 @@
<string name="abort_registration_procedure">确定放弃注册?</string> <string name="abort_registration_procedure">确定放弃注册?</string>
<string name="yes"></string> <string name="yes"></string>
<string name="no"></string> <string name="no"></string>
<string name="verifying">正在验证......</string> <string name="verifying">正在验证.....</string>
<string name="requesting_sms">请求短信...</string> <string name="requesting_sms">请求短信...</string>
<string name="incorrect_pin">验证码错误。</string> <string name="incorrect_pin">验证码错误。</string>
<string name="pin_expired">验证码已失效</string> <string name="pin_expired">验证码已失效</string>
@ -830,7 +841,7 @@
<string name="allow_participants_to_edit_subject">允许任何成员修改主题</string> <string name="allow_participants_to_edit_subject">允许任何成员修改主题</string>
<string name="allow_participants_to_invite_others">允许任何成员邀请其他人</string> <string name="allow_participants_to_invite_others">允许任何成员邀请其他人</string>
<string name="anyone_can_edit_subject">允许任何成员修改主题</string> <string name="anyone_can_edit_subject">允许任何成员修改主题</string>
<string name="owners_can_edit_subject">拥有者可修改</string> <string name="owners_can_edit_subject">拥有者可修改</string>
<string name="admins_can_edit_subject">管理员可修改主题</string> <string name="admins_can_edit_subject">管理员可修改主题</string>
<string name="owners_can_invite_others">所有者可以邀请其他人</string> <string name="owners_can_invite_others">所有者可以邀请其他人</string>
<string name="anyone_can_invite_others">允许任何成员邀请其他人</string> <string name="anyone_can_invite_others">允许任何成员邀请其他人</string>
@ -845,7 +856,7 @@
<string name="discover_channels">发现群聊</string> <string name="discover_channels">发现群聊</string>
<string name="search_channels">搜索群聊</string> <string name="search_channels">搜索群聊</string>
<string name="channel_discovery_opt_in_title">可能侵犯隐私!</string> <string name="channel_discovery_opt_in_title">可能侵犯隐私!</string>
<string name="channel_discover_opt_in_message"><![CDATA[频道发现使用了名为<a href="https://search.jabber.network">search.jabber.network</a>。<br><br>的第三方服务。使用此功能会将您的IP地址和搜索字词传输到该服务。 有关更多信息,请参见其<a href="https://search.jabber.network/privacy">Privacy Policy</a>。]]></string> <string name="channel_discover_opt_in_message"><![CDATA[群聊发现使用了名为<a href="https://search.jabber.network">search.jabber.network</a>。<br><br>的第三方服务。使用此功能会将您的IP地址和搜索字词传输到该服务。 有关更多信息,请参见其<a href="https://search.jabber.network/privacy">Privacy Policy</a>。]]></string>
<string name="i_already_have_an_account">我已有账户</string> <string name="i_already_have_an_account">我已有账户</string>
<string name="add_existing_account">添加已有账户</string> <string name="add_existing_account">添加已有账户</string>
<string name="register_new_account">注册新账户</string> <string name="register_new_account">注册新账户</string>
@ -866,10 +877,38 @@
<string name="jabber_network">jabber.network</string> <string name="jabber_network">jabber.network</string>
<string name="local_server">本地服务器</string> <string name="local_server">本地服务器</string>
<string name="pref_channel_discovery_summary">大多数用户应该选择“ jabber.network”以从整个XMPP生态系统中获得更好的建议。</string> <string name="pref_channel_discovery_summary">大多数用户应该选择“ jabber.network”以从整个XMPP生态系统中获得更好的建议。</string>
<string name="pref_channel_discovery">频道发现方法</string> <string name="pref_channel_discovery">群聊发现方法</string>
<string name="backup">备份</string> <string name="backup">备份</string>
<string name="category_about">关于</string> <string name="category_about">关于</string>
<string name="please_enable_an_account">请启用一个帐户</string> <string name="please_enable_an_account">请启用一个帐户</string>
<string name="make_call">拨打电话</string>
<string name="rtp_state_incoming_call">来电</string>
<string name="rtp_state_incoming_video_call">视频来电</string>
<string name="rtp_state_connecting">正在连接</string>
<string name="rtp_state_connected">已连接</string>
<string name="rtp_state_accepting_call">正在接受通话</string>
<string name="rtp_state_ending_call">正在结束通话</string>
<string name="answer_call">接电话</string>
<string name="dismiss_call">忽略</string>
<string name="rtp_state_finding_device">正在确定设备位置</string>
<string name="rtp_state_ringing">正在响铃</string>
<string name="rtp_state_declined_or_busy">忙碌</string>
<string name="rtp_state_connectivity_error">无法接通来电</string>
<string name="rtp_state_retracted">撤销的通话</string>
<string name="rtp_state_application_failure">程序错误</string>
<string name="hang_up">挂断</string>
<string name="ongoing_call">正在进行的通话</string>
<string name="ongoing_video_call">正在进行的视频通话</string>
<string name="disable_tor_to_make_call">禁用Tor以拨打电话</string>
<string name="incoming_call">来电</string>
<string name="incoming_call_duration">来电%s</string>
<string name="outgoing_call">去电</string>
<string name="outgoing_call_duration">去电%s</string>
<string name="missed_call">未接电话</string>
<string name="audio_call">语音通话</string>
<string name="video_call">视频通话</string>
<string name="microphone_unavailable">麦克风不可用</string>
<string name="only_one_call_at_a_time">在同一时间只能打一通电话</string>
<plurals name="view_users"> <plurals name="view_users">
<item quantity="other">查看%1$d成员</item> <item quantity="other">查看%1$d成员</item>
</plurals> </plurals>

View File

@ -91,6 +91,8 @@
<attr name="icon_new_attachment" format="reference"/> <attr name="icon_new_attachment" format="reference"/>
<attr name="icon_not_secure" format="reference"/> <attr name="icon_not_secure" format="reference"/>
<attr name="icon_call" format="reference"/> <attr name="icon_call" format="reference"/>
<attr name="icon_ongoing_call" format="reference"/>
<attr name="ic_ongoing_call_hint" format="reference"/>
<attr name="icon_quote" format="reference"/> <attr name="icon_quote" format="reference"/>
<attr name="icon_refresh" format="reference"/> <attr name="icon_refresh" format="reference"/>
<attr name="icon_remove" format="reference"/> <attr name="icon_remove" format="reference"/>

View File

@ -917,6 +917,7 @@
<string name="video_call">Video call</string> <string name="video_call">Video call</string>
<string name="microphone_unavailable">Your microphone is unavailable</string> <string name="microphone_unavailable">Your microphone is unavailable</string>
<string name="only_one_call_at_a_time">You can only have one call at a time.</string> <string name="only_one_call_at_a_time">You can only have one call at a time.</string>
<string name="return_to_ongoing_call">Return to ongoing call</string>
<plurals name="view_users"> <plurals name="view_users">
<item quantity="one">View %1$d Participant</item> <item quantity="one">View %1$d Participant</item>
<item quantity="other">View %1$d Participants</item> <item quantity="other">View %1$d Participants</item>

View File

@ -99,6 +99,8 @@
<item type="reference" name="icon_new_attachment">@drawable/ic_attach_file_white_24dp</item> <item type="reference" name="icon_new_attachment">@drawable/ic_attach_file_white_24dp</item>
<item type="reference" name="icon_not_secure">@drawable/ic_lock_open_white_24dp</item> <item type="reference" name="icon_not_secure">@drawable/ic_lock_open_white_24dp</item>
<item type="reference" name="icon_call">@drawable/ic_call_white_24dp</item> <item type="reference" name="icon_call">@drawable/ic_call_white_24dp</item>
<item type="reference" name="icon_ongoing_call">@drawable/ic_phone_in_talk_white_24dp</item>
<item type="reference" name="ic_ongoing_call_hint">@drawable/ic_phone_in_talk_black_18dp</item>
<item type="reference" name="icon_remove">@drawable/ic_delete_black_24dp</item> <item type="reference" name="icon_remove">@drawable/ic_delete_black_24dp</item>
<item type="reference" name="icon_search">@drawable/ic_search_white_24dp</item> <item type="reference" name="icon_search">@drawable/ic_search_white_24dp</item>
<item type="reference" name="icon_secure">@drawable/ic_lock_open_white_24dp</item> <item type="reference" name="icon_secure">@drawable/ic_lock_open_white_24dp</item>
@ -219,6 +221,8 @@
<item type="reference" name="icon_new_attachment">@drawable/ic_attach_file_white_24dp</item> <item type="reference" name="icon_new_attachment">@drawable/ic_attach_file_white_24dp</item>
<item type="reference" name="icon_not_secure">@drawable/ic_lock_open_white_24dp</item> <item type="reference" name="icon_not_secure">@drawable/ic_lock_open_white_24dp</item>
<item type="reference" name="icon_call">@drawable/ic_call_white_24dp</item> <item type="reference" name="icon_call">@drawable/ic_call_white_24dp</item>
<item type="reference" name="icon_ongoing_call">@drawable/ic_phone_in_talk_white_24dp</item>
<item type="reference" name="ic_ongoing_call_hint">@drawable/ic_phone_in_talk_white_18dp</item>
<item type="reference" name="icon_remove">@drawable/ic_delete_white_24dp</item> <item type="reference" name="icon_remove">@drawable/ic_delete_white_24dp</item>
<item type="reference" name="icon_search">@drawable/ic_search_white_24dp</item> <item type="reference" name="icon_search">@drawable/ic_search_white_24dp</item>
<item type="reference" name="icon_secure">@drawable/ic_lock_open_white_24dp</item> <item type="reference" name="icon_secure">@drawable/ic_lock_open_white_24dp</item>

View File

@ -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) { private void enablePushOnServer(final Account account, final Jid appServer, final String node, final String secret) {
final IqPacket enable = mXmppConnectionService.getIqGenerator().enablePush(appServer, node, secret); final IqPacket enable = mXmppConnectionService.getIqGenerator().enablePush(appServer, node, secret);
mXmppConnectionService.sendIqPacket(account, enable, (a, p) -> { 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) { private void retrieveFcmInstanceToken(final OnGcmInstanceTokenRetrieved instanceTokenRetrieved) {
final FirebaseInstanceId firebaseInstanceId; final FirebaseInstanceId firebaseInstanceId;
try { try {