...
 
Commits (22)
......@@ -10,6 +10,7 @@ android:
licenses:
- '.+'
before_script:
- mkdir libs
- wget -O libs/libwebrtc-m81.aar http://gultsch.de/files/libwebrtc-m81.aar
script:
- ./gradlew assembleConversationsFreeSystemRelease
......
# Changelog
### Version 2.8.5
* Reduce echo during calls on some devices
* Fix login when passwords contains special characters
* Play dial and busy tones on speaker during video calls
### Version 2.8.4
* Rework Login with certificate UI
......
......@@ -43,7 +43,7 @@
* End-to-end encryption with [OMEMO](http://conversations.im/omemo/) or [OpenPGP](http://openpgp.org/about/)
* Send and receive images as well as other kind of files
* Make audio and video calls
* Encrypted audio and video calls (DLTS-SRTP)
* Share your location
* Send voice messages
* Indication when your contact has read your message
......@@ -361,7 +361,7 @@ There are XMPP Clients available for all major platforms.
#### Windows / Linux
For your desktop computer we recommend that you use [Gajim](https://gajim.org). You need to install the plugins `OMEMO`, `HTTP Upload` and `URL image preview` to get the best compatibility with Conversations. Plugins can be installed from within the app.
#### iOS
Unfortunately we don‘t have a recommendation for iPhones right now. There are two clients available [ChatSecure](https://chatsecure.org/) and [Monal](https://monal.im/). Both with their own pros and cons.
Unfortunately we don‘t have a recommendation for iPhones right now. There are three clients available [Siskin](https://siskin.im/), [ChatSecure](https://chatsecure.org/) and [Monal](https://monal.im/). Each with their own pros and cons.
### Development
......@@ -385,25 +385,6 @@ There are two build flavors available. *free* and *playstore*. Unless you know w
[![Build Status](https://dev.sum7.eu/sum7/Conversations/badges/develop/build.svg)](https://dev.sum7.eu/sum7/Conversations/pipelines)
#### How do I update/add external libraries?
If the library you want to update is in Maven Central or JCenter (or has its own
Maven repo), add it or update its version in `build.gradle`. If the library is
in the `libs/` directory, you can update it using a subtree merge by doing the
following (using `minidns` as an example):
git remote add minidns https://github.com/rtreffer/minidns.git
git fetch minidns
git merge -s subtree minidns master
To add a new dependency to the `libs/` directory (replacing "name", "branch" and
"url" as necessary):
git remote add name url
git merge -s ours --no-commit name/branch
git read-tree --prefix=libs/name -u name/branch
git commit -m "Subtree merged in name"
#### How do I debug Conversations
If something goes wrong Conversations usually exposes very little information in
......
......@@ -77,7 +77,7 @@ dependencies {
implementation "com.squareup.retrofit2:retrofit:2.6.4"
implementation "com.squareup.retrofit2:converter-gson:2.6.4"
//okhttp needs to stick with 3.12.x
implementation 'com.squareup.okhttp3:okhttp:3.12.10'
implementation 'com.squareup.okhttp3:okhttp:3.12.12'
implementation 'com.google.guava:guava:27.1-android'
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.11.1'
//implementation fileTree(include: ['libwebrtc-m81.aar'], dir: 'libs')
......@@ -96,8 +96,8 @@ android {
defaultConfig {
minSdkVersion 16
targetSdkVersion 25
versionCode 387
versionName "2.8.4"
versionCode 388
versionName "2.8.5"
archivesBaseName += "-$versionName"
applicationId "eu.sum7.conversations"
resValue "string", "applicationId", applicationId
......
• Reduce echo during calls on some devices
• Fix login when passwords contains special characters
• Play dial and busy tones on speaker during video calls
......@@ -4,8 +4,7 @@ Changes to origin:
• replace the hardcoded IPv4 preference to easy Happy Eyeball, for faster connection and fair to both IP version.
• rebrands it as chat.sum7.eu (to run both version together)
Easy to use, reliable, battery friendly. With built-in support for images, group
chats and e2e encryption.
Easy to use, reliable, battery friendly. With built-in support for images, group chats and e2e encryption.
Design principles:
......@@ -18,6 +17,7 @@ Features:
• End-to-end encryption with either <a href="https://conversations.im/omemo/">OMEMO</a> or <a href="https://openpgp.org/about/">OpenPGP</a>
• Sending and receiving images
• Encrypted audio and video calls (DLTS-SRTP)
• Intuitive UI that follows Android Design guidelines
• Pictures / Avatars for your Contacts
• Syncs with desktop client
......@@ -26,19 +26,11 @@ Features:
• Multiple accounts / unified inbox
• Very low impact on battery life
Conversations makes it very easy to create an account on the chat.sum7.eu
server. 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.
Conversations makes it very easy to create an account on the chat.sum7.eu server. However Conversations will work with any other XMPP server as well. A lot of XMPP servers are run by volunteers and are free of charge.
XMPP Features:
Conversations works with every XMPP server out there. However XMPP is an
extensible protocol. These extensions are standardized as well in so called
XEP’s. Conversations supports a couple of those to make the overall user
experience better. There is a chance that your current XMPP server does not
support these extensions. Therefore to get the most out of Conversations you
should consider either switching to an XMPP server that does or - even better -
run your own XMPP server for you and your friends.
Conversations works with every XMPP server out there. However XMPP is an extensible protocol. These extensions are standardized as well in so called XEP’s. Conversations supports a couple of those to make the overall user experience better. There is a chance that your current XMPP server does not support these extensions. Therefore to get the most out of Conversations you should consider either switching to an XMPP server that does or - even better - run your own XMPP server for you and your friends.
These XEPs are - as of now:
......
......@@ -85,7 +85,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
String jid = savedInstanceState.getString(STATE_SELECTED_ACCOUNT);
if (jid != null) {
try {
this.selectedAccountJid = Jid.of(jid);
this.selectedAccountJid = Jid.ofEscaped(jid);
} catch (IllegalArgumentException e) {
this.selectedAccountJid = null;
}
......@@ -111,7 +111,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
@Override
public void onSaveInstanceState(final Bundle savedInstanceState) {
if (selectedAccount != null) {
savedInstanceState.putString(STATE_SELECTED_ACCOUNT, selectedAccount.getJid().asBareJid().toString());
savedInstanceState.putString(STATE_SELECTED_ACCOUNT, selectedAccount.getJid().asBareJid().toEscapedString());
}
super.onSaveInstanceState(savedInstanceState);
}
......@@ -286,7 +286,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
private void publishAvatar(Account account) {
Intent intent = new Intent(getApplicationContext(),
PublishProfilePictureActivity.class);
intent.putExtra(EXTRA_ACCOUNT, account.getJid().toString());
intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
startActivity(intent);
}
......
......@@ -62,7 +62,7 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) {
intent = SignupUtils.getTokenRegistrationIntent(this, jid, preauth);
} else if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter("ibr"))) {
intent = SignupUtils.getTokenRegistrationIntent(this, Jid.ofDomain(jid.getDomain()), preauth);
intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preauth);
intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
} else {
intent = null;
......
......@@ -34,11 +34,11 @@ abstract class ScramMechanism extends SaslMechanism {
// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations,SASL-Mechanism".
// Changing any of these values forces a cache miss. `CryptoHelper.bytesToHex()'
// is applied to prevent commas in the strings breaking things.
final String[] kparts = k.split(",", 5);
final String[] kParts = k.split(",", 5);
try {
final byte[] saltedPassword, serverKey, clientKey;
saltedPassword = hi(CryptoHelper.hexToString(kparts[1]).getBytes(),
Base64.decode(CryptoHelper.hexToString(kparts[2]), Base64.DEFAULT), Integer.valueOf(kparts[3]));
saltedPassword = hi(CryptoHelper.hexToString(kParts[1]).getBytes(),
Base64.decode(CryptoHelper.hexToString(kParts[2]), Base64.DEFAULT), Integer.parseInt(kParts[3]));
serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);
clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
......@@ -173,10 +173,10 @@ abstract class ScramMechanism extends SaslMechanism {
// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations,SASL-Mechanism".
final KeyPair keys = CACHE.get(
CryptoHelper.bytesToHex(account.getJid().asBareJid().toEscapedString().getBytes()) + ","
+ CryptoHelper.bytesToHex(account.getPassword().getBytes()) + ","
CryptoHelper.bytesToHex(CryptoHelper.saslPrep(account.getJid().asBareJid().toEscapedString()).getBytes()) + ","
+ CryptoHelper.bytesToHex(CryptoHelper.saslPrep(account.getPassword()).getBytes()) + ","
+ CryptoHelper.bytesToHex(salt.getBytes()) + ","
+ String.valueOf(iterationCount) + ","
+ iterationCount + ","
+ getMechanism()
);
if (keys == null) {
......
......@@ -143,6 +143,16 @@ public class Contact implements ListItem, Blockable {
}
}
public String getPublicDisplayName() {
if (!TextUtils.isEmpty(this.presenceName)) {
return this.presenceName;
} else if (jid.getLocal() != null) {
return JidHelper.localPartOrFallback(jid);
} else {
return jid.getDomain().toEscapedString();
}
}
public String getProfilePhoto() {
return this.photoUri;
}
......@@ -468,7 +478,7 @@ public class Contact implements ListItem, Blockable {
}
boolean isOwnServer() {
return account.getJid().getDomain().equals(jid.asBareJid().toString());
return account.getJid().getDomain().equals(jid.asBareJid());
}
public void setCommonName(String cn) {
......
......@@ -643,8 +643,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
!this.isOOb() &&
!message.treatAsDownloadable() &&
!this.treatAsDownloadable() &&
!message.getBody().startsWith(ME_COMMAND) &&
!this.getBody().startsWith(ME_COMMAND) &&
!message.hasMeCommand() &&
!this.hasMeCommand() &&
!this.bodyIsOnlyEmojis() &&
!message.bodyIsOnlyEmojis() &&
((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
......
......@@ -217,7 +217,7 @@ public class MessageGenerator extends AbstractGenerator {
Element x = new Element("x");
x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user");
Element invite = new Element("invite");
invite.setAttribute("to", contact.asBareJid().toString());
invite.setAttribute("to", contact.asBareJid());
x.addChild(invite);
packet.addChild(x);
return packet;
......
......@@ -42,7 +42,7 @@ public abstract class AbstractParser {
for(Element child : element.getChildren()) {
if ("delay".equals(child.getName()) && "urn:xmpp:delay".equals(child.getNamespace())) {
final Jid f = to == null ? null : InvalidJid.getNullForInvalid(child.getAttributeAsJid("from"));
if (f != null && (to.asBareJid().equals(f) || to.getDomain().equals(f.toString()))) {
if (f != null && (to.asBareJid().equals(f) || to.getDomain().equals(f))) {
continue;
}
final String stamp = child.getAttribute("stamp");
......
......@@ -452,7 +452,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null || xP1S3 != null) && !isMucStatusMessage) {
final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain());
final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain().toEscapedString());
final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
......@@ -837,13 +837,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace()) && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) {
final String action = child.getName();
if (query == null) {
if (!account.getJid().asBareJid().equals(from.asBareJid())) {
processMessageReceipts(account, packet, query);
}
if (serverMsgId == null) {
serverMsgId = extractStanzaId(account, packet);
}
mXmppConnectionService.getJingleConnectionManager().deliverMessage(account, packet.getTo(), packet.getFrom(), child, remoteMsgId, serverMsgId, timestamp);
if (!account.getJid().asBareJid().equals(from.asBareJid())) {
processMessageReceipts(account, packet, query);
}
} else if (query.isCatchup()) {
final String sessionId = child.getAttribute("id");
if (sessionId == null) {
......
......@@ -51,7 +51,6 @@ public class AppRTCAudioManager {
@Nullable
private AudioManagerEvents audioManagerEvents;
private AudioManagerState amState;
private int savedAudioMode = AudioManager.MODE_INVALID;
private boolean savedIsSpeakerPhoneOn;
private boolean savedIsMicrophoneMute;
private boolean hasWiredHeadset;
......@@ -178,21 +177,17 @@ public class AppRTCAudioManager {
}
@SuppressWarnings("deprecation")
// TODO(henrika): audioManager.requestAudioFocus() is deprecated.
public void start(AudioManagerEvents audioManagerEvents) {
Log.d(Config.LOGTAG, "start");
Log.d(Config.LOGTAG, AppRTCAudioManager.class.getName() + ".start()");
ThreadUtils.checkIsOnMainThread();
if (amState == AudioManagerState.RUNNING) {
Log.e(Config.LOGTAG, "AudioManager is already active");
return;
}
awaitMicrophoneLatch();
// TODO(henrika): perhaps call new method called preInitAudio() here if UNINITIALIZED.
Log.d(Config.LOGTAG, "AudioManager starts...");
this.audioManagerEvents = audioManagerEvents;
amState = AudioManagerState.RUNNING;
// Store current audio state so we can restore it when stop() is called.
savedAudioMode = audioManager.getMode();
savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn();
savedIsMicrophoneMute = audioManager.isMicrophoneMute();
hasWiredHeadset = hasWiredHeadset();
......@@ -280,9 +275,8 @@ public class AppRTCAudioManager {
}
@SuppressWarnings("deprecation")
// TODO(henrika): audioManager.abandonAudioFocus() is deprecated.
public void stop() {
Log.d(Config.LOGTAG, "stop");
Log.d(Config.LOGTAG, AppRTCAudioManager.class.getName() + ".stop()");
ThreadUtils.checkIsOnMainThread();
if (amState != AudioManagerState.RUNNING) {
Log.e(Config.LOGTAG, "Trying to stop AudioManager in incorrect state: " + amState);
......@@ -294,7 +288,7 @@ public class AppRTCAudioManager {
// Restore previously stored audio states.
setSpeakerphoneOn(savedIsSpeakerPhoneOn);
setMicrophoneMute(savedIsMicrophoneMute);
audioManager.setMode(savedAudioMode);
audioManager.setMode(AudioManager.MODE_NORMAL);
// Abandon audio focus. Gives the previous focus owner, if any, focus.
audioManager.abandonAudioFocus(audioFocusChangeListener);
audioFocusChangeListener = null;
......@@ -304,7 +298,6 @@ public class AppRTCAudioManager {
proximitySensor = null;
}
audioManagerEvents = null;
Log.d(Config.LOGTAG, "AudioManager stopped");
}
/**
......@@ -318,11 +311,7 @@ public class AppRTCAudioManager {
setSpeakerphoneOn(true);
break;
case EARPIECE:
setSpeakerphoneOn(false);
break;
case WIRED_HEADSET:
setSpeakerphoneOn(false);
break;
case BLUETOOTH:
setSpeakerphoneOn(false);
break;
......
......@@ -222,7 +222,7 @@ public class ChannelDiscoveryService {
continue;
}
for (final String mucService : xmppConnection.getMucServers()) {
Jid jid = Jid.of(mucService);
Jid jid = Jid.ofEscaped(mucService);
if (!localMucServices.containsKey(jid)) {
localMucServices.put(jid, account);
}
......
......@@ -1710,12 +1710,7 @@ public class XmppConnectionService extends Service {
for (Bookmark bookmark : account.getBookmarks()) {
storage.addChild(bookmark);
}
pushNodeAndEnforcePublishOptions(account, Namespace.BOOKMARKS, storage, PublishOptions.persistentWhitelistAccess());
}
private void pushNodeAndEnforcePublishOptions(final Account account, final String node, final Element element, final Bundle options) {
pushNodeAndEnforcePublishOptions(account, node, element, null, options, true);
pushNodeAndEnforcePublishOptions(account, Namespace.BOOKMARKS, storage, "current", PublishOptions.persistentWhitelistAccess());
}
......
......@@ -29,9 +29,9 @@ public final class BlockContactDialog {
builder.setTitle(isBlocked ? R.string.action_unblock_participant : R.string.action_block_participant);
value = blockable.getJid().toEscapedString();
res = isBlocked ? R.string.unblock_contact_text : R.string.block_contact_text;
} else if (blockable.getJid().getLocal() == null || blockable.getAccount().isBlocked(Jid.ofDomain(blockable.getJid().getDomain()))) {
} else if (blockable.getJid().getLocal() == null || blockable.getAccount().isBlocked(blockable.getJid().getDomain())) {
builder.setTitle(isBlocked ? R.string.action_unblock_domain : R.string.action_block_domain);
value = Jid.ofDomain(blockable.getJid().getDomain()).toString();
value =blockable.getJid().getDomain().toEscapedString();
res = isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text;
} else {
int resBlockAction = blockable instanceof Conversation && ((Conversation) blockable).isWithStranger() ? R.string.block_stranger : R.string.action_block_contact;
......
......@@ -35,7 +35,7 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem
@Override
public void onBackendConnected() {
for (final Account account : xmppConnectionService.getAccounts()) {
if (account.getJid().toString().equals(getIntent().getStringExtra(EXTRA_ACCOUNT))) {
if (account.getJid().toEscapedString().equals(getIntent().getStringExtra(EXTRA_ACCOUNT))) {
this.account = account;
break;
}
......
......@@ -73,7 +73,7 @@ public class ChooseAccountForProfilePictureActivity extends XmppActivity {
final Uri uri = startIntent == null ? null : startIntent.getData();
if (uri != null) {
Intent intent = new Intent(this, PublishProfilePictureActivity.class);
intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toString());
intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
intent.setData(uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
......
......@@ -75,7 +75,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im
intent.putExtra(EXTRA_CONVERSATION, conversation.getUuid());
intent.putExtra(EXTRA_SELECT_MULTIPLE, true);
intent.putExtra(EXTRA_SHOW_ENTER_JID, true);
intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString());
intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toEscapedString());
return intent;
}
......@@ -321,7 +321,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im
final Intent request = getIntent();
final Intent data = new Intent();
data.putExtra("contact", contactJid.toString());
data.putExtra(EXTRA_ACCOUNT, accountJid.toString());
data.putExtra(EXTRA_ACCOUNT, accountJid.toEscapedString());
data.putExtra(EXTRA_SELECT_MULTIPLE, false);
copy(request, data);
setResult(RESULT_OK, data);
......@@ -401,7 +401,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im
data.putExtra("contact", item.getJid().toString());
String account = request.getStringExtra(EXTRA_ACCOUNT);
if (account == null && item instanceof Contact) {
account = ((Contact) item).getAccount().getJid().asBareJid().toString();
account = ((Contact) item).getAccount().getJid().asBareJid().toEscapedString();
}
data.putExtra(EXTRA_ACCOUNT, account);
data.putExtra(EXTRA_SELECT_MULTIPLE, false);
......
......@@ -188,11 +188,11 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
showInactiveOmemo = savedInstanceState != null && savedInstanceState.getBoolean("show_inactive_omemo", false);
if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) {
try {
this.accountJid = Jid.of(getIntent().getExtras().getString(EXTRA_ACCOUNT));
this.accountJid = Jid.ofEscaped(getIntent().getExtras().getString(EXTRA_ACCOUNT));
} catch (final IllegalArgumentException ignored) {
}
try {
this.contactJid = Jid.of(getIntent().getExtras().getString("contact"));
this.contactJid = Jid.ofEscaped(getIntent().getExtras().getString("contact"));
} catch (final IllegalArgumentException ignored) {
}
}
......
......@@ -772,7 +772,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
contacts[i] = targets.get(i).toString();
}
intent.putExtra("contacts", contacts);
intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString());
intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toEscapedString());
intent.putExtra("conversation", conversation.getUuid());
startActivityForResult(intent, requestCode);
return true;
......@@ -1313,6 +1313,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
final boolean pinned = conversation.getBooleanAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP, false);
conversation.setAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP, !pinned);
activity.xmppConnectionService.updateConversation(conversation);
activity.invalidateOptionsMenu();
}
private void checkPermissionAndTriggerAudioCall() {
......@@ -2205,7 +2206,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
updateSnackBar(conversation);
return true;
case R.id.block_domain:
blockable = conversation.getAccount().getRoster().getContact(Jid.ofDomain(jid.getDomain()));
blockable = conversation.getAccount().getRoster().getContact(jid.getDomain());
break;
default:
blockable = conversation;
......
......@@ -121,7 +121,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
public void onClick(final View view) {
if (mAccount != null) {
final Intent intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class);
intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toString());
intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString());
startActivity(intent);
}
}
......@@ -409,7 +409,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
if (mAccount.isOptionSet(Account.OPTION_FIXED_USERNAME)) {
preset = jid.asBareJid();
} else {
preset = Jid.ofDomain(jid.getDomain());
preset = jid.getDomain();
}
final Intent intent = SignupUtils.getTokenRegistrationIntent(this, preset, mAccount.getKey(Account.PRE_AUTH_REGISTRATION_TOKEN));
StartConversationActivity.addInviteUri(intent, getIntent());
......@@ -445,7 +445,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString());
} else {
intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class);
intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toString());
intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString());
intent.putExtra("setup", true);
}
if (wasFirstAccount) {
......@@ -693,7 +693,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
recreate();
} else if (intent != null) {
try {
this.jidToEdit = Jid.of(intent.getStringExtra("jid"));
this.jidToEdit = Jid.ofEscaped(intent.getStringExtra("jid"));
} catch (final IllegalArgumentException | NullPointerException ignored) {
this.jidToEdit = null;
}
......@@ -754,7 +754,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
@Override
public void onSaveInstanceState(final Bundle savedInstanceState) {
if (mAccount != null) {
savedInstanceState.putString("account", mAccount.getJid().asBareJid().toString());
savedInstanceState.putString("account", mAccount.getJid().asBareJid().toEscapedString());
savedInstanceState.putBoolean("initMode", mInitMode);
savedInstanceState.putBoolean("showMoreTable", binding.serverInfoMore.getVisibility() == View.VISIBLE);
}
......@@ -765,7 +765,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
boolean init = true;
if (mSavedInstanceAccount != null) {
try {
this.mAccount = xmppConnectionService.findAccountByJid(Jid.of(mSavedInstanceAccount));
this.mAccount = xmppConnectionService.findAccountByJid(Jid.ofEscaped(mSavedInstanceAccount));
this.mInitMode = mSavedInstanceInit;
init = false;
} catch (IllegalArgumentException e) {
......@@ -833,7 +833,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
break;
case R.id.action_show_block_list:
final Intent showBlocklistIntent = new Intent(this, BlocklistActivity.class);
showBlocklistIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toString());
showBlocklistIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toEscapedString());
startActivity(showBlocklistIntent);
break;
case R.id.action_server_info_show_more:
......@@ -882,7 +882,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
private void gotoChangePassword(String newPassword) {
final Intent changePasswordIntent = new Intent(this, ChangePasswordActivity.class);
changePasswordIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toString());
changePasswordIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toEscapedString());
if (newPassword != null) {
changePasswordIntent.putExtra("password", newPassword);
}
......
......@@ -165,7 +165,7 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
final Conversation conversation;
Account account;
try {
account = xmppConnectionService.findAccountByJid(Jid.of(share.account));
account = xmppConnectionService.findAccountByJid(Jid.ofEscaped(share.account));
} catch (final IllegalArgumentException e) {
account = null;
}
......
......@@ -1000,7 +1000,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
intent.putExtra(ChooseContactActivity.EXTRA_SHOW_ENTER_JID, false);
intent.putExtra(ChooseContactActivity.EXTRA_SELECT_MULTIPLE, true);
intent.putExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME, name.trim());
intent.putExtra(ChooseContactActivity.EXTRA_ACCOUNT, account.getJid().asBareJid().toString());
intent.putExtra(ChooseContactActivity.EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
intent.putExtra(ChooseContactActivity.EXTRA_TITLE_RES_ID, R.string.choose_participants);
startActivityForResult(intent, REQUEST_CREATE_CONFERENCE);
}
......
......@@ -100,7 +100,7 @@ public class UriHandlerActivity extends AppCompatActivity {
return;
}
if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter("ibr"))) {
intent = SignupUtils.getTokenRegistrationIntent(this, Jid.ofDomain(jid.getDomain()), preauth);
intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preauth);
intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
startActivity(intent);
return;
......
......@@ -522,8 +522,8 @@ public abstract class XmppActivity extends ActionBarActivity {
public void switchToContactDetails(Contact contact, String messageFingerprint) {
Intent intent = new Intent(this, ContactDetailsActivity.class);
intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
intent.putExtra(EXTRA_ACCOUNT, contact.getAccount().getJid().asBareJid().toString());
intent.putExtra("contact", contact.getJid().toString());
intent.putExtra(EXTRA_ACCOUNT, contact.getAccount().getJid().asBareJid().toEscapedString());
intent.putExtra("contact", contact.getJid().toEscapedString());
intent.putExtra("fingerprint", messageFingerprint);
startActivity(intent);
}
......@@ -538,7 +538,7 @@ public abstract class XmppActivity extends ActionBarActivity {
public void switchToAccount(Account account, boolean init, String fingerprint) {
Intent intent = new Intent(this, EditAccountActivity.class);
intent.putExtra("jid", account.getJid().asBareJid().toString());
intent.putExtra("jid", account.getJid().asBareJid().toEscapedString());
intent.putExtra("init", init);
if (init) {
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION);
......
......@@ -75,8 +75,8 @@ import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.StylingHelper;
import eu.siacs.conversations.utils.TimeFrameUtils;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.mam.MamReference;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.mam.MamReference;
public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextView.CopyHandler {
......@@ -117,6 +117,10 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
public void setVolumeControl(final int stream) {
activity.setVolumeControlStream(stream);
}
public void setOnContactPictureClicked(OnContactPictureClicked listener) {
this.mOnContactPictureClickedListener = listener;
}
......
......@@ -363,12 +363,13 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
if (AudioPlayer.player == null || !AudioPlayer.player.isPlaying()) {
return;
}
int streamType;
final int streamType;
if (event.values[0] < 5f && event.values[0] != proximitySensor.getMaximumRange()) {
streamType = AudioManager.STREAM_VOICE_CALL;
} else {
streamType = AudioManager.STREAM_MUSIC;
}
messageAdapter.setVolumeControl(streamType);
double position = AudioPlayer.player.getCurrentPosition();
double duration = AudioPlayer.player.getDuration();
double progress = position / duration;
......@@ -407,6 +408,7 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
wakeLock.release();
}
}
messageAdapter.setVolumeControl(AudioManager.STREAM_MUSIC);
}
private ViewHolder getCurrentViewHolder() {
......
......@@ -29,10 +29,13 @@
package eu.siacs.conversations.utils;
import com.google.common.base.Strings;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.regex.Pattern;
import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.http.AesGcmURLStreamHandler;
import eu.siacs.conversations.http.P1S3UrlStreamHandler;
......@@ -45,7 +48,22 @@ public class MessageUtils {
public static String prepareQuote(Message message) {
final StringBuilder builder = new StringBuilder();
final String body = message.getMergedBody().toString();
final String body;
if (message.hasMeCommand()) {
final String nick;
if (message.getStatus() == Message.STATUS_RECEIVED) {
if (message.getConversation().getMode() == Conversational.MODE_MULTI) {
nick = Strings.nullToEmpty(message.getCounterpart().getResource());
} else {
nick = message.getContact().getPublicDisplayName();
}
} else {
nick = UIHelper.getMessageDisplayName(message);
}
body = nick + " " + message.getBody().substring(Message.ME_COMMAND.length());
} else {
body = message.getMergedBody().toString();;
}
for (String line : body.split("\n")) {
if (line.length() <= 0) {
continue;
......
......@@ -111,7 +111,7 @@ public class XmppConnection implements Runnable {
public void onIqPacketReceived(Account account, IqPacket packet) {
if (packet.getType() == IqPacket.TYPE.RESULT) {
account.setOption(Account.OPTION_REGISTER, false);
Log.d(Config.LOGTAG, account.getJid().asBareJid()+": successfully registered new account on server");
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully registered new account on server");
throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL);
} else {
final List<String> PASSWORD_TOO_WEAK_MSGS = Arrays.asList(
......@@ -272,7 +272,7 @@ public class XmppConnection implements Runnable {
final int port = account.getPort();
final boolean directTls = Resolver.useDirectTls(port);
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": connect to " + destination + " via Tor. directTls="+directTls);
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": connect to " + destination + " via Tor. directTls=" + directTls);
localSocket = SocksSocketFactory.createSocketOverTor(destination, port);
if (directTls) {
......@@ -356,12 +356,8 @@ public class XmppConnection implements Runnable {
this.changeStatus(Account.State.SERVER_NOT_FOUND);
} catch (final SocksSocketFactory.SocksProxyNotFoundException e) {
this.changeStatus(Account.State.TOR_NOT_AVAILABLE);
} catch (final IOException e) {
Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": socket io :" + e.getMessage());
this.changeStatus(Account.State.OFFLINE);
this.attempt = Math.max(0, this.attempt - 1);
} catch (final XmlPullParserException e) {
Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": xml parser :" + e.getMessage());
} catch (final IOException | XmlPullParserException e) {
Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": " + e.getMessage());
this.changeStatus(Account.State.OFFLINE);
this.attempt = Math.max(0, this.attempt - 1);
} finally {
......@@ -567,7 +563,7 @@ public class XmppConnection implements Runnable {
if (mWaitingForSmCatchup.compareAndSet(true, false)) {
final int messageCount = mSmCatchupMessageCounter.get();
final int pendingIQs = packetCallbacks.size();
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": SM catchup complete (messages=" + messageCount + ", pending IQs="+pendingIQs+")");
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": SM catchup complete (messages=" + messageCount + ", pending IQs=" + pendingIQs + ")");
accountUiNeedsRefresh = true;
if (messageCount > 0) {
mXmppConnectionService.getNotificationService().finishBacklog(true, account);
......@@ -812,7 +808,7 @@ public class XmppConnection implements Runnable {
if (isSecure) {
register();
} else {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to find STARTTLS for registration process "+ XmlHelper.printElementNames(this.streamFeatures));
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to find STARTTLS for registration process " + XmlHelper.printElementNames(this.streamFeatures));
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
}
} else if (!this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER)) {
......@@ -831,7 +827,7 @@ public class XmppConnection implements Runnable {
if (this.streamFeatures.hasChild("bind") && isSecure) {
sendBindRequest();
} else {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to find bind feature "+ XmlHelper.printElementNames(this.streamFeatures));
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to find bind feature " + XmlHelper.printElementNames(this.streamFeatures));
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
}
}
......@@ -847,7 +843,7 @@ public class XmppConnection implements Runnable {
saslMechanism = new ScramSha256(tagWriter, account, mXmppConnectionService.getRNG());
} else if (mechanisms.contains("SCRAM-SHA-1")) {
saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG());
} else if (mechanisms.contains("PLAIN") && !account.getJid().getDomain().equals("nimbuzz.com")) {
} else if (mechanisms.contains("PLAIN") && !account.getJid().getDomain().toEscapedString().equals("nimbuzz.com")) {
saslMechanism = new Plain(tagWriter, account);
} else if (mechanisms.contains("DIGEST-MD5")) {
saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG());
......@@ -870,7 +866,7 @@ public class XmppConnection implements Runnable {
}
tagWriter.writeElement(auth);
} else {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to find supported SASL mechanism in "+mechanisms);
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to find supported SASL mechanism in " + mechanisms);
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
}
}
......@@ -895,7 +891,7 @@ public class XmppConnection implements Runnable {
sendRegistryRequest();
} else {
final Element error = response.getError();
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": failed to pre auth. "+error);
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": failed to pre auth. " + error);
throw new StateChangingError(Account.State.REGISTRATION_INVALID_TOKEN);
}
}, true);
......@@ -1132,7 +1128,7 @@ public class XmppConnection implements Runnable {
}
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery");
mPendingServiceDiscoveries.set(0);
if (smVersion == 0 || Patches.DISCO_EXCEPTIONS.contains(account.getJid().getDomain())) {
if (smVersion == 0 || Patches.DISCO_EXCEPTIONS.contains(account.getJid().getDomain().toEscapedString())) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": do not wait for service discovery");
mWaitForDisco.set(false);
} else {
......@@ -1218,10 +1214,10 @@ public class XmppConnection implements Runnable {
IqPacket request = new IqPacket(IqPacket.TYPE.GET);
request.addChild("prefs", MessageArchiveService.Version.MAM_2.namespace);
sendIqPacket(request, (account, response) -> {
if (response.getType() == IqPacket.TYPE.RESULT) {
Element prefs = response.findChild("prefs", MessageArchiveService.Version.MAM_2.namespace);
isMamPreferenceAlways = "always".equals(prefs == null ? null : prefs.getAttribute("default"));
}
if (response.getType() == IqPacket.TYPE.RESULT) {
Element prefs = response.findChild("prefs", MessageArchiveService.Version.MAM_2.namespace);
isMamPreferenceAlways = "always".equals(prefs == null ? null : prefs.getAttribute("default"));
}
});
}
......@@ -1315,7 +1311,8 @@ public class XmppConnection implements Runnable {
} else if (streamError.hasChild("policy-violation")) {
this.lastConnect = SystemClock.elapsedRealtime();
final String text = streamError.findChildContent("text");
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": policy violation. "+text);
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": policy violation. " + text);
failPendingMessages(text);
throw new StateChangingException(Account.State.POLICY_VIOLATION);
} else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError.toString());
......@@ -1323,6 +1320,24 @@ public class XmppConnection implements Runnable {
}
}
private void failPendingMessages(final String error) {
synchronized (this.mStanzaQueue) {
for (int i = 0; i < mStanzaQueue.size(); ++i) {
final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
if (stanza instanceof MessagePacket) {
final MessagePacket packet = (MessagePacket) stanza;
final String id = packet.getId();
final Jid to = packet.getTo();
mXmppConnectionService.markMessage(account,
to.asBareJid(),
id,
Message.STATUS_SEND_FAILED,
error);
}
}
}
}
private void sendStartStream() throws IOException {
final Tag stream = Tag.start("stream:stream");
stream.setAttribute("to", account.getServer());
......@@ -1880,7 +1895,7 @@ public class XmppConnection implements Runnable {
}
public boolean externalServiceDiscovery() {
return hasDiscoFeature(account.getDomain(),Namespace.EXTERNAL_SERVICE_DISCOVERY);
return hasDiscoFeature(account.getDomain(), Namespace.EXTERNAL_SERVICE_DISCOVERY);
}
}
}
......@@ -38,6 +38,7 @@ import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
......@@ -48,11 +49,10 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
import eu.siacs.conversations.xmpp.Jid;
public class JingleConnectionManager extends AbstractConnectionManager {
static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor();
final ToneManager toneManager = new ToneManager();
final ToneManager toneManager;
private final HashMap<RtpSessionProposal, DeviceDiscoveryState> rtpSessionProposals = new HashMap<>();
private final ConcurrentHashMap<AbstractJingleConnection.Id, AbstractJingleConnection> connections = new ConcurrentHashMap<>();
......@@ -64,6 +64,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
public JingleConnectionManager(XmppConnectionService service) {
super(service);
this.toneManager = new ToneManager(service);
}
static String nextRandomId() {
......@@ -333,11 +334,11 @@ public class JingleConnectionManager extends AbstractConnectionManager {
}
}
} else if (addressedDirectly && "reject".equals(message.getName())) {
final RtpSessionProposal proposal = new RtpSessionProposal(account, from.asBareJid(), sessionId);
final RtpSessionProposal proposal = getRtpSessionProposal(account, from.asBareJid(), sessionId);
synchronized (rtpSessionProposals) {
if (rtpSessionProposals.remove(proposal) != null) {
if (proposal != null && rtpSessionProposals.remove(proposal) != null) {
writeLogMissedOutgoing(account, proposal.with, proposal.sessionId, serverMsgId, timestamp);
toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY);
toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY, proposal.media);
mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, proposal.with, proposal.sessionId, RtpEndUserState.DECLINED_OR_BUSY);
} else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver reject");
......@@ -511,7 +512,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
private void retractSessionProposal(RtpSessionProposal rtpSessionProposal) {
final Account account = rtpSessionProposal.account;
toneManager.transition(RtpEndUserState.ENDED);
toneManager.transition(RtpEndUserState.ENDED, rtpSessionProposal.media);
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retracting rtp session proposal with " + rtpSessionProposal.with);
this.rtpSessionProposals.remove(rtpSessionProposal);
final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal);
......@@ -527,7 +528,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
final DeviceDiscoveryState preexistingState = entry.getValue();
if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) {
final RtpEndUserState endUserState = preexistingState.toEndUserState();
toneManager.transition(endUserState);
toneManager.transition(endUserState, media);
mXmppConnectionService.notifyJingleRtpConnectionUpdate(
account,
with,
......@@ -623,7 +624,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
}
this.rtpSessionProposals.put(sessionProposal, target);
final RtpEndUserState endUserState = target.toEndUserState();
toneManager.transition(endUserState);
toneManager.transition(endUserState, sessionProposal.media);
mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, sessionProposal.with, sessionProposal.sessionId, endUserState);
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": flagging session " + sessionId + " as " + target);
}
......
package eu.siacs.conversations.xmpp.jingle;
import android.content.Context;
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;
......@@ -16,27 +16,23 @@ import static java.util.Arrays.asList;
class ToneManager {
private final ToneGenerator toneGenerator;
private final Context context;
private ToneState state = null;
private ScheduledFuture<?> currentTone;
private ScheduledFuture<?> currentResetFuture;
private boolean appRtcAudioManagerHasControl = false;
ToneManager() {
ToneManager(final Context context) {
ToneGenerator toneGenerator;
try {
toneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, 35);
toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 60);
} catch (final RuntimeException e) {
Log.e(Config.LOGTAG, "unable to instantiate ToneGenerator", e);
toneGenerator = null;
}
this.toneGenerator = toneGenerator;
}
void transition(final RtpEndUserState state) {
transition(of(true, state, Collections.emptySet()));
}
void transition(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
transition(of(isInitiator, state, media));
this.context = context;
}
private static ToneState of(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
......@@ -65,7 +61,15 @@ class ToneManager {
return ToneState.NULL;
}
private synchronized void transition(ToneState state) {
void transition(final RtpEndUserState state, final Set<Media> media) {
transition(of(true, state, media), media);
}
void transition(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
transition(of(isInitiator, state, media), media);
}
private synchronized void transition(ToneState state, final Set<Media> media) {
if (this.state == state) {
return;
}
......@@ -74,6 +78,9 @@ class ToneManager {
}
cancelCurrentTone();
Log.d(Config.LOGTAG, getClass().getName() + ".transition(" + state + ")");
if (state != ToneState.NULL) {
configureAudioManagerForCall(media);
}
switch (state) {
case RINGING:
scheduleWaitingTone();
......@@ -87,10 +94,21 @@ class ToneManager {
case ENDING_CALL:
scheduleEnding();
break;
case NULL:
if (noResetScheduled()) {
resetAudioManager();
}
break;
default:
throw new IllegalStateException("Unable to handle transition to "+state);
}
this.state = state;
}
void setAppRtcAudioManagerHasControl(final boolean appRtcAudioManagerHasControl) {
this.appRtcAudioManagerHasControl = appRtcAudioManagerHasControl;
}
private void scheduleConnected() {
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
startTone(ToneGenerator.TONE_PROP_PROMPT, 200);
......@@ -101,12 +119,14 @@ class ToneManager {
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
}, 0, TimeUnit.SECONDS);
this.currentResetFuture = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(this::resetAudioManager, 375, TimeUnit.MILLISECONDS);
}
private void scheduleBusy() {
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500);
}, 0, TimeUnit.SECONDS);
this.currentResetFuture = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(this::resetAudioManager, 2500, TimeUnit.MILLISECONDS);
}
private void scheduleWaitingTone() {
......@@ -115,6 +135,10 @@ class ToneManager {
}, 0, 3, TimeUnit.SECONDS);
}
private boolean noResetScheduled() {
return this.currentResetFuture == null || this.currentResetFuture.isDone();
}
private void cancelCurrentTone() {
if (currentTone != null) {
currentTone.cancel(true);
......@@ -132,6 +156,35 @@ class ToneManager {
}
}
private void configureAudioManagerForCall(final Set<Media> media) {
if (appRtcAudioManagerHasControl) {
Log.d(Config.LOGTAG, ToneManager.class.getName() + ": do not configure audio manager because RTC has control");
return;
}
final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
if (audioManager == null) {
return;
}
final boolean isSpeakerPhone = media.contains(Media.VIDEO);
Log.d(Config.LOGTAG, ToneManager.class.getName() + ": putting AudioManager into communication mode. speaker=" + isSpeakerPhone);
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
audioManager.setSpeakerphoneOn(isSpeakerPhone);
}
private void resetAudioManager() {
if (appRtcAudioManagerHasControl) {
Log.d(Config.LOGTAG, ToneManager.class.getName() + ": do not reset audio manager because RTC has control");
return;
}
final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
if (audioManager == null) {
return;
}
Log.d(Config.LOGTAG, ToneManager.class.getName() + ": putting AudioManager back into normal mode");
audioManager.setMode(AudioManager.MODE_NORMAL);
audioManager.setSpeakerphoneOn(false);
}
private enum ToneState {
NULL, RINGING, CONNECTED, BUSY, ENDING_CALL
}
......
......@@ -8,7 +8,6 @@ import android.util.Log;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.Futures;
......@@ -41,6 +40,8 @@ import org.webrtc.SessionDescription;
import org.webrtc.SurfaceTextureHelper;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;
import org.webrtc.audio.JavaAudioDeviceModule;
import org.webrtc.voiceengine.WebRtcAudioEffects;
import java.util.ArrayList;
import java.util.Collections;
......@@ -52,11 +53,28 @@ import javax.annotation.Nullable;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.services.AppRTCAudioManager;
import eu.siacs.conversations.services.XmppConnectionService;
public class WebRTCWrapper {
private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName();
//we should probably keep this in sync with: https://github.com/signalapp/Signal-Android/blob/master/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java#L296
private static final Set<String> HARDWARE_AEC_BLACKLIST = new ImmutableSet.Builder<String>()
.add("Pixel")
.add("Pixel XL")
.add("Moto G5")
.add("Moto G (5S) Plus")
.add("Moto G4")
.add("TA-1053")
.add("Mi A1")
.add("Mi A2")
.add("E5823") // Sony z5 compact
.add("Redmi Note 5")
.add("FP2") // Fairphone FP2
.add("MI 5")
.build();
private static final int CAPTURING_RESOLUTION = 1920;
private static final int CAPTURING_MAX_FRAME_RATE = 30;
......@@ -157,6 +175,7 @@ public class WebRTCWrapper {
private PeerConnection peerConnection = null;
private AudioTrack localAudioTrack = null;
private AppRTCAudioManager appRTCAudioManager = null;
private ToneManager toneManager = null;
private Context context = null;
private EglBase eglBase = null;
private CapturerChoice capturerChoice;
......@@ -165,18 +184,44 @@ public class WebRTCWrapper {
this.eventCallback = eventCallback;
}
public void setup(final Context context, final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) throws InitializationException {
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);
}
}
@Nullable
private static CapturerChoice of(CameraEnumerator enumerator, final String deviceName, Set<String> availableCameras) {
final CameraVideoCapturer capturer = enumerator.createCapturer(deviceName, null);
if (capturer == null) {
return null;
}
final ArrayList<CameraEnumerationAndroid.CaptureFormat> choices = new ArrayList<>(enumerator.getSupportedFormats(deviceName));
Collections.sort(choices, (a, b) -> b.width - a.width);
for (final CameraEnumerationAndroid.CaptureFormat captureFormat : choices) {
if (captureFormat.width <= CAPTURING_RESOLUTION) {
return new CapturerChoice(capturer, captureFormat, availableCameras);
}
}
return null;
}
public void setup(final XmppConnectionService service, final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) throws InitializationException {
try {
PeerConnectionFactory.initialize(
PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions()
PeerConnectionFactory.InitializationOptions.builder(service).createInitializationOptions()
);
} catch (final UnsatisfiedLinkError e) {
throw new InitializationException(e);
}
this.eglBase = EglBase.create();
this.context = context;
this.context = service;
this.toneManager = service.getJingleConnectionManager().toneManager;
mainHandler.post(() -> {
appRTCAudioManager = AppRTCAudioManager.create(context, speakerPhonePreference);
appRTCAudioManager = AppRTCAudioManager.create(service, speakerPhonePreference);
toneManager.setAppRtcAudioManagerHasControl(true);
appRTCAudioManager.start(audioManagerEvents);
eventCallback.onAudioDeviceChanged(appRTCAudioManager.getSelectedAudioDevice(), appRTCAudioManager.getAudioDevices());
});
......@@ -186,9 +231,15 @@ public class WebRTCWrapper {
Preconditions.checkState(this.eglBase != null);
Preconditions.checkNotNull(media);
Preconditions.checkArgument(media.size() > 0, "media can not be empty when initializing peer connection");
final boolean setUseHardwareAcousticEchoCanceler = WebRtcAudioEffects.canUseAcousticEchoCanceler() && !HARDWARE_AEC_BLACKLIST.contains(Build.MODEL);
Log.d(Config.LOGTAG, String.format("setUseHardwareAcousticEchoCanceler(%s) model=%s", setUseHardwareAcousticEchoCanceler, Build.MODEL));
PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder()
.setVideoDecoderFactory(new DefaultVideoDecoderFactory(eglBase.getEglBaseContext()))
.setVideoEncoderFactory(new DefaultVideoEncoderFactory(eglBase.getEglBaseContext(), true, true))
.setAudioDeviceModule(JavaAudioDeviceModule.builder(context)
.setUseHardwareAcousticEchoCanceler(setUseHardwareAcousticEchoCanceler)
.createAudioDeviceModule()
)
.createPeerConnectionFactory();
......@@ -221,6 +272,7 @@ public class WebRTCWrapper {
final PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; //XEP-0176 doesn't support tcp
rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, peerConnectionObserver);
if (peerConnection == null) {
throw new InitializationException("Unable to create PeerConnection");
......@@ -241,6 +293,7 @@ public class WebRTCWrapper {
this.peerConnection = null;
}
if (audioManager != null) {
toneManager.setAppRtcAudioManagerHasControl(false);
mainHandler.post(audioManager::stop);
}
this.localVideoTrack = null;
......@@ -258,14 +311,6 @@ public class WebRTCWrapper {
}
}
private static void dispose(final PeerConnection peerConnection) {
try {
peerConnection.dispose();
} catch (final IllegalStateException e) {
Log.e(Config.LOGTAG, "unable to dispose of peer connection", e);
}
}
synchronized void verifyClosed() {
if (this.peerConnection != null
|| this.eglBase != null
......@@ -469,22 +514,6 @@ public class WebRTCWrapper {
}
}
@Nullable
private static CapturerChoice of(CameraEnumerator enumerator, final String deviceName, Set<String> availableCameras) {
final CameraVideoCapturer capturer = enumerator.createCapturer(deviceName, null);
if (capturer == null) {
return null;
}
final ArrayList<CameraEnumerationAndroid.CaptureFormat> choices = new ArrayList<>(enumerator.getSupportedFormats(deviceName));
Collections.sort(choices, (a, b) -> b.width - a.width);
for (final CameraEnumerationAndroid.CaptureFormat captureFormat : choices) {
if (captureFormat.width <= CAPTURING_RESOLUTION) {
return new CapturerChoice(capturer, captureFormat, availableCameras);
}
}
return null;
}
public PeerConnection.PeerConnectionState getState() {
return requirePeerConnection().connectionState();
}
......
......@@ -469,6 +469,7 @@
<string name="pref_show_connection_options">Paramètres de connexion avancés</string>
<string name="pref_show_connection_options_summary">Montrer le nom d\'hôte et le port lors du paramétrage d\'un compte</string>
<string name="hostname_example">xmpp.example.com</string>
<string name="action_add_account_with_certificate">Se connecter avec certificat</string>
<string name="unable_to_parse_certificate">Impossible d\'analyser le certificat</string>
<string name="mam_prefs">Paramètres d\'archivage</string>
<string name="server_side_mam_prefs">Paramètres d\'archivage du serveur</string>
......
......@@ -209,12 +209,12 @@
<string name="install_openkeychain">Mensaxe cifrada. Instala OpenKeychain para descifrala.</string>
<string name="openpgp_messages_found">Atoparonse novas mensaxes cifradas cn OpenPGP</string>
<string name="openpgp_key_id">ID da chave OpenPGP</string>
<string name="omemo_fingerprint">Pegada OMEMO</string>
<string name="omemo_fingerprint_x509">v\\pegada OMEMO </string>
<string name="omemo_fingerprint_selected_message">Pegada OMEMO da mensaxe</string>
<string name="omemo_fingerprint_x509_selected_message">v\\Pegada OMEMO da mensaxe</string>
<string name="omemo_fingerprint">Impresión dixital OMEMO</string>
<string name="omemo_fingerprint_x509">v\\impresión OMEMO </string>
<string name="omemo_fingerprint_selected_message">Impresión OMEMO da mensaxe</string>
<string name="omemo_fingerprint_x509_selected_message">v\\Impresión OMEMO da mensaxe</string>
<string name="other_devices">Outros dispositivos</string>
<string name="trust_omemo_fingerprints">Confiar en pegadas OMEMO</string>
<string name="trust_omemo_fingerprints">Confiar en impresións dixitais OMEMO</string>
<string name="fetching_keys">Obtendo chaves...</string>
<string name="done">Feito</string>
<string name="decrypt">Descifrar</string>
......@@ -287,7 +287,7 @@
<string name="pref_expert_options_other">Outro</string>
<string name="pref_autojoin">Sincronizar cos marcadores</string>
<string name="pref_autojoin_summary">Unirte as conversas en grupo automáticamente se o marcador así o indica</string>
<string name="toast_message_omemo_fingerprint">Copiouse a pegada dixital OMEMO ao portapapeis</string>
<string name="toast_message_omemo_fingerprint">Copiouse a impresión dixital OMEMO ao portapapeis</string>
<string name="conference_banned">Non podes acceder a esta conversa en grupo</string>
<string name="conference_members_only">Esta conversa en grupo é so para membros</string>
<string name="conference_resource_constraint">Restrición do recurso</string>
......@@ -348,7 +348,7 @@
<string name="no_conference_server_found">Non se atopou ningún servidor de conversa en grupo</string>
<string name="conference_creation_failed">Non se puido crear a conversa en grupo</string>
<string name="account_image_description">Avatar da conta</string>
<string name="copy_omemo_clipboard_description">Copiar pegada OMEMO ao portapapeis</string>
<string name="copy_omemo_clipboard_description">Copiar impresión OMEMO ao portapapeis</string>
<string name="regenerate_omemo_key">Rexenerar a chave OMEMO</string>
<string name="clear_other_devices">Limplar dispositivos</string>
<string name="clear_other_devices_desc">Tes a certeza de que queres eliminar os outros dispositivos OMEMO publicados? A próxima vez que un dos teus dispositivos se conecte, deberá voltar a anunciarse, mais podería non recibir mensaxes mentras tanto.</string>
......@@ -523,7 +523,7 @@
<string name="this_field_is_required">Este campo é requerido</string>
<string name="correct_message">Correxir mensaxe</string>
<string name="send_corrected_message">Enviar mensaxe correxida</string>
<string name="no_keys_just_confirm">Xa validaches as pegadas destas persoas de xeito seguro para confiar nelas. Ao escoller \"Feito\" estás simplemente confirmando que %s é parte desta conversa en grupo.</string>
<string name="no_keys_just_confirm">Xa validaches as impresións dixitais destas persoas de xeito seguro para confiar nelas. Ao escoller \"Feito\" estás simplemente confirmando que %s é parte desta conversa en grupo.</string>
<string name="this_account_is_disabled">Desactivou esta conta</string>
<string name="security_error_invalid_file_access">Fallo de seguridade: Acceso non válido ao ficheiro!</string>
<string name="no_application_to_share_uri">Non se atopou unha app para compartir URI</string>
......@@ -592,10 +592,10 @@
<string name="device_does_not_support_data_saver">O seu dispositivo non admite deshabilitar o aforro de datos para Conversations.</string>
<string name="error_unable_to_create_temporary_file">Non se puido crear o ficheiro temporal</string>
<string name="this_device_has_been_verified">Este dispositivo foi verificado</string>
<string name="copy_fingerprint">Copiar pegada dixital</string>