Merge tag '2.8.6' into develop
This commit is contained in:
commit
c44b948228
|
@ -1,5 +1,9 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
### Version 2.8.6
|
||||||
|
|
||||||
|
* Offer to record voice message when callee is busy
|
||||||
|
|
||||||
### Version 2.8.5
|
### Version 2.8.5
|
||||||
|
|
||||||
* Reduce echo during calls on some devices
|
* Reduce echo during calls on some devices
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
|
|
||||||
* End-to-end encryption with [OMEMO](http://conversations.im/omemo/) or [OpenPGP](http://openpgp.org/about/)
|
* 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
|
* Send and receive images as well as other kind of files
|
||||||
* Encrypted audio and video calls (DLTS-SRTP)
|
* Encrypted audio and video calls (DTLS-SRTP)
|
||||||
* Share your location
|
* Share your location
|
||||||
* Send voice messages
|
* Send voice messages
|
||||||
* Indication when your contact has read your message
|
* Indication when your contact has read your message
|
||||||
|
|
|
@ -96,8 +96,8 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 25
|
targetSdkVersion 25
|
||||||
versionCode 388
|
versionCode 390
|
||||||
versionName "2.8.5"
|
versionName "2.8.6"
|
||||||
archivesBaseName += "-$versionName"
|
archivesBaseName += "-$versionName"
|
||||||
applicationId "eu.sum7.conversations"
|
applicationId "eu.sum7.conversations"
|
||||||
resValue "string", "applicationId", applicationId
|
resValue "string", "applicationId", applicationId
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
• Offer to record voice message when callee is busy
|
BIN
screenshots.xcf
BIN
screenshots.xcf
Binary file not shown.
|
@ -195,6 +195,10 @@ abstract class ScramMechanism extends SaslMechanism {
|
||||||
|
|
||||||
final byte[] clientProof = new byte[keys.clientKey.length];
|
final byte[] clientProof = new byte[keys.clientKey.length];
|
||||||
|
|
||||||
|
if (clientSignature.length < keys.clientKey.length) {
|
||||||
|
throw new AuthenticationException("client signature was shorter than clientKey");
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < clientProof.length; i++) {
|
for (int i = 0; i < clientProof.length; i++) {
|
||||||
clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]);
|
clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -432,7 +432,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
||||||
|
|
||||||
public int activeDevicesWithRtpCapability() {
|
public int activeDevicesWithRtpCapability() {
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for(Presence presence : getSelfContact().getPresences().getPresences().values()) {
|
for(Presence presence : getSelfContact().getPresences().getPresences()) {
|
||||||
if (RtpCapability.check(presence) != RtpCapability.Capability.NONE) {
|
if (RtpCapability.check(presence) != RtpCapability.Capability.NONE) {
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,138 +9,164 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class Presences {
|
public class Presences {
|
||||||
private final Hashtable<String, Presence> presences = new Hashtable<>();
|
private final Hashtable<String, Presence> presences = new Hashtable<>();
|
||||||
|
|
||||||
public Hashtable<String, Presence> getPresences() {
|
private static String nameWithoutVersion(String name) {
|
||||||
return this.presences;
|
String[] parts = name.split(" ");
|
||||||
}
|
if (parts.length > 1 && Character.isDigit(parts[parts.length - 1].charAt(0))) {
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
for (int i = 0; i < parts.length - 1; ++i) {
|
||||||
|
if (output.length() != 0) {
|
||||||
|
output.append(' ');
|
||||||
|
}
|
||||||
|
output.append(parts[i]);
|
||||||
|
}
|
||||||
|
return output.toString();
|
||||||
|
} else {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void updatePresence(String resource, Presence presence) {
|
public List<Presence> getPresences() {
|
||||||
synchronized (this.presences) {
|
synchronized (this.presences) {
|
||||||
this.presences.put(resource, presence);
|
return new ArrayList<>(this.presences.values());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removePresence(String resource) {
|
public Map<String, Presence> getPresencesMap() {
|
||||||
synchronized (this.presences) {
|
synchronized (this.presences) {
|
||||||
this.presences.remove(resource);
|
return new HashMap<>(this.presences);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearPresences() {
|
public Presence get(String resource) {
|
||||||
synchronized (this.presences) {
|
synchronized (this.presences) {
|
||||||
this.presences.clear();
|
return this.presences.get(resource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Presence.Status getShownStatus() {
|
public void updatePresence(String resource, Presence presence) {
|
||||||
Presence.Status status = Presence.Status.OFFLINE;
|
synchronized (this.presences) {
|
||||||
synchronized (this.presences) {
|
this.presences.put(resource, presence);
|
||||||
for(Presence p : presences.values()) {
|
}
|
||||||
if (p.getStatus() == Presence.Status.DND) {
|
}
|
||||||
return p.getStatus();
|
|
||||||
} else if (p.getStatus().compareTo(status) < 0){
|
|
||||||
status = p.getStatus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int size() {
|
public void removePresence(String resource) {
|
||||||
synchronized (this.presences) {
|
synchronized (this.presences) {
|
||||||
return presences.size();
|
this.presences.remove(resource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String[] toResourceArray() {
|
public void clearPresences() {
|
||||||
synchronized (this.presences) {
|
synchronized (this.presences) {
|
||||||
final String[] presencesArray = new String[presences.size()];
|
this.presences.clear();
|
||||||
presences.keySet().toArray(presencesArray);
|
}
|
||||||
return presencesArray;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<PresenceTemplate> asTemplates() {
|
public Presence.Status getShownStatus() {
|
||||||
synchronized (this.presences) {
|
Presence.Status status = Presence.Status.OFFLINE;
|
||||||
ArrayList<PresenceTemplate> templates = new ArrayList<>(presences.size());
|
synchronized (this.presences) {
|
||||||
for(Presence p : presences.values()) {
|
for (Presence p : presences.values()) {
|
||||||
if (p.getMessage() != null && !p.getMessage().trim().isEmpty()) {
|
if (p.getStatus() == Presence.Status.DND) {
|
||||||
templates.add(new PresenceTemplate(p.getStatus(), p.getMessage()));
|
return p.getStatus();
|
||||||
}
|
} else if (p.getStatus().compareTo(status) < 0) {
|
||||||
}
|
status = p.getStatus();
|
||||||
return templates;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean has(String presence) {
|
public int size() {
|
||||||
synchronized (this.presences) {
|
synchronized (this.presences) {
|
||||||
return presences.containsKey(presence);
|
return presences.size();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getStatusMessages() {
|
public String[] toResourceArray() {
|
||||||
ArrayList<String> messages = new ArrayList<>();
|
synchronized (this.presences) {
|
||||||
synchronized (this.presences) {
|
final String[] presencesArray = new String[presences.size()];
|
||||||
for(Presence presence : this.presences.values()) {
|
presences.keySet().toArray(presencesArray);
|
||||||
String message = presence.getMessage() == null ? null : presence.getMessage().trim();
|
return presencesArray;
|
||||||
if (message != null && !message.isEmpty() && !messages.contains(message)) {
|
}
|
||||||
messages.add(message);
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean allOrNonSupport(String namespace) {
|
public List<PresenceTemplate> asTemplates() {
|
||||||
synchronized (this.presences) {
|
synchronized (this.presences) {
|
||||||
for(Presence presence : this.presences.values()) {
|
ArrayList<PresenceTemplate> templates = new ArrayList<>(presences.size());
|
||||||
ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
|
for (Presence p : presences.values()) {
|
||||||
if (disco == null || !disco.getFeatures().contains(namespace)) {
|
if (p.getMessage() != null && !p.getMessage().trim().isEmpty()) {
|
||||||
return false;
|
templates.add(new PresenceTemplate(p.getStatus(), p.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return templates;
|
||||||
return true;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Pair<Map<String, String>,Map<String,String>> toTypeAndNameMap() {
|
public boolean has(String presence) {
|
||||||
Map<String,String> typeMap = new HashMap<>();
|
synchronized (this.presences) {
|
||||||
Map<String,String> nameMap = new HashMap<>();
|
return presences.containsKey(presence);
|
||||||
synchronized (this.presences) {
|
}
|
||||||
for(Map.Entry<String,Presence> presenceEntry : this.presences.entrySet()) {
|
}
|
||||||
String resource = presenceEntry.getKey();
|
|
||||||
Presence presence = presenceEntry.getValue();
|
|
||||||
ServiceDiscoveryResult serviceDiscoveryResult = presence == null ? null : presence.getServiceDiscoveryResult();
|
|
||||||
if (serviceDiscoveryResult != null && serviceDiscoveryResult.getIdentities().size() > 0) {
|
|
||||||
ServiceDiscoveryResult.Identity identity = serviceDiscoveryResult.getIdentities().get(0);
|
|
||||||
String type = identity.getType();
|
|
||||||
String name = identity.getName();
|
|
||||||
if (type != null) {
|
|
||||||
typeMap.put(resource,type);
|
|
||||||
}
|
|
||||||
if (name != null) {
|
|
||||||
nameMap.put(resource, nameWithoutVersion(name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Pair<>(typeMap,nameMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String nameWithoutVersion(String name) {
|
public List<String> getStatusMessages() {
|
||||||
String[] parts = name.split(" ");
|
ArrayList<String> messages = new ArrayList<>();
|
||||||
if (parts.length > 1 && Character.isDigit(parts[parts.length -1].charAt(0))) {
|
synchronized (this.presences) {
|
||||||
StringBuilder output = new StringBuilder();
|
for (Presence presence : this.presences.values()) {
|
||||||
for(int i = 0; i < parts.length -1; ++i) {
|
String message = presence.getMessage() == null ? null : presence.getMessage().trim();
|
||||||
if (output.length() != 0) {
|
if (message != null && !message.isEmpty() && !messages.contains(message)) {
|
||||||
output.append(' ');
|
messages.add(message);
|
||||||
}
|
}
|
||||||
output.append(parts[i]);
|
}
|
||||||
}
|
}
|
||||||
return output.toString();
|
return messages;
|
||||||
} else {
|
}
|
||||||
return name;
|
|
||||||
}
|
public boolean allOrNonSupport(String namespace) {
|
||||||
}
|
synchronized (this.presences) {
|
||||||
|
for (Presence presence : this.presences.values()) {
|
||||||
|
ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
|
||||||
|
if (disco == null || !disco.getFeatures().contains(namespace)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean anySupport(final String namespace) {
|
||||||
|
synchronized (this.presences) {
|
||||||
|
for (Presence presence : this.presences.values()) {
|
||||||
|
ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
|
||||||
|
if (disco != null && disco.getFeatures().contains(namespace)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pair<Map<String, String>, Map<String, String>> toTypeAndNameMap() {
|
||||||
|
Map<String, String> typeMap = new HashMap<>();
|
||||||
|
Map<String, String> nameMap = new HashMap<>();
|
||||||
|
synchronized (this.presences) {
|
||||||
|
for (Map.Entry<String, Presence> presenceEntry : this.presences.entrySet()) {
|
||||||
|
String resource = presenceEntry.getKey();
|
||||||
|
Presence presence = presenceEntry.getValue();
|
||||||
|
ServiceDiscoveryResult serviceDiscoveryResult = presence == null ? null : presence.getServiceDiscoveryResult();
|
||||||
|
if (serviceDiscoveryResult != null && serviceDiscoveryResult.getIdentities().size() > 0) {
|
||||||
|
ServiceDiscoveryResult.Identity identity = serviceDiscoveryResult.getIdentities().get(0);
|
||||||
|
String type = identity.getType();
|
||||||
|
String name = identity.getName();
|
||||||
|
if (type != null) {
|
||||||
|
typeMap.put(resource, type);
|
||||||
|
}
|
||||||
|
if (name != null) {
|
||||||
|
nameMap.put(resource, nameWithoutVersion(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Pair<>(typeMap, nameMap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,8 @@ import android.os.IBinder;
|
||||||
import android.support.v4.app.NotificationCompat;
|
import android.support.v4.app.NotificationCompat;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
@ -70,7 +72,7 @@ public class ExportBackupService extends Service {
|
||||||
if (Compatibility.runsAndTargetsTwentyFour(context)) {
|
if (Compatibility.runsAndTargetsTwentyFour(context)) {
|
||||||
openIntent.setType("resource/folder");
|
openIntent.setType("resource/folder");
|
||||||
} else {
|
} else {
|
||||||
openIntent.setDataAndType(Uri.parse("file://"+path),"resource/folder");
|
openIntent.setDataAndType(Uri.parse("file://" + path), "resource/folder");
|
||||||
}
|
}
|
||||||
openIntent.putExtra("org.openintents.extra.ABSOLUTE_PATH", path);
|
openIntent.putExtra("org.openintents.extra.ABSOLUTE_PATH", path);
|
||||||
|
|
||||||
|
@ -87,7 +89,7 @@ public class ExportBackupService extends Service {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void accountExport(SQLiteDatabase db, String uuid, PrintWriter writer) {
|
private static void accountExport(final SQLiteDatabase db, final String uuid, final PrintWriter writer) {
|
||||||
final StringBuilder builder = new StringBuilder();
|
final StringBuilder builder = new StringBuilder();
|
||||||
final Cursor accountCursor = db.query(Account.TABLENAME, null, Account.UUID + "=?", new String[]{uuid}, null, null, null);
|
final Cursor accountCursor = db.query(Account.TABLENAME, null, Account.UUID + "=?", new String[]{uuid}, null, null, null);
|
||||||
while (accountCursor != null && accountCursor.moveToNext()) {
|
while (accountCursor != null && accountCursor.moveToNext()) {
|
||||||
|
@ -136,27 +138,28 @@ public class ExportBackupService extends Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] getKey(String password, byte[] salt) {
|
public static byte[] getKey(final String password, final byte[] salt) throws InvalidKeySpecException {
|
||||||
|
final SecretKeyFactory factory;
|
||||||
try {
|
try {
|
||||||
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
|
||||||
return factory.generateSecret(new PBEKeySpec(password.toCharArray(), salt, 1024, 128)).getEncoded();
|
} catch (NoSuchAlgorithmException e) {
|
||||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
|
throw new IllegalStateException(e);
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
}
|
||||||
|
return factory.generateSecret(new PBEKeySpec(password.toCharArray(), salt, 1024, 128)).getEncoded();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String cursorToString(String tablename, Cursor cursor, int max) {
|
private static String cursorToString(final String table, final Cursor cursor, final int max) {
|
||||||
return cursorToString(tablename, cursor, max, false);
|
return cursorToString(table, cursor, max, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String cursorToString(final String tablename, final Cursor cursor, int max, boolean ignore) {
|
private static String cursorToString(final String table, final Cursor cursor, int max, boolean ignore) {
|
||||||
final boolean identities = SQLiteAxolotlStore.IDENTITIES_TABLENAME.equals(tablename);
|
final boolean identities = SQLiteAxolotlStore.IDENTITIES_TABLENAME.equals(table);
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
builder.append("INSERT ");
|
builder.append("INSERT ");
|
||||||
if (ignore) {
|
if (ignore) {
|
||||||
builder.append("OR IGNORE ");
|
builder.append("OR IGNORE ");
|
||||||
}
|
}
|
||||||
builder.append("INTO ").append(tablename).append("(");
|
builder.append("INTO ").append(table).append("(");
|
||||||
int skipColumn = -1;
|
int skipColumn = -1;
|
||||||
for (int i = 0; i < cursor.getColumnCount(); ++i) {
|
for (int i = 0; i < cursor.getColumnCount(); ++i) {
|
||||||
final String name = cursor.getColumnName(i);
|
final String name = cursor.getColumnName(i);
|
||||||
|
@ -222,7 +225,8 @@ public class ExportBackupService extends Service {
|
||||||
try {
|
try {
|
||||||
files = export();
|
files = export();
|
||||||
success = true;
|
success = true;
|
||||||
} catch (Exception e) {
|
} catch (final Exception e) {
|
||||||
|
Log.d(Config.LOGTAG, "unable to create backup", e);
|
||||||
success = false;
|
success = false;
|
||||||
files = Collections.emptyList();
|
files = Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
@ -234,6 +238,8 @@ public class ExportBackupService extends Service {
|
||||||
stopSelf();
|
stopSelf();
|
||||||
}).start();
|
}).start();
|
||||||
return START_STICKY;
|
return START_STICKY;
|
||||||
|
} else {
|
||||||
|
Log.d(Config.LOGTAG, "ExportBackupService. ignoring start command because already running");
|
||||||
}
|
}
|
||||||
return START_NOT_STICKY;
|
return START_NOT_STICKY;
|
||||||
}
|
}
|
||||||
|
@ -241,7 +247,7 @@ public class ExportBackupService extends Service {
|
||||||
private void messageExport(SQLiteDatabase db, String uuid, PrintWriter writer, Progress progress) {
|
private void messageExport(SQLiteDatabase db, String uuid, PrintWriter writer, Progress progress) {
|
||||||
Cursor cursor = db.rawQuery("select messages.* from messages join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?", new String[]{uuid});
|
Cursor cursor = db.rawQuery("select messages.* from messages join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?", new String[]{uuid});
|
||||||
int size = cursor != null ? cursor.getCount() : 0;
|
int size = cursor != null ? cursor.getCount() : 0;
|
||||||
Log.d(Config.LOGTAG, "exporting " + size + " messages");
|
Log.d(Config.LOGTAG, "exporting " + size + " messages for account " + uuid);
|
||||||
int i = 0;
|
int i = 0;
|
||||||
int p = 0;
|
int p = 0;
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
|
@ -272,7 +278,14 @@ public class ExportBackupService extends Service {
|
||||||
final int max = this.mAccounts.size();
|
final int max = this.mAccounts.size();
|
||||||
final SecureRandom secureRandom = new SecureRandom();
|
final SecureRandom secureRandom = new SecureRandom();
|
||||||
final List<File> files = new ArrayList<>();
|
final List<File> files = new ArrayList<>();
|
||||||
for (Account account : this.mAccounts) {
|
Log.d(Config.LOGTAG, "starting backup for " + max + " accounts");
|
||||||
|
for (final Account account : this.mAccounts) {
|
||||||
|
final String password = account.getPassword();
|
||||||
|
if (Strings.nullToEmpty(password).trim().isEmpty()) {
|
||||||
|
Log.d(Config.LOGTAG, String.format("skipping backup for %s because password is empty. unable to encrypt", account.getJid().asBareJid()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Log.d(Config.LOGTAG, String.format("exporting data for account %s (%s)", account.getJid().asBareJid(), account.getUuid()));
|
||||||
final byte[] IV = new byte[12];
|
final byte[] IV = new byte[12];
|
||||||
final byte[] salt = new byte[16];
|
final byte[] salt = new byte[16];
|
||||||
secureRandom.nextBytes(IV);
|
secureRandom.nextBytes(IV);
|
||||||
|
@ -281,8 +294,9 @@ public class ExportBackupService extends Service {
|
||||||
final Progress progress = new Progress(mBuilder, max, count);
|
final Progress progress = new Progress(mBuilder, max, count);
|
||||||
final File file = new File(FileBackend.getBackupDirectory(this) + account.getJid().asBareJid().toEscapedString() + ".ceb");
|
final File file = new File(FileBackend.getBackupDirectory(this) + account.getJid().asBareJid().toEscapedString() + ".ceb");
|
||||||
files.add(file);
|
files.add(file);
|
||||||
if (file.getParentFile().mkdirs()) {
|
final File directory = file.getParentFile();
|
||||||
Log.d(Config.LOGTAG, "created backup directory " + file.getParentFile().getAbsolutePath());
|
if (directory != null && directory.mkdirs()) {
|
||||||
|
Log.d(Config.LOGTAG, "created backup directory " + directory.getAbsolutePath());
|
||||||
}
|
}
|
||||||
final FileOutputStream fileOutputStream = new FileOutputStream(file);
|
final FileOutputStream fileOutputStream = new FileOutputStream(file);
|
||||||
final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
|
final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
|
||||||
|
@ -290,8 +304,7 @@ public class ExportBackupService extends Service {
|
||||||
dataOutputStream.flush();
|
dataOutputStream.flush();
|
||||||
|
|
||||||
final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
|
final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
|
||||||
byte[] key = getKey(account.getPassword(), salt);
|
final byte[] key = getKey(password, salt);
|
||||||
Log.d(Config.LOGTAG, backupFileHeader.toString());
|
|
||||||
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
|
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
|
||||||
IvParameterSpec ivSpec = new IvParameterSpec(IV);
|
IvParameterSpec ivSpec = new IvParameterSpec(IV);
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
|
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
|
||||||
|
@ -315,7 +328,7 @@ public class ExportBackupService extends Service {
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifySuccess(List<File> files) {
|
private void notifySuccess(final List<File> files) {
|
||||||
final String path = FileBackend.getBackupDirectory(this);
|
final String path = FileBackend.getBackupDirectory(this);
|
||||||
|
|
||||||
PendingIntent openFolderIntent = null;
|
PendingIntent openFolderIntent = null;
|
||||||
|
@ -331,14 +344,14 @@ public class ExportBackupService extends Service {
|
||||||
if (files.size() > 0) {
|
if (files.size() > 0) {
|
||||||
final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
|
final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
|
||||||
ArrayList<Uri> uris = new ArrayList<>();
|
ArrayList<Uri> uris = new ArrayList<>();
|
||||||
for(File file : files) {
|
for (File file : files) {
|
||||||
uris.add(FileBackend.getUriForFile(this, file));
|
uris.add(FileBackend.getUriForFile(this, file));
|
||||||
}
|
}
|
||||||
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
|
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
intent.setType(MIME_TYPE);
|
intent.setType(MIME_TYPE);
|
||||||
final Intent chooser = Intent.createChooser(intent, getString(R.string.share_backup_files));
|
final Intent chooser = Intent.createChooser(intent, getString(R.string.share_backup_files));
|
||||||
shareFilesIntent = PendingIntent.getActivity(this,190, chooser, PendingIntent.FLAG_UPDATE_CURRENT);
|
shareFilesIntent = PendingIntent.getActivity(this, 190, chooser, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
|
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
|
||||||
|
@ -361,7 +374,7 @@ public class ExportBackupService extends Service {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Progress {
|
private static class Progress {
|
||||||
private final NotificationCompat.Builder builder;
|
private final NotificationCompat.Builder builder;
|
||||||
private final int max;
|
private final int max;
|
||||||
private final int count;
|
private final int count;
|
||||||
|
|
|
@ -4468,8 +4468,8 @@ public class XmppConnectionService extends Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void injectServiceDiscoveryResult(Roster roster, String hash, String ver, ServiceDiscoveryResult disco) {
|
private void injectServiceDiscoveryResult(Roster roster, String hash, String ver, ServiceDiscoveryResult disco) {
|
||||||
for (Contact contact : roster.getContacts()) {
|
for (final Contact contact : roster.getContacts()) {
|
||||||
for (Presence presence : contact.getPresences().getPresences().values()) {
|
for (final Presence presence : contact.getPresences().getPresences()) {
|
||||||
if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) {
|
if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) {
|
||||||
presence.setServiceDiscoveryResult(disco);
|
presence.setServiceDiscoveryResult(disco);
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,6 +116,7 @@ import eu.siacs.conversations.utils.QuickLoader;
|
||||||
import eu.siacs.conversations.utils.StylingHelper;
|
import eu.siacs.conversations.utils.StylingHelper;
|
||||||
import eu.siacs.conversations.utils.TimeFrameUtils;
|
import eu.siacs.conversations.utils.TimeFrameUtils;
|
||||||
import eu.siacs.conversations.utils.UIHelper;
|
import eu.siacs.conversations.utils.UIHelper;
|
||||||
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
import eu.siacs.conversations.xmpp.XmppConnection;
|
import eu.siacs.conversations.xmpp.XmppConnection;
|
||||||
import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
||||||
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
|
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
|
||||||
|
@ -1342,11 +1343,28 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
Toast.makeText(getActivity(), R.string.only_one_call_at_a_time, Toast.LENGTH_LONG).show();
|
Toast.makeText(getActivity(), R.string.only_one_call_at_a_time, Toast.LENGTH_LONG).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Contact contact = conversation.getContact();
|
final Contact contact = conversation.getContact();
|
||||||
|
if (contact.getPresences().anySupport(Namespace.JINGLE_MESSAGE)) {
|
||||||
|
triggerRtpSession(contact.getAccount(),contact.getJid().asBareJid(),action);
|
||||||
|
} else {
|
||||||
|
final RtpCapability.Capability capability;
|
||||||
|
if (action.equals(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL)) {
|
||||||
|
capability = RtpCapability.Capability.VIDEO;
|
||||||
|
} else {
|
||||||
|
capability = RtpCapability.Capability.AUDIO;
|
||||||
|
}
|
||||||
|
PresenceSelector.selectFullJidForDirectRtpConnection(activity, contact, capability, fullJid -> {
|
||||||
|
triggerRtpSession(contact.getAccount(), fullJid, action);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void triggerRtpSession(final Account account, final Jid with, final String action) {
|
||||||
final Intent intent = new Intent(activity, RtpSessionActivity.class);
|
final Intent intent = new Intent(activity, RtpSessionActivity.class);
|
||||||
intent.setAction(action);
|
intent.setAction(action);
|
||||||
intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, contact.getAccount().getJid().toEscapedString());
|
intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString());
|
||||||
intent.putExtra(RtpSessionActivity.EXTRA_WITH, contact.getJid().asBareJid().toEscapedString());
|
intent.putExtra(RtpSessionActivity.EXTRA_WITH, with.toEscapedString());
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
|
@ -2120,6 +2138,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
final String downloadUuid = extras.getString(ConversationsActivity.EXTRA_DOWNLOAD_UUID);
|
final String downloadUuid = extras.getString(ConversationsActivity.EXTRA_DOWNLOAD_UUID);
|
||||||
final String text = extras.getString(Intent.EXTRA_TEXT);
|
final String text = extras.getString(Intent.EXTRA_TEXT);
|
||||||
final String nick = extras.getString(ConversationsActivity.EXTRA_NICK);
|
final String nick = extras.getString(ConversationsActivity.EXTRA_NICK);
|
||||||
|
final String postInitAction = extras.getString(ConversationsActivity.EXTRA_POST_INIT_ACTION);
|
||||||
final boolean asQuote = extras.getBoolean(ConversationsActivity.EXTRA_AS_QUOTE);
|
final boolean asQuote = extras.getBoolean(ConversationsActivity.EXTRA_AS_QUOTE);
|
||||||
final boolean pm = extras.getBoolean(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, false);
|
final boolean pm = extras.getBoolean(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, false);
|
||||||
final boolean doNotAppend = extras.getBoolean(ConversationsActivity.EXTRA_DO_NOT_APPEND, false);
|
final boolean doNotAppend = extras.getBoolean(ConversationsActivity.EXTRA_DO_NOT_APPEND, false);
|
||||||
|
@ -2160,6 +2179,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
|
||||||
appendText(text, doNotAppend);
|
appendText(text, doNotAppend);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (ConversationsActivity.POST_ACTION_RECORD_VOICE.equals(postInitAction)) {
|
||||||
|
attachFile(ATTACHMENT_CHOICE_RECORD_VOICE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
final Message message = downloadUuid == null ? null : conversation.findMessageWithFileAndUuid(downloadUuid);
|
final Message message = downloadUuid == null ? null : conversation.findMessageWithFileAndUuid(downloadUuid);
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
startDownloadable(message);
|
startDownloadable(message);
|
||||||
|
|
|
@ -94,6 +94,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
|
||||||
public static final String EXTRA_NICK = "nick";
|
public static final String EXTRA_NICK = "nick";
|
||||||
public static final String EXTRA_IS_PRIVATE_MESSAGE = "pm";
|
public static final String EXTRA_IS_PRIVATE_MESSAGE = "pm";
|
||||||
public static final String EXTRA_DO_NOT_APPEND = "do_not_append";
|
public static final String EXTRA_DO_NOT_APPEND = "do_not_append";
|
||||||
|
public static final String EXTRA_POST_INIT_ACTION = "post_init_action";
|
||||||
|
public static final String POST_ACTION_RECORD_VOICE = "record_voice";
|
||||||
|
|
||||||
private static List<String> VIEW_AND_SHARE_ACTIONS = Arrays.asList(
|
private static List<String> VIEW_AND_SHARE_ACTIONS = Arrays.asList(
|
||||||
ACTION_VIEW_CONVERSATION,
|
ACTION_VIEW_CONVERSATION,
|
||||||
|
|
|
@ -44,12 +44,14 @@ import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.databinding.ActivityRtpSessionBinding;
|
import eu.siacs.conversations.databinding.ActivityRtpSessionBinding;
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.entities.Contact;
|
import eu.siacs.conversations.entities.Contact;
|
||||||
|
import eu.siacs.conversations.entities.Conversation;
|
||||||
import eu.siacs.conversations.services.AppRTCAudioManager;
|
import eu.siacs.conversations.services.AppRTCAudioManager;
|
||||||
import eu.siacs.conversations.services.XmppConnectionService;
|
import eu.siacs.conversations.services.XmppConnectionService;
|
||||||
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
|
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
|
||||||
import eu.siacs.conversations.ui.util.MainThreadExecutor;
|
import eu.siacs.conversations.ui.util.MainThreadExecutor;
|
||||||
import eu.siacs.conversations.utils.PermissionUtils;
|
import eu.siacs.conversations.utils.PermissionUtils;
|
||||||
import eu.siacs.conversations.utils.TimeFrameUtils;
|
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.AbstractJingleConnection;
|
||||||
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
|
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
|
||||||
import eu.siacs.conversations.xmpp.jingle.Media;
|
import eu.siacs.conversations.xmpp.jingle.Media;
|
||||||
|
@ -101,6 +103,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void addSink(final VideoTrack videoTrack, final SurfaceViewRenderer surfaceViewRenderer) {
|
||||||
|
try {
|
||||||
|
videoTrack.addSink(surfaceViewRenderer);
|
||||||
|
} catch (final IllegalStateException e) {
|
||||||
|
Log.e(Config.LOGTAG, "possible race condition on trying to display video track. ignoring", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
@ -297,7 +307,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
|
|
||||||
private void proposeJingleRtpSession(final Account account, final Jid with, final Set<Media> media) {
|
private void proposeJingleRtpSession(final Account account, final Jid with, final Set<Media> media) {
|
||||||
checkMicrophoneAvailability();
|
checkMicrophoneAvailability();
|
||||||
xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with, media);
|
if (with.isBareJid()) {
|
||||||
|
xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with, media);
|
||||||
|
} else {
|
||||||
|
final String sessionId = xmppConnectionService.getJingleConnectionManager().initializeRtpSession(account, with, media);
|
||||||
|
initializeActivityWithRunningRtpSession(account, with, sessionId);
|
||||||
|
resetIntent(account, with, sessionId);
|
||||||
|
}
|
||||||
putScreenInCallMode(media);
|
putScreenInCallMode(media);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -372,7 +388,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
private void startPictureInPicture() {
|
private void startPictureInPicture() {
|
||||||
try {
|
try {
|
||||||
|
@ -412,12 +427,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
final WeakReference<JingleRtpConnection> reference = xmppConnectionService.getJingleConnectionManager()
|
final WeakReference<JingleRtpConnection> reference = xmppConnectionService.getJingleConnectionManager()
|
||||||
.findJingleRtpConnection(account, with, sessionId);
|
.findJingleRtpConnection(account, with, sessionId);
|
||||||
if (reference == null || reference.get() == null) {
|
if (reference == null || reference.get() == null) {
|
||||||
finish();
|
throw new IllegalStateException("failed to initialize activity with running rtp session. session not found");
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
this.rtpConnectionReference = reference;
|
this.rtpConnectionReference = reference;
|
||||||
final RtpEndUserState currentState = requireRtpConnection().getEndUserState();
|
final RtpEndUserState currentState = requireRtpConnection().getEndUserState();
|
||||||
if (currentState == RtpEndUserState.ENDED) {
|
if (currentState == RtpEndUserState.ENDED) {
|
||||||
|
reference.get().throwStateTransitionException();
|
||||||
finish();
|
finish();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -436,8 +451,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reInitializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) {
|
private void reInitializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) {
|
||||||
runOnUiThread(() -> initializeActivityWithRunningRtpSession(account, with, sessionId));
|
runOnUiThread(() -> initializeActivityWithRunningRtpSession(account, with, sessionId));
|
||||||
|
resetIntent(account, with, sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetIntent(final Account account, final Jid with, final String sessionId) {
|
||||||
final Intent intent = new Intent(Intent.ACTION_VIEW);
|
final Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||||
intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
|
intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
|
||||||
intent.putExtra(EXTRA_WITH, with.toEscapedString());
|
intent.putExtra(EXTRA_WITH, with.toEscapedString());
|
||||||
|
@ -551,11 +570,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp);
|
this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp);
|
||||||
this.binding.acceptCall.setVisibility(View.VISIBLE);
|
this.binding.acceptCall.setVisibility(View.VISIBLE);
|
||||||
} else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
|
} else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
|
||||||
this.binding.rejectCall.setVisibility(View.INVISIBLE);
|
this.binding.rejectCall.setOnClickListener(this::exit);
|
||||||
this.binding.endCall.setOnClickListener(this::exit);
|
this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
|
||||||
this.binding.endCall.setImageResource(R.drawable.ic_clear_white_48dp);
|
this.binding.rejectCall.setVisibility(View.VISIBLE);
|
||||||
this.binding.endCall.setVisibility(View.VISIBLE);
|
this.binding.endCall.setVisibility(View.INVISIBLE);
|
||||||
this.binding.acceptCall.setVisibility(View.INVISIBLE);
|
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.APPLICATION_ERROR, RtpEndUserState.RETRACTED).contains(state)) {
|
||||||
this.binding.rejectCall.setOnClickListener(this::exit);
|
this.binding.rejectCall.setOnClickListener(this::exit);
|
||||||
this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
|
this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
|
||||||
|
@ -762,14 +783,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
//paint local view over remote view
|
//paint local view over remote view
|
||||||
binding.localVideo.setZOrderMediaOverlay(true);
|
binding.localVideo.setZOrderMediaOverlay(true);
|
||||||
binding.localVideo.setMirror(requireRtpConnection().isFrontCamera());
|
binding.localVideo.setMirror(requireRtpConnection().isFrontCamera());
|
||||||
localVideoTrack.get().addSink(binding.localVideo);
|
addSink(localVideoTrack.get(), binding.localVideo);
|
||||||
} else {
|
} else {
|
||||||
binding.localVideo.setVisibility(View.GONE);
|
binding.localVideo.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
final Optional<VideoTrack> remoteVideoTrack = getRemoteVideoTrack();
|
final Optional<VideoTrack> remoteVideoTrack = getRemoteVideoTrack();
|
||||||
if (remoteVideoTrack.isPresent()) {
|
if (remoteVideoTrack.isPresent()) {
|
||||||
ensureSurfaceViewRendererIsSetup(binding.remoteVideo);
|
ensureSurfaceViewRendererIsSetup(binding.remoteVideo);
|
||||||
remoteVideoTrack.get().addSink(binding.remoteVideo);
|
addSink(remoteVideoTrack.get(), binding.remoteVideo);
|
||||||
if (state == RtpEndUserState.CONNECTED) {
|
if (state == RtpEndUserState.CONNECTED) {
|
||||||
binding.appBarLayout.setVisibility(View.GONE);
|
binding.appBarLayout.setVisibility(View.GONE);
|
||||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||||
|
@ -828,7 +849,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
}
|
}
|
||||||
|
|
||||||
private void retry(View view) {
|
private void retry(View view) {
|
||||||
Log.d(Config.LOGTAG, "attempting retry");
|
|
||||||
final Intent intent = getIntent();
|
final Intent intent = getIntent();
|
||||||
final Account account = extractAccount(intent);
|
final Account account = extractAccount(intent);
|
||||||
final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
|
final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
|
||||||
|
@ -836,10 +856,25 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
final String action = intent.getAction();
|
final String action = intent.getAction();
|
||||||
final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
|
final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
|
||||||
this.rtpConnectionReference = null;
|
this.rtpConnectionReference = null;
|
||||||
|
Log.d(Config.LOGTAG, "attempting retry with " + with.toEscapedString());
|
||||||
proposeJingleRtpSession(account, with, media);
|
proposeJingleRtpSession(account, with, media);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exit(View view) {
|
private void exit(final View view) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recordVoiceMail(final View view) {
|
||||||
|
final Intent intent = getIntent();
|
||||||
|
final Account account = extractAccount(intent);
|
||||||
|
final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
|
||||||
|
final Conversation conversation = xmppConnectionService.findOrCreateConversation(account, with, false, true);
|
||||||
|
final Intent launchIntent = new Intent(this, ConversationsActivity.class);
|
||||||
|
launchIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
|
||||||
|
launchIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
|
||||||
|
launchIntent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
|
launchIntent.putExtra(ConversationsActivity.EXTRA_POST_INIT_ACTION, ConversationsActivity.POST_ACTION_RECORD_VOICE);
|
||||||
|
startActivity(launchIntent);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -875,7 +910,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//this happens when going from proposed session to actual session
|
//this happens when going from proposed session to actual session
|
||||||
reInitializeActivityWithRunningRapSession(account, with, sessionId);
|
reInitializeActivityWithRunningRtpSession(account, with, sessionId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final AbstractJingleConnection.Id id = requireRtpConnection().getId();
|
final AbstractJingleConnection.Id id = requireRtpConnection().getId();
|
||||||
|
@ -952,8 +987,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
||||||
|
|
||||||
private void resetIntent(final Account account, Jid with, final RtpEndUserState state, final Set<Media> media) {
|
private void resetIntent(final Account account, Jid with, final RtpEndUserState state, final Set<Media> media) {
|
||||||
final Intent intent = new Intent(Intent.ACTION_VIEW);
|
final Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||||
intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString());
|
|
||||||
intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
|
intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
|
||||||
|
if (account.getRoster().getContact(with).getPresences().anySupport(Namespace.JINGLE_MESSAGE)) {
|
||||||
|
intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString());
|
||||||
|
} else {
|
||||||
|
intent.putExtra(EXTRA_WITH, with.toEscapedString());
|
||||||
|
}
|
||||||
intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString());
|
intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString());
|
||||||
intent.putExtra(EXTRA_LAST_ACTION, media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL);
|
intent.putExtra(EXTRA_LAST_ACTION, media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL);
|
||||||
setIntent(intent);
|
setIntent(intent);
|
||||||
|
|
|
@ -44,92 +44,110 @@ import eu.siacs.conversations.entities.Conversation;
|
||||||
import eu.siacs.conversations.entities.Presences;
|
import eu.siacs.conversations.entities.Presences;
|
||||||
import eu.siacs.conversations.utils.CryptoHelper;
|
import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import eu.siacs.conversations.xmpp.jingle.RtpCapability;
|
||||||
|
|
||||||
public class PresenceSelector {
|
public class PresenceSelector {
|
||||||
|
|
||||||
public static void showPresenceSelectionDialog(Activity activity, final Conversation conversation, final OnPresenceSelected listener) {
|
public static void showPresenceSelectionDialog(Activity activity, final Conversation conversation, final OnPresenceSelected listener) {
|
||||||
final Contact contact = conversation.getContact();
|
final Contact contact = conversation.getContact();
|
||||||
final Presences presences = contact.getPresences();
|
final String[] resourceArray = contact.getPresences().toResourceArray();
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
showPresenceSelectionDialog(activity, contact, resourceArray, fullJid -> {
|
||||||
builder.setTitle(activity.getString(R.string.choose_presence));
|
conversation.setNextCounterpart(fullJid);
|
||||||
final String[] resourceArray = presences.toResourceArray();
|
listener.onPresenceSelected();
|
||||||
Pair<Map<String, String>, Map<String, String>> typeAndName = presences.toTypeAndNameMap();
|
});
|
||||||
final Map<String, String> resourceTypeMap = typeAndName.first;
|
}
|
||||||
final Map<String, String> resourceNameMap = typeAndName.second;
|
|
||||||
final String[] readableIdentities = new String[resourceArray.length];
|
|
||||||
final AtomicInteger selectedResource = new AtomicInteger(0);
|
|
||||||
for (int i = 0; i < resourceArray.length; ++i) {
|
|
||||||
String resource = resourceArray[i];
|
|
||||||
if (resource.equals(contact.getLastResource())) {
|
|
||||||
selectedResource.set(i);
|
|
||||||
}
|
|
||||||
String type = resourceTypeMap.get(resource);
|
|
||||||
String name = resourceNameMap.get(resource);
|
|
||||||
if (type != null) {
|
|
||||||
if (Collections.frequency(resourceTypeMap.values(), type) == 1) {
|
|
||||||
readableIdentities[i] = translateType(activity, type);
|
|
||||||
} else if (name != null) {
|
|
||||||
if (Collections.frequency(resourceNameMap.values(), name) == 1
|
|
||||||
|| CryptoHelper.UUID_PATTERN.matcher(resource).matches()) {
|
|
||||||
readableIdentities[i] = translateType(activity, type) + " (" + name + ")";
|
|
||||||
} else {
|
|
||||||
readableIdentities[i] = translateType(activity, type) + " (" + name + " / " + resource + ")";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
readableIdentities[i] = translateType(activity, type) + " (" + resource + ")";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
readableIdentities[i] = resource;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
builder.setSingleChoiceItems(readableIdentities,
|
|
||||||
selectedResource.get(),
|
|
||||||
(dialog, which) -> selectedResource.set(which));
|
|
||||||
builder.setNegativeButton(R.string.cancel, null);
|
|
||||||
builder.setPositiveButton(R.string.ok, (dialog, which) -> {
|
|
||||||
try {
|
|
||||||
Jid next = Jid.of(contact.getJid().getLocal(), contact.getJid().getDomain(), resourceArray[selectedResource.get()]);
|
|
||||||
conversation.setNextCounterpart(next);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
conversation.setNextCounterpart(null);
|
|
||||||
}
|
|
||||||
listener.onPresenceSelected();
|
|
||||||
});
|
|
||||||
builder.create().show();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void warnMutualPresenceSubscription(Activity activity, final Conversation conversation, final OnPresenceSelected listener) {
|
public static void selectFullJidForDirectRtpConnection(final Activity activity, final Contact contact, final RtpCapability.Capability required, final OnFullJidSelected onFullJidSelected) {
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
final String[] resources = RtpCapability.filterPresences(contact, required);
|
||||||
builder.setTitle(conversation.getContact().getJid().toString());
|
if (resources.length == 1) {
|
||||||
builder.setMessage(R.string.without_mutual_presence_updates);
|
onFullJidSelected.onFullJidSelected(contact.getJid().withResource(resources[0]));
|
||||||
builder.setNegativeButton(R.string.cancel, null);
|
} else {
|
||||||
builder.setPositiveButton(R.string.ignore, (dialog, which) -> {
|
showPresenceSelectionDialog(activity, contact, resources, onFullJidSelected);
|
||||||
conversation.setNextCounterpart(null);
|
}
|
||||||
if (listener != null) {
|
}
|
||||||
listener.onPresenceSelected();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
builder.create().show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String translateType(Context context, String type) {
|
private static void showPresenceSelectionDialog(final Activity activity, final Contact contact, final String[] resourceArray, final OnFullJidSelected onFullJidSelected) {
|
||||||
switch (type.toLowerCase()) {
|
final Presences presences = contact.getPresences();
|
||||||
case "pc":
|
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||||
return context.getString(R.string.type_pc);
|
builder.setTitle(activity.getString(R.string.choose_presence));
|
||||||
case "phone":
|
Pair<Map<String, String>, Map<String, String>> typeAndName = presences.toTypeAndNameMap();
|
||||||
return context.getString(R.string.type_phone);
|
final Map<String, String> resourceTypeMap = typeAndName.first;
|
||||||
case "tablet":
|
final Map<String, String> resourceNameMap = typeAndName.second;
|
||||||
return context.getString(R.string.type_tablet);
|
final String[] readableIdentities = new String[resourceArray.length];
|
||||||
case "web":
|
final AtomicInteger selectedResource = new AtomicInteger(0);
|
||||||
return context.getString(R.string.type_web);
|
for (int i = 0; i < resourceArray.length; ++i) {
|
||||||
case "console":
|
String resource = resourceArray[i];
|
||||||
return context.getString(R.string.type_console);
|
if (resource.equals(contact.getLastResource())) {
|
||||||
default:
|
selectedResource.set(i);
|
||||||
return type;
|
}
|
||||||
}
|
String type = resourceTypeMap.get(resource);
|
||||||
}
|
String name = resourceNameMap.get(resource);
|
||||||
|
if (type != null) {
|
||||||
|
if (Collections.frequency(resourceTypeMap.values(), type) == 1) {
|
||||||
|
readableIdentities[i] = translateType(activity, type);
|
||||||
|
} else if (name != null) {
|
||||||
|
if (Collections.frequency(resourceNameMap.values(), name) == 1
|
||||||
|
|| CryptoHelper.UUID_PATTERN.matcher(resource).matches()) {
|
||||||
|
readableIdentities[i] = translateType(activity, type) + " (" + name + ")";
|
||||||
|
} else {
|
||||||
|
readableIdentities[i] = translateType(activity, type) + " (" + name + " / " + resource + ")";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
readableIdentities[i] = translateType(activity, type) + " (" + resource + ")";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
readableIdentities[i] = resource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.setSingleChoiceItems(readableIdentities,
|
||||||
|
selectedResource.get(),
|
||||||
|
(dialog, which) -> selectedResource.set(which));
|
||||||
|
builder.setNegativeButton(R.string.cancel, null);
|
||||||
|
builder.setPositiveButton(
|
||||||
|
R.string.ok,
|
||||||
|
(dialog, which) -> onFullJidSelected.onFullJidSelected(
|
||||||
|
Jid.of(contact.getJid().getLocal(), contact.getJid().getDomain(), resourceArray[selectedResource.get()])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
builder.create().show();
|
||||||
|
}
|
||||||
|
|
||||||
public interface OnPresenceSelected {
|
public static void warnMutualPresenceSubscription(Activity activity, final Conversation conversation, final OnPresenceSelected listener) {
|
||||||
void onPresenceSelected();
|
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||||
}
|
builder.setTitle(conversation.getContact().getJid().toString());
|
||||||
|
builder.setMessage(R.string.without_mutual_presence_updates);
|
||||||
|
builder.setNegativeButton(R.string.cancel, null);
|
||||||
|
builder.setPositiveButton(R.string.ignore, (dialog, which) -> {
|
||||||
|
conversation.setNextCounterpart(null);
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onPresenceSelected();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
builder.create().show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String translateType(Context context, String type) {
|
||||||
|
switch (type.toLowerCase()) {
|
||||||
|
case "pc":
|
||||||
|
return context.getString(R.string.type_pc);
|
||||||
|
case "phone":
|
||||||
|
return context.getString(R.string.type_phone);
|
||||||
|
case "tablet":
|
||||||
|
return context.getString(R.string.type_tablet);
|
||||||
|
case "web":
|
||||||
|
return context.getString(R.string.type_web);
|
||||||
|
case "console":
|
||||||
|
return context.getString(R.string.type_console);
|
||||||
|
default:
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnPresenceSelected {
|
||||||
|
void onPresenceSelected();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnFullJidSelected {
|
||||||
|
void onFullJidSelected(Jid jid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle;
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
|
import com.google.common.base.MoreObjects;
|
||||||
import com.google.common.base.Objects;
|
import com.google.common.base.Objects;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
|
||||||
|
@ -61,6 +62,10 @@ public abstract class AbstractJingleConnection {
|
||||||
return new Id(account, with, sessionId);
|
return new Id(account, with, sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Id of(Account account, Jid with) {
|
||||||
|
return new Id(account, with, JingleConnectionManager.nextRandomId());
|
||||||
|
}
|
||||||
|
|
||||||
public static Id of(Message message) {
|
public static Id of(Message message) {
|
||||||
return new Id(
|
return new Id(
|
||||||
message.getConversation().getAccount(),
|
message.getConversation().getAccount(),
|
||||||
|
@ -78,14 +83,14 @@ public abstract class AbstractJingleConnection {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
Id id = (Id) o;
|
Id id = (Id) o;
|
||||||
return Objects.equal(account.getJid(), id.account.getJid()) &&
|
return Objects.equal(account.getUuid(), id.account.getUuid()) &&
|
||||||
Objects.equal(with, id.with) &&
|
Objects.equal(with, id.with) &&
|
||||||
Objects.equal(sessionId, id.sessionId);
|
Objects.equal(sessionId, id.sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hashCode(account.getJid(), with, sessionId);
|
return Objects.hashCode(account.getUuid(), with, sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -102,6 +107,15 @@ public abstract class AbstractJingleConnection {
|
||||||
public String getSessionId() {
|
public String getSessionId() {
|
||||||
return sessionId;
|
return sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return MoreObjects.toStringHelper(this)
|
||||||
|
.add("account", account.getJid())
|
||||||
|
.add("with", with)
|
||||||
|
.add("sessionId", sessionId)
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import com.google.common.cache.CacheBuilder;
|
||||||
import com.google.common.collect.Collections2;
|
import com.google.common.collect.Collections2;
|
||||||
import com.google.common.collect.ComparisonChain;
|
import com.google.common.collect.ComparisonChain;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.google.j2objc.annotations.Weak;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
@ -134,7 +135,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<RtpSessionProposal> findMatchingSessionProposal(final Account account, final Jid with, final Set<Media> media) {
|
private Optional<RtpSessionProposal> findMatchingSessionProposal(final Account account, final Jid with, final Set<Media> media) {
|
||||||
synchronized (this.rtpSessionProposals) {
|
synchronized (this.rtpSessionProposals) {
|
||||||
for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry : this.rtpSessionProposals.entrySet()) {
|
for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry : this.rtpSessionProposals.entrySet()) {
|
||||||
final RtpSessionProposal proposal = entry.getKey();
|
final RtpSessionProposal proposal = entry.getKey();
|
||||||
|
@ -446,7 +447,10 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
void finishConnection(final AbstractJingleConnection connection) {
|
void finishConnection(final AbstractJingleConnection connection) {
|
||||||
this.connections.remove(connection.getId());
|
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()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void getPrimaryCandidate(final Account account, final boolean initiator, final OnPrimaryCandidateFound listener) {
|
void getPrimaryCandidate(final Account account, final boolean initiator, final OnPrimaryCandidateFound listener) {
|
||||||
|
@ -520,6 +524,15 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
mXmppConnectionService.sendMessagePacket(account, messagePacket);
|
mXmppConnectionService.sendMessagePacket(account, messagePacket);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String initializeRtpSession(final Account account, final Jid with, final Set<Media> media) {
|
||||||
|
final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with);
|
||||||
|
final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, account.getJid());
|
||||||
|
rtpConnection.setProposedMedia(media);
|
||||||
|
this.connections.put(id, rtpConnection);
|
||||||
|
rtpConnection.sendSessionInitiate();
|
||||||
|
return id.sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
public void proposeJingleRtpSession(final Account account, final Jid with, final Set<Media> media) {
|
public void proposeJingleRtpSession(final Account account, final Jid with, final Set<Media> media) {
|
||||||
synchronized (this.rtpSessionProposals) {
|
synchronized (this.rtpSessionProposals) {
|
||||||
for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry : this.rtpSessionProposals.entrySet()) {
|
for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> entry : this.rtpSessionProposals.entrySet()) {
|
||||||
|
@ -667,7 +680,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void endSession(AbstractJingleConnection.Id id, final AbstractJingleConnection.State state) {
|
void endSession(AbstractJingleConnection.Id id, final AbstractJingleConnection.State state) {
|
||||||
this.endedSessions.put(PersistableSessionId.of(id), state);
|
this.endedSessions.put(PersistableSessionId.of(id), state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -417,7 +417,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple
|
||||||
final Jid jid = this.id.with;
|
final Jid jid = this.id.with;
|
||||||
String resource = jid != null ? jid.getResource() : null;
|
String resource = jid != null ? jid.getResource() : null;
|
||||||
if (resource != null) {
|
if (resource != null) {
|
||||||
Presence presence = this.id.account.getRoster().getContact(jid).getPresences().getPresences().get(resource);
|
Presence presence = this.id.account.getRoster().getContact(jid).getPresences().get(resource);
|
||||||
ServiceDiscoveryResult result = presence != null ? presence.getServiceDiscoveryResult() : null;
|
ServiceDiscoveryResult result = presence != null ? presence.getServiceDiscoveryResult() : null;
|
||||||
return result == null ? Collections.emptyList() : result.getFeatures();
|
return result == null ? Collections.emptyList() : result.getFeatures();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -123,6 +123,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
||||||
private final ArrayDeque<IceCandidate> pendingIceCandidates = new ArrayDeque<>();
|
private final ArrayDeque<IceCandidate> pendingIceCandidates = new ArrayDeque<>();
|
||||||
private final Message message;
|
private final Message message;
|
||||||
private State state = State.NULL;
|
private State state = State.NULL;
|
||||||
|
private StateTransitionException stateTransitionException;
|
||||||
private Set<Media> proposedMedia;
|
private Set<Media> proposedMedia;
|
||||||
private RtpContentMap initiatorRtpContentMap;
|
private RtpContentMap initiatorRtpContentMap;
|
||||||
private RtpContentMap responderRtpContentMap;
|
private RtpContentMap responderRtpContentMap;
|
||||||
|
@ -639,6 +640,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void sendSessionInitiate() {
|
||||||
|
sendSessionInitiate(this.proposedMedia, State.SESSION_INITIALIZED);
|
||||||
|
}
|
||||||
|
|
||||||
private void sendSessionInitiate(final Set<Media> media, final State targetState) {
|
private void sendSessionInitiate(final Set<Media> media, final State targetState) {
|
||||||
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate");
|
Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate");
|
||||||
discoverIceServers(iceServers -> sendSessionInitiate(media, targetState, iceServers));
|
discoverIceServers(iceServers -> sendSessionInitiate(media, targetState, iceServers));
|
||||||
|
@ -771,8 +776,16 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
||||||
xmppConnectionService.sendIqPacket(id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null);
|
xmppConnectionService.sendIqPacket(id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void throwStateTransitionException() {
|
||||||
|
final StateTransitionException exception = this.stateTransitionException;
|
||||||
|
if (exception != null) {
|
||||||
|
throw new IllegalStateException(String.format("Transition to %s did not call finish", exception.state), exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public RtpEndUserState getEndUserState() {
|
public RtpEndUserState getEndUserState() {
|
||||||
switch (this.state) {
|
switch (this.state) {
|
||||||
|
case NULL:
|
||||||
case PROPOSED:
|
case PROPOSED:
|
||||||
case SESSION_INITIALIZED:
|
case SESSION_INITIALIZED:
|
||||||
if (isInitiator()) {
|
if (isInitiator()) {
|
||||||
|
@ -828,10 +841,19 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
||||||
public Set<Media> getMedia() {
|
public Set<Media> getMedia() {
|
||||||
final State current = getState();
|
final State current = getState();
|
||||||
if (current == State.NULL) {
|
if (current == State.NULL) {
|
||||||
|
if (isInitiator()) {
|
||||||
|
return Preconditions.checkNotNull(
|
||||||
|
this.proposedMedia,
|
||||||
|
"RTP connection has not been initialized properly"
|
||||||
|
);
|
||||||
|
}
|
||||||
throw new IllegalStateException("RTP connection has not been initialized yet");
|
throw new IllegalStateException("RTP connection has not been initialized yet");
|
||||||
}
|
}
|
||||||
if (Arrays.asList(State.PROPOSED, State.PROCEED).contains(current)) {
|
if (Arrays.asList(State.PROPOSED, State.PROCEED).contains(current)) {
|
||||||
return Preconditions.checkNotNull(this.proposedMedia, "RTP connection has not been initialized properly");
|
return Preconditions.checkNotNull(
|
||||||
|
this.proposedMedia,
|
||||||
|
"RTP connection has not been initialized properly"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final RtpContentMap initiatorContentMap = initiatorRtpContentMap;
|
final RtpContentMap initiatorContentMap = initiatorRtpContentMap;
|
||||||
if (initiatorContentMap != null) {
|
if (initiatorContentMap != null) {
|
||||||
|
@ -983,6 +1005,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
||||||
final Collection<State> validTransitions = VALID_TRANSITIONS.get(this.state);
|
final Collection<State> validTransitions = VALID_TRANSITIONS.get(this.state);
|
||||||
if (validTransitions != null && validTransitions.contains(target)) {
|
if (validTransitions != null && validTransitions.contains(target)) {
|
||||||
this.state = target;
|
this.state = target;
|
||||||
|
this.stateTransitionException = new StateTransitionException(target);
|
||||||
if (runnable != null) {
|
if (runnable != null) {
|
||||||
runnable.run();
|
runnable.run();
|
||||||
}
|
}
|
||||||
|
@ -1231,4 +1254,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
||||||
private interface OnIceServersDiscovered {
|
private interface OnIceServersDiscovered {
|
||||||
void onIceServersDiscovered(List<PeerConnection.IceServer> iceServers);
|
void onIceServersDiscovered(List<PeerConnection.IceServer> iceServers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class StateTransitionException extends Exception {
|
||||||
|
private final State state;
|
||||||
|
|
||||||
|
private StateTransitionException(final State state) {
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle;
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import eu.siacs.conversations.entities.Contact;
|
import eu.siacs.conversations.entities.Contact;
|
||||||
import eu.siacs.conversations.entities.Presence;
|
import eu.siacs.conversations.entities.Presence;
|
||||||
|
@ -37,10 +39,25 @@ public class RtpCapability {
|
||||||
return Capability.NONE;
|
return Capability.NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String[] filterPresences(final Contact contact, Capability required) {
|
||||||
|
final Presences presences = contact.getPresences();
|
||||||
|
final ArrayList<String> resources = new ArrayList<>();
|
||||||
|
for(final Map.Entry<String,Presence> presence : presences.getPresencesMap().entrySet()) {
|
||||||
|
final Capability capability = check(presence.getValue());
|
||||||
|
if (capability == Capability.NONE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (required == Capability.AUDIO || capability == required) {
|
||||||
|
resources.add(presence.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resources.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
public static Capability check(final Contact contact) {
|
public static Capability check(final Contact contact) {
|
||||||
final Presences presences = contact.getPresences();
|
final Presences presences = contact.getPresences();
|
||||||
Capability result = Capability.NONE;
|
Capability result = Capability.NONE;
|
||||||
for(Presence presence : presences.getPresences().values()) {
|
for(Presence presence : presences.getPresences()) {
|
||||||
Capability capability = check(presence);
|
Capability capability = check(presence);
|
||||||
if (capability == Capability.VIDEO) {
|
if (capability == Capability.VIDEO) {
|
||||||
result = capability;
|
result = capability;
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 478 B |
Binary file not shown.
After Width: | Height: | Size: 221 B |
Binary file not shown.
After Width: | Height: | Size: 487 B |
Binary file not shown.
After Width: | Height: | Size: 625 B |
Binary file not shown.
After Width: | Height: | Size: 971 B |
|
@ -246,7 +246,7 @@
|
||||||
<string name="contact_added_you">Votre correspondant vous a ajouté dans sa liste de contacts</string>
|
<string name="contact_added_you">Votre correspondant vous a ajouté dans sa liste de contacts</string>
|
||||||
<string name="add_back">Ajouter en retour</string>
|
<string name="add_back">Ajouter en retour</string>
|
||||||
<string name="contact_has_read_up_to_this_point">%s a tout lu jusqu\'ici</string>
|
<string name="contact_has_read_up_to_this_point">%s a tout lu jusqu\'ici</string>
|
||||||
<string name="contacts_have_read_up_to_this_point">%s a tout lu jusqu\'ici</string>
|
<string name="contacts_have_read_up_to_this_point">%s ont tout lu jusqu\'ici</string>
|
||||||
<string name="contacts_and_n_more_have_read_up_to_this_point">%1$s+%2$d autres ont tout lu jusqu\'ici</string>
|
<string name="contacts_and_n_more_have_read_up_to_this_point">%1$s+%2$d autres ont tout lu jusqu\'ici</string>
|
||||||
<string name="everyone_has_read_up_to_this_point">Tout le monde a lu jusqu\'ici</string>
|
<string name="everyone_has_read_up_to_this_point">Tout le monde a lu jusqu\'ici</string>
|
||||||
<string name="publish">Publier</string>
|
<string name="publish">Publier</string>
|
||||||
|
@ -398,7 +398,7 @@
|
||||||
<string name="until_further_notice">Jusqu\'à nouvel ordre</string>
|
<string name="until_further_notice">Jusqu\'à nouvel ordre</string>
|
||||||
<string name="snooze">Répéter les notifications</string>
|
<string name="snooze">Répéter les notifications</string>
|
||||||
<string name="reply">Répondre</string>
|
<string name="reply">Répondre</string>
|
||||||
<string name="mark_as_read">Marqué comme lu</string>
|
<string name="mark_as_read">Marquer comme lu</string>
|
||||||
<string name="pref_input_options">Saisie</string>
|
<string name="pref_input_options">Saisie</string>
|
||||||
<string name="pref_enter_is_send">Touche Entrée pour envoyer</string>
|
<string name="pref_enter_is_send">Touche Entrée pour envoyer</string>
|
||||||
<string name="pref_enter_is_send_summary">Utiliser la touche Entrée pour envoyer un message.</string>
|
<string name="pref_enter_is_send_summary">Utiliser la touche Entrée pour envoyer un message.</string>
|
||||||
|
@ -918,6 +918,8 @@
|
||||||
<string name="only_one_call_at_a_time">Vous ne pouvez prendre qu\'un appel à la fois.</string>
|
<string name="only_one_call_at_a_time">Vous ne pouvez prendre qu\'un appel à la fois.</string>
|
||||||
<string name="return_to_ongoing_call">Reprendre l\'appel en cours</string>
|
<string name="return_to_ongoing_call">Reprendre l\'appel en cours</string>
|
||||||
<string name="could_not_switch_camera">Impossible de changer de caméra</string>
|
<string name="could_not_switch_camera">Impossible de changer de caméra</string>
|
||||||
|
<string name="add_to_favorites">Ajouter aux favoris</string>
|
||||||
|
<string name="remove_from_favorites">Enlever des favoris</string>
|
||||||
<plurals name="view_users">
|
<plurals name="view_users">
|
||||||
<item quantity="one">Voir %1$d participant</item>
|
<item quantity="one">Voir %1$d participant</item>
|
||||||
<item quantity="other">Voir %1$d participants</item>
|
<item quantity="other">Voir %1$d participants</item>
|
||||||
|
|
|
@ -272,6 +272,8 @@
|
||||||
<string name="pref_quiet_hours_summary">Az értesítések el lesznek némítva a csendes órák alatt</string>
|
<string name="pref_quiet_hours_summary">Az értesítések el lesznek némítva a csendes órák alatt</string>
|
||||||
<string name="pref_expert_options_other">Egyéb</string>
|
<string name="pref_expert_options_other">Egyéb</string>
|
||||||
<string name="pref_autojoin">Szinkronizálás a könyvjelzőkkel</string>
|
<string name="pref_autojoin">Szinkronizálás a könyvjelzőkkel</string>
|
||||||
|
<string name="pref_autojoin_summary">Automatikusan csatlakozzon a csoportos csevegésekhez, ha ez szerepel a könyvjelzőben</string>
|
||||||
|
<string name="toast_message_omemo_fingerprint">OMEMO ujjlenyomat a vágólapra lett másolva</string>
|
||||||
<string name="conference_banned">Ki van tiltva ebből a csoportos csevegésből</string>
|
<string name="conference_banned">Ki van tiltva ebből a csoportos csevegésből</string>
|
||||||
<string name="conference_members_only">Ez a csoportos csevegés csak tagoknak szól</string>
|
<string name="conference_members_only">Ez a csoportos csevegés csak tagoknak szól</string>
|
||||||
<string name="conference_resource_constraint">Erőforráskényszer</string>
|
<string name="conference_resource_constraint">Erőforráskényszer</string>
|
||||||
|
@ -886,6 +888,8 @@
|
||||||
<string name="only_one_call_at_a_time">Egyszerre csak egy hívásban vehet részt.</string>
|
<string name="only_one_call_at_a_time">Egyszerre csak egy hívásban vehet részt.</string>
|
||||||
<string name="return_to_ongoing_call">Visszatérés a kimenő híváshoz</string>
|
<string name="return_to_ongoing_call">Visszatérés a kimenő híváshoz</string>
|
||||||
<string name="could_not_switch_camera">Nem sikerült átváltani a kamerát</string>
|
<string name="could_not_switch_camera">Nem sikerült átváltani a kamerát</string>
|
||||||
|
<string name="add_to_favorites">Hozzáadás a kedvencekhez</string>
|
||||||
|
<string name="remove_from_favorites">Eltávolítás a kedvencekből</string>
|
||||||
<plurals name="view_users">
|
<plurals name="view_users">
|
||||||
<item quantity="one">%1$d résztvevő megtekintése</item>
|
<item quantity="one">%1$d résztvevő megtekintése</item>
|
||||||
<item quantity="other">%1$d résztvevő megtekintése</item>
|
<item quantity="other">%1$d résztvevő megtekintése</item>
|
||||||
|
|
|
@ -207,25 +207,32 @@
|
||||||
<string name="add_back">Добавить в ответ</string>
|
<string name="add_back">Добавить в ответ</string>
|
||||||
<string name="contact_has_read_up_to_this_point">%s прочит. сообщ. до этого момента</string>
|
<string name="contact_has_read_up_to_this_point">%s прочит. сообщ. до этого момента</string>
|
||||||
<string name="contacts_have_read_up_to_this_point">%s прочитали сообщения до этого момента</string>
|
<string name="contacts_have_read_up_to_this_point">%s прочитали сообщения до этого момента</string>
|
||||||
|
<string name="contacts_and_n_more_have_read_up_to_this_point">%1$s + ещё %2$d прочитали до этого места</string>
|
||||||
<string name="everyone_has_read_up_to_this_point">Все прочитали сообщения до этого момента</string>
|
<string name="everyone_has_read_up_to_this_point">Все прочитали сообщения до этого момента</string>
|
||||||
<string name="publish">Опубликовать</string>
|
<string name="publish">Опубликовать</string>
|
||||||
|
<string name="touch_to_choose_picture">Нажмите на аватар, чтобы выбрать новую фотографию из галереи</string>
|
||||||
<string name="publishing">Установка…</string>
|
<string name="publishing">Установка…</string>
|
||||||
<string name="error_publish_avatar_server_reject">Сервер отклонил размещение аватара</string>
|
<string name="error_publish_avatar_server_reject">Сервер отклонил размещение аватара</string>
|
||||||
|
<string name="error_publish_avatar_converting">Не удалось преобразовать вашу фотографию</string>
|
||||||
<string name="error_saving_avatar">Не удалось сохранить аватар</string>
|
<string name="error_saving_avatar">Не удалось сохранить аватар</string>
|
||||||
<string name="or_long_press_for_default">(Или долгое прикосновение, чтобы вернуть значения по умолчанию)</string>
|
<string name="or_long_press_for_default">(Или долгое прикосновение, чтобы вернуть значения по умолчанию)</string>
|
||||||
|
<string name="error_publish_avatar_no_server_support">Ваш сервер не поддерживает публикацию аватаров</string>
|
||||||
<string name="private_message">шёпот</string>
|
<string name="private_message">шёпот</string>
|
||||||
<string name="private_message_to">отправить %s</string>
|
<string name="private_message_to">отправить %s</string>
|
||||||
<string name="send_private_message_to">Приватное сообщение %s</string>
|
<string name="send_private_message_to">Приватное сообщение %s</string>
|
||||||
<string name="connect">Подключиться</string>
|
<string name="connect">Подключиться</string>
|
||||||
<string name="account_already_exists">Аккаунт уже существует</string>
|
<string name="account_already_exists">Аккаунт уже существует</string>
|
||||||
<string name="next">Далее</string>
|
<string name="next">Далее</string>
|
||||||
|
<string name="server_info_session_established">Сеанс установлен</string>
|
||||||
<string name="skip">Пропустить</string>
|
<string name="skip">Пропустить</string>
|
||||||
<string name="disable_notifications">Отключить уведомления</string>
|
<string name="disable_notifications">Отключить уведомления</string>
|
||||||
<string name="enable">Включить</string>
|
<string name="enable">Включить</string>
|
||||||
<string name="conference_requires_password">Конференция требует авторизации</string>
|
<string name="conference_requires_password">Конференция требует авторизации</string>
|
||||||
<string name="enter_password">Введите пароль</string>
|
<string name="enter_password">Введите пароль</string>
|
||||||
|
<string name="request_presence_updates">Пожалуйста, сначала запросите обновления присутствия у вашего собеседника.\n\n<small>Эта информация будет использоваться для определения того, каким клиентом пользуется ваш собеседник</small>.</string>
|
||||||
<string name="request_now">Запросить сейчас</string>
|
<string name="request_now">Запросить сейчас</string>
|
||||||
<string name="ignore">Игнорировать</string>
|
<string name="ignore">Игнорировать</string>
|
||||||
|
<string name="without_mutual_presence_updates"><b>Внимание:</b> Если обновления присутствия не включены на обеих сторонах, это может привести к возникновению неожиданных проблем.\n\n<small>Просмотрите сведения о контакте для проверки настроек обновлений присутствия.</small></string>
|
||||||
<string name="pref_security_settings">Безопасность</string>
|
<string name="pref_security_settings">Безопасность</string>
|
||||||
<string name="pref_allow_message_correction">Разрешить исправление сообщений</string>
|
<string name="pref_allow_message_correction">Разрешить исправление сообщений</string>
|
||||||
<string name="pref_allow_message_correction_summary">Позволить контактам редактировать сообщения</string>
|
<string name="pref_allow_message_correction_summary">Позволить контактам редактировать сообщения</string>
|
||||||
|
@ -248,6 +255,7 @@
|
||||||
<string name="conference_shutdown">Конференция была остановлена</string>
|
<string name="conference_shutdown">Конференция была остановлена</string>
|
||||||
<string name="conference_unknown_error">Вы больше не состоите в этой конференции</string>
|
<string name="conference_unknown_error">Вы больше не состоите в этой конференции</string>
|
||||||
<string name="using_account">используется аккаунт %s</string>
|
<string name="using_account">используется аккаунт %s</string>
|
||||||
|
<string name="hosted_on">размещено на %s</string>
|
||||||
<string name="checking_x">Проверка %s на сервере HTTP</string>
|
<string name="checking_x">Проверка %s на сервере HTTP</string>
|
||||||
<string name="not_connected_try_again">Вы неподключены. Попробуйте позже</string>
|
<string name="not_connected_try_again">Вы неподключены. Попробуйте позже</string>
|
||||||
<string name="check_x_filesize">Проверить размер %s</string>
|
<string name="check_x_filesize">Проверить размер %s</string>
|
||||||
|
@ -778,6 +786,7 @@
|
||||||
<string name="enter_your_name_instructions">Пожалуйста, введите ваше имя, чтобы другие люди, у которых нет вас в списке контактов, знали кто вы.</string>
|
<string name="enter_your_name_instructions">Пожалуйста, введите ваше имя, чтобы другие люди, у которых нет вас в списке контактов, знали кто вы.</string>
|
||||||
<string name="your_name">Ваше имя</string>
|
<string name="your_name">Ваше имя</string>
|
||||||
<string name="enter_your_name">Введите ваше имя</string>
|
<string name="enter_your_name">Введите ваше имя</string>
|
||||||
|
<string name="no_name_set_instructions">Используйте кнопку редактирования, чтобы задать ваше имя.</string>
|
||||||
<string name="reject_request">Отклонить запрос</string>
|
<string name="reject_request">Отклонить запрос</string>
|
||||||
<string name="install_orbot">Установите Orbot</string>
|
<string name="install_orbot">Установите Orbot</string>
|
||||||
<string name="start_orbot">Запустите Orbot</string>
|
<string name="start_orbot">Запустите Orbot</string>
|
||||||
|
|
|
@ -3,7 +3,11 @@
|
||||||
<string name="action_settings">Ayarlar</string>
|
<string name="action_settings">Ayarlar</string>
|
||||||
<string name="action_add">Yeni konuşma</string>
|
<string name="action_add">Yeni konuşma</string>
|
||||||
<string name="action_accounts">Hesapları yönet</string>
|
<string name="action_accounts">Hesapları yönet</string>
|
||||||
|
<string name="action_account">Hesabı yönet</string>
|
||||||
|
<string name="action_end_conversation">Bu konuşmayı kapat</string>
|
||||||
<string name="action_contact_details">Kişi bilgileri</string>
|
<string name="action_contact_details">Kişi bilgileri</string>
|
||||||
|
<string name="action_muc_details">Küme konuşması ayrıntıları</string>
|
||||||
|
<string name="channel_details">Kanal ayrıntıları</string>
|
||||||
<string name="action_secure">Güvenli konuşma</string>
|
<string name="action_secure">Güvenli konuşma</string>
|
||||||
<string name="action_add_account">Hesap ekle</string>
|
<string name="action_add_account">Hesap ekle</string>
|
||||||
<string name="action_edit_contact">İsmi düzenle</string>
|
<string name="action_edit_contact">İsmi düzenle</string>
|
||||||
|
@ -13,14 +17,20 @@
|
||||||
<string name="action_unblock_contact">Kişiyi engellemekten vazgeç</string>
|
<string name="action_unblock_contact">Kişiyi engellemekten vazgeç</string>
|
||||||
<string name="action_block_domain">Alan adını engelle</string>
|
<string name="action_block_domain">Alan adını engelle</string>
|
||||||
<string name="action_unblock_domain">Alan adını engellemekten vazgeç</string>
|
<string name="action_unblock_domain">Alan adını engellemekten vazgeç</string>
|
||||||
|
<string name="action_block_participant">Katılımcıyı engelle</string>
|
||||||
|
<string name="action_unblock_participant">Katılımcının engelini kaldır</string>
|
||||||
<string name="title_activity_manage_accounts">Hesapları yönet</string>
|
<string name="title_activity_manage_accounts">Hesapları yönet</string>
|
||||||
<string name="title_activity_settings">Ayarlar</string>
|
<string name="title_activity_settings">Ayarlar</string>
|
||||||
<string name="title_activity_sharewith">Konuşmayla paylaş</string>
|
<string name="title_activity_sharewith">Konuşmayla paylaş</string>
|
||||||
<string name="title_activity_start_conversation">Konuşma Başlat</string>
|
<string name="title_activity_start_conversation">Konuşma Başlat</string>
|
||||||
|
<string name="title_activity_choose_contact">Kişi Seç</string>
|
||||||
|
<string name="title_activity_choose_contacts">Kişi Seç</string>
|
||||||
|
<string name="title_activity_share_via_account">Hesap aracılığıyla paylaş</string>
|
||||||
<string name="title_activity_block_list">Listeyi blokla</string>
|
<string name="title_activity_block_list">Listeyi blokla</string>
|
||||||
<string name="just_now">şimdi</string>
|
<string name="just_now">şimdi</string>
|
||||||
<string name="minute_ago">1 dakika önce</string>
|
<string name="minute_ago">1 dakika önce</string>
|
||||||
<string name="minutes_ago">%d dakika önc</string>
|
<string name="minutes_ago">%d dakika önc</string>
|
||||||
|
<string name="x_unread_conversations">%d okunmamış konuşma</string>
|
||||||
<string name="sending">gönderiyor…</string>
|
<string name="sending">gönderiyor…</string>
|
||||||
<string name="message_decrypting">İleti deşifre ediliyor. Lütfen bekleyin…</string>
|
<string name="message_decrypting">İleti deşifre ediliyor. Lütfen bekleyin…</string>
|
||||||
<string name="pgp_message">OpenPGP şifreli ileti</string>
|
<string name="pgp_message">OpenPGP şifreli ileti</string>
|
||||||
|
@ -35,10 +45,14 @@
|
||||||
<string name="block_domain_text">%s üzerinden gelen tüm kişileri engellemek istiyor musunuz? </string>
|
<string name="block_domain_text">%s üzerinden gelen tüm kişileri engellemek istiyor musunuz? </string>
|
||||||
<string name="unblock_domain_text">%s üzerinden gelen kişilerdeki engellemeyi kaldırmak istiyor musunuz?</string>
|
<string name="unblock_domain_text">%s üzerinden gelen kişilerdeki engellemeyi kaldırmak istiyor musunuz?</string>
|
||||||
<string name="contact_blocked">Kişi engellendi</string>
|
<string name="contact_blocked">Kişi engellendi</string>
|
||||||
|
<string name="blocked">Engellendi</string>
|
||||||
<string name="register_account">Sunucuda yeni bir hesap oluştur</string>
|
<string name="register_account">Sunucuda yeni bir hesap oluştur</string>
|
||||||
<string name="change_password_on_server">Sunucudaki şifreni değiştir</string>
|
<string name="change_password_on_server">Sunucudaki şifreni değiştir</string>
|
||||||
<string name="share_with">Paylaş...</string>
|
<string name="share_with">Paylaş...</string>
|
||||||
|
<string name="start_conversation">Konuşma başlat</string>
|
||||||
|
<string name="invite_contact">Kişi davet et</string>
|
||||||
<string name="contacts">Kişiler</string>
|
<string name="contacts">Kişiler</string>
|
||||||
|
<string name="contact">Kişi</string>
|
||||||
<string name="cancel">İptal et</string>
|
<string name="cancel">İptal et</string>
|
||||||
<string name="set">Ayarla</string>
|
<string name="set">Ayarla</string>
|
||||||
<string name="add">Ekle</string>
|
<string name="add">Ekle</string>
|
||||||
|
|
|
@ -924,9 +924,9 @@
|
||||||
<string name="ongoing_video_call">Активний відеовиклик</string>
|
<string name="ongoing_video_call">Активний відеовиклик</string>
|
||||||
<string name="disable_tor_to_make_call">Вимкнути ToR для здійснення викликів</string>
|
<string name="disable_tor_to_make_call">Вимкнути ToR для здійснення викликів</string>
|
||||||
<string name="incoming_call">Вхідний виклик</string>
|
<string name="incoming_call">Вхідний виклик</string>
|
||||||
<string name="incoming_call_duration">%s · Вхідний виклик · %s</string>
|
<string name="incoming_call_duration">Вхідний виклик · %s</string>
|
||||||
<string name="outgoing_call">Вихідний виклик</string>
|
<string name="outgoing_call">Вихідний виклик</string>
|
||||||
<string name="outgoing_call_duration">%s · Вихідний виклик · %s</string>
|
<string name="outgoing_call_duration">Вихідний виклик · %s</string>
|
||||||
<string name="missed_call">Пропущені виклики</string>
|
<string name="missed_call">Пропущені виклики</string>
|
||||||
<string name="audio_call">Голосовий виклик</string>
|
<string name="audio_call">Голосовий виклик</string>
|
||||||
<string name="video_call">Відеовиклик</string>
|
<string name="video_call">Відеовиклик</string>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="action_muc_details">群聊详情</string>
|
<string name="action_muc_details">群聊详情</string>
|
||||||
<string name="channel_details">频道详情</string>
|
<string name="channel_details">频道详情</string>
|
||||||
<string name="action_secure">加密聊天</string>
|
<string name="action_secure">加密聊天</string>
|
||||||
<string name="action_add_account">添加账号</string>
|
<string name="action_add_account">添加账户</string>
|
||||||
<string name="action_edit_contact">编辑名称</string>
|
<string name="action_edit_contact">编辑名称</string>
|
||||||
<string name="action_add_phone_book">添加到通讯录</string>
|
<string name="action_add_phone_book">添加到通讯录</string>
|
||||||
<string name="action_delete_contact">从畅聊通讯录中删除</string>
|
<string name="action_delete_contact">从畅聊通讯录中删除</string>
|
||||||
|
@ -167,7 +167,7 @@
|
||||||
<string name="encryption_choice_otr">OTR</string>
|
<string name="encryption_choice_otr">OTR</string>
|
||||||
<string name="encryption_choice_pgp">OpenPGP</string>
|
<string name="encryption_choice_pgp">OpenPGP</string>
|
||||||
<string name="encryption_choice_omemo">OMEMO</string>
|
<string name="encryption_choice_omemo">OMEMO</string>
|
||||||
<string name="mgmt_account_delete">删除账号</string>
|
<string name="mgmt_account_delete">删除账户</string>
|
||||||
<string name="mgmt_account_disable">暂时不可用</string>
|
<string name="mgmt_account_disable">暂时不可用</string>
|
||||||
<string name="mgmt_account_publish_avatar">发布头像</string>
|
<string name="mgmt_account_publish_avatar">发布头像</string>
|
||||||
<string name="mgmt_account_publish_pgp">发布OpenPGP公钥</string>
|
<string name="mgmt_account_publish_pgp">发布OpenPGP公钥</string>
|
||||||
|
@ -261,7 +261,7 @@
|
||||||
<string name="private_message_to">至%s</string>
|
<string name="private_message_to">至%s</string>
|
||||||
<string name="send_private_message_to">与%s私聊</string>
|
<string name="send_private_message_to">与%s私聊</string>
|
||||||
<string name="connect">连接</string>
|
<string name="connect">连接</string>
|
||||||
<string name="account_already_exists">该账号已存在</string>
|
<string name="account_already_exists">该账户已存在</string>
|
||||||
<string name="next">下一步</string>
|
<string name="next">下一步</string>
|
||||||
<string name="server_info_session_established">会话已建立</string>
|
<string name="server_info_session_established">会话已建立</string>
|
||||||
<string name="skip">跳过</string>
|
<string name="skip">跳过</string>
|
||||||
|
|
Loading…
Reference in New Issue