...
 
Commits (30)
......@@ -11,7 +11,7 @@ android:
- '.+'
before_script:
- mkdir libs
- wget -O libs/libwebrtc-m81.aar http://gultsch.de/files/libwebrtc-m81.aar
- wget -O libs/libwebrtc-m83.aar http://gultsch.de/files/libwebrtc-m83.aar
script:
- ./gradlew assembleConversationsFreeSystemRelease
- ./gradlew assembleQuicksyFreeCompatRelease
......
# Changelog
### Version 2.8.7
* Show help button if A/V call fails
* Fixed some annoying crashes
* Fixed Jingle connections (file transfer + calls) with bare JIDs
### Version 2.8.6
* Offer to record voice message when callee is busy
......
......@@ -80,7 +80,7 @@ dependencies {
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')
//implementation fileTree(include: ['libwebrtc-m83.aar'], dir: 'libs')
implementation 'org.webrtc:google-webrtc:1.0.+'
}
......@@ -96,8 +96,8 @@ android {
defaultConfig {
minSdkVersion 16
targetSdkVersion 25
versionCode 390
versionName "2.8.6"
versionCode 393
versionName "2.8.7"
archivesBaseName += "-$versionName"
applicationId "eu.sum7.conversations"
resValue "string", "applicationId", applicationId
......
• Show help button if A/V call fails
• Fixed some annoying crashes
• Fixed Jingle connections (file transfer + calls) with bare JIDs
package eu.siacs.conversations;
import android.graphics.Bitmap;
import android.net.Uri;
import java.util.Collections;
import java.util.List;
......@@ -35,11 +36,11 @@ public final class Config {
public static final String LOGTAG = BuildConfig.LOGTAG;
public static final Jid BUG_REPORTS = Jid.of("bugs@chat.sum7.eu");
public static final Uri HELP = Uri.parse("https://sum7.eu/chat");
public static final String DOMAIN_LOCK = null; //only allow account creation for this domain
public static final String MAGIC_CREATE_DOMAIN = "chat.sum7.eu";
public static final String QUICKSY_DOMAIN = "quicksy.im";
public static final Jid QUICKSY_DOMAIN = Jid.of("quicksy.im");
public static final String CHANNEL_DISCOVERY = "https://search.jabber.network";
......
......@@ -134,7 +134,7 @@ public class Contact implements ListItem, Blockable {
return this.systemName;
} else if (!TextUtils.isEmpty(this.serverName)) {
return this.serverName;
} else if (!TextUtils.isEmpty(this.presenceName) && ((QuickConversationsService.isQuicksy() && Config.QUICKSY_DOMAIN.equals(jid.getDomain().toEscapedString())) ||mutualPresenceSubscription())) {
} else if (!TextUtils.isEmpty(this.presenceName) && ((QuickConversationsService.isQuicksy() && JidHelper.isQuicksyDomain(jid.getDomain())) ||mutualPresenceSubscription())) {
return this.presenceName;
} else if (jid.getLocal() != null) {
return JidHelper.localPartOrFallback(jid);
......
......@@ -1006,7 +1006,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
&& !contact.isOwnServer()
&& !contact.showInContactList()
&& !contact.isSelf()
&& !Config.QUICKSY_DOMAIN.equals(contact.getJid().toEscapedString())
&& !JidHelper.isQuicksyDomain(contact.getJid())
&& sentMessagesCount() == 0;
}
......
......@@ -6,6 +6,9 @@ import android.graphics.Color;
import android.text.SpannableStringBuilder;
import android.util.Log;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import org.json.JSONException;
import java.lang.ref.WeakReference;
......@@ -21,6 +24,7 @@ import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.ui.util.PresenceSelector;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.Emoticons;
import eu.siacs.conversations.utils.GeoHelper;
......@@ -531,7 +535,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
public Set<ReadByMarker> getReadByMarkers() {
return Collections.unmodifiableSet(this.readByMarkers);
return ImmutableSet.copyOf(this.readByMarkers);
}
boolean similar(Message message) {
......@@ -745,19 +749,12 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
public boolean fixCounterpart() {
Presences presences = conversation.getContact().getPresences();
if (counterpart != null && presences.has(counterpart.getResource())) {
final Presences presences = conversation.getContact().getPresences();
if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
return true;
} else if (presences.size() >= 1) {
try {
counterpart = Jid.of(conversation.getJid().getLocal(),
conversation.getJid().getDomain(),
presences.toResourceArray()[0]);
return true;
} catch (IllegalArgumentException e) {
counterpart = null;
return false;
}
counterpart = PresenceSelector.getNextCounterpart(getContact(),presences.toResourceArray()[0]);
return true;
} else {
counterpart = null;
return false;
......
......@@ -836,6 +836,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
for (Element child : packet.getChildren()) {
if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace()) && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) {
final String action = child.getName();
final String sessionId = child.getAttribute("id");
if (sessionId == null) {
break;
}
if (query == null) {
if (serverMsgId == null) {
serverMsgId = extractStanzaId(account, packet);
......@@ -845,10 +849,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
processMessageReceipts(account, packet, query);
}
} else if (query.isCatchup()) {
final String sessionId = child.getAttribute("id");
if (sessionId == null) {
break;
}
if ("propose".equals(action)) {
final Element description = child.findChild("description");
final String namespace = description == null ? null : description.getNamespace();
......@@ -872,7 +872,6 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
c.add(message);
mXmppConnectionService.databaseBackend.createMessage(message);
}
} else if ("proceed".equals(action)) {
//status needs to be flipped to find the original propose
final Conversation c = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), false, false);
......@@ -890,6 +889,37 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
}
} else {
//MAM reloads (non catchups
if ("propose".equals(action)) {
final Element description = child.findChild("description");
final String namespace = description == null ? null : description.getNamespace();
if (Namespace.JINGLE_APPS_RTP.equals(namespace)) {
final Conversation c = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), false, false);
final Message preExistingMessage = c.findRtpSession(sessionId, status);
if (preExistingMessage != null) {
preExistingMessage.setServerMsgId(serverMsgId);
mXmppConnectionService.updateMessage(preExistingMessage);
break;
}
final Message message = new Message(
c,
status,
Message.TYPE_RTP_SESSION,
sessionId
);
message.setServerMsgId(serverMsgId);
message.setTime(timestamp);
message.setBody(new RtpSessionStatus(true, 0).toString());
if (query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
c.prepend(query.getActualInThisQuery(), message);
} else {
c.add(message);
}
query.incrementActualMessageCount();
mXmppConnectionService.databaseBackend.createMessage(message);
}
}
}
break;
}
......
......@@ -704,7 +704,7 @@ public class FileBackend {
return pos > 0 ? filename.substring(pos + 1) : null;
}
private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException {
private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException, NotAnImageFileException {
file.getParentFile().mkdirs();
InputStream is = null;
OutputStream os = null;
......@@ -724,7 +724,7 @@ public class FileBackend {
originalBitmap = BitmapFactory.decodeStream(is, null, options);
is.close();
if (originalBitmap == null) {
throw new FileCopyException(R.string.error_not_an_image_file);
throw new NotAnImageFileException();
}
Bitmap scaledBitmap = resize(originalBitmap, Config.IMAGE_SIZE);
int rotation = getRotation(image);
......@@ -763,12 +763,12 @@ public class FileBackend {
}
}
public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException {
public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException, NotAnImageFileException {
Log.d(Config.LOGTAG, "copy image (" + image.toString() + ") to private storage " + file.getAbsolutePath());
copyImageToPrivateStorage(file, image, 0);
}
public void copyImageToPrivateStorage(Message message, Uri image) throws FileCopyException {
public void copyImageToPrivateStorage(Message message, Uri image) throws FileCopyException, NotAnImageFileException {
switch (Config.IMAGE_FORMAT) {
case JPEG:
message.setRelativeFilePath(message.getUuid() + ".jpg");
......@@ -1420,11 +1420,14 @@ public class FileBackend {
}
}
public class FileCopyException extends Exception {
private static final long serialVersionUID = -1010013599132881427L;
public static class NotAnImageFileException extends Exception {
}
public static class FileCopyException extends Exception {
private int resId;
public FileCopyException(int resId) {
private FileCopyException(int resId) {
this.resId = resId;
}
......
......@@ -567,19 +567,23 @@ public class XmppConnectionService extends Service {
mFileAddingExecutor.execute(() -> {
try {
getFileBackend().copyImageToPrivateStorage(message, uri);
if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
final PgpEngine pgpEngine = getPgpEngine();
if (pgpEngine != null) {
pgpEngine.encrypt(message, callback);
} else if (callback != null) {
callback.error(R.string.unable_to_connect_to_keychain, null);
}
} else {
sendMessage(message);
callback.success(message);
}
} catch (FileBackend.NotAnImageFileException e) {
attachFileToConversation(conversation, uri, mimeType, callback);
return;
} catch (final FileBackend.FileCopyException e) {
callback.error(e.getResId(), message);
return;
}
if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
final PgpEngine pgpEngine = getPgpEngine();
if (pgpEngine != null) {
pgpEngine.encrypt(message, callback);
} else if (callback != null) {
callback.error(R.string.unable_to_connect_to_keychain, null);
}
} else {
sendMessage(message);
callback.success(message);
}
});
}
......@@ -2774,6 +2778,7 @@ public class XmppConnectionService extends Service {
updateConversationUi();
}
}
private void fetchConferenceMembers(final Conversation conversation) {
final Account account = conversation.getAccount();
final AxolotlService axolotlService = account.getAxolotlService();
......@@ -3299,6 +3304,10 @@ public class XmppConnectionService extends Service {
updateConversationUi();
}
public void createMessageAsync(final Message message) {
mDatabaseWriterExecutor.execute(() -> databaseBackend.createMessage(message));
}
public void updateMessage(Message message, String uuid) {
if (!databaseBackend.updateMessage(message, uuid)) {
Log.e(Config.LOGTAG, "error updated message in DB after edit");
......@@ -4137,7 +4146,7 @@ public class XmppConnectionService extends Service {
}
}
if (Config.QUICKSY_DOMAIN != null) {
hosts.remove(Config.QUICKSY_DOMAIN); //we only want to show this when we type a e164 number
hosts.remove(Config.QUICKSY_DOMAIN.toEscapedString()); //we only want to show this when we type a e164 number
}
if (Config.DOMAIN_LOCK != null) {
hosts.add(Config.DOMAIN_LOCK);
......@@ -4436,34 +4445,38 @@ public class XmppConnectionService extends Service {
public void fetchCaps(Account account, final Jid jid, final Presence presence) {
final Pair<String, String> key = new Pair<>(presence.getHash(), presence.getVer());
ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key);
final ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key);
if (disco != null) {
presence.setServiceDiscoveryResult(disco);
} else {
if (!account.inProgressDiscoFetches.contains(key)) {
account.inProgressDiscoFetches.add(key);
IqPacket request = new IqPacket(IqPacket.TYPE.GET);
request.setTo(jid);
final String node = presence.getNode();
final String ver = presence.getVer();
final Element query = request.query(Namespace.DISCO_INFO);
if (node != null && ver != null) {
query.setAttribute("node", node + "#" + ver);
}
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": making disco request for " + key.second + " to " + jid);
sendIqPacket(account, request, (a, response) -> {
if (response.getType() == IqPacket.TYPE.RESULT) {
ServiceDiscoveryResult discoveryResult = new ServiceDiscoveryResult(response);
if (presence.getVer().equals(discoveryResult.getVer())) {
databaseBackend.insertDiscoveryResult(discoveryResult);
injectServiceDiscoveryResult(a.getRoster(), presence.getHash(), presence.getVer(), discoveryResult);
} else {
Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + discoveryResult.getVer());
}
}
a.inProgressDiscoFetches.remove(key);
});
if (account.inProgressDiscoFetches.contains(key)) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping duplicate disco request for " + key.second + " to " + jid);
return;
}
account.inProgressDiscoFetches.add(key);
final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
request.setTo(jid);
final String node = presence.getNode();
final String ver = presence.getVer();
final Element query = request.query(Namespace.DISCO_INFO);
if (node != null && ver != null) {
query.setAttribute("node", node + "#" + ver);
}
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": making disco request for " + key.second + " to " + jid);
sendIqPacket(account, request, (a, response) -> {
if (response.getType() == IqPacket.TYPE.RESULT) {
ServiceDiscoveryResult discoveryResult = new ServiceDiscoveryResult(response);
if (presence.getVer().equals(discoveryResult.getVer())) {
databaseBackend.insertDiscoveryResult(discoveryResult);
injectServiceDiscoveryResult(a.getRoster(), presence.getHash(), presence.getVer(), discoveryResult);
} else {
Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + discoveryResult.getVer());
}
} else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to fetch caps from " + jid);
}
a.inProgressDiscoFetches.remove(key);
});
}
}
......
......@@ -263,7 +263,7 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O
}
public void joinChannelSearchResult(String selectedAccount, Room result) {
final Jid jid = Config.DOMAIN_LOCK == null ? Jid.ofEscaped(selectedAccount) : Jid.ofEscaped(selectedAccount, Config.DOMAIN_LOCK, null);
final Jid jid = Config.DOMAIN_LOCK == null ? Jid.ofEscaped(selectedAccount) : Jid.ofLocalAndDomainEscaped(selectedAccount, Config.DOMAIN_LOCK);
final boolean syncAutoJoin = getBooleanPreference("autojoin", R.bool.autojoin);
final Account account = xmppConnectionService.findAccountByJid(jid);
final Conversation conversation = xmppConnectionService.findOrCreateConversation(account, result.getRoom(), true, true, true);
......
......@@ -3,6 +3,7 @@ package eu.siacs.conversations.ui;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.PictureInPictureParams;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
......@@ -17,6 +18,8 @@ import android.support.annotation.RequiresApi;
import android.support.annotation.StringRes;
import android.util.Log;
import android.util.Rational;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.widget.Toast;
......@@ -53,6 +56,7 @@ import eu.siacs.conversations.utils.PermissionUtils;
import eu.siacs.conversations.utils.TimeFrameUtils;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
......@@ -77,8 +81,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
RtpEndUserState.APPLICATION_ERROR,
RtpEndUserState.DECLINED_OR_BUSY,
RtpEndUserState.CONNECTIVITY_ERROR,
RtpEndUserState.CONNECTIVITY_LOST_ERROR,
RtpEndUserState.RETRACTED
);
private static final List<RtpEndUserState> STATES_SHOWING_HELP_BUTTON = Arrays.asList(
RtpEndUserState.APPLICATION_ERROR,
RtpEndUserState.CONNECTIVITY_ERROR
);
private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session";
private static final int REQUEST_ACCEPT_CALL = 0x1111;
private WeakReference<JingleRtpConnection> rtpConnectionReference;
......@@ -122,6 +131,45 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
setSupportActionBar(binding.toolbar);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.activity_rtp_session, menu);
final MenuItem help = menu.findItem(R.id.action_help);
help.setVisible(isHelpButtonVisible());
return super.onCreateOptionsMenu(menu);
}
private boolean isHelpButtonVisible() {
try {
return STATES_SHOWING_HELP_BUTTON.contains(requireRtpConnection().getEndUserState());
} catch (IllegalStateException e) {
final Intent intent = getIntent();
final String state = intent != null ? intent.getStringExtra(EXTRA_LAST_REPORTED_STATE) : null;
if (state != null) {
return STATES_SHOWING_HELP_BUTTON.contains(RtpEndUserState.valueOf(state));
} else {
return false;
}
}
}
public boolean onOptionsItemSelected(final MenuItem item) {
if (item.getItemId() == R.id.action_help) {
launchHelpInBrowser();
return true;
}
return super.onOptionsItemSelected(item);
}
private void launchHelpInBrowser() {
final Intent intent = new Intent(Intent.ACTION_VIEW, Config.HELP);
try {
startActivity(intent);
} catch (final ActivityNotFoundException e) {
Toast.makeText(this, R.string.no_application_found_to_open_link, Toast.LENGTH_LONG).show();
}
}
private void endCall(View view) {
endCall();
}
......@@ -232,9 +280,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
this.mProximityWakeLock = null;
}
}
private void putProximityWakeLockInProperState() {
if (requireRtpConnection().getAudioManager().getSelectedAudioDevice() == AppRTCAudioManager.AudioDevice.EARPIECE) {
private void putProximityWakeLockInProperState(final AppRTCAudioManager.AudioDevice audioDevice) {
if (audioDevice == AppRTCAudioManager.AudioDevice.EARPIECE) {
acquireProximityWakeLock();
} else {
releaseProximityWakeLock();
......@@ -300,6 +348,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
updateButtonConfiguration(state);
updateStateDisplay(state);
updateProfilePicture(state);
invalidateOptionsMenu();
}
binding.with.setText(account.getRoster().getContact(with).getDisplayName());
}
......@@ -427,7 +476,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
final WeakReference<JingleRtpConnection> reference = xmppConnectionService.getJingleConnectionManager()
.findJingleRtpConnection(account, with, sessionId);
if (reference == null || reference.get() == null) {
throw new IllegalStateException("failed to initialize activity with running rtp session. session not found");
final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession = xmppConnectionService
.getJingleConnectionManager().getTerminalSessionState(with, sessionId);
if (terminatedRtpSession == null) {
throw new IllegalStateException("failed to initialize activity with running rtp session. session not found");
}
initializeWithTerminatedSessionState(account, with, terminatedRtpSession);
return true;
}
this.rtpConnectionReference = reference;
final RtpEndUserState currentState = requireRtpConnection().getEndUserState();
......@@ -448,9 +503,26 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
updateStateDisplay(currentState, media);
updateButtonConfiguration(currentState, media);
updateProfilePicture(currentState);
invalidateOptionsMenu();
return false;
}
private void initializeWithTerminatedSessionState(final Account account, final Jid with, final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession) {
Log.d(Config.LOGTAG, "initializeWithTerminatedSessionState()");
if (terminatedRtpSession.state == RtpEndUserState.ENDED) {
finish();
return;
}
RtpEndUserState state = terminatedRtpSession.state;
resetIntent(account, with, terminatedRtpSession.state, terminatedRtpSession.media);
updateButtonConfiguration(state);
updateStateDisplay(state);
updateProfilePicture(state);
updateCallDuration();
invalidateOptionsMenu();
binding.with.setText(account.getRoster().getContact(with).getDisplayName());
}
private void reInitializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) {
runOnUiThread(() -> initializeActivityWithRunningRtpSession(account, with, sessionId));
resetIntent(account, with, sessionId);
......@@ -512,6 +584,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
case CONNECTIVITY_ERROR:
setTitle(R.string.rtp_state_connectivity_error);
break;
case CONNECTIVITY_LOST_ERROR:
setTitle(R.string.rtp_state_connectivity_lost_error);
break;
case RETRACTED:
setTitle(R.string.rtp_state_retracted);
break;
......@@ -577,7 +652,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
this.binding.acceptCall.setOnClickListener(this::recordVoiceMail);
this.binding.acceptCall.setImageResource(R.drawable.ic_voicemail_white_24dp);
this.binding.acceptCall.setVisibility(View.VISIBLE);
} else if (asList(RtpEndUserState.CONNECTIVITY_ERROR, RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.RETRACTED).contains(state)) {
} else if (asList(
RtpEndUserState.CONNECTIVITY_ERROR,
RtpEndUserState.CONNECTIVITY_LOST_ERROR,
RtpEndUserState.APPLICATION_ERROR,
RtpEndUserState.RETRACTED
).contains(state)) {
this.binding.rejectCall.setOnClickListener(this::exit);
this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
this.binding.rejectCall.setVisibility(View.VISIBLE);
......@@ -926,6 +1006,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
updateButtonConfiguration(state, media);
updateVideoViews(state);
updateProfilePicture(state, contact);
invalidateOptionsMenu();
});
if (END_CARD.contains(state)) {
final JingleRtpConnection rtpConnection = requireRtpConnection();
......@@ -956,7 +1037,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
} else if (END_CARD.contains(endUserState)) {
Log.d(Config.LOGTAG, "onAudioDeviceChanged() nothing to do because end card has been reached");
} else {
putProximityWakeLockInProperState();
putProximityWakeLockInProperState(selectedAudioDevice);
}
} catch (IllegalStateException e) {
Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed");
......@@ -974,6 +1055,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
updateStateDisplay(state);
updateButtonConfiguration(state);
updateProfilePicture(state);
invalidateOptionsMenu();
});
resetIntent(account, with, state, actionToMedia(currentIntent.getAction()));
}
......
......@@ -27,7 +27,7 @@ public class KnownHostsAdapter extends ArrayAdapter<String> {
if (split.length == 1) {
final String local = split[0].toLowerCase(Locale.ENGLISH);
if (Config.QUICKSY_DOMAIN != null && E164_PATTERN.matcher(local).matches()) {
suggestions.add(local + '@' + Config.QUICKSY_DOMAIN);
suggestions.add(local + '@' + Config.QUICKSY_DOMAIN.toEscapedString());
} else {
for (String domain : domains) {
suggestions.add(local + '@' + domain);
......
......@@ -35,7 +35,6 @@ import android.content.Intent;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import java.io.File;
import java.util.ArrayList;
......@@ -43,7 +42,6 @@ import java.util.Collections;
import java.util.List;
import java.util.UUID;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.MimeUtils;
......@@ -113,7 +111,7 @@ public class Attachment implements Parcelable {
}
public static boolean canBeSendInband(final List<Attachment> attachments) {
for(Attachment attachment : attachments) {
for (Attachment attachment : attachments) {
if (attachment.type != Type.LOCATION) {
return false;
}
......@@ -122,21 +120,21 @@ public class Attachment implements Parcelable {
}
public static List<Attachment> of(final Context context, Uri uri, Type type) {
final String mime = type == Type.LOCATION ?null :MimeUtils.guessMimeTypeFromUri(context, uri);
final String mime = type == Type.LOCATION ? null : MimeUtils.guessMimeTypeFromUri(context, uri);
return Collections.singletonList(new Attachment(uri, type, mime));
}
public static List<Attachment> of(final Context context, List<Uri> uris) {
List<Attachment> attachments = new ArrayList<>();
for(Uri uri : uris) {
for (Uri uri : uris) {
final String mime = MimeUtils.guessMimeTypeFromUri(context, uri);
attachments.add(new Attachment(uri, mime != null && mime.startsWith("image/") ? Type.IMAGE : Type.FILE,mime));
attachments.add(new Attachment(uri, mime != null && isImage(mime) ? Type.IMAGE : Type.FILE, mime));
}
return attachments;
}
public static Attachment of(UUID uuid, final File file, String mime) {
return new Attachment(uuid, Uri.fromFile(file),mime != null && (mime.startsWith("image/") || mime.startsWith("video/")) ? Type.IMAGE : Type.FILE, mime);
return new Attachment(uuid, Uri.fromFile(file), mime != null && (isImage(mime) || mime.startsWith("video/")) ? Type.IMAGE : Type.FILE, mime);
}
public static List<Attachment> extractAttachments(final Context context, final Intent intent, Type type) {
......@@ -151,9 +149,7 @@ public class Attachment implements Parcelable {
if (clipData != null) {
for (int i = 0; i < clipData.getItemCount(); ++i) {
final Uri uri = clipData.getItemAt(i).getUri();
Log.d(Config.LOGTAG,"uri="+uri+" contentType="+contentType);
final String mime = MimeUtils.guessMimeTypeFromUriAndMime(context, uri, contentType);
Log.d(Config.LOGTAG,"mime="+mime);
uris.add(new Attachment(uri, type, mime));
}
}
......@@ -165,12 +161,12 @@ public class Attachment implements Parcelable {
}
public boolean renderThumbnail() {
return type == Type.IMAGE || (type == Type.FILE && mime != null && renderFileThumbnail(mime));
return type == Type.IMAGE || (type == Type.FILE && mime != null && renderFileThumbnail(mime));
}
private static boolean renderFileThumbnail(final String mime) {
return mime.startsWith("video/")
|| mime.startsWith("image/")
|| isImage(mime)
|| (Compatibility.runsTwentyOne() && "application/pdf".equals(mime));
}
......@@ -181,4 +177,8 @@ public class Attachment implements Parcelable {
public UUID getUuid() {
return uuid;
}
private static boolean isImage(final String mime) {
return mime.startsWith("image/") && !mime.equals("image/svg+xml");
}
}
......@@ -106,12 +106,24 @@ public class PresenceSelector {
builder.setPositiveButton(
R.string.ok,
(dialog, which) -> onFullJidSelected.onFullJidSelected(
Jid.of(contact.getJid().getLocal(), contact.getJid().getDomain(), resourceArray[selectedResource.get()])
getNextCounterpart(contact, resourceArray[selectedResource.get()])
)
);
builder.create().show();
}
public static Jid getNextCounterpart(final Contact contact, final String resource) {
return getNextCounterpart(contact.getJid(), resource);
}
public static Jid getNextCounterpart(final Jid jid, final String resource) {
if (resource.isEmpty()) {
return jid.asBareJid();
} else {
return jid.withResource(resource);
}
}
public static void warnMutualPresenceSubscription(Activity activity, final Conversation conversation, final OnPresenceSelected listener) {
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(conversation.getContact().getJid().toString());
......
......@@ -39,7 +39,7 @@ public class AccountUtils {
for (Account account : service.getAccounts()) {
if (account.getStatus() != Account.State.DISABLED) {
if (Config.DOMAIN_LOCK != null) {
accounts.add(account.getJid().toEscapedString());
accounts.add(account.getJid().getEscapedLocal());
} else {
accounts.add(account.getJid().asBareJid().toEscapedString());
}
......
......@@ -34,6 +34,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xmpp.InvalidJid;
import eu.siacs.conversations.xmpp.Jid;
......@@ -59,4 +60,8 @@ public class JidHelper {
}
}
public static boolean isQuicksyDomain(final Jid jid) {
return Config.QUICKSY_DOMAIN != null && Config.QUICKSY_DOMAIN.equals(jid.getDomain());
}
}
......@@ -16,10 +16,10 @@ public abstract class AbstractJingleConnection {
public static final String JINGLE_MESSAGE_PROPOSE_ID_PREFIX = "jm-propose-";
public static final String JINGLE_MESSAGE_PROCEED_ID_PREFIX = "jm-proceed-";
protected final JingleConnectionManager jingleConnectionManager;
final JingleConnectionManager jingleConnectionManager;
protected final XmppConnectionService xmppConnectionService;
protected final Id id;
protected final Jid initiator;
private final Jid initiator;
AbstractJingleConnection(final JingleConnectionManager jingleConnectionManager, final Id id, final Jid initiator) {
this.jingleConnectionManager = jingleConnectionManager;
......@@ -47,8 +47,9 @@ public abstract class AbstractJingleConnection {
public final String sessionId;
private Id(final Account account, final Jid with, final String sessionId) {
Preconditions.checkNotNull(account);
Preconditions.checkNotNull(with);
Preconditions.checkArgument(with.isFullJid());
Preconditions.checkNotNull(sessionId);
this.account = account;
this.with = with;
this.sessionId = sessionId;
......
......@@ -147,7 +147,6 @@ public class JingleCandidate {
}
public String toString() {
return this.getHost() + ":" + this.getPort() + " (prio="
+ this.getPriority() + ")";
return String.format("%s:%s (priority=%s,ours=%s)", getHost(), getPort(), getPriority(), isOurs());
}
}
......@@ -11,7 +11,6 @@ import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Collections2;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableSet;
import com.google.j2objc.annotations.Weak;
import java.lang.ref.WeakReference;
import java.security.SecureRandom;
......@@ -57,8 +56,8 @@ public class JingleConnectionManager extends AbstractConnectionManager {
private final HashMap<RtpSessionProposal, DeviceDiscoveryState> rtpSessionProposals = new HashMap<>();
private final ConcurrentHashMap<AbstractJingleConnection.Id, AbstractJingleConnection> connections = new ConcurrentHashMap<>();
private final Cache<PersistableSessionId, JingleRtpConnection.State> endedSessions = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES)
private final Cache<PersistableSessionId, TerminatedRtpSession> terminatedSessions = CacheBuilder.newBuilder()
.expireAfterWrite(24, TimeUnit.HOURS)
.build();
private HashMap<Jid, JingleCandidate> primaryCandidates = new HashMap<>();
......@@ -92,7 +91,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) {
connection = new JingleFileTransferConnection(this, id, from);
} else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace) && !usesTor(account)) {
final boolean sessionEnded = this.endedSessions.asMap().containsKey(PersistableSessionId.of(id));
final boolean sessionEnded = this.terminatedSessions.asMap().containsKey(PersistableSessionId.of(id));
final boolean stranger = isWithStrangerAndStrangerNotificationsAreOff(account, id.with);
if (isBusy() || sessionEnded || stranger) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": rejected session with " + id.with + " because busy. sessionEnded=" + sessionEnded + ", stranger=" + stranger);
......@@ -447,6 +446,10 @@ public class JingleConnectionManager extends AbstractConnectionManager {
}
void finishConnection(final AbstractJingleConnection connection) {
this.connections.remove(connection.getId());
}
void finishConnectionOrThrow(final AbstractJingleConnection connection) {
final AbstractJingleConnection.Id id = connection.getId();
if (this.connections.remove(id) == null) {
throw new IllegalStateException(String.format("Unable to finish connection with id=%s", id.toString()));
......@@ -680,8 +683,12 @@ public class JingleConnectionManager extends AbstractConnectionManager {
throw e;
}
void endSession(AbstractJingleConnection.Id id, final AbstractJingleConnection.State state) {
this.endedSessions.put(PersistableSessionId.of(id), state);
void setTerminalSessionState(AbstractJingleConnection.Id id, final RtpEndUserState state, final Set<Media> media) {
this.terminatedSessions.put(PersistableSessionId.of(id), new TerminatedRtpSession(state, media));
}
public TerminatedRtpSession getTerminalSessionState(final Jid with, final String sessionId) {
return this.terminatedSessions.getIfPresent(new PersistableSessionId(with, sessionId));
}
private static class PersistableSessionId {
......@@ -712,6 +719,16 @@ public class JingleConnectionManager extends AbstractConnectionManager {
}
}
public static class TerminatedRtpSession {
public final RtpEndUserState state;
public final Set<Media> media;
TerminatedRtpSession(RtpEndUserState state, Set<Media> media) {
this.state = state;
this.media = media;
}
}
public enum DeviceDiscoveryState {
SEARCHING, DISCOVERED, FAILED;
......
......@@ -4,7 +4,10 @@ import android.util.Base64;
import android.util.Log;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Iterables;
import java.io.File;
import java.io.FileInputStream;
......@@ -197,7 +200,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
}
};
public JingleFileTransferConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) {
JingleFileTransferConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) {
super(jingleConnectionManager, id, initiator);
}
......@@ -414,15 +417,10 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
}
private List<String> getRemoteFeatures() {
final Jid jid = this.id.with;
String resource = jid != null ? jid.getResource() : null;
if (resource != null) {
Presence presence = this.id.account.getRoster().getContact(jid).getPresences().get(resource);
ServiceDiscoveryResult result = presence != null ? presence.getServiceDiscoveryResult() : null;
return result == null ? Collections.emptyList() : result.getFeatures();
} else {
return Collections.emptyList();
}
final String resource = Strings.nullToEmpty(this.id.with.getResource());
final Presence presence = this.id.account.getRoster().getContact(id.with).getPresences().get(resource);
final ServiceDiscoveryResult result = presence != null ? presence.getServiceDiscoveryResult() : null;
return result == null ? Collections.emptyList() : result.getFeatures();
}
private void init(JinglePacket packet) { //should move to deliverPacket
......@@ -442,7 +440,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
try {
senders = content.getSenders();
} catch (final Exception e) {
senders = Content.Senders.INITIATOR;
senders = Content.Senders.INITIATOR;
}
this.contentSenders = senders;
this.contentName = content.getAttribute("name");
......@@ -632,10 +630,14 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INFO);
packet.addJingleChild(checksum);
this.sendJinglePacket(packet);
xmppConnectionService.sendIqPacket(id.account, packet, (account, response) -> {
if (response.getType() == IqPacket.TYPE.ERROR) {
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignoring error response to our session-info (hash transmission)");
}
});
}
public Collection<JingleCandidate> getOurCandidates() {
private Collection<JingleCandidate> getOurCandidates() {
return Collections2.filter(this.candidates, c -> c != null && c.isOurs());
}
......@@ -825,8 +827,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
this.sendFallbackToIbb();
}
} else {
//TODO at this point we can already close other connections to free some resources
final JingleCandidate candidate = connection.getCandidate();
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": elected candidate " + candidate.getHost() + ":" + candidate.getPort());
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": elected candidate " + candidate.toString());
this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
if (connection.needsActivation()) {
if (connection.getCandidate().isOurs()) {
......@@ -875,38 +878,23 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
}
private JingleSocks5Transport chooseConnection() {
JingleSocks5Transport connection = null;
for (Entry<String, JingleSocks5Transport> cursor : connections
.entrySet()) {
JingleSocks5Transport currentConnection = cursor.getValue();
// Log.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString());
if (currentConnection.isEstablished()
&& (currentConnection.getCandidate().isUsedByCounterpart() || (!currentConnection
.getCandidate().isOurs()))) {
// Log.d(Config.LOGTAG,"is usable");
if (connection == null) {
connection = currentConnection;
} else {
if (connection.getCandidate().getPriority() < currentConnection
.getCandidate().getPriority()) {
connection = currentConnection;
} else if (connection.getCandidate().getPriority() == currentConnection
.getCandidate().getPriority()) {
// Log.d(Config.LOGTAG,"found two candidates with same priority");
final List<JingleSocks5Transport> establishedConnections = FluentIterable.from(connections.entrySet())
.transform(Entry::getValue)
.filter(c -> (c != null && c.isEstablished() && (c.getCandidate().isUsedByCounterpart() || !c.getCandidate().isOurs())))
.toSortedList((a, b) -> {
final int compare = Integer.compare(b.getCandidate().getPriority(), a.getCandidate().getPriority());
if (compare == 0) {
if (isInitiator()) {
if (currentConnection.getCandidate().isOurs()) {
connection = currentConnection;
}
//pick the one we sent a candidate-used for (meaning not ours)
return a.getCandidate().isOurs() ? 1 : -1;
} else {
if (!currentConnection.getCandidate().isOurs()) {
connection = currentConnection;
}
//pick the one they sent a candidate-used for (meaning ours)
return a.getCandidate().isOurs() ? -1 : 1;
}
}
}
}
}
return connection;
return compare;
});
return Iterables.getFirst(establishedConnections, null);
}
private void sendSuccess() {
......@@ -1035,7 +1023,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
abort(Reason.CANCEL);
}
void abort(final Reason reason) {
private void abort(final Reason reason) {
this.disconnectSocks5Connections();
if (this.transport instanceof JingleInBandTransport) {
this.transport.disconnect();
......@@ -1179,7 +1167,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
}
}
public int getJingleStatus() {
private int getJingleStatus() {
return this.mJingleStatus;
}
......@@ -1222,11 +1210,11 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
jingleConnectionManager.updateConversationUi(false);
}
public String getTransportId() {
String getTransportId() {
return this.transportId;
}
public FileTransferDescription.Version getFtVersion() {
FileTransferDescription.Version getFtVersion() {
return this.description.getVersion();
}
......
......@@ -814,7 +814,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
} else if (state == PeerConnection.PeerConnectionState.CLOSED) {
return RtpEndUserState.ENDING_CALL;
} else {
return RtpEndUserState.CONNECTIVITY_ERROR;
return rtpConnectionStarted == 0 ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR;
}
case REJECTED:
case TERMINATED_DECLINED_OR_BUSY:
......@@ -831,7 +831,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
case RETRACTED_RACED:
return RtpEndUserState.RETRACTED;
case TERMINATED_CONNECTIVITY_ERROR:
return RtpEndUserState.CONNECTIVITY_ERROR;
return rtpConnectionStarted == 0 ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR;
case TERMINATED_APPLICATION_FAILURE:
return RtpEndUserState.APPLICATION_ERROR;
}
......@@ -912,7 +912,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
}
if (isInState(State.PROCEED)) {
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ending call while in state PROCEED just means ending the connection");
this.jingleConnectionManager.endSession(id, State.TERMINATED_SUCCESS);
this.webRTCWrapper.close();
transitionOrThrow(State.TERMINATED_SUCCESS); //arguably this wasn't success; but not a real failure either
this.finish();
......@@ -1189,7 +1188,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
if (isTerminated()) {
this.cancelRingingTimeout();
this.webRTCWrapper.verifyClosed();
this.jingleConnectionManager.finishConnection(this);
this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia());
this.jingleConnectionManager.finishConnectionOrThrow(this);
} else {
throw new IllegalStateException(String.format("Unable to call finish from %s", this.state));
}
......@@ -1219,7 +1219,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
final Conversational conversational = message.getConversation();
if (conversational instanceof Conversation) {
((Conversation) conversational).add(this.message);
xmppConnectionService.databaseBackend.createMessage(message);
xmppConnectionService.createMessageAsync(message);
xmppConnectionService.updateConversationUi();
} else {
throw new IllegalStateException("Somehow the conversation in a message was a stub");
......
......@@ -11,6 +11,7 @@ public enum RtpEndUserState {
ENDED, //close UI
DECLINED_OR_BUSY, //other party declined; no retry button
CONNECTIVITY_ERROR, //network error; retry button
CONNECTIVITY_LOST_ERROR, //network error but for call duration > 0
RETRACTED, //user pressed home or power button during 'ringing' - shows retry button
APPLICATION_ERROR //something rather bad happened; libwebrtc failed or we got in IQ-error
}
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_help"
android:icon="?attr/icon_help"
android:title="@string/help"
app:showAsAction="always"/>
</menu>
\ No newline at end of file
......@@ -41,7 +41,7 @@
<string name="moderator">Moderator</string>
<string name="participant">Teilnehmer</string>
<string name="visitor">Besucher</string>
<string name="remove_contact_text">Möchtest du %svon deiner Kontaktliste entfernen? Unterhaltungen mit diesem Kontakt werden dabei nicht entfernt.</string>
<string name="remove_contact_text">Möchtest du %s von deiner Kontaktliste entfernen? Unterhaltungen mit diesem Kontakt werden dabei nicht entfernt.</string>
<string name="block_contact_text">Möchtest du %s sperren und keine Nachrichten mehr erhalten?</string>
<string name="unblock_contact_text">Möchtest du %s entsperren und wieder Nachrichten empfangen?</string>
<string name="block_domain_text">Alle Kontakte von %s sperren?</string>
......@@ -901,7 +901,8 @@
<string name="rtp_state_ringing">Klingelt</string>
<string name="rtp_state_declined_or_busy">Besetzt</string>
<string name="rtp_state_connectivity_error">Verbindungsaufbau fehlgeschlagen</string>
<string name="rtp_state_retracted">Rückrufruf</string>
<string name="rtp_state_connectivity_lost_error">Verbindung unterbrochen</string>
<string name="rtp_state_retracted">Anruf zurückgenommen</string>
<string name="rtp_state_application_failure">App-Fehler</string>
<string name="hang_up">Auflegen</string>
<string name="ongoing_call">Laufender Anruf</string>
......@@ -914,6 +915,7 @@
<string name="missed_call">Entgangener Anruf</string>
<string name="audio_call">Audioanruf</string>
<string name="video_call">Videoanruf</string>
<string name="help">Hilfe</string>
<string name="microphone_unavailable">Dein Mikrofon ist nicht verfügbar</string>
<string name="only_one_call_at_a_time">Du kannst immer nur einen Anruf zur gleichen Zeit machen.</string>
<string name="return_to_ongoing_call">Zurück zum laufenden Aufruf</string>
......
......@@ -901,6 +901,7 @@
<string name="rtp_state_ringing">Llamando</string>
<string name="rtp_state_declined_or_busy">Ocupado</string>
<string name="rtp_state_connectivity_error">No se ha podido realizar la llamada</string>
<string name="rtp_state_connectivity_lost_error">Conexión perdida</string>
<string name="rtp_state_retracted">Llamada rechazada</string>
<string name="rtp_state_application_failure">Fallo en la aplicación</string>
<string name="hang_up">Colgar</string>
......@@ -914,6 +915,7 @@
<string name="missed_call">Llamada perdida</string>
<string name="audio_call">Audio llamada</string>
<string name="video_call">Video llamada</string>
<string name="help">Ayuda</string>
<string name="microphone_unavailable">Tu micrófono no está disponible</string>
<string name="only_one_call_at_a_time">Solo puedes hacer una llamada a la vez</string>
<string name="return_to_ongoing_call">Volver a la llamada en curso</string>
......
......@@ -901,6 +901,7 @@
<string name="rtp_state_ringing">Sonando</string>
<string name="rtp_state_declined_or_busy">Ocupado</string>
<string name="rtp_state_connectivity_error">Non se pode establecer a chamada</string>
<string name="rtp_state_connectivity_lost_error">Perdeuse a conexión</string>
<string name="rtp_state_retracted">Chamada cortada</string>
<string name="rtp_state_application_failure">Fallo na aplicación</string>
<string name="hang_up">Colgar</string>
......@@ -914,6 +915,7 @@
<string name="missed_call">Chamada perdida</string>
<string name="audio_call">Chamada de audio</string>
<string name="video_call">Chamada de vídeo</string>
<string name="help">Axuda</string>
<string name="microphone_unavailable">O micrófono non está dispoñible</string>
<string name="only_one_call_at_a_time">Só podes manter unha chamada en cada momento.</string>
<string name="return_to_ongoing_call">Voltar á chamada activa</string>
......
......@@ -901,6 +901,7 @@
<string name="rtp_state_ringing">Sta squillando</string>
<string name="rtp_state_declined_or_busy">Occupato</string>
<string name="rtp_state_connectivity_error">Impossibile connettere la chiamata</string>
<string name="rtp_state_connectivity_lost_error">Connessione persa</string>
<string name="rtp_state_retracted">Chiamata ritirata</string>
<string name="rtp_state_application_failure">Errore dell\'app</string>
<string name="hang_up">Riaggancia</string>
......@@ -914,6 +915,7 @@
<string name="missed_call">Chiamata persa</string>
<string name="audio_call">Chiamata vocale</string>
<string name="video_call">Chiamata video</string>
<string name="help">Aiuto</string>
<string name="microphone_unavailable">Il tuo microfono non è disponibile</string>
<string name="only_one_call_at_a_time">Puoi fare solo una chiamata alla volta.</string>
<string name="return_to_ongoing_call">Torna alla chiamata in corso</string>
......
......@@ -919,6 +919,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż
<string name="rtp_state_ringing">Dzwonienie</string>
<string name="rtp_state_declined_or_busy">Zajęty</string>
<string name="rtp_state_connectivity_error">Nie można wykonać połączenia</string>
<string name="rtp_state_connectivity_lost_error">Utracono połączenie</string>
<string name="rtp_state_retracted">Anulowane połączenie</string>
<string name="rtp_state_application_failure">Błąd aplikacji</string>
<string name="hang_up">Rozłącz</string>
......@@ -932,6 +933,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż
<string name="missed_call">Nieodebrane połączenie</string>
<string name="audio_call">Połączenie audio</string>
<string name="video_call">Połączenie wideo</string>
<string name="help">Pomoc</string>
<string name="microphone_unavailable">Twój mikrofon jest niedostępny</string>
<string name="only_one_call_at_a_time">Możesz mieć tylko jedno połączenie na raz.</string>
<string name="return_to_ongoing_call">Powróć do trwającego połączenia</string>
......
......@@ -909,6 +909,7 @@
<string name="rtp_state_ringing">Sună</string>
<string name="rtp_state_declined_or_busy">Ocupat</string>
<string name="rtp_state_connectivity_error">Nu s-a putut conecta apelul</string>
<string name="rtp_state_connectivity_lost_error">Conexiune pierdută</string>
<string name="rtp_state_retracted">Apel anulat</string>
<string name="rtp_state_application_failure">Eroare de aplicație</string>
<string name="hang_up">Închide</string>
......@@ -922,6 +923,7 @@
<string name="missed_call">Apel pierdut</string>
<string name="audio_call">Apel audio</string>
<string name="video_call">Apel video</string>
<string name="help">Ajutor</string>
<string name="microphone_unavailable">Microfonul nu este disponibil</string>
<string name="only_one_call_at_a_time">Puteți avea un singur apel simultan.</string>
<string name="return_to_ongoing_call">Reveniți la apelul în curs</string>
......
This diff is collapsed.
......@@ -3,6 +3,7 @@
<string name="action_settings">Inställningar</string>
<string name="action_add">Ny konversation</string>
<string name="action_accounts">Kontoinställningar</string>
<string name="action_account">Hantera konto</string>
<string name="action_end_conversation">Stäng denna konversation</string>
<string name="action_contact_details">Kontaktdetaljer</string>
<string name="action_muc_details">Gruppchattdetaljer</string>
......@@ -34,18 +35,20 @@
<string name="message_decrypting">Avkrypterar meddelande. Vänta…</string>
<string name="pgp_message">OpenPGP-krypterat meddelande</string>
<string name="nick_in_use">Nick används redan</string>
<string name="invalid_muc_nick">Ogiltigt nick</string>
<string name="invalid_muc_nick">Ogiltigt smeknamn</string>
<string name="admin">Admin</string>
<string name="owner">Ägare</string>
<string name="moderator">Moderator</string>
<string name="participant">Deltagare</string>
<string name="visitor">Besökare</string>
<string name="remove_contact_text">Vill du ta bort %s från din kontaktlista? Konversationer med denna kontakt kommer inte tas bort.</string>
<string name="block_contact_text">Vill du blockera %s från att skicka dig meddelanden?</string>
<string name="unblock_contact_text">Vill du avblockera %s och tillåta denne att skicka dig meddelanden?</string>
<string name="block_domain_text">Blockera alla kontakter från %s?</string>
<string name="unblock_domain_text">Avblockera alla kontakter från %s?</string>
<string name="contact_blocked">Kontakt blockerad</string>
<string name="blocked">Blockerad</string>
<string name="remove_bookmark_text">Vill du ta bort %s som ett bokmärke? Konversationer med detta bokmärke kommer inte tas bort.</string>
<string name="register_account">Registrera nytt konto på servern</string>
<string name="change_password_on_server">Byt lösenord på server</string>
<string name="share_with">Dela med…</string>
......@@ -66,9 +69,13 @@
<string name="crash_report_title">Conversations har kraschat</string>
<string name="send_now">Skicka nu</string>
<string name="send_never">Fråga aldrig igen</string>
<string name="problem_connecting_to_account">Kunde inte ansluta till konto</string>
<string name="problem_connecting_to_accounts">Kunde inte ansluta till flera konton</string>
<string name="attach_file">Bifoga fil</string>
<string name="add_contact">Lägg till kontakt</string>
<string name="send_failed">sändning misslyckades</string>
<string name="preparing_image">Förbereder att skicka bild</string>
<string name="preparing_images">Förbereder att skicka bilder</string>
<string name="sharing_files_please_wait">Delar filer. Vänta...</string>
<string name="action_clear_history">Rensa historik</string>
<string name="clear_conversation_history">Rensa konversationshistorik</string>
......@@ -81,6 +88,7 @@
<string name="send_omemo_message">Skicka OMEMO-krypterat meddelande</string>
<string name="send_omemo_x509_message">Skicka v\\OMEMO-krypterat meddelande</string>
<string name="send_pgp_message">Skicka OpenPGP-krypterat meddelande</string>
<string name="your_nick_has_been_changed">Nytt smeknamn används</string>
<string name="send_unencrypted">Skicka okrypterat</string>
<string name="decryption_failed">Avkryptering misslyckades. Du har kanske kanske inte rätt privat nyckel.</string>
<string name="openkeychain_required">OpenKeychain</string>
......@@ -304,6 +312,7 @@
<string name="ban_now">Bannlys nu</string>
<string name="could_not_change_role">Kunde inte ändra rollen för %s</string>
<string name="members_only">Privat, medlemsskap krävs</string>
<string name="non_anonymous">Gör XMPP-adresser synliga för alla</string>
<string name="you_are_not_participating">Du deltar ej</string>
<string name="never">Aldrig</string>
<string name="until_further_notice">Tills vidare</string>
......@@ -389,6 +398,8 @@
<item quantity="other">%d meddelanden</item>
</plurals>
<string name="load_more_messages">Ladda fler meddelanden</string>
<string name="no_storage_permission">Ge Conversations tillgång till extern lagring</string>
<string name="no_camera_permission">Ge Conversations tillgång till kameran</string>
<string name="sync_with_contacts">Synkronisera med kontakter</string>
<string name="notify_on_all_messages">Notifiera för alla meddelanden</string>
<string name="notify_never">Notifieringar deaktiverade</string>
......@@ -442,6 +453,7 @@
<string name="allow">Tillåt</string>
<string name="no_permission_to_access_x">Saknar rättigheter för access till %s</string>
<string name="remote_server_not_found">Fjärrserver hittas inte</string>
<string name="unable_to_update_account">Kunde inte uppdatera konto</string>
<string name="pref_delete_omemo_identities">Ta bort OMEMO identiteter</string>
<string name="delete_selected_keys">Ta bort valda nycklar</string>
<string name="error_publish_avatar_offline">Du måste vara ansluten för att publicera din avatarbild</string>
......@@ -451,6 +463,7 @@
<string name="device_does_not_support_data_saver">Din enhet stödjer inte att deaktivera databesparing för Conversations.</string>
<string name="this_device_has_been_verified">Denna enhet har verifierats</string>
<string name="copy_fingerprint">Kopiera fingeravtryck</string>
<string name="barcode_does_not_contain_fingerprints_for_this_conversation">Streckkoden innehåller inte fingeravtryck för denna konversation.</string>
<string name="verified_fingerprints">Verifierade fingeravtryck</string>
<string name="use_camera_icon_to_scan_barcode">Använd kameran för att scanna en kontakts streckkod</string>
<string name="please_wait_for_keys_to_be_fetched">Vänta medans nycklar hämtas</string>
......@@ -554,6 +567,7 @@
<string name="ebook">e-bok</string>
<string name="open_with">Öppna med...</string>
<string name="choose_account">Välj konto</string>
<string name="enter_password_to_restore">Ange ditt lösenord till kontot %s för att återställa säkerhetskopian.</string>
<string name="create_group_chat">Skapa gruppchatt</string>
<string name="create_private_group_chat">Skapa sluten gruppchatt</string>
<string name="create_dialog_channel_name">Kanalnamn</string>
......@@ -576,5 +590,7 @@
<string name="this_looks_like_a_domain">Detta verkar vara ett domännamn</string>
<string name="add_anway">Lägg till ändå</string>
<string name="this_looks_like_channel">Detta ser ut som en kanaladress</string>
<string name="not_a_backup_file">Filen du valde är inte en säkerhetskopia till Conversations</string>
<string name="category_about">Om</string>
<string name="please_enable_an_account">Aktivera ett konto</string>
</resources>