From 41ada3480cbb0fd3948ddd7630ca8cb9676ab585 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 12 Mar 2020 21:28:54 +0100 Subject: [PATCH 001/182] add jfif and jif as jpeg mime types --- src/main/java/eu/siacs/conversations/utils/MimeUtils.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java index 3040ce15e..e26010d69 100644 --- a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java @@ -16,7 +16,6 @@ package eu.siacs.conversations.utils; import android.content.Context; import android.net.Uri; -import android.os.Build; import android.util.Log; import java.io.File; @@ -274,6 +273,8 @@ public final class MimeUtils { add("image/jpeg", "jpg"); add("image/jpeg", "jpeg"); add("image/jpeg", "jpe"); + add("image/jpeg", "jfif"); + add("image/jpeg", "jif"); add("image/pcx", "pcx"); add("image/png", "png"); add("image/svg+xml", "svg"); From 0718c70f6b5df76b78baa68f16b51ce039c460ea Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 19 Mar 2020 09:51:32 +0000 Subject: [PATCH 002/182] clarify foreground notification for fdroid users --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0bfc8b77c..a5ae2fb0e 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ However you can disable the notification via settings of the operating system. ( **The battery consumption and the entire behaviour of Conversations will remain the same (as good or as bad as it was before). Why is Google doing this to you? We have no idea.** -##### Android <= 7.1 +##### Android <= 7.1 or Conversations from F-Droid (all Android versions) The foreground notification is still controlled over the expert settings within Conversations as it always has been. Whether or not you need to enable it depends on how aggressive the non-standard 'power saving' features are that your phone vendor has built into the operating system. ##### Android 8.x From e49ad3d5735999107b12706c223ff45b493eb66a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 20 Mar 2020 12:59:14 +0100 Subject: [PATCH 003/182] pulled translations from transifex --- src/conversations/res/values-fr/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/conversations/res/values-fr/strings.xml b/src/conversations/res/values-fr/strings.xml index 8fa8a3b9f..fe5c492c7 100644 --- a/src/conversations/res/values-fr/strings.xml +++ b/src/conversations/res/values-fr/strings.xml @@ -3,8 +3,8 @@ Choisissez votre fournisseur XMPP Utiliser conversations.im Créer un nouveau compte - Avez-vous déjà un compte XMPP ? Cela peut être le cas si vous utilisez déjà un autre client XMPP ou si vous avez déjà utilisé Conversations auparavant. Sinon, vous pouvez créer un nouveau compte XMPP dès maintenant. Remarque : Certains fournisseurs de messagerie proposent également des comptes XMPP. - XMPP est un réseau de messagerie instantanée indépendant du fournisseur. Vous pouvez utiliser ce client avec n\'importe quel serveur XMPP de votre choix. Toutefois, pour votre commodité, nous avons facilité la création d\'un compte sur conversations.im¹ ; un fournisseur spécialement conçu pour l\'utilisation avec Conversations. + Avez-vous déjà un compte XMPP ? Cela peut être le cas si vous utilisez déjà un autre client XMPP ou si vous avez déjà utilisé Conversations auparavant. Sinon, vous pouvez créer un nouveau compte XMPP dès maintenant.\nRemarque : Certains fournisseurs de messagerie proposent également des comptes XMPP. + XMPP est un réseau de messagerie instantanée indépendant du fournisseur. Vous pouvez utiliser ce client avec n’importe quel serveur XMPP de votre choix.\nToutefois, pour votre commodité, nous avons facilité la création d’un compte sur conversations.im¹ ; un fournisseur spécialement conçu pour Conversations. Vous avez été invité à %1$s. Nous vous guiderons dans le processus de création d\'un compte.\nEn choisissant %1$s comme fournisseur, vous pourrez communiquer avec les utilisateurs d\'autres fournisseurs en leur donnant votre adresse XMPP complète. Vous avez été invité par %1$s . Un nom d\'utilisateur a déjà été choisi pour vous. Nous vous guiderons dans le processus de création d\'un compte. Vous pourrez communiquer avec les utilisateurs d\'autres fournisseurs en leur donnant votre adresse XMPP complète. Votre invitation au serveur From 3cf469a43b96e03386e70bcc9aff4d4b0b071031 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 20 Mar 2020 12:59:30 +0100 Subject: [PATCH 004/182] update some dependencies --- build.gradle | 9 +++++---- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index cd378aa9e..610bc6e00 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.3' + classpath 'com.android.tools.build:gradle:3.6.1' } } @@ -33,13 +33,14 @@ ext { } dependencies { + //should remain that low because later versions introduce dependency to androidx (not sure exactly from what version) playstoreImplementation('com.google.firebase:firebase-messaging:17.3.4') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' } - conversationsPlaystoreCompatImplementation("com.android.installreferrer:installreferrer:1.1") - conversationsPlaystoreSystemImplementation("com.android.installreferrer:installreferrer:1.1") + conversationsPlaystoreCompatImplementation("com.android.installreferrer:installreferrer:1.1.2") + conversationsPlaystoreSystemImplementation("com.android.installreferrer:installreferrer:1.1.2") implementation 'org.sufficientlysecure:openpgp-api:10.0' implementation('com.theartofdev.edmodo:android-image-cropper:2.7.+') { exclude group: 'com.android.support', module: 'appcompat-v7' @@ -74,7 +75,7 @@ dependencies { implementation "com.squareup.retrofit2:retrofit:2.6.4" implementation "com.squareup.retrofit2:converter-gson:2.6.4" //okhttp needs to stick with 3.12.x - implementation 'com.squareup.okhttp3:okhttp:3.12.7' + implementation 'com.squareup.okhttp3:okhttp:3.12.10' implementation 'com.google.guava:guava:27.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.11.1' } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 79bc0165e..852a0aba2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Apr 24 10:50:09 CEST 2019 +#Thu Mar 19 11:51:26 CET 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip From 4e33ebb3084a5e9216d71a081f66d83ce8a99d01 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 26 Mar 2020 08:25:22 +0100 Subject: [PATCH 005/182] close FileInputStream in MTM. fixes #1150 --- .../services/MemorizingTrustManager.java | 1633 +++++++++-------- 1 file changed, 818 insertions(+), 815 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java index bb028f96c..7caf80da0 100644 --- a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java +++ b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java @@ -26,18 +26,18 @@ */ package eu.siacs.conversations.services; -import android.support.v7.app.AppCompatActivity ; import android.app.Application; import android.app.NotificationManager; import android.app.Service; import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.os.Handler; import android.preference.PreferenceManager; +import android.support.v7.app.AppCompatActivity; import android.util.Base64; import android.util.Log; import android.util.SparseArray; -import android.os.Handler; import org.json.JSONArray; import org.json.JSONException; @@ -52,19 +52,24 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; -import java.security.NoSuchAlgorithmException; -import java.security.cert.*; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.MessageDigest; -import java.util.ArrayList; -import java.util.logging.Level; -import java.util.logging.Logger; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.List; import java.util.Locale; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.regex.Pattern; import javax.net.ssl.HostnameVerifier; @@ -77,6 +82,7 @@ import javax.net.ssl.X509TrustManager; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.DomainHostnameVerifier; import eu.siacs.conversations.entities.MTMDecision; +import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.ui.MemorizingActivity; /** @@ -92,812 +98,809 @@ import eu.siacs.conversations.ui.MemorizingActivity; public class MemorizingTrustManager { - private static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - private static final Pattern PATTERN_IPV6_6HEX4DEC = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z"); - private static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z"); - - final static String DECISION_INTENT = "de.duenndns.ssl.DECISION"; - public final static String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId"; - public final static String DECISION_INTENT_CERT = DECISION_INTENT + ".cert"; - final static String DECISION_INTENT_CHOICE = DECISION_INTENT + ".decisionChoice"; - - private final static Logger LOGGER = Logger.getLogger(MemorizingTrustManager.class.getName()); - public final static String DECISION_TITLE_ID = DECISION_INTENT + ".titleId"; - private final static int NOTIFICATION_ID = 100509; - - final static String NO_TRUST_ANCHOR = "Trust anchor for certification path not found."; - - static String KEYSTORE_DIR = "KeyStore"; - static String KEYSTORE_FILE = "KeyStore.bks"; - - Context master; - AppCompatActivity foregroundAct; - NotificationManager notificationManager; - private static int decisionId = 0; - private static SparseArray openDecisions = new SparseArray(); - - Handler masterHandler; - private File keyStoreFile; - private KeyStore appKeyStore; - private X509TrustManager defaultTrustManager; - private X509TrustManager appTrustManager; - private String poshCacheDir; - - /** Creates an instance of the MemorizingTrustManager class that falls back to a custom TrustManager. - * - * You need to supply the application context. This has to be one of: - * - Application - * - Activity - * - Service - * - * The context is used for file management, to display the dialog / - * notification and for obtaining translated strings. - * - * @param m Context for the application. - * @param defaultTrustManager Delegate trust management to this TM. If null, the user must accept every certificate. - */ - public MemorizingTrustManager(Context m, X509TrustManager defaultTrustManager) { - init(m); - this.appTrustManager = getTrustManager(appKeyStore); - this.defaultTrustManager = defaultTrustManager; - } - - /** Creates an instance of the MemorizingTrustManager class using the system X509TrustManager. - * - * You need to supply the application context. This has to be one of: - * - Application - * - Activity - * - Service - * - * The context is used for file management, to display the dialog / - * notification and for obtaining translated strings. - * - * @param m Context for the application. - */ - public MemorizingTrustManager(Context m) { - init(m); - this.appTrustManager = getTrustManager(appKeyStore); - this.defaultTrustManager = getTrustManager(null); - } - - void init(Context m) { - master = m; - masterHandler = new Handler(m.getMainLooper()); - notificationManager = (NotificationManager)master.getSystemService(Context.NOTIFICATION_SERVICE); - - Application app; - if (m instanceof Application) { - app = (Application)m; - } else if (m instanceof Service) { - app = ((Service)m).getApplication(); - } else if (m instanceof AppCompatActivity) { - app = ((AppCompatActivity)m).getApplication(); - } else throw new ClassCastException("MemorizingTrustManager context must be either Activity or Service!"); - - File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE); - keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE); - - poshCacheDir = app.getCacheDir().getAbsolutePath()+"/posh_cache/"; - - appKeyStore = loadAppKeyStore(); - } - - - /** - * Binds an Activity to the MTM for displaying the query dialog. - * - * This is useful if your connection is run from a service that is - * triggered by user interaction -- in such cases the activity is - * visible and the user tends to ignore the service notification. - * - * You should never have a hidden activity bound to MTM! Use this - * function in onResume() and @see unbindDisplayActivity in onPause(). - * - * @param act Activity to be bound - */ - public void bindDisplayActivity(AppCompatActivity act) { - foregroundAct = act; - } - - /** - * Removes an Activity from the MTM display stack. - * - * Always call this function when the Activity added with - * {@link #bindDisplayActivity(AppCompatActivity)} is hidden. - * - * @param act Activity to be unbound - */ - public void unbindDisplayActivity(AppCompatActivity act) { - // do not remove if it was overridden by a different activity - if (foregroundAct == act) - foregroundAct = null; - } - - /** - * Changes the path for the KeyStore file. - * - * The actual filename relative to the app's directory will be - * app_dirname/filename. - * - * @param dirname directory to store the KeyStore. - * @param filename file name for the KeyStore. - */ - public static void setKeyStoreFile(String dirname, String filename) { - KEYSTORE_DIR = dirname; - KEYSTORE_FILE = filename; - } - - /** - * Get a list of all certificate aliases stored in MTM. - * - * @return an {@link Enumeration} of all certificates - */ - public Enumeration getCertificates() { - try { - return appKeyStore.aliases(); - } catch (KeyStoreException e) { - // this should never happen, however... - throw new RuntimeException(e); - } - } - - /** - * Get a certificate for a given alias. - * - * @param alias the certificate's alias as returned by {@link #getCertificates()}. - * - * @return the certificate associated with the alias or null if none found. - */ - public Certificate getCertificate(String alias) { - try { - return appKeyStore.getCertificate(alias); - } catch (KeyStoreException e) { - // this should never happen, however... - throw new RuntimeException(e); - } - } - - /** - * Removes the given certificate from MTMs key store. - * - *

- * WARNING: this does not immediately invalidate the certificate. It is - * well possible that (a) data is transmitted over still existing connections or - * (b) new connections are created using TLS renegotiation, without a new cert - * check. - *

- * @param alias the certificate's alias as returned by {@link #getCertificates()}. - * - * @throws KeyStoreException if the certificate could not be deleted. - */ - public void deleteCertificate(String alias) throws KeyStoreException { - appKeyStore.deleteEntry(alias); - keyStoreUpdated(); - } - - /** - * Creates a new hostname verifier supporting user interaction. - * - *

This method creates a new {@link HostnameVerifier} that is bound to - * the given instance of {@link MemorizingTrustManager}, and leverages an - * existing {@link HostnameVerifier}. The returned verifier performs the - * following steps, returning as soon as one of them succeeds: - *

- *
    - *
  1. Success, if the wrapped defaultVerifier accepts the certificate.
  2. - *
  3. Success, if the server certificate is stored in the keystore under the given hostname.
  4. - *
  5. Ask the user and return accordingly.
  6. - *
  7. Failure on exception.
  8. - *
- * - * @param defaultVerifier the {@link HostnameVerifier} that should perform the actual check - * @return a new hostname verifier using the MTM's key store - * - * @throws IllegalArgumentException if the defaultVerifier parameter is null - */ - public DomainHostnameVerifier wrapHostnameVerifier(final HostnameVerifier defaultVerifier, final boolean interactive) { - if (defaultVerifier == null) - throw new IllegalArgumentException("The default verifier may not be null"); - - return new MemorizingHostnameVerifier(defaultVerifier, interactive); - } - - X509TrustManager getTrustManager(KeyStore ks) { - try { - TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); - tmf.init(ks); - for (TrustManager t : tmf.getTrustManagers()) { - if (t instanceof X509TrustManager) { - return (X509TrustManager)t; - } - } - } catch (Exception e) { - // Here, we are covering up errors. It might be more useful - // however to throw them out of the constructor so the - // embedding app knows something went wrong. - LOGGER.log(Level.SEVERE, "getTrustManager(" + ks + ")", e); - } - return null; - } - - KeyStore loadAppKeyStore() { - KeyStore ks; - try { - ks = KeyStore.getInstance(KeyStore.getDefaultType()); - } catch (KeyStoreException e) { - LOGGER.log(Level.SEVERE, "getAppKeyStore()", e); - return null; - } - try { - ks.load(null, null); - ks.load(new java.io.FileInputStream(keyStoreFile), "MTM".toCharArray()); - } catch (java.io.FileNotFoundException e) { - LOGGER.log(Level.INFO, "getAppKeyStore(" + keyStoreFile + ") - file does not exist"); - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "getAppKeyStore(" + keyStoreFile + ")", e); - } - return ks; - } - - void storeCert(String alias, Certificate cert) { - try { - appKeyStore.setCertificateEntry(alias, cert); - } catch (KeyStoreException e) { - LOGGER.log(Level.SEVERE, "storeCert(" + cert + ")", e); - return; - } - keyStoreUpdated(); - } - - void storeCert(X509Certificate cert) { - storeCert(cert.getSubjectDN().toString(), cert); - } - - void keyStoreUpdated() { - // reload appTrustManager - appTrustManager = getTrustManager(appKeyStore); - - // store KeyStore to file - java.io.FileOutputStream fos = null; - try { - fos = new java.io.FileOutputStream(keyStoreFile); - appKeyStore.store(fos, "MTM".toCharArray()); - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "storeCert(" + keyStoreFile + ")", e); - } finally { - if (fos != null) { - try { - fos.close(); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "storeCert(" + keyStoreFile + ")", e); - } - } - } - } - - // if the certificate is stored in the app key store, it is considered "known" - private boolean isCertKnown(X509Certificate cert) { - try { - return appKeyStore.getCertificateAlias(cert) != null; - } catch (KeyStoreException e) { - return false; - } - } - - private boolean isExpiredException(Throwable e) { - do { - if (e instanceof CertificateExpiredException) - return true; - e = e.getCause(); - } while (e != null); - return false; - } - - public void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive) - throws CertificateException - { - LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")"); - try { - LOGGER.log(Level.FINE, "checkCertTrusted: trying appTrustManager"); - if (isServer) - appTrustManager.checkServerTrusted(chain, authType); - else - appTrustManager.checkClientTrusted(chain, authType); - } catch (CertificateException ae) { - LOGGER.log(Level.FINER, "checkCertTrusted: appTrustManager failed", ae); - // if the cert is stored in our appTrustManager, we ignore expiredness - if (isExpiredException(ae)) { - LOGGER.log(Level.INFO, "checkCertTrusted: accepting expired certificate from keystore"); - return; - } - if (isCertKnown(chain[0])) { - LOGGER.log(Level.INFO, "checkCertTrusted: accepting cert already stored in keystore"); - return; - } - try { - if (defaultTrustManager == null) - throw ae; - LOGGER.log(Level.FINE, "checkCertTrusted: trying defaultTrustManager"); - if (isServer) - defaultTrustManager.checkServerTrusted(chain, authType); - else - defaultTrustManager.checkClientTrusted(chain, authType); - } catch (CertificateException e) { - boolean trustSystemCAs = !PreferenceManager.getDefaultSharedPreferences(master).getBoolean("dont_trust_system_cas", false); - if (domain != null && isServer && trustSystemCAs && !isIp(domain)) { - final String hash = getBase64Hash(chain[0],"SHA-256"); - final List fingerprints = getPoshFingerprints(domain); - if (hash != null && fingerprints.size() > 0) { - if (fingerprints.contains(hash)) { - Log.d("mtm","trusted cert fingerprint of "+domain+" via posh"); - return; - } - if (getPoshCacheFile(domain).delete()) { - Log.d("mtm", "deleted posh file for "+domain+" after not being able to verify"); - } - } - } - if (interactive) { - interactCert(chain, authType, e); - } else { - throw e; - } - } - } - } - - private List getPoshFingerprints(String domain) { - List cached = getPoshFingerprintsFromCache(domain); - if (cached == null) { - return getPoshFingerprintsFromServer(domain); - } else { - return cached; - } - } - - private List getPoshFingerprintsFromServer(String domain) { - return getPoshFingerprintsFromServer(domain, "https://"+domain+"/.well-known/posh/xmpp-client.json",-1,true); - } - - private List getPoshFingerprintsFromServer(String domain, String url, int maxTtl, boolean followUrl) { - Log.d("mtm","downloading json for "+domain+" from "+url); - try { - List results = new ArrayList<>(); - HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); - connection.setConnectTimeout(5000); - connection.setReadTimeout(5000); - BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); - String inputLine; - StringBuilder builder = new StringBuilder(); - while ((inputLine = in.readLine()) != null) { - builder.append(inputLine); - } - JSONObject jsonObject = new JSONObject(builder.toString()); - in.close(); - int expires = jsonObject.getInt("expires"); - if (expires <= 0) { - return new ArrayList<>(); - } - if (maxTtl >= 0) { - expires = Math.min(maxTtl,expires); - } - String redirect; - try { - redirect = jsonObject.getString("url"); - } catch (JSONException e) { - redirect = null; - } - if (followUrl && redirect != null && redirect.toLowerCase().startsWith("https")) { - return getPoshFingerprintsFromServer(domain, redirect, expires, false); - } - JSONArray fingerprints = jsonObject.getJSONArray("fingerprints"); - for(int i = 0; i < fingerprints.length(); i++) { - JSONObject fingerprint = fingerprints.getJSONObject(i); - String sha256 = fingerprint.getString("sha-256"); - if (sha256 != null) { - results.add(sha256); - } - } - writeFingerprintsToCache(domain, results,1000L * expires+System.currentTimeMillis()); - return results; - } catch (Exception e) { - Log.d("mtm","error fetching posh "+e.getMessage()); - return new ArrayList<>(); - } - } - - private File getPoshCacheFile(String domain) { - return new File(poshCacheDir+domain+".json"); - } - - private void writeFingerprintsToCache(String domain, List results, long expires) { - File file = getPoshCacheFile(domain); - file.getParentFile().mkdirs(); - try { - file.createNewFile(); - JSONObject jsonObject = new JSONObject(); - jsonObject.put("expires",expires); - jsonObject.put("fingerprints",new JSONArray(results)); - FileOutputStream outputStream = new FileOutputStream(file); - outputStream.write(jsonObject.toString().getBytes()); - outputStream.flush(); - outputStream.close(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - private List getPoshFingerprintsFromCache(String domain) { - File file = getPoshCacheFile(domain); - try { - InputStream is = new FileInputStream(file); - BufferedReader buf = new BufferedReader(new InputStreamReader(is)); - - String line = buf.readLine(); - StringBuilder sb = new StringBuilder(); - - while(line != null){ - sb.append(line).append("\n"); - line = buf.readLine(); - } - JSONObject jsonObject = new JSONObject(sb.toString()); - is.close(); - long expires = jsonObject.getLong("expires"); - long expiresIn = expires - System.currentTimeMillis(); - if (expiresIn < 0) { - file.delete(); - return null; - } else { - Log.d("mtm","posh fingerprints expire in "+(expiresIn/1000)+"s"); - } - List result = new ArrayList<>(); - JSONArray jsonArray = jsonObject.getJSONArray("fingerprints"); - for(int i = 0; i < jsonArray.length(); ++i) { - result.add(jsonArray.getString(i)); - } - return result; - } catch (FileNotFoundException e) { - return null; - } catch (IOException e) { - return null; - } catch (JSONException e) { - file.delete(); - return null; - } - } - - private static boolean isIp(final String server) { - return server != null && ( - PATTERN_IPV4.matcher(server).matches() - || PATTERN_IPV6.matcher(server).matches() - || PATTERN_IPV6_6HEX4DEC.matcher(server).matches() - || PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches() - || PATTERN_IPV6_HEXCOMPRESSED.matcher(server).matches()); - } - - private static String getBase64Hash(X509Certificate certificate, String digest) throws CertificateEncodingException { - MessageDigest md; - try { - md = MessageDigest.getInstance(digest); - } catch (NoSuchAlgorithmException e) { - return null; - } - md.update(certificate.getEncoded()); - return Base64.encodeToString(md.digest(),Base64.NO_WRAP); - } - - private X509Certificate[] getAcceptedIssuers() { - LOGGER.log(Level.FINE, "getAcceptedIssuers()"); - return defaultTrustManager.getAcceptedIssuers(); - } - - private int createDecisionId(MTMDecision d) { - int myId; - synchronized(openDecisions) { - myId = decisionId; - openDecisions.put(myId, d); - decisionId += 1; - } - return myId; - } - - private static String hexString(byte[] data) { - StringBuffer si = new StringBuffer(); - for (int i = 0; i < data.length; i++) { - si.append(String.format("%02x", data[i])); - if (i < data.length - 1) - si.append(":"); - } - return si.toString(); - } - - private static String certHash(final X509Certificate cert, String digest) { - try { - MessageDigest md = MessageDigest.getInstance(digest); - md.update(cert.getEncoded()); - return hexString(md.digest()); - } catch (java.security.cert.CertificateEncodingException e) { - return e.getMessage(); - } catch (java.security.NoSuchAlgorithmException e) { - return e.getMessage(); - } - } - - private void certDetails(StringBuffer si, X509Certificate c) { - SimpleDateFormat validityDateFormater = new SimpleDateFormat("yyyy-MM-dd"); - si.append("\n"); - si.append(c.getSubjectDN().toString()); - si.append("\n"); - si.append(validityDateFormater.format(c.getNotBefore())); - si.append(" - "); - si.append(validityDateFormater.format(c.getNotAfter())); - si.append("\nSHA-256: "); - si.append(certHash(c, "SHA-256")); - si.append("\nSHA-1: "); - si.append(certHash(c, "SHA-1")); - si.append("\nSigned by: "); - si.append(c.getIssuerDN().toString()); - si.append("\n"); - } - - private String certChainMessage(final X509Certificate[] chain, CertificateException cause) { - Throwable e = cause; - LOGGER.log(Level.FINE, "certChainMessage for " + e); - StringBuffer si = new StringBuffer(); - if (e.getCause() != null) { - e = e.getCause(); - // HACK: there is no sane way to check if the error is a "trust anchor - // not found", so we use string comparison. - if (NO_TRUST_ANCHOR.equals(e.getMessage())) { - si.append(master.getString(R.string.mtm_trust_anchor)); - } else - si.append(e.getLocalizedMessage()); - si.append("\n"); - } - si.append("\n"); - si.append(master.getString(R.string.mtm_connect_anyway)); - si.append("\n\n"); - si.append(master.getString(R.string.mtm_cert_details)); - for (X509Certificate c : chain) { - certDetails(si, c); - } - return si.toString(); - } - - private String hostNameMessage(X509Certificate cert, String hostname) { - StringBuffer si = new StringBuffer(); - - si.append(master.getString(R.string.mtm_hostname_mismatch, hostname)); - si.append("\n\n"); - try { - Collection> sans = cert.getSubjectAlternativeNames(); - if (sans == null) { - si.append(cert.getSubjectDN()); - si.append("\n"); - } else for (List altName : sans) { - Object name = altName.get(1); - if (name instanceof String) { - si.append("["); - si.append((Integer)altName.get(0)); - si.append("] "); - si.append(name); - si.append("\n"); - } - } - } catch (CertificateParsingException e) { - e.printStackTrace(); - si.append("\n"); - } - si.append("\n"); - si.append(master.getString(R.string.mtm_connect_anyway)); - si.append("\n\n"); - si.append(master.getString(R.string.mtm_cert_details)); - certDetails(si, cert); - return si.toString(); - } - /** - * Returns the top-most entry of the activity stack. - * - * @return the Context of the currently bound UI or the master context if none is bound - */ - Context getUI() { - return (foregroundAct != null) ? foregroundAct : master; - } - - int interact(final String message, final int titleId) { - /* prepare the MTMDecision blocker object */ - MTMDecision choice = new MTMDecision(); - final int myId = createDecisionId(choice); - - masterHandler.post(new Runnable() { - public void run() { - Intent ni = new Intent(master, MemorizingActivity.class); - ni.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - ni.setData(Uri.parse(MemorizingTrustManager.class.getName() + "/" + myId)); - ni.putExtra(DECISION_INTENT_ID, myId); - ni.putExtra(DECISION_INTENT_CERT, message); - ni.putExtra(DECISION_TITLE_ID, titleId); - - // we try to directly start the activity and fall back to - // making a notification - try { - getUI().startActivity(ni); - } catch (Exception e) { - LOGGER.log(Level.FINE, "startActivity(MemorizingActivity)", e); - } - } - }); - - LOGGER.log(Level.FINE, "openDecisions: " + openDecisions + ", waiting on " + myId); - try { - synchronized(choice) { choice.wait(); } - } catch (InterruptedException e) { - LOGGER.log(Level.FINER, "InterruptedException", e); - } - LOGGER.log(Level.FINE, "finished wait on " + myId + ": " + choice.state); - return choice.state; - } - - void interactCert(final X509Certificate[] chain, String authType, CertificateException cause) - throws CertificateException - { - switch (interact(certChainMessage(chain, cause), R.string.mtm_accept_cert)) { - case MTMDecision.DECISION_ALWAYS: - storeCert(chain[0]); // only store the server cert, not the whole chain - case MTMDecision.DECISION_ONCE: - break; - default: - throw (cause); - } - } - - boolean interactHostname(X509Certificate cert, String hostname) - { - switch (interact(hostNameMessage(cert, hostname), R.string.mtm_accept_servername)) { - case MTMDecision.DECISION_ALWAYS: - storeCert(hostname, cert); - case MTMDecision.DECISION_ONCE: - return true; - default: - return false; - } - } - - public static void interactResult(int decisionId, int choice) { - MTMDecision d; - synchronized(openDecisions) { - d = openDecisions.get(decisionId); - openDecisions.remove(decisionId); - } - if (d == null) { - LOGGER.log(Level.SEVERE, "interactResult: aborting due to stale decision reference!"); - return; - } - synchronized(d) { - d.state = choice; - d.notify(); - } - } - - class MemorizingHostnameVerifier implements DomainHostnameVerifier { - private final HostnameVerifier defaultVerifier; - private final boolean interactive; - - public MemorizingHostnameVerifier(HostnameVerifier wrapped, boolean interactive) { - this.defaultVerifier = wrapped; - this.interactive = interactive; - } - - @Override - public boolean verify(String domain, String hostname, SSLSession session) { - LOGGER.log(Level.FINE, "hostname verifier for " + domain + ", trying default verifier first"); - // if the default verifier accepts the hostname, we are done - if (defaultVerifier instanceof DomainHostnameVerifier) { - if (((DomainHostnameVerifier) defaultVerifier).verify(domain,hostname, session)) { - return true; - } - } else { - if (defaultVerifier.verify(domain, session)) { - return true; - } - } - - - // otherwise, we check if the hostname is an alias for this cert in our keystore - try { - X509Certificate cert = (X509Certificate)session.getPeerCertificates()[0]; - //Log.d(TAG, "cert: " + cert); - if (cert.equals(appKeyStore.getCertificate(domain.toLowerCase(Locale.US)))) { - LOGGER.log(Level.FINE, "certificate for " + domain + " is in our keystore. accepting."); - return true; - } else { - LOGGER.log(Level.FINE, "server " + domain + " provided wrong certificate, asking user."); - if (interactive) { - return interactHostname(cert, domain); - } else { - return false; - } - } - } catch (Exception e) { - e.printStackTrace(); - return false; - } - } - - @Override - public boolean verify(String domain, SSLSession sslSession) { - return verify(domain,null,sslSession); - } - } - - - public X509TrustManager getNonInteractive(String domain) { - return new NonInteractiveMemorizingTrustManager(domain); - } - - public X509TrustManager getInteractive(String domain) { - return new InteractiveMemorizingTrustManager(domain); - } - - public X509TrustManager getNonInteractive() { - return new NonInteractiveMemorizingTrustManager(null); - } - - public X509TrustManager getInteractive() { - return new InteractiveMemorizingTrustManager(null); - } - - private class NonInteractiveMemorizingTrustManager implements X509TrustManager { - - private final String domain; - - public NonInteractiveMemorizingTrustManager(String domain) { - this.domain = domain; - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { - MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, false); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, false); - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return MemorizingTrustManager.this.getAcceptedIssuers(); - } - - } - - private class InteractiveMemorizingTrustManager implements X509TrustManager { - private final String domain; - - public InteractiveMemorizingTrustManager(String domain) { - this.domain = domain; - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { - MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, true); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, true); - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return MemorizingTrustManager.this.getAcceptedIssuers(); - } - } + final static String DECISION_INTENT = "de.duenndns.ssl.DECISION"; + public final static String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId"; + public final static String DECISION_INTENT_CERT = DECISION_INTENT + ".cert"; + public final static String DECISION_TITLE_ID = DECISION_INTENT + ".titleId"; + final static String DECISION_INTENT_CHOICE = DECISION_INTENT + ".decisionChoice"; + final static String NO_TRUST_ANCHOR = "Trust anchor for certification path not found."; + private static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_6HEX4DEC = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z"); + private static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z"); + private final static Logger LOGGER = Logger.getLogger(MemorizingTrustManager.class.getName()); + private final static int NOTIFICATION_ID = 100509; + static String KEYSTORE_DIR = "KeyStore"; + static String KEYSTORE_FILE = "KeyStore.bks"; + private static int decisionId = 0; + private static SparseArray openDecisions = new SparseArray(); + Context master; + AppCompatActivity foregroundAct; + NotificationManager notificationManager; + Handler masterHandler; + private File keyStoreFile; + private KeyStore appKeyStore; + private X509TrustManager defaultTrustManager; + private X509TrustManager appTrustManager; + private String poshCacheDir; + + /** + * Creates an instance of the MemorizingTrustManager class that falls back to a custom TrustManager. + *

+ * You need to supply the application context. This has to be one of: + * - Application + * - Activity + * - Service + *

+ * The context is used for file management, to display the dialog / + * notification and for obtaining translated strings. + * + * @param m Context for the application. + * @param defaultTrustManager Delegate trust management to this TM. If null, the user must accept every certificate. + */ + public MemorizingTrustManager(Context m, X509TrustManager defaultTrustManager) { + init(m); + this.appTrustManager = getTrustManager(appKeyStore); + this.defaultTrustManager = defaultTrustManager; + } + + /** + * Creates an instance of the MemorizingTrustManager class using the system X509TrustManager. + *

+ * You need to supply the application context. This has to be one of: + * - Application + * - Activity + * - Service + *

+ * The context is used for file management, to display the dialog / + * notification and for obtaining translated strings. + * + * @param m Context for the application. + */ + public MemorizingTrustManager(Context m) { + init(m); + this.appTrustManager = getTrustManager(appKeyStore); + this.defaultTrustManager = getTrustManager(null); + } + + /** + * Changes the path for the KeyStore file. + *

+ * The actual filename relative to the app's directory will be + * app_dirname/filename. + * + * @param dirname directory to store the KeyStore. + * @param filename file name for the KeyStore. + */ + public static void setKeyStoreFile(String dirname, String filename) { + KEYSTORE_DIR = dirname; + KEYSTORE_FILE = filename; + } + + private static boolean isIp(final String server) { + return server != null && ( + PATTERN_IPV4.matcher(server).matches() + || PATTERN_IPV6.matcher(server).matches() + || PATTERN_IPV6_6HEX4DEC.matcher(server).matches() + || PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches() + || PATTERN_IPV6_HEXCOMPRESSED.matcher(server).matches()); + } + + private static String getBase64Hash(X509Certificate certificate, String digest) throws CertificateEncodingException { + MessageDigest md; + try { + md = MessageDigest.getInstance(digest); + } catch (NoSuchAlgorithmException e) { + return null; + } + md.update(certificate.getEncoded()); + return Base64.encodeToString(md.digest(), Base64.NO_WRAP); + } + + private static String hexString(byte[] data) { + StringBuffer si = new StringBuffer(); + for (int i = 0; i < data.length; i++) { + si.append(String.format("%02x", data[i])); + if (i < data.length - 1) + si.append(":"); + } + return si.toString(); + } + + private static String certHash(final X509Certificate cert, String digest) { + try { + MessageDigest md = MessageDigest.getInstance(digest); + md.update(cert.getEncoded()); + return hexString(md.digest()); + } catch (java.security.cert.CertificateEncodingException e) { + return e.getMessage(); + } catch (java.security.NoSuchAlgorithmException e) { + return e.getMessage(); + } + } + + public static void interactResult(int decisionId, int choice) { + MTMDecision d; + synchronized (openDecisions) { + d = openDecisions.get(decisionId); + openDecisions.remove(decisionId); + } + if (d == null) { + LOGGER.log(Level.SEVERE, "interactResult: aborting due to stale decision reference!"); + return; + } + synchronized (d) { + d.state = choice; + d.notify(); + } + } + + void init(Context m) { + master = m; + masterHandler = new Handler(m.getMainLooper()); + notificationManager = (NotificationManager) master.getSystemService(Context.NOTIFICATION_SERVICE); + + Application app; + if (m instanceof Application) { + app = (Application) m; + } else if (m instanceof Service) { + app = ((Service) m).getApplication(); + } else if (m instanceof AppCompatActivity) { + app = ((AppCompatActivity) m).getApplication(); + } else + throw new ClassCastException("MemorizingTrustManager context must be either Activity or Service!"); + + File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE); + keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE); + + poshCacheDir = app.getCacheDir().getAbsolutePath() + "/posh_cache/"; + + appKeyStore = loadAppKeyStore(); + } + + /** + * Binds an Activity to the MTM for displaying the query dialog. + *

+ * This is useful if your connection is run from a service that is + * triggered by user interaction -- in such cases the activity is + * visible and the user tends to ignore the service notification. + *

+ * You should never have a hidden activity bound to MTM! Use this + * function in onResume() and @see unbindDisplayActivity in onPause(). + * + * @param act Activity to be bound + */ + public void bindDisplayActivity(AppCompatActivity act) { + foregroundAct = act; + } + + /** + * Removes an Activity from the MTM display stack. + *

+ * Always call this function when the Activity added with + * {@link #bindDisplayActivity(AppCompatActivity)} is hidden. + * + * @param act Activity to be unbound + */ + public void unbindDisplayActivity(AppCompatActivity act) { + // do not remove if it was overridden by a different activity + if (foregroundAct == act) + foregroundAct = null; + } + + /** + * Get a list of all certificate aliases stored in MTM. + * + * @return an {@link Enumeration} of all certificates + */ + public Enumeration getCertificates() { + try { + return appKeyStore.aliases(); + } catch (KeyStoreException e) { + // this should never happen, however... + throw new RuntimeException(e); + } + } + + /** + * Get a certificate for a given alias. + * + * @param alias the certificate's alias as returned by {@link #getCertificates()}. + * @return the certificate associated with the alias or null if none found. + */ + public Certificate getCertificate(String alias) { + try { + return appKeyStore.getCertificate(alias); + } catch (KeyStoreException e) { + // this should never happen, however... + throw new RuntimeException(e); + } + } + + /** + * Removes the given certificate from MTMs key store. + * + *

+ * WARNING: this does not immediately invalidate the certificate. It is + * well possible that (a) data is transmitted over still existing connections or + * (b) new connections are created using TLS renegotiation, without a new cert + * check. + *

+ * + * @param alias the certificate's alias as returned by {@link #getCertificates()}. + * @throws KeyStoreException if the certificate could not be deleted. + */ + public void deleteCertificate(String alias) throws KeyStoreException { + appKeyStore.deleteEntry(alias); + keyStoreUpdated(); + } + + /** + * Creates a new hostname verifier supporting user interaction. + * + *

This method creates a new {@link HostnameVerifier} that is bound to + * the given instance of {@link MemorizingTrustManager}, and leverages an + * existing {@link HostnameVerifier}. The returned verifier performs the + * following steps, returning as soon as one of them succeeds: + * /p> + *

    + *
  1. Success, if the wrapped defaultVerifier accepts the certificate.
  2. + *
  3. Success, if the server certificate is stored in the keystore under the given hostname.
  4. + *
  5. Ask the user and return accordingly.
  6. + *
  7. Failure on exception.
  8. + *
+ * + * @param defaultVerifier the {@link HostnameVerifier} that should perform the actual check + * @return a new hostname verifier using the MTM's key store + * @throws IllegalArgumentException if the defaultVerifier parameter is null + */ + public DomainHostnameVerifier wrapHostnameVerifier(final HostnameVerifier defaultVerifier, final boolean interactive) { + if (defaultVerifier == null) + throw new IllegalArgumentException("The default verifier may not be null"); + + return new MemorizingHostnameVerifier(defaultVerifier, interactive); + } + + X509TrustManager getTrustManager(KeyStore ks) { + try { + TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); + tmf.init(ks); + for (TrustManager t : tmf.getTrustManagers()) { + if (t instanceof X509TrustManager) { + return (X509TrustManager) t; + } + } + } catch (Exception e) { + // Here, we are covering up errors. It might be more useful + // however to throw them out of the constructor so the + // embedding app knows something went wrong. + LOGGER.log(Level.SEVERE, "getTrustManager(" + ks + ")", e); + } + return null; + } + + KeyStore loadAppKeyStore() { + KeyStore ks; + try { + ks = KeyStore.getInstance(KeyStore.getDefaultType()); + } catch (KeyStoreException e) { + LOGGER.log(Level.SEVERE, "getAppKeyStore()", e); + return null; + } + FileInputStream fileInputStream = null; + try { + ks.load(null, null); + fileInputStream = new FileInputStream(keyStoreFile); + ks.load(fileInputStream, "MTM".toCharArray()); + } catch (java.io.FileNotFoundException e) { + LOGGER.log(Level.INFO, "getAppKeyStore(" + keyStoreFile + ") - file does not exist"); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "getAppKeyStore(" + keyStoreFile + ")", e); + } finally { + FileBackend.close(fileInputStream); + } + return ks; + } + + void storeCert(String alias, Certificate cert) { + try { + appKeyStore.setCertificateEntry(alias, cert); + } catch (KeyStoreException e) { + LOGGER.log(Level.SEVERE, "storeCert(" + cert + ")", e); + return; + } + keyStoreUpdated(); + } + + void storeCert(X509Certificate cert) { + storeCert(cert.getSubjectDN().toString(), cert); + } + + void keyStoreUpdated() { + // reload appTrustManager + appTrustManager = getTrustManager(appKeyStore); + + // store KeyStore to file + java.io.FileOutputStream fos = null; + try { + fos = new java.io.FileOutputStream(keyStoreFile); + appKeyStore.store(fos, "MTM".toCharArray()); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "storeCert(" + keyStoreFile + ")", e); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "storeCert(" + keyStoreFile + ")", e); + } + } + } + } + + // if the certificate is stored in the app key store, it is considered "known" + private boolean isCertKnown(X509Certificate cert) { + try { + return appKeyStore.getCertificateAlias(cert) != null; + } catch (KeyStoreException e) { + return false; + } + } + + private boolean isExpiredException(Throwable e) { + do { + if (e instanceof CertificateExpiredException) + return true; + e = e.getCause(); + } while (e != null); + return false; + } + + public void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive) + throws CertificateException { + LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")"); + try { + LOGGER.log(Level.FINE, "checkCertTrusted: trying appTrustManager"); + if (isServer) + appTrustManager.checkServerTrusted(chain, authType); + else + appTrustManager.checkClientTrusted(chain, authType); + } catch (CertificateException ae) { + LOGGER.log(Level.FINER, "checkCertTrusted: appTrustManager failed", ae); + // if the cert is stored in our appTrustManager, we ignore expiredness + if (isExpiredException(ae)) { + LOGGER.log(Level.INFO, "checkCertTrusted: accepting expired certificate from keystore"); + return; + } + if (isCertKnown(chain[0])) { + LOGGER.log(Level.INFO, "checkCertTrusted: accepting cert already stored in keystore"); + return; + } + try { + if (defaultTrustManager == null) + throw ae; + LOGGER.log(Level.FINE, "checkCertTrusted: trying defaultTrustManager"); + if (isServer) + defaultTrustManager.checkServerTrusted(chain, authType); + else + defaultTrustManager.checkClientTrusted(chain, authType); + } catch (CertificateException e) { + boolean trustSystemCAs = !PreferenceManager.getDefaultSharedPreferences(master).getBoolean("dont_trust_system_cas", false); + if (domain != null && isServer && trustSystemCAs && !isIp(domain)) { + final String hash = getBase64Hash(chain[0], "SHA-256"); + final List fingerprints = getPoshFingerprints(domain); + if (hash != null && fingerprints.size() > 0) { + if (fingerprints.contains(hash)) { + Log.d("mtm", "trusted cert fingerprint of " + domain + " via posh"); + return; + } + if (getPoshCacheFile(domain).delete()) { + Log.d("mtm", "deleted posh file for " + domain + " after not being able to verify"); + } + } + } + if (interactive) { + interactCert(chain, authType, e); + } else { + throw e; + } + } + } + } + + private List getPoshFingerprints(String domain) { + List cached = getPoshFingerprintsFromCache(domain); + if (cached == null) { + return getPoshFingerprintsFromServer(domain); + } else { + return cached; + } + } + + private List getPoshFingerprintsFromServer(String domain) { + return getPoshFingerprintsFromServer(domain, "https://" + domain + "/.well-known/posh/xmpp-client.json", -1, true); + } + + private List getPoshFingerprintsFromServer(String domain, String url, int maxTtl, boolean followUrl) { + Log.d("mtm", "downloading json for " + domain + " from " + url); + try { + List results = new ArrayList<>(); + HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String inputLine; + StringBuilder builder = new StringBuilder(); + while ((inputLine = in.readLine()) != null) { + builder.append(inputLine); + } + JSONObject jsonObject = new JSONObject(builder.toString()); + in.close(); + int expires = jsonObject.getInt("expires"); + if (expires <= 0) { + return new ArrayList<>(); + } + if (maxTtl >= 0) { + expires = Math.min(maxTtl, expires); + } + String redirect; + try { + redirect = jsonObject.getString("url"); + } catch (JSONException e) { + redirect = null; + } + if (followUrl && redirect != null && redirect.toLowerCase().startsWith("https")) { + return getPoshFingerprintsFromServer(domain, redirect, expires, false); + } + JSONArray fingerprints = jsonObject.getJSONArray("fingerprints"); + for (int i = 0; i < fingerprints.length(); i++) { + JSONObject fingerprint = fingerprints.getJSONObject(i); + String sha256 = fingerprint.getString("sha-256"); + if (sha256 != null) { + results.add(sha256); + } + } + writeFingerprintsToCache(domain, results, 1000L * expires + System.currentTimeMillis()); + return results; + } catch (Exception e) { + Log.d("mtm", "error fetching posh " + e.getMessage()); + return new ArrayList<>(); + } + } + + private File getPoshCacheFile(String domain) { + return new File(poshCacheDir + domain + ".json"); + } + + private void writeFingerprintsToCache(String domain, List results, long expires) { + File file = getPoshCacheFile(domain); + file.getParentFile().mkdirs(); + try { + file.createNewFile(); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("expires", expires); + jsonObject.put("fingerprints", new JSONArray(results)); + FileOutputStream outputStream = new FileOutputStream(file); + outputStream.write(jsonObject.toString().getBytes()); + outputStream.flush(); + outputStream.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private List getPoshFingerprintsFromCache(String domain) { + File file = getPoshCacheFile(domain); + try { + InputStream is = new FileInputStream(file); + BufferedReader buf = new BufferedReader(new InputStreamReader(is)); + + String line = buf.readLine(); + StringBuilder sb = new StringBuilder(); + + while (line != null) { + sb.append(line).append("\n"); + line = buf.readLine(); + } + JSONObject jsonObject = new JSONObject(sb.toString()); + is.close(); + long expires = jsonObject.getLong("expires"); + long expiresIn = expires - System.currentTimeMillis(); + if (expiresIn < 0) { + file.delete(); + return null; + } else { + Log.d("mtm", "posh fingerprints expire in " + (expiresIn / 1000) + "s"); + } + List result = new ArrayList<>(); + JSONArray jsonArray = jsonObject.getJSONArray("fingerprints"); + for (int i = 0; i < jsonArray.length(); ++i) { + result.add(jsonArray.getString(i)); + } + return result; + } catch (FileNotFoundException e) { + return null; + } catch (IOException e) { + return null; + } catch (JSONException e) { + file.delete(); + return null; + } + } + + private X509Certificate[] getAcceptedIssuers() { + LOGGER.log(Level.FINE, "getAcceptedIssuers()"); + return defaultTrustManager.getAcceptedIssuers(); + } + + private int createDecisionId(MTMDecision d) { + int myId; + synchronized (openDecisions) { + myId = decisionId; + openDecisions.put(myId, d); + decisionId += 1; + } + return myId; + } + + private void certDetails(StringBuffer si, X509Certificate c) { + SimpleDateFormat validityDateFormater = new SimpleDateFormat("yyyy-MM-dd"); + si.append("\n"); + si.append(c.getSubjectDN().toString()); + si.append("\n"); + si.append(validityDateFormater.format(c.getNotBefore())); + si.append(" - "); + si.append(validityDateFormater.format(c.getNotAfter())); + si.append("\nSHA-256: "); + si.append(certHash(c, "SHA-256")); + si.append("\nSHA-1: "); + si.append(certHash(c, "SHA-1")); + si.append("\nSigned by: "); + si.append(c.getIssuerDN().toString()); + si.append("\n"); + } + + private String certChainMessage(final X509Certificate[] chain, CertificateException cause) { + Throwable e = cause; + LOGGER.log(Level.FINE, "certChainMessage for " + e); + StringBuffer si = new StringBuffer(); + if (e.getCause() != null) { + e = e.getCause(); + // HACK: there is no sane way to check if the error is a "trust anchor + // not found", so we use string comparison. + if (NO_TRUST_ANCHOR.equals(e.getMessage())) { + si.append(master.getString(R.string.mtm_trust_anchor)); + } else + si.append(e.getLocalizedMessage()); + si.append("\n"); + } + si.append("\n"); + si.append(master.getString(R.string.mtm_connect_anyway)); + si.append("\n\n"); + si.append(master.getString(R.string.mtm_cert_details)); + for (X509Certificate c : chain) { + certDetails(si, c); + } + return si.toString(); + } + + private String hostNameMessage(X509Certificate cert, String hostname) { + StringBuffer si = new StringBuffer(); + + si.append(master.getString(R.string.mtm_hostname_mismatch, hostname)); + si.append("\n\n"); + try { + Collection> sans = cert.getSubjectAlternativeNames(); + if (sans == null) { + si.append(cert.getSubjectDN()); + si.append("\n"); + } else for (List altName : sans) { + Object name = altName.get(1); + if (name instanceof String) { + si.append("["); + si.append((Integer) altName.get(0)); + si.append("] "); + si.append(name); + si.append("\n"); + } + } + } catch (CertificateParsingException e) { + e.printStackTrace(); + si.append("\n"); + } + si.append("\n"); + si.append(master.getString(R.string.mtm_connect_anyway)); + si.append("\n\n"); + si.append(master.getString(R.string.mtm_cert_details)); + certDetails(si, cert); + return si.toString(); + } + + /** + * Returns the top-most entry of the activity stack. + * + * @return the Context of the currently bound UI or the master context if none is bound + */ + Context getUI() { + return (foregroundAct != null) ? foregroundAct : master; + } + + int interact(final String message, final int titleId) { + /* prepare the MTMDecision blocker object */ + MTMDecision choice = new MTMDecision(); + final int myId = createDecisionId(choice); + + masterHandler.post(new Runnable() { + public void run() { + Intent ni = new Intent(master, MemorizingActivity.class); + ni.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + ni.setData(Uri.parse(MemorizingTrustManager.class.getName() + "/" + myId)); + ni.putExtra(DECISION_INTENT_ID, myId); + ni.putExtra(DECISION_INTENT_CERT, message); + ni.putExtra(DECISION_TITLE_ID, titleId); + + // we try to directly start the activity and fall back to + // making a notification + try { + getUI().startActivity(ni); + } catch (Exception e) { + LOGGER.log(Level.FINE, "startActivity(MemorizingActivity)", e); + } + } + }); + + LOGGER.log(Level.FINE, "openDecisions: " + openDecisions + ", waiting on " + myId); + try { + synchronized (choice) { + choice.wait(); + } + } catch (InterruptedException e) { + LOGGER.log(Level.FINER, "InterruptedException", e); + } + LOGGER.log(Level.FINE, "finished wait on " + myId + ": " + choice.state); + return choice.state; + } + + void interactCert(final X509Certificate[] chain, String authType, CertificateException cause) + throws CertificateException { + switch (interact(certChainMessage(chain, cause), R.string.mtm_accept_cert)) { + case MTMDecision.DECISION_ALWAYS: + storeCert(chain[0]); // only store the server cert, not the whole chain + case MTMDecision.DECISION_ONCE: + break; + default: + throw (cause); + } + } + + boolean interactHostname(X509Certificate cert, String hostname) { + switch (interact(hostNameMessage(cert, hostname), R.string.mtm_accept_servername)) { + case MTMDecision.DECISION_ALWAYS: + storeCert(hostname, cert); + case MTMDecision.DECISION_ONCE: + return true; + default: + return false; + } + } + + public X509TrustManager getNonInteractive(String domain) { + return new NonInteractiveMemorizingTrustManager(domain); + } + + public X509TrustManager getInteractive(String domain) { + return new InteractiveMemorizingTrustManager(domain); + } + + public X509TrustManager getNonInteractive() { + return new NonInteractiveMemorizingTrustManager(null); + } + + public X509TrustManager getInteractive() { + return new InteractiveMemorizingTrustManager(null); + } + + class MemorizingHostnameVerifier implements DomainHostnameVerifier { + private final HostnameVerifier defaultVerifier; + private final boolean interactive; + + public MemorizingHostnameVerifier(HostnameVerifier wrapped, boolean interactive) { + this.defaultVerifier = wrapped; + this.interactive = interactive; + } + + @Override + public boolean verify(String domain, String hostname, SSLSession session) { + LOGGER.log(Level.FINE, "hostname verifier for " + domain + ", trying default verifier first"); + // if the default verifier accepts the hostname, we are done + if (defaultVerifier instanceof DomainHostnameVerifier) { + if (((DomainHostnameVerifier) defaultVerifier).verify(domain, hostname, session)) { + return true; + } + } else { + if (defaultVerifier.verify(domain, session)) { + return true; + } + } + + + // otherwise, we check if the hostname is an alias for this cert in our keystore + try { + X509Certificate cert = (X509Certificate) session.getPeerCertificates()[0]; + //Log.d(TAG, "cert: " + cert); + if (cert.equals(appKeyStore.getCertificate(domain.toLowerCase(Locale.US)))) { + LOGGER.log(Level.FINE, "certificate for " + domain + " is in our keystore. accepting."); + return true; + } else { + LOGGER.log(Level.FINE, "server " + domain + " provided wrong certificate, asking user."); + if (interactive) { + return interactHostname(cert, domain); + } else { + return false; + } + } + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + @Override + public boolean verify(String domain, SSLSession sslSession) { + return verify(domain, null, sslSession); + } + } + + private class NonInteractiveMemorizingTrustManager implements X509TrustManager { + + private final String domain; + + public NonInteractiveMemorizingTrustManager(String domain) { + this.domain = domain; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, false); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, false); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return MemorizingTrustManager.this.getAcceptedIssuers(); + } + + } + + private class InteractiveMemorizingTrustManager implements X509TrustManager { + private final String domain; + + public InteractiveMemorizingTrustManager(String domain) { + this.domain = domain; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, true); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, true); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return MemorizingTrustManager.this.getAcceptedIssuers(); + } + } } From 7c13c8a4e51bc8589dd4d0ba84994a2667650468 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 28 Mar 2020 10:13:27 +0100 Subject: [PATCH 006/182] pulled translations from transifex --- src/conversations/res/values-el/strings.xml | 5 +- src/conversations/res/values-fr/strings.xml | 4 +- src/conversations/res/values-nl/strings.xml | 5 +- src/main/res/values-el/strings.xml | 18 +++++++ src/main/res/values-fr/strings.xml | 16 +++++++ src/main/res/values-nl/strings.xml | 4 ++ src/main/res/values-pt-rBR/strings.xml | 2 + src/main/res/values-ru/strings.xml | 53 +++++++++++++++++++-- src/quicksy/res/values-fr/strings.xml | 38 +++++++-------- 9 files changed, 117 insertions(+), 28 deletions(-) diff --git a/src/conversations/res/values-el/strings.xml b/src/conversations/res/values-el/strings.xml index a3d8b4086..64a05095f 100644 --- a/src/conversations/res/values-el/strings.xml +++ b/src/conversations/res/values-el/strings.xml @@ -5,4 +5,7 @@ Δημιουργία νέου λογαριασμού Έχετε ήδη λογαριασμό XMPP; Αυτό μπορεί να συμβαίνει αν ήδη χρησιμοποιείτε ένα άλλο πρόγραμμα XMPP ή έχετε χρησιμοποιήσει το Conversations παλιότερα. Αν όχι, μπορείτε να δημιουργήσετε ένα νέο λογαριασμό XMPP τώρα.\nΧρήσιμη πληροφορία: Κάποιοι πάροχοι e-mail παρέχουν επίσης και λογαριασμούς XMPP. Το XMPP είναι ένα δίκτυο άμεσης ανταλλαγής μηνυμάτων ανεξάρτητο παρόχου. Μπορείτε να χρησιμοποιήσετε αυτό το πρόγραμμα με όποιον διακομιστή XMPP επιθυμείτε.\nΓια διευκόλυνση πάντως μπορείτε να δημιουργήσετε έναν λογαριασμό στο conversations.im¹, έναν πάροχο ειδικά σχεδιασμένο για χρήση με το Conversations. - \ No newline at end of file + Έχετε προσκληθεί στο %1$s. Ένα όνομα χρήστη έχει ήδη επιλεγεί για εσάς. Θα σας καθοδηγήσουμε στη διαδικασία δημιουργίας λογαριασμού.\nΕπιλέγοντας τον %1$s ως πάροχο θα μπορείτε να επικοινωνείτε με χρήστες άλλων παρόχων δίνοντάς τους την πλήρη διεύθυνση XMPP σας. + Έχετε προσκληθεί στο %1$s. Ένα όνομα χρήστη έχει ήδη επιλεγεί για εσάς. Θα σας καθοδηγήσουμε στη διαδικασία δημιουργίας λογαριασμού.\nΘα μπορείτε να επικοινωνείτε με χρήστες άλλων παρόχων δίνοντάς τους την πλήρη διεύθυνση XMPP σας. + Η πρόσκλησή σας στον διακομιστή + \ No newline at end of file diff --git a/src/conversations/res/values-fr/strings.xml b/src/conversations/res/values-fr/strings.xml index fe5c492c7..0ea5ca4f1 100644 --- a/src/conversations/res/values-fr/strings.xml +++ b/src/conversations/res/values-fr/strings.xml @@ -5,7 +5,7 @@ Créer un nouveau compte Avez-vous déjà un compte XMPP ? Cela peut être le cas si vous utilisez déjà un autre client XMPP ou si vous avez déjà utilisé Conversations auparavant. Sinon, vous pouvez créer un nouveau compte XMPP dès maintenant.\nRemarque : Certains fournisseurs de messagerie proposent également des comptes XMPP. XMPP est un réseau de messagerie instantanée indépendant du fournisseur. Vous pouvez utiliser ce client avec n’importe quel serveur XMPP de votre choix.\nToutefois, pour votre commodité, nous avons facilité la création d’un compte sur conversations.im¹ ; un fournisseur spécialement conçu pour Conversations. - Vous avez été invité à %1$s. Nous vous guiderons dans le processus de création d\'un compte.\nEn choisissant %1$s comme fournisseur, vous pourrez communiquer avec les utilisateurs d\'autres fournisseurs en leur donnant votre adresse XMPP complète. - Vous avez été invité par %1$s . Un nom d\'utilisateur a déjà été choisi pour vous. Nous vous guiderons dans le processus de création d\'un compte. Vous pourrez communiquer avec les utilisateurs d\'autres fournisseurs en leur donnant votre adresse XMPP complète. + Vous avez été invité à %1$s. Nous allons vous guider à travers le processus de création d’un compte.\nEn choisissant %1$s comme fournisseur, vous pourrez communiquer avec les utilisateurs des autres fournisseurs en leur donnant votre adresse XMPP complète. + Vous avez été invité à %1$s. Un nom d’utilisateur a déjà été choisi pour vous. Nous allons vous guider à travers le processus de création d’un compte.\nVous pourrez communiquer avec les utilisateurs des autres fournisseurs en leur donnant votre adresse XMPP complète. Votre invitation au serveur \ No newline at end of file diff --git a/src/conversations/res/values-nl/strings.xml b/src/conversations/res/values-nl/strings.xml index adeb372dd..9ab739372 100644 --- a/src/conversations/res/values-nl/strings.xml +++ b/src/conversations/res/values-nl/strings.xml @@ -5,4 +5,7 @@ Nieuwe account registreren Heb je al een XMPP-account? Als je al een andere XMPP-cliënt gebruikt, of Conversations vroeger al eens hebt gebruikt, is dit waarschijnlijk het geval. Zo niet, kan je nu een nieuwe XMPP-account aanmaken.\nTip: sommige e-mailproviders bieden ook XMPP-accounts aan. XMPP is een provider-onafhankelijk berichtennetwerk. Je kan deze cliënt gebruiken met eender welke XMPP-server.\nOm het je gemakkelijker te maken kun je simpelweg een account aanmaken op conversations.im¹; een provider speciaal geschikt voor Conversations. - \ No newline at end of file + Je ontving een uitnodiging voor %1$s. We zullen je helpen een account aan te maken.\nWanneer je %1$s als je provider kiest kan je met gebruikers van andere providers communiceren door hen je volledige XMPP-adres te geven. + Je ontving een uitnodiging voor %1$s. Er werd reeds een gebruikersnaam voor jou gekozen. We zullen je helpen een account aan te maken.\nJe zal met gebruikers van andere providers communiceren door hen je volledige XMPP-adres te geven. + Je server uitnodiging + \ No newline at end of file diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml index 1d0115b1c..d41cf52e2 100644 --- a/src/main/res/values-el/strings.xml +++ b/src/main/res/values-el/strings.xml @@ -30,6 +30,7 @@ μόλις τώρα πριν από 1 λεπτό πριν από %d λεπτά + %d αδιάβαστες συζητήσεις αποστολή... Αποκρυπτογράφηση μηνύματος. Παρακαλώ περιμένετε... OpenPGP κρυπτογραφημένο μήνυμα @@ -118,6 +119,7 @@ Ήχος Κουδούνισμα όταν δέχεστε νέο μήνυμα Περίοδος Χάριτος + Ο χρόνος σίγασης ειδοποιήσεων αφότου ανιχνευθεί δραστηριότητα σε μια από τις άλλες συσκευές σας. Προχωρημένος Να μην αποστέλλονται αναφορές λαθών Στέλνοντας ίχνη στοίβας βοηθάτε την συνεχόμενη ανάπτυξη του Conversations @@ -153,6 +155,7 @@ Το όνομα χρησιμοποιείται ήδη Ολοκλήρωση εγγραφής Ο διακομιστής δεν υποστηρίζει εγγραφή + Άκυρο κουπόνι εγγραφής TLS αποτυχία επικοινωνίας Πολιτική παραβίασης Μη συμβατός διακομιστής @@ -331,6 +334,7 @@ το %s προσφέρθηκε για μεταφόρτωση Ακύρωση μετάδοσης η μετάδοση του αρχείου απέτυχε + η μεταφορά αρχείου ακυρώθηκε Το αρχείο έχει διαγραφεί Δεν βρέθηκε εφαρμογή για να ανοίξει το αρχείο Δεν βρέθηκε εφαρμογή για να ανοίξει τον σύνδεσμο @@ -506,6 +510,7 @@ Παύση ειδοποιήσεων Συμπίεση εικόνας Πάντα + Μεγάλες εικόνες μόνο Ενεργοποίηση βελτιστοποίησης χρήσης μπαταρίας Η συσκευή σας χρησιμοποιεί βελτιστοποίηση στην χρήση μπαταρίας του Conversations που μπορεί να οδηγήσει σε αργοπορημένες ειδοποιήσεις ή ακόμα και σε απώλεια μηνυμάτων.\nΠροτείνεται να την απενεργοποιήσετε Η συσκευή σας χρησιμοποιεί βελτιστοποίηση στην χρήση μπαταρίας του Conversations που μπορεί να οδηγήσει σε αργοπορημένες ειδοποιήσεις ή ακόμα και σε απώλεια μηνυμάτων.\nΘα σας ζητηθεί να την απενεργοποιήσετε. @@ -551,6 +556,7 @@ Ιδιωτικότητα Θέμα Επιλογή παλέτας χρωμάτων + Αυτόματο Ανοιχτόχρωμο θέμα Σκουρόχρωμο θέμα Πράσινο φόντο @@ -864,4 +870,16 @@ Αυτός ο λογαριασμός έχει προστεθεί ήδη Παρακαλώ εισάγετε τον κωδικό για αυτό το λογαριασμό Αδυναμία εκτέλεσης αυτής της λειτουργίας + Είσοδος σε δημόσιο κανάλι... + Η εφαρμογή από την οποία έγινε διαμοίραση δεν έδωσε δικαιώματα πρόσβασης στο αρχείο. + + Τοπικός διακομιστής + Οι περισσότεροι χρήστες πρέπει να επιλέξουν ‘jabber.network’ για καλύτερες προτάσεις από το σύνολο του οικοσυστήματος XMPP. + Μέθοδος ανακάλυψης καναλιού + Αντίγραφο ασφαλείας + Σχετικά με + + Εμφάνιση %1$d συμμετέχοντα + Εμφάνιση %1$d συμμετεχόντων + diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 1f2fd6d4f..37978425b 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -155,6 +155,7 @@ Identifiant déjà utilisé Enregistrement réussi Le serveur ne permet pas l\'enregistrement + Jeton d’inscription invalide La négociation TLS a echoué Violation de politique Serveur incompatible @@ -291,6 +292,7 @@ La discussion de groupe a été interrompue Vous n\'appartenez plus à ce groupe de discussion avec le compte %s + hébergé sur %s Vérification de %s sur l\'hôte HTTP Vous n\'êtes pas connecté. Essayez plus tard. Vérification de la taille de %s @@ -507,7 +509,9 @@ Notifications désactivées Notifications en pause Compression de l\'image + Remarque : Utiliser « Choisir un fichier » au lieu de « Choisir une image » pour envoyer des images individuelles non compressées sans tenir compte de ce paramètre. Toujours + Grandes images seulement Optimisations de batterie activées Votre appareil applique sur Conversations des optimisations de batterie très strictes qui pourraient provoquer des retards dans les notifications, voire des pertes de messages.\nNous vous recommandons de les désactiver. Votre appareil applique sur Conversations des optimisations de batterie très strictes qui pourraient provoquer des retards dans les notifications, voire des pertes de messages.\nVous allez maintenant avoir la possibilité de les désactiver. @@ -553,6 +557,7 @@ Confidentialité Thème Choisir la palette de couleurs + Automatique Thème Clair Thème Sombre Fond Vert @@ -869,4 +874,15 @@ Action impossible à réaliser Rejoindre le canal public ... L\'application de partage n\'a pas accordé la permission d\'accéder à ce fichier. + + jabber.network + Serveur local + La plupart des utilisateurs devraient choisir « jabber.network » pour de meilleures suggestions provenant de l’écosystème public entier de XMPP. + Méthode de découverte des canaux + Sauvegarde + À propos + + Voir %1$d participant + Voir %1$d participants + diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index 89693ed51..901135d04 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -507,6 +507,7 @@ Meldingen gepauzeerd Afbeeldingscompressie Altijd + Enkel grote afbeeldingen Batterij-optimalisaties ingeschakeld Je apparaat voert sterke batterij-optimalisaties uit op Conversations, die kunnen leiden tot vertraagde meldingen of zelfs verlies van berichten.\nHet is aangeraden deze optimalisaties uit te schakelen. Je apparaat voert sterke batterij-optimalisaties uit op Conversations, die kunnen leiden tot vertraagde meldingen of zelfs verlies van berichten.\nJe zal nu gevraagd worden deze optimalisaties uit te schakelen. @@ -552,6 +553,7 @@ Privacy Thema Kies het kleurenpalet + Automatisch Licht thema Donker thema Groene achtergrond @@ -867,4 +869,6 @@ Voer het wachtwoord voor deze account in Kan deze actie niet uitvoeren Deelnemen aan openbaar kanaal… + Lokale server + Over diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index c856ac249..4d29df038 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -292,6 +292,7 @@ A conversa em grupo foi encerrada Você não está mais nesta conversa em grupo usando a conta %s + hospedado em %s Verificando %s no host HTTP Você não está conectado. Tente novamente mais tarde. Verificar o tamanho de %s @@ -556,6 +557,7 @@ Privacidade Tema Selecione a paleta de cores + Automático Tema claro Tema escuro Fundo verde diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index cc504d3fe..24598898e 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -3,6 +3,7 @@ Настройки Новая беседа Управление аккаунтами + Управление аккаунтом Закрыть текущую беседу Сведения о контакте Подробности конференции @@ -16,6 +17,8 @@ Разблокировать контакт Заблокировать домен Разблокировать домен + Заблокировать участника + Разблокировать участника Управление аккаунтами Настройки Поделиться @@ -43,6 +46,7 @@ Заблокировать всех пользователей домена %s? Разблокировать всех пользователей домена %s? Контакт заблокирован + Заблокирован Вы хотите удалить %s из избранного? Беседы, связанные с данной закладкой будут сохранены Создать новый аккаунт на сервере Изменить пароль на сервере @@ -118,7 +122,7 @@ Не отправлять отчёты об ошибках Отправляя отчёты об ошибках, вы помогаете исправить и улучшить Conversations Отчёты о получении - Позволяет вашим контактам видеть когда вы получили и прочитали их сообщения + Позволяет вашим контактам видеть, когда вы получили и прочитали их сообщения Интерфейс Принять Произошла ошибка @@ -221,6 +225,8 @@ Удалить закладку Уничтожить конференцию Уничтожить канал + Не удалось уничтожить конференцию + Не удалось уничтожить канал Редактировать тему конференции Тема Вход в конференцию… @@ -267,6 +273,7 @@ Включить режим «тихих часов» Уведомления будут отключены во время «тихих часов» Другие + Синхронизировать с закладками OMEMO отпечаток скопирован в буфер обмена! Вы заблокированы из этой конференции Эта конференция — только для участников @@ -315,6 +322,7 @@ %s предлагается скачать Отменить передачу передача файла не удалась + передача файла отменена Файл был удалён Не найдено приложения для открытия файла Не найдено приложения, способного открыть эту ссылку @@ -353,10 +361,13 @@ Снять административные права Убрать из конференции Не удалось изменить принадлежность %s - Заблокировать из конференции + Заблокировать в конференции Заблокировать Не удалось сменить роль %s + Настройки приватной конференции + Настройки публичного канала Приватная + Сделать XMPP адрес видимым для всех Вы не участвуете Настройки конференции изменены! Не удалось изменить настройки конференции @@ -384,12 +395,14 @@ %s печатают... %s перестали печатать Оповещения о наборе - Позволяет вашим контактам видеть когда вы пишете им новое сообщение + Позволяет вашим контактам видеть, когда вы пишете им новое сообщение Отправить местоположение Показать местоположение Не найдено приложений для отображения местоположения Местоположение Беседа окончена + Покинул приватную конференцию + Покинул публичный канал Не доверять системным УЦ Все сертификаты должны быть подтверждены вручную Удалить сертификаты @@ -477,6 +490,7 @@ Уведомления приостановлены Сжатие изображений Всегда + Только большие изображения Оптимизации энергопотребления разрешены Ваше устройство использует сильные оптимизации энергопотребления, что может привести к задержке уведомлений и даже потере сообщений.\nРекомендуется их отключить. Ваше устройство использует сильные оптимизации энергопотребления, что может привести к задержке уведомлений и даже потере сообщений.\nСейчас появится предложение их отключить. @@ -515,10 +529,11 @@ Средний Длинный Оповещать о взаимодействии - Извещать контакты когда вы используете Conversations + Извещать контакты, когда вы используете Conversations Приватность Тема Выбрать цветовую палитру + Автоматически Светлая тема Тёмная тема Зелёный фон @@ -543,7 +558,7 @@ Создать заново OMEMO ключи. Необходимо повторное подтверждение. Используйте только в крайнем случае. Удалить отмеченные Вы должны подключиться для публикации аватара. - Текст ошибки + Показать текст ошибки Текст ошибки Режим экономии трафика включен Ваша операционная система не позволяет Conversations получать доступ в Интернет в фоновом режиме. Для получения уведомлений вы должны разрешить Conversations неограниченный доступ когда режим экономии трафика включен.\nConversations постарается сохранить трафик когда это возможно. @@ -642,6 +657,7 @@ Сообщение Личные сообщения выключены Принять Неизвестный Сертификат? + Вы все равно хотите подключиться? Прокручивать вниз Прокручивать вниз после отправки сообщения Редактировать статусное сообщение @@ -654,6 +670,7 @@ OMEMO будет всегда использоваться для одиночных бесед и закрытых конференций. OMEMO будет использоваться по умолчанию для новых бесед. OMEMO нужно будет явно включать для новых бесед. + Создать ярлык Размер шрифта Относительный размер шрифта используемый в приложении. Включено по умолчанию @@ -678,17 +695,43 @@ Копировать XMPP-адрес Быстрый поиск На экране \"Начать беседу\" открывать клавиатуру и ставить курсор в поле поиска + Аватар конференции + Имя контакта + Никнейм + Название + Название конференции + Эта конференция была уничтожена + Проблемы с подключением Настройки уведомлений + Сжатие видео Просмотр медиа + Участники Качество видео Низкое качество означает меньшие файлы Среднее (360p) Высокое (720р) + отменено + Функция не реализована + Неверный код страны + Выберите страну + номер телефона + Слишком много попыток + Вы используете устаревшую версию приложения + Ваше имя + Введите ваше имя Оригинал (без сжатия) + Открыть с помощью... + Выбрать аккаунт + Восстановить из резервной копии + Восстановить + Невозможно восстановить из резервной копии + Невозможно расшифровать резервную копию. Правильно ли введен пароль? Создать конференцию Присоединиться к каналу Создать закрытую конференцию Создать публичный канал + Название канала + Создание публичного канала... Найти каналы У меня уже есть аккаунт Добавить существующий аккаунт diff --git a/src/quicksy/res/values-fr/strings.xml b/src/quicksy/res/values-fr/strings.xml index 3168c7676..f90e22769 100644 --- a/src/quicksy/res/values-fr/strings.xml +++ b/src/quicksy/res/values-fr/strings.xml @@ -1,26 +1,26 @@ Quicksy a planté - En envoyant des traces de pile, vous contribuez au développement en cours de Quicks \ Warning: cela utilisera votre compte XMPP pour envoyer la trace de pile au développeur. - Quicksy requiert une application tierce nommée OpenKeychain pour chiffrer et déchiffrer les messages.\n\nOpenKeychain est sous licence GPLv3 et est disponible sur F-Droid et Google Play.\n\n(Veuillez redémarrer Quicksy après l\'installation de l\'app) - Quicksy ne peut pas chiffrer vos messages, car votre contact n’annonce pas sa clé publique. \ N \ nVeuillez demander à votre contact de configurer OpenPGP. - Quicksy ne peut pas chiffrer vos messages car votre contact n\'a pas communiqué sa clef publique.\n\nDemandez-lui de configurer OpenPGP. - Durée d\'inactivité de Quicksy après avoir repéré un changement sur un autre appareil - En envoyant des logs vous aidez le développement de Quicksy. - Quicksy a besoin d\'accéder à un stockage externe - Quicksy a besoin d\'accéder à la caméra - Votre appareil effectue actuellement des optimisations de batterie lourdes sur Quicksy pouvant entraîner des notifications tardives, voire la perte de messages. \ NIl est recommandé de les désactiver. - Votre appareil effectue actuellement des optimisations lourdes de la batterie sur Quicksy, susceptibles de retarder les notifications ou même de faire perdre des messages. \ N \ nVous serez invité à les désactiver. + En envoyant des traces d’appels, vous contribuez au développement en cours de Quicksy\nAvertissement : cela utilisera votre compte XMPP pour envoyer la trace d’appels au développeur. + Quicksy utilise une application tierce nommée OpenKeychain pour chiffrer et déchiffrer les messages et gérer vos clés publiques.\n\nOpenKeychain est sous licence GPLv3 et est disponible sur F-Droid et Google Play.\n\n(Veuillez redémarrer Quicksy après son installation.) + Quicksy ne peut pas chiffrer vos messages car votre contact n’annonce pas sa clé publique.\n\nVeuillez demander à votre contact de configurer OpenPGP. + Quicksy ne peut pas chiffrer vos messages car vos contacts n’annoncent pas leurs clés publiques.\n\nVeuillez demander à vos contacts de configurer OpenPGP. + Durée d’inactivité de Quicksy après avoir repéré un changement sur un autre appareil + En envoyant des traces d’appels, vous aidez le développement de Quicksy + Quicksy a besoin d’accéder au stockage externe + Quicksy a besoin d’accéder à la caméra + Votre appareil applique des optimisations de batterie lourdes sur Quicksy pouvant entraîner des notifications tardives, voire la perte de messages.\nIl est recommandé de les désactiver. + Votre appareil applique des optimisations de batterie lourdes sur Quicksy pouvant entraîner des notifications tardives, voire la perte de messages.\nVous allez maintenant être invité à les désactiver. Faites savoir à tous vos contacts quand vous utilisez Quicksy - Votre système d’exploitation limite votre navigation sur Internet lorsque vous êtes en arrière-plan. Pour recevoir des notifications de nouveaux messages, vous devez autoriser l\'enregistrement des données. - Votre appareil ne prend pas en charge la désactivation de Data Saver pour Quicksy. - Pour continuer à recevoir des notifications, même lorsque l\'écran est éteint, vous devez ajouter Quicksy à la liste des applications protégées. - Quicksy est incapable d\'envoyer des messages cryptés à %1$s. Cela peut être dû au fait que votre contact utilise un serveur ou un client obsolète qui ne peut pas gérer OMEMO. - Quicksy doit avoir accès au microphone - Cette catégorie de notification est utilisée pour afficher une notification permanente indiquant que Quicksy est en cours d\'exécution. + Votre système d’exploitation restreint l’accès à Internet à Quicksy lorsqu’il est en arrière-plan. Pour recevoir les notifications des nouveaux messages reçus, vous devriez accorder à Quicksy un accès illimité lorsque l’économie de la consommation des données est activée.\nQuicksy essaiera quand même d’économiser la consommation lorsque c’est possible. + Votre appareil ne prend pas en charge la désactivation du mode économie de données pour Quicksy. + Pour continuer à recevoir des notifications, même lorsque l’écran est éteint, vous devez ajouter Quicksy à la liste des applications protégées. + Quicksy ne peut pas envoyer des messages chiffrés à %1$s. Cela peut être dû au fait que votre contact utilise un serveur ou un client obsolète qui ne peut pas gérer OMEMO. + Quicksy a besoin d’accéder au microphone + Cette catégorie de notification est utilisée pour afficher une notification permanente indiquant que Quicksy est en cours d’exécution. Photo de profil Quicksy - Quicksy n\'est pas disponible dans votre pays. - Vérification de l\'identité du serveur impossible. + Quicksy n’est pas disponible dans votre pays. + Impossible de vérifier l’identité du serveur. Erreur de sécurité inconnue. - Timeout lors de la connexion au serveur. + Délai expiré lors de la connexion au serveur. From 972e537ea1e14c1f2df6aa2a9a4121ca71687a27 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 29 Mar 2020 22:37:57 +0200 Subject: [PATCH 007/182] =?UTF-8?q?conversations=E2=80=99=20own=20backup?= =?UTF-8?q?=20makes=20system=20backup=20obsolete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #3666 --- src/main/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 347372013..5516758fa 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -51,7 +51,7 @@ Date: Tue, 31 Mar 2020 11:18:16 +0200 Subject: [PATCH 008/182] fixed typo. closes #3667 --- src/main/java/eu/siacs/conversations/ui/OmemoActivity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java index b69f189a1..9d41be658 100644 --- a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java @@ -81,8 +81,8 @@ public abstract class OmemoActivity extends XmppActivity { } @Override - public void onActivityResult(int requestCode, int resultCode, Intent intent) { - super.onActivityResult(requestCode, requestCode, intent); + public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) { + super.onActivityResult(requestCode, resultCode, intent); if (requestCode == ScanActivity.REQUEST_SCAN_QR_CODE && resultCode == RESULT_OK) { String result = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT); XmppUri uri = new XmppUri(result == null ? "" : result); From ff18ea452df97d0ee87200bc7179dae39e511f19 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 31 Mar 2020 19:46:05 +0200 Subject: [PATCH 009/182] display toast when trying to join channel with no enabled accounts --- .../siacs/conversations/ui/ChannelDiscoveryActivity.java | 7 +++++-- .../eu/siacs/conversations/ui/EditAccountActivity.java | 3 ++- src/main/res/values/strings.xml | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java index 567a2f3d6..cb1baa39b 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java @@ -18,6 +18,7 @@ import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.TextView; +import android.widget.Toast; import java.util.Collections; import java.util.List; @@ -224,10 +225,12 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O @Override public void onChannelSearchResult(final Room result) { - List accounts = AccountUtils.getEnabledAccounts(xmppConnectionService); + final List accounts = AccountUtils.getEnabledAccounts(xmppConnectionService); if (accounts.size() == 1) { joinChannelSearchResult(accounts.get(0), result); - } else if (accounts.size() > 0) { + } else if (accounts.size() == 0) { + Toast.makeText(this, R.string.please_enable_your_account_first, Toast.LENGTH_LONG).show(); + } else { final AtomicReference account = new AtomicReference<>(accounts.get(0)); AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.choose_account); diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index c2bdd05de..cc3b22be0 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -420,7 +420,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } - if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) { + final List accounts = xmppConnectionService == null ? null : xmppConnectionService.getAccounts(); + if (accounts != null && accounts.size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) { Intent intent = SignupUtils.getSignUpIntent(this, mForceRegister != null && mForceRegister); StartConversationActivity.addInviteUri(intent, getIntent()); startActivity(intent); diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index a6c61e8d8..4601600dd 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -883,6 +883,7 @@ Channel discovery method Backup About + Please enable your account first View %1$d Participant View %1$d Participants From 62934e6487a3007af20ca3f5d9c2804d4e66a6d3 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 31 Mar 2020 19:49:08 +0200 Subject: [PATCH 010/182] change wording of previous commit --- .../eu/siacs/conversations/ui/ChannelDiscoveryActivity.java | 2 +- src/main/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java index cb1baa39b..df129152b 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java @@ -229,7 +229,7 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O if (accounts.size() == 1) { joinChannelSearchResult(accounts.get(0), result); } else if (accounts.size() == 0) { - Toast.makeText(this, R.string.please_enable_your_account_first, Toast.LENGTH_LONG).show(); + Toast.makeText(this, R.string.please_enable_an_account, Toast.LENGTH_LONG).show(); } else { final AtomicReference account = new AtomicReference<>(accounts.get(0)); AlertDialog.Builder builder = new AlertDialog.Builder(this); diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 4601600dd..ef5dbbf0d 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -883,7 +883,7 @@ Channel discovery method Backup About - Please enable your account first + Please enable an account View %1$d Participant View %1$d Participants From c5da699afeb2b9e1925daecac10ff0a30b5d339f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 20 Apr 2020 15:53:52 +0200 Subject: [PATCH 011/182] dont crash when fields names in caps are null --- .../conversations/entities/ServiceDiscoveryResult.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java index c4babe81c..d0bdb5632 100644 --- a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java +++ b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java @@ -6,6 +6,8 @@ import android.support.annotation.NonNull; import android.util.Base64; import android.util.Log; +import com.google.common.base.Strings; + import java.io.UnsupportedEncodingException; import java.lang.Comparable; import java.security.MessageDigest; @@ -222,9 +224,9 @@ public class ServiceDiscoveryResult { for (Data form : forms) { s.append(clean(form.getFormType())).append("<"); List fields = form.getFields(); - Collections.sort(fields, (lhs, rhs) -> lhs.getFieldName().compareTo(rhs.getFieldName())); + Collections.sort(fields, (lhs, rhs) -> Strings.nullToEmpty(lhs.getFieldName()).compareTo(Strings.nullToEmpty(rhs.getFieldName()))); for (Field field : fields) { - s.append(clean(field.getFieldName())).append("<"); + s.append(Strings.nullToEmpty(field.getFieldName())).append("<"); List values = field.getValues(); Collections.sort(values); for (String value : values) { From 1d62cb0024b00a03079bdcd83cb1840eddd46882 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 20 Apr 2020 15:57:06 +0200 Subject: [PATCH 012/182] pdf renderer might throw security exception on password protected pdf --- .../java/eu/siacs/conversations/persistance/FileBackend.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 8958278eb..1ff94c7c0 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -943,7 +943,7 @@ public class FileBackend { final Bitmap rendered = renderPdfDocument(fileDescriptor, size, true); drawOverlay(rendered, paintOverlayBlackPdf(rendered) ? R.drawable.open_pdf_black : R.drawable.open_pdf_white, 0.75f); return rendered; - } catch (IOException e) { + } catch (final IOException | SecurityException e) { Log.d(Config.LOGTAG, "unable to render PDF document preview", e); final Bitmap placeholder = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); placeholder.eraseColor(0xff000000); @@ -1357,7 +1357,7 @@ public class FileBackend { page.close(); pdfRenderer.close(); return scalePdfDimensions(new Dimensions(height, width)); - } catch (IOException e) { + } catch (IOException | SecurityException e) { Log.d(Config.LOGTAG, "unable to get dimensions for pdf document", e); return new Dimensions(0, 0); } From e964bb78ef6f7538afba838e69b5c2e77a6d1134 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 29 Aug 2019 09:26:57 +0200 Subject: [PATCH 013/182] added libwebrtc --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 610bc6e00..3f1523ca7 100644 --- a/build.gradle +++ b/build.gradle @@ -78,6 +78,7 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:3.12.10' implementation 'com.google.guava:guava:27.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.11.1' + implementation 'org.webrtc:google-webrtc:1.0.+' } ext { From b40a65652fbb90b92927f6c8bb3cb7dd9b70706f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 1 Apr 2020 08:27:54 +0200 Subject: [PATCH 014/182] disable HTTP upload during jingle development we are going to refactor jingle a lot. in order to better spot potential bugs in the Jingle File Transfer implementation we are going to disable HTTP upload during development. --- src/main/java/eu/siacs/conversations/Config.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 777953735..d12de90b7 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -105,7 +105,7 @@ public final class Config { public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true; - public static final boolean DISABLE_HTTP_UPLOAD = false; + public static final boolean DISABLE_HTTP_UPLOAD = true; public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts public static final boolean BACKGROUND_STANZA_LOGGING = false; //log all stanzas that were received while the app is in background public static final boolean RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE = true; //setting to true might increase power consumption From 75f753e957a484d1f22f0e4768777f1da7e185db Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 1 Apr 2020 10:35:05 +0200 Subject: [PATCH 015/182] increase version name for easier debugging with multiple devices --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3f1523ca7..b8bb32878 100644 --- a/build.gradle +++ b/build.gradle @@ -93,7 +93,7 @@ android { minSdkVersion 16 targetSdkVersion 28 versionCode 367 - versionName "2.7.1" + versionName "2.8.0-alpha" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 23ebb6ae80638b9814aac07f33596b0b063dcacd Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 1 Apr 2020 10:45:03 +0200 Subject: [PATCH 016/182] rename JingleConnection to JingleFileTransferConnection; use ID tuple to identify sessions --- .../services/XmppConnectionService.java | 2 +- .../ui/ConversationFragment.java | 4 +- .../eu/siacs/conversations/xml/Namespace.java | 1 + .../xmpp/jingle/AbstractJingleConnection.java | 71 +++++ .../xmpp/jingle/JingleConnectionManager.java | 113 ++++---- ...java => JingleFileTransferConnection.java} | 269 ++++++++---------- .../xmpp/jingle/JingleInBandTransport.java | 8 +- .../xmpp/jingle/JingleSocks5Transport.java | 42 +-- .../xmpp/jingle/stanzas/Content.java | 4 + .../xmpp/jingle/stanzas/JinglePacket.java | 186 ++++++------ 10 files changed, 377 insertions(+), 323 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java rename src/main/java/eu/siacs/conversations/xmpp/jingle/{JingleConnection.java => JingleFileTransferConnection.java} (83%) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 63d6a1911..b16da4517 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1353,7 +1353,7 @@ public class XmppConnectionService extends Service { || message.getConversation().getMode() == Conversation.MODE_MULTI) { mHttpConnectionManager.createNewUploadConnection(message, delay); } else { - mJingleConnectionManager.createNewConnection(message); + mJingleConnectionManager.startJingleFileTransfer(message); } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 036e35633..9d9ebf819 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -115,7 +115,7 @@ import eu.siacs.conversations.utils.TimeframeUtils; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.chatstate.ChatState; -import eu.siacs.conversations.xmpp.jingle.JingleConnection; +import eu.siacs.conversations.xmpp.jingle.JingleFileTransferConnection; import rocks.xmpp.addr.Jid; import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT; @@ -1051,7 +1051,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke final boolean deleted = m.isDeleted(); final boolean encrypted = m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED || m.getEncryption() == Message.ENCRYPTION_PGP; - final boolean receiving = m.getStatus() == Message.STATUS_RECEIVED && (t instanceof JingleConnection || t instanceof HttpDownloadConnection); + final boolean receiving = m.getStatus() == Message.STATUS_RECEIVED && (t instanceof JingleFileTransferConnection || t instanceof HttpDownloadConnection); activity.getMenuInflater().inflate(R.menu.message_context, menu); menu.setHeaderTitle(R.string.message_options); MenuItem openWith = menu.findItem(R.id.open_with); diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 921a2f580..3f365642f 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -29,6 +29,7 @@ public final class Namespace { public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0"; public static final String JINGLE_TRANSPORTS_S5B = "urn:xmpp:jingle:transports:s5b:1"; public static final String JINGLE_TRANSPORTS_IBB = "urn:xmpp:jingle:transports:ibb:1"; + public static final String IBB = "http://jabber.org/protocol/ibb"; public static final String PING = "urn:xmpp:ping"; public static final String PUSH = "urn:xmpp:push:0"; public static final String COMMANDS = "http://jabber.org/protocol/commands"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java new file mode 100644 index 000000000..19ad2822a --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -0,0 +1,71 @@ +package eu.siacs.conversations.xmpp.jingle; + +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import rocks.xmpp.addr.Jid; + +public abstract class AbstractJingleConnection { + + protected final JingleConnectionManager jingleConnectionManager; + protected final XmppConnectionService xmppConnectionService; + protected final Id id; + + public AbstractJingleConnection(final JingleConnectionManager jingleConnectionManager, final Id id) { + this.jingleConnectionManager = jingleConnectionManager; + this.xmppConnectionService = jingleConnectionManager.getXmppConnectionService(); + this.id = id; + } + + abstract void deliverPacket(JinglePacket jinglePacket); + + public Id getId() { + return id; + } + + + public static class Id { + public final Account account; + public final Jid counterPart; + public final String sessionId; + + private Id(final Account account, final Jid counterPart, final String sessionId) { + Preconditions.checkNotNull(counterPart); + Preconditions.checkArgument(counterPart.isFullJid()); + this.account = account; + this.counterPart = counterPart; + this.sessionId = sessionId; + } + + public static Id of(Account account, JinglePacket jinglePacket) { + return new Id(account, jinglePacket.getFrom(), jinglePacket.getSessionId()); + } + + public static Id of(Message message) { + return new Id( + message.getConversation().getAccount(), + message.getCounterpart(), + JingleConnectionManager.nextRandomId() + ); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Id id = (Id) o; + return Objects.equal(account.getJid(), id.account.getJid()) && + Objects.equal(counterPart, id.counterPart) && + Objects.equal(sessionId, id.sessionId); + } + + @Override + public int hashCode() { + return Objects.hashCode(account.getJid(), counterPart, sessionId); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 60d6ebfe2..8c2139bb0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -1,13 +1,13 @@ package eu.siacs.conversations.xmpp.jingle; -import android.annotation.SuppressLint; import android.util.Log; -import java.math.BigInteger; -import java.security.SecureRandom; +import com.google.common.base.Preconditions; + import java.util.HashMap; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; @@ -15,74 +15,63 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import rocks.xmpp.addr.Jid; public class JingleConnectionManager extends AbstractConnectionManager { - private List connections = new CopyOnWriteArrayList<>(); + private Map connections = new ConcurrentHashMap<>(); private HashMap primaryCandidates = new HashMap<>(); - @SuppressLint("TrulyRandom") - private SecureRandom random = new SecureRandom(); - public JingleConnectionManager(XmppConnectionService service) { super(service); } - public void deliverPacket(Account account, JinglePacket packet) { - if (packet.isAction("session-initiate")) { - JingleConnection connection = new JingleConnection(this); + public void deliverPacket(final Account account, final JinglePacket packet) { + final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, packet); + if (packet.isAction("session-initiate")) { //TODO check that id doesn't exist yet + JingleFileTransferConnection connection = new JingleFileTransferConnection(this, id); connection.init(account, packet); - connections.add(connection); + connections.put(id, connection); } else { - for (JingleConnection connection : connections) { - if (connection.getAccount() == account - && connection.getSessionId().equals( - packet.getSessionId()) - && connection.getCounterPart().equals(packet.getFrom())) { - connection.deliverPacket(packet); - return; - } + final AbstractJingleConnection abstractJingleConnection = connections.get(id); + if (abstractJingleConnection != null) { + abstractJingleConnection.deliverPacket(packet); + } else { + Log.d(Config.LOGTAG, "unable to route jingle packet: " + packet); + IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); + Element error = response.addChild("error"); + error.setAttribute("type", "cancel"); + error.addChild("item-not-found", + "urn:ietf:params:xml:ns:xmpp-stanzas"); + error.addChild("unknown-session", "urn:xmpp:jingle:errors:1"); + account.getXmppConnection().sendIqPacket(response, null); } - Log.d(Config.LOGTAG, "unable to route jingle packet: " + packet); - IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); - Element error = response.addChild("error"); - error.setAttribute("type", "cancel"); - error.addChild("item-not-found", - "urn:ietf:params:xml:ns:xmpp-stanzas"); - error.addChild("unknown-session", "urn:xmpp:jingle:errors:1"); - account.getXmppConnection().sendIqPacket(response, null); } } - public JingleConnection createNewConnection(Message message) { - Transferable old = message.getTransferable(); + public void startJingleFileTransfer(final Message message) { + Preconditions.checkArgument(message.isFileOrImage(), "Message is not of type file or image"); + final Transferable old = message.getTransferable(); if (old != null) { old.cancel(); } - JingleConnection connection = new JingleConnection(this); + final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(message); + final JingleFileTransferConnection connection = new JingleFileTransferConnection(this, id); mXmppConnectionService.markMessage(message, Message.STATUS_WAITING); connection.init(message); - this.connections.add(connection); - return connection; + this.connections.put(id, connection); } - public JingleConnection createNewConnection(final JinglePacket packet) { - JingleConnection connection = new JingleConnection(this); - this.connections.add(connection); - return connection; + void finishConnection(final AbstractJingleConnection connection) { + this.connections.remove(connection.getId()); } - public void finishConnection(JingleConnection connection) { - this.connections.remove(connection); - } - - public void getPrimaryCandidate(final Account account, final boolean initiator, final OnPrimaryCandidateFound listener) { + void getPrimaryCandidate(final Account account, final boolean initiator, final OnPrimaryCandidateFound listener) { if (Config.DISABLE_PROXY_LOOKUP) { listener.onPrimaryCandidateFound(false, null); return; @@ -97,7 +86,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - Element streamhost = packet.query().findChild("streamhost", Namespace.BYTE_STREAMS); + final Element streamhost = packet.query().findChild("streamhost", Namespace.BYTE_STREAMS); final String host = streamhost == null ? null : streamhost.getAttribute("host"); final String port = streamhost == null ? null : streamhost.getAttribute("port"); if (host != null && port != null) { @@ -112,7 +101,6 @@ public class JingleConnectionManager extends AbstractConnectionManager { listener.onPrimaryCandidateFound(true, candidate); } catch (final NumberFormatException e) { listener.onPrimaryCandidateFound(false, null); - return; } } else { listener.onPrimaryCandidateFound(false, null); @@ -129,31 +117,30 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } - public String nextRandomId() { - return new BigInteger(50, random).toString(32); + static String nextRandomId() { + return UUID.randomUUID().toString(); } public void deliverIbbPacket(Account account, IqPacket packet) { String sid = null; Element payload = null; - if (packet.hasChild("open", "http://jabber.org/protocol/ibb")) { - payload = packet.findChild("open", "http://jabber.org/protocol/ibb"); + if (packet.hasChild("open", Namespace.IBB)) { + payload = packet.findChild("open", Namespace.IBB); sid = payload.getAttribute("sid"); - } else if (packet.hasChild("data", "http://jabber.org/protocol/ibb")) { - payload = packet.findChild("data", "http://jabber.org/protocol/ibb"); + } else if (packet.hasChild("data", Namespace.IBB)) { + payload = packet.findChild("data", Namespace.IBB); sid = payload.getAttribute("sid"); - } else if (packet.hasChild("close", "http://jabber.org/protocol/ibb")) { - payload = packet.findChild("close", "http://jabber.org/protocol/ibb"); + } else if (packet.hasChild("close", Namespace.IBB)) { + payload = packet.findChild("close", Namespace.IBB); sid = payload.getAttribute("sid"); } if (sid != null) { - for (JingleConnection connection : connections) { - if (connection.getAccount() == account - && connection.hasTransportId(sid)) { - JingleTransport transport = connection.getTransport(); + for (final AbstractJingleConnection connection : this.connections.values()) { + if (connection instanceof JingleFileTransferConnection) { + final JingleFileTransferConnection fileTransfer = (JingleFileTransferConnection) connection; + final JingleTransport transport = fileTransfer.getTransport(); if (transport instanceof JingleInBandTransport) { - JingleInBandTransport inbandTransport = (JingleInBandTransport) transport; - inbandTransport.deliverPayload(packet, payload); + ((JingleInBandTransport) transport).deliverPayload(packet, payload); return; } } @@ -164,10 +151,10 @@ public class JingleConnectionManager extends AbstractConnectionManager { } public void cancelInTransmission() { - for (JingleConnection connection : this.connections) { - if (connection.getJingleStatus() == JingleConnection.JINGLE_STATUS_TRANSMITTING) { + for (AbstractJingleConnection connection : this.connections.values()) { + /*if (connection.getJingleStatus() == JingleFileTransferConnection.JINGLE_STATUS_TRANSMITTING) { connection.abort("connectivity-error"); - } + }*/ } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java similarity index 83% rename from src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java rename to src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index 3b2909cc7..6586474da 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -3,6 +3,7 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Base64; import android.util.Log; +import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; @@ -41,27 +42,22 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import rocks.xmpp.addr.Jid; -public class JingleConnection implements Transferable { +public class JingleFileTransferConnection extends AbstractJingleConnection implements Transferable { + private static final int JINGLE_STATUS_TRANSMITTING = 5; private static final String JET_OMEMO_CIPHER = "urn:xmpp:ciphers:aes-128-gcm-nopadding"; - private static final int JINGLE_STATUS_INITIATED = 0; private static final int JINGLE_STATUS_ACCEPTED = 1; private static final int JINGLE_STATUS_FINISHED = 4; - static final int JINGLE_STATUS_TRANSMITTING = 5; private static final int JINGLE_STATUS_FAILED = 99; private static final int JINGLE_STATUS_OFFERED = -1; - private JingleConnectionManager mJingleConnectionManager; - private XmppConnectionService mXmppConnectionService; private Content.Version ftVersion = Content.Version.FT_3; private int ibbBlockSize = 8192; - private int mJingleStatus = JINGLE_STATUS_OFFERED; + private int mJingleStatus = JINGLE_STATUS_OFFERED; //migrate to enum private int mStatus = Transferable.STATUS_UNKNOWN; private Message message; - private String sessionId; - private Account account; private Jid initiator; private Jid responder; private List candidates = new ArrayList<>(); @@ -98,7 +94,7 @@ public class JingleConnection implements Transferable { if (mJingleStatus != JINGLE_STATUS_FAILED && mJingleStatus != JINGLE_STATUS_FINISHED) { fail(IqParser.extractErrorMessage(packet)); } else { - Log.d(Config.LOGTAG,"ignoring late delivery of jingle packet to jingle session with status="+mJingleStatus+": "+packet.toString()); + Log.d(Config.LOGTAG, "ignoring late delivery of jingle packet to jingle session with status=" + mJingleStatus + ": " + packet.toString()); } } }; @@ -109,32 +105,32 @@ public class JingleConnection implements Transferable { public void onFileTransmitted(DownloadableFile file) { if (responding()) { if (expectedHash.length > 0 && !Arrays.equals(expectedHash, file.getSha1Sum())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": hashes did not match"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": hashes did not match"); } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": file transmitted(). we are responding"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": file transmitted(). we are responding"); sendSuccess(); - mXmppConnectionService.getFileBackend().updateFileParams(message); - mXmppConnectionService.databaseBackend.createMessage(message); - mXmppConnectionService.markMessage(message, Message.STATUS_RECEIVED); + xmppConnectionService.getFileBackend().updateFileParams(message); + xmppConnectionService.databaseBackend.createMessage(message); + xmppConnectionService.markMessage(message, Message.STATUS_RECEIVED); if (acceptedAutomatically) { message.markUnread(); if (message.getEncryption() == Message.ENCRYPTION_PGP) { - account.getPgpDecryptionService().decrypt(message, true); + id.account.getPgpDecryptionService().decrypt(message, true); } else { - mXmppConnectionService.getFileBackend().updateMediaScanner(file, () -> JingleConnection.this.mXmppConnectionService.getNotificationService().push(message)); + xmppConnectionService.getFileBackend().updateMediaScanner(file, () -> JingleFileTransferConnection.this.xmppConnectionService.getNotificationService().push(message)); } Log.d(Config.LOGTAG, "successfully transmitted file:" + file.getAbsolutePath() + " (" + CryptoHelper.bytesToHex(file.getSha1Sum()) + ")"); return; } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { - account.getPgpDecryptionService().decrypt(message, true); + id.account.getPgpDecryptionService().decrypt(message, true); } } else { if (ftVersion == Content.Version.FT_5) { //older Conversations will break when receiving a session-info sendHash(); } if (message.getEncryption() == Message.ENCRYPTION_PGP) { - account.getPgpDecryptionService().decrypt(message, false); + id.account.getPgpDecryptionService().decrypt(message, false); } if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { file.delete(); @@ -142,14 +138,14 @@ public class JingleConnection implements Transferable { } Log.d(Config.LOGTAG, "successfully transmitted file:" + file.getAbsolutePath() + " (" + CryptoHelper.bytesToHex(file.getSha1Sum()) + ")"); if (message.getEncryption() != Message.ENCRYPTION_PGP) { - mXmppConnectionService.getFileBackend().updateMediaScanner(file); + xmppConnectionService.getFileBackend().updateMediaScanner(file); } } @Override public void onFileTransferAborted() { - JingleConnection.this.sendSessionTerminate("connectivity-error"); - JingleConnection.this.fail(); + JingleFileTransferConnection.this.sendSessionTerminate("connectivity-error"); + JingleFileTransferConnection.this.fail(); } }; private OnTransportConnected onIbbTransportConnected = new OnTransportConnected() { @@ -160,16 +156,16 @@ public class JingleConnection implements Transferable { @Override public void established() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ibb transport connected. sending file"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ibb transport connected. sending file"); mJingleStatus = JINGLE_STATUS_TRANSMITTING; - JingleConnection.this.transport.send(file, onFileTransmissionStatusChanged); + JingleFileTransferConnection.this.transport.send(file, onFileTransmissionStatusChanged); } }; private OnProxyActivated onProxyActivated = new OnProxyActivated() { @Override public void success() { - if (initiator.equals(account.getJid())) { + if (initiator.equals(id.account.getJid())) { Log.d(Config.LOGTAG, "we were initiating. sending file"); transport.send(file, onFileTransmissionStatusChanged); } else { @@ -180,7 +176,7 @@ public class JingleConnection implements Transferable { @Override public void failed() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": proxy activation failed"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": proxy activation failed"); proxyActivationFailed = true; if (initiating()) { sendFallbackToIbb(); @@ -188,18 +184,28 @@ public class JingleConnection implements Transferable { } }; - public JingleConnection(JingleConnectionManager mJingleConnectionManager) { - this.mJingleConnectionManager = mJingleConnectionManager; - this.mXmppConnectionService = mJingleConnectionManager - .getXmppConnectionService(); + public JingleFileTransferConnection(JingleConnectionManager jingleConnectionManager, Id id) { + super(jingleConnectionManager, id); + } + + private static long parseLong(final Element element, final long l) { + final String input = element == null ? null : element.getContent(); + if (input == null) { + return l; + } + try { + return Long.parseLong(input); + } catch (Exception e) { + return l; + } } private boolean responding() { - return responder != null && responder.equals(account.getJid()); + return responder != null && responder.equals(id.account.getJid()); } private boolean initiating() { - return initiator.equals(account.getJid()); + return initiator.equals(id.account.getJid()); } InputStream getFileInputStream() { @@ -211,25 +217,19 @@ public class JingleConnection implements Transferable { Log.d(Config.LOGTAG, "file object was not assigned"); return null; } - this.file.getParentFile().mkdirs(); - this.file.createNewFile(); + final File parent = this.file.getParentFile(); + if (parent != null && parent.mkdirs()) { + Log.d(Config.LOGTAG,"created parent directories for file "+file.getAbsolutePath()); + } + if (this.file.createNewFile()) { + Log.d(Config.LOGTAG,"created output file "+file.getAbsolutePath()); + } this.mFileOutputStream = AbstractConnectionManager.createOutputStream(this.file, false, true); return this.mFileOutputStream; } - public String getSessionId() { - return this.sessionId; - } - - public Account getAccount() { - return this.account; - } - - public Jid getCounterPart() { - return this.message.getCounterpart(); - } - - void deliverPacket(JinglePacket packet) { + @Override + void deliverPacket(final JinglePacket packet) { if (packet.isAction("session-terminate")) { Reason reason = packet.getReason(); if (reason != null) { @@ -289,7 +289,7 @@ public class JingleConnection implements Transferable { final Element error = response.addChild("error").setAttribute("type", "cancel"); error.addChild("not-acceptable", "urn:ietf:params:xml:ns:xmpp-stanzas"); } - mXmppConnectionService.sendIqPacket(account, response, null); + xmppConnectionService.sendIqPacket(id.account, response, null); } private void respondToIqWithOutOfOrder(final IqPacket packet) { @@ -297,7 +297,7 @@ public class JingleConnection implements Transferable { final Element error = response.addChild("error").setAttribute("type", "wait"); error.addChild("unexpected-request", "urn:ietf:params:xml:ns:xmpp-stanzas"); error.addChild("out-of-order", "urn:xmpp:jingle:errors:1"); - mXmppConnectionService.sendIqPacket(account, response, null); + xmppConnectionService.sendIqPacket(id.account, response, null); } public void init(final Message message) { @@ -318,24 +318,22 @@ public class JingleConnection implements Transferable { private void init(Message message, XmppAxolotlMessage xmppAxolotlMessage) { this.mXmppAxolotlMessage = xmppAxolotlMessage; this.contentCreator = "initiator"; - this.contentName = this.mJingleConnectionManager.nextRandomId(); + this.contentName = JingleConnectionManager.nextRandomId(); this.message = message; - this.account = message.getConversation().getAccount(); final List remoteFeatures = getRemoteFeatures(); upgradeNamespace(remoteFeatures); this.initialTransport = remoteFeatures.contains(Namespace.JINGLE_TRANSPORTS_S5B) ? Transport.SOCKS : Transport.IBB; this.remoteSupportsOmemoJet = remoteFeatures.contains(Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO); this.message.setTransferable(this); this.mStatus = Transferable.STATUS_UPLOADING; - this.initiator = this.account.getJid(); - this.responder = this.message.getCounterpart(); - this.sessionId = this.mJingleConnectionManager.nextRandomId(); - this.transportId = this.mJingleConnectionManager.nextRandomId(); + this.initiator = this.id.account.getJid(); + this.responder = this.id.counterPart; + this.transportId = JingleConnectionManager.nextRandomId(); if (this.initialTransport == Transport.IBB) { this.sendInitRequest(); } else { gatherAndConnectDirectCandidates(); - this.mJingleConnectionManager.getPrimaryCandidate(account, initiating(), (success, candidate) -> { + this.jingleConnectionManager.getPrimaryCandidate(id.account, initiating(), (success, candidate) -> { if (success) { final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate); connections.put(candidate.getCid(), socksConnection); @@ -367,10 +365,10 @@ public class JingleConnection implements Transferable { private void gatherAndConnectDirectCandidates() { final List directCandidates; if (Config.USE_DIRECT_JINGLE_CANDIDATES) { - if (account.isOnion() || mXmppConnectionService.useTorToConnect()) { + if (id.account.isOnion() || xmppConnectionService.useTorToConnect()) { directCandidates = Collections.emptyList(); } else { - directCandidates = DirectConnectionUtils.getLocalCandidates(account.getJid()); + directCandidates = DirectConnectionUtils.getLocalCandidates(id.account.getJid()); } } else { directCandidates = Collections.emptyList(); @@ -391,10 +389,10 @@ public class JingleConnection implements Transferable { } private List getRemoteFeatures() { - Jid jid = this.message.getCounterpart(); + final Jid jid = this.id.counterPart; String resource = jid != null ? jid.getResource() : null; if (resource != null) { - Presence presence = this.account.getRoster().getContact(jid).getPresences().getPresences().get(resource); + Presence presence = this.id.account.getRoster().getContact(jid).getPresences().getPresences().get(resource); ServiceDiscoveryResult result = presence != null ? presence.getServiceDiscoveryResult() : null; return result == null ? Collections.emptyList() : result.getFeatures(); } else { @@ -402,22 +400,19 @@ public class JingleConnection implements Transferable { } } - public void init(Account account, JinglePacket packet) { + public void init(Account account, JinglePacket packet) { //should move to deliverPacket this.mJingleStatus = JINGLE_STATUS_INITIATED; - Conversation conversation = this.mXmppConnectionService + Conversation conversation = this.xmppConnectionService .findOrCreateConversation(account, packet.getFrom().asBareJid(), false, false); this.message = new Message(conversation, "", Message.ENCRYPTION_NONE); this.message.setStatus(Message.STATUS_RECEIVED); this.mStatus = Transferable.STATUS_OFFER; this.message.setTransferable(this); - final Jid from = packet.getFrom(); - this.message.setCounterpart(from); - this.account = account; - this.initiator = packet.getFrom(); - this.responder = this.account.getJid(); - this.sessionId = packet.getSessionId(); - Content content = packet.getJingleContent(); + this.message.setCounterpart(this.id.counterPart); + this.initiator = this.id.counterPart; + this.responder = this.id.account.getJid(); + final Content content = packet.getJingleContent(); this.contentCreator = content.getAttribute("creator"); this.initialTransport = content.hasSocks5Transport() ? Transport.SOCKS : Transport.IBB; this.contentName = content.getAttribute("name"); @@ -459,7 +454,7 @@ public class JingleConnection implements Transferable { if (encrypted == null) { final Element security = content.findChild("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT); if (security != null && AxolotlService.PEP_PREFIX.equals(security.getAttribute("type"))) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received jingle file offer with JET"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received jingle file offer with JET"); encrypted = security.findChild("encrypted", AxolotlService.PEP_PREFIX); remoteIsUsingJet = true; } @@ -490,10 +485,10 @@ public class JingleConnection implements Transferable { long size = parseLong(fileSize, 0); message.setBody(Long.toString(size)); conversation.add(message); - mJingleConnectionManager.updateConversationUi(true); - this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false); + jingleConnectionManager.updateConversationUi(true); + this.file = this.xmppConnectionService.getFileBackend().getFile(message, false); if (mXmppAxolotlMessage != null) { - XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage, false); + XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = id.account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage, false); if (transportMessage != null) { message.setEncryption(Message.ENCRYPTION_AXOLOTL); this.file.setKey(transportMessage.getKey()); @@ -511,11 +506,11 @@ public class JingleConnection implements Transferable { respondToIq(packet, true); - if (account.getRoster().getContact(from).showInContactList() - && mJingleConnectionManager.hasStoragePermission() - && size < this.mJingleConnectionManager.getAutoAcceptFileSize() - && mXmppConnectionService.isDataSaverDisabled()) { - Log.d(Config.LOGTAG, "auto accepting file from " + from); + if (id.account.getRoster().getContact(id.counterPart).showInContactList() + && jingleConnectionManager.hasStoragePermission() + && size < this.jingleConnectionManager.getAutoAcceptFileSize() + && xmppConnectionService.isDataSaverDisabled()) { + Log.d(Config.LOGTAG, "auto accepting file from " + id.counterPart); this.acceptedAutomatically = true; this.sendAccept(); } else { @@ -524,9 +519,9 @@ public class JingleConnection implements Transferable { "not auto accepting new file offer with size: " + size + " allowed size:" - + this.mJingleConnectionManager + + this.jingleConnectionManager .getAutoAcceptFileSize()); - this.mXmppConnectionService.getNotificationService().push(message); + this.xmppConnectionService.getNotificationService().push(message); } Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize()); return; @@ -535,24 +530,12 @@ public class JingleConnection implements Transferable { } } - private static long parseLong(final Element element, final long l) { - final String input = element == null ? null : element.getContent(); - if (input == null) { - return l; - } - try { - return Long.parseLong(input); - } catch (Exception e) { - return l; - } - } - private void sendInitRequest() { JinglePacket packet = this.bootstrapPacket("session-initiate"); Content content = new Content(this.contentCreator, this.contentName); if (message.isFileOrImage()) { content.setTransportId(this.transportId); - this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false); + this.file = this.xmppConnectionService.getFileBackend().getFile(message, false); if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { this.file.setKey(mXmppAxolotlMessage.getInnerKey()); this.file.setIv(mXmppAxolotlMessage.getIV()); @@ -561,7 +544,7 @@ public class JingleConnection implements Transferable { this.file.setExpectedSize(file.getSize() + (this.remoteSupportsOmemoJet ? 0 : 16)); final Element file = content.setFileOffer(this.file, false, this.ftVersion); if (remoteSupportsOmemoJet) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": remote announced support for JET"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote announced support for JET"); final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT); security.setAttribute("name", this.contentName); security.setAttribute("cipher", JET_OMEMO_CIPHER); @@ -585,19 +568,19 @@ public class JingleConnection implements Transferable { content.setTransportId(this.transportId); if (this.initialTransport == Transport.IBB) { content.ibbTransport().setAttribute("block-size", Integer.toString(this.ibbBlockSize)); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending IBB offer"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending IBB offer"); } else { final List candidates = getCandidatesAsElements(); - Log.d(Config.LOGTAG, String.format("%s: sending S5B offer with %d candidates", account.getJid().asBareJid(), candidates.size())); + Log.d(Config.LOGTAG, String.format("%s: sending S5B offer with %d candidates", id.account.getJid().asBareJid(), candidates.size())); content.socks5transport().setChildren(candidates); } packet.setContent(content); this.sendJinglePacket(packet, (account, response) -> { if (response.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": other party received offer"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": other party received offer"); if (mJingleStatus == JINGLE_STATUS_OFFERED) { mJingleStatus = JINGLE_STATUS_INITIATED; - mXmppConnectionService.markMessage(message, Message.STATUS_OFFERED); + xmppConnectionService.markMessage(message, Message.STATUS_OFFERED); } else { Log.d(Config.LOGTAG, "received ack for offer when status was " + mJingleStatus); } @@ -628,7 +611,7 @@ public class JingleConnection implements Transferable { private void sendAccept() { mJingleStatus = JINGLE_STATUS_ACCEPTED; this.mStatus = Transferable.STATUS_DOWNLOADING; - this.mJingleConnectionManager.updateConversationUi(true); + this.jingleConnectionManager.updateConversationUi(true); if (initialTransport == Transport.SOCKS) { sendAcceptSocks(); } else { @@ -638,7 +621,7 @@ public class JingleConnection implements Transferable { private void sendAcceptSocks() { gatherAndConnectDirectCandidates(); - this.mJingleConnectionManager.getPrimaryCandidate(this.account, initiating(), (success, candidate) -> { + this.jingleConnectionManager.getPrimaryCandidate(this.id.account, initiating(), (success, candidate) -> { final JinglePacket packet = bootstrapPacket("session-accept"); final Content content = new Content(contentCreator, contentName); content.setFileOffer(fileOffer, ftVersion); @@ -692,34 +675,34 @@ public class JingleConnection implements Transferable { private JinglePacket bootstrapPacket(String action) { JinglePacket packet = new JinglePacket(); packet.setAction(action); - packet.setFrom(account.getJid()); - packet.setTo(this.message.getCounterpart()); - packet.setSessionId(this.sessionId); + packet.setFrom(id.account.getJid()); + packet.setTo(id.counterPart); + packet.setSessionId(this.id.sessionId); packet.setInitiator(this.initiator); return packet; } private void sendJinglePacket(JinglePacket packet) { - mXmppConnectionService.sendIqPacket(account, packet, responseListener); + xmppConnectionService.sendIqPacket(id.account, packet, responseListener); } private void sendJinglePacket(JinglePacket packet, OnIqPacketReceived callback) { - mXmppConnectionService.sendIqPacket(account, packet, callback); + xmppConnectionService.sendIqPacket(id.account, packet, callback); } private void receiveAccept(JinglePacket packet) { if (responding()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received out of order session-accept (we were responding)"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order session-accept (we were responding)"); respondToIqWithOutOfOrder(packet); return; } if (this.mJingleStatus != JINGLE_STATUS_INITIATED) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received out of order session-accept"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order session-accept"); respondToIqWithOutOfOrder(packet); return; } this.mJingleStatus = JINGLE_STATUS_ACCEPTED; - mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); + xmppConnectionService.markMessage(message, Message.STATUS_UNSEND); Content content = packet.getJingleContent(); if (content.hasSocks5Transport()) { respondToIq(packet, true); @@ -734,7 +717,7 @@ public class JingleConnection implements Transferable { this.ibbBlockSize = bs; } } catch (Exception e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to parse block size in session-accept"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to parse block size in session-accept"); } } respondToIq(packet, true); @@ -769,7 +752,7 @@ public class JingleConnection implements Transferable { respondToIq(packet, true); onProxyActivated.failed(); } else if (content.socks5transport().hasChild("candidate-error")) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received candidate error"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received candidate error"); respondToIq(packet, true); this.receivedCandidate = true; if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) { @@ -808,21 +791,21 @@ public class JingleConnection implements Transferable { final JingleSocks5Transport connection = chooseConnection(); this.transport = connection; if (connection == null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not find suitable candidate"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": could not find suitable candidate"); this.disconnectSocks5Connections(); if (initiating()) { this.sendFallbackToIbb(); } } else { final JingleCandidate candidate = connection.getCandidate(); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": elected candidate " + candidate.getHost() + ":" + candidate.getPort()); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": elected candidate " + candidate.getHost() + ":" + candidate.getPort()); this.mJingleStatus = JINGLE_STATUS_TRANSMITTING; if (connection.needsActivation()) { if (connection.getCandidate().isOurs()) { final String sid; if (ftVersion == Content.Version.FT_3) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": use session ID instead of transport ID to activate proxy"); - sid = getSessionId(); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": use session ID instead of transport ID to activate proxy"); + sid = id.sessionId; } else { sid = getTransportId(); } @@ -834,10 +817,10 @@ public class JingleConnection implements Transferable { activation.query("http://jabber.org/protocol/bytestreams") .setAttribute("sid", sid); activation.query().addChild("activate") - .setContent(this.getCounterPart().toString()); - mXmppConnectionService.sendIqPacket(account, activation, (account, response) -> { + .setContent(this.id.counterPart.toEscapedString()); + xmppConnectionService.sendIqPacket(this.id.account, activation, (account, response) -> { if (response.getType() != IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + response.toString()); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": " + response.toString()); sendProxyError(); onProxyActivated.failed(); } else { @@ -904,15 +887,15 @@ public class JingleConnection implements Transferable { this.mJingleStatus = JINGLE_STATUS_FINISHED; this.message.setStatus(Message.STATUS_RECEIVED); this.message.setTransferable(null); - this.mXmppConnectionService.updateMessage(message, false); - this.mJingleConnectionManager.finishConnection(this); + this.xmppConnectionService.updateMessage(message, false); + this.jingleConnectionManager.finishConnection(this); } private void sendFallbackToIbb() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending fallback to ibb"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending fallback to ibb"); JinglePacket packet = this.bootstrapPacket("transport-replace"); Content content = new Content(this.contentCreator, this.contentName); - this.transportId = this.mJingleConnectionManager.nextRandomId(); + this.transportId = this.jingleConnectionManager.nextRandomId(); content.setTransportId(this.transportId); content.ibbTransport().setAttribute("block-size", Integer.toString(this.ibbBlockSize)); @@ -923,18 +906,18 @@ public class JingleConnection implements Transferable { private void receiveFallbackToIbb(JinglePacket packet) { if (initiating()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received out of order transport-replace (we were initiating)"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-replace (we were initiating)"); respondToIqWithOutOfOrder(packet); return; } final boolean validState = mJingleStatus == JINGLE_STATUS_ACCEPTED || (proxyActivationFailed && mJingleStatus == JINGLE_STATUS_TRANSMITTING); if (!validState) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received out of order transport-replace"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-replace"); respondToIqWithOutOfOrder(packet); return; } this.proxyActivationFailed = false; //fallback received; now we no longer need to accept another one; - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": receiving fallback to ibb"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": receiving fallback to ibb"); final String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size"); if (receivedBlockSize != null) { try { @@ -943,7 +926,7 @@ public class JingleConnection implements Transferable { this.ibbBlockSize = bs; } } catch (NumberFormatException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to parse block size in transport-replace"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to parse block size in transport-replace"); } } this.transportId = packet.getJingleContent().getTransportId(); @@ -961,7 +944,7 @@ public class JingleConnection implements Transferable { if (initiating()) { this.sendJinglePacket(answer, (account, response) -> { if (response.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + " recipient ACKed our transport-accept. creating ibb"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + " recipient ACKed our transport-accept. creating ibb"); transport.connect(onIbbTransportConnected); } }); @@ -973,13 +956,13 @@ public class JingleConnection implements Transferable { private void receiveTransportAccept(JinglePacket packet) { if (responding()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received out of order transport-accept (we were responding)"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-accept (we were responding)"); respondToIqWithOutOfOrder(packet); return; } final boolean validState = mJingleStatus == JINGLE_STATUS_ACCEPTED || (proxyActivationFailed && mJingleStatus == JINGLE_STATUS_TRANSMITTING); if (!validState) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received out of order transport-accept"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-accept"); respondToIqWithOutOfOrder(packet); return; } @@ -995,13 +978,13 @@ public class JingleConnection implements Transferable { this.ibbBlockSize = bs; } } catch (NumberFormatException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to parse block size in transport-accept"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to parse block size in transport-accept"); } } this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize); if (sid == null || !sid.equals(this.transportId)) { - Log.w(Config.LOGTAG, String.format("%s: sid in transport-accept (%s) did not match our sid (%s) ", account.getJid().asBareJid(), sid, transportId)); + Log.w(Config.LOGTAG, String.format("%s: sid in transport-accept (%s) did not match our sid (%s) ", id.account.getJid().asBareJid(), sid, transportId)); } respondToIq(packet, true); //might be receive instead if we are not initiating @@ -1009,7 +992,7 @@ public class JingleConnection implements Transferable { this.transport.connect(onIbbTransportConnected); } } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received invalid transport-accept"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received invalid transport-accept"); respondToIq(packet, false); } } @@ -1017,15 +1000,15 @@ public class JingleConnection implements Transferable { private void receiveSuccess() { if (initiating()) { this.mJingleStatus = JINGLE_STATUS_FINISHED; - this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_RECEIVED); + this.xmppConnectionService.markMessage(this.message, Message.STATUS_SEND_RECEIVED); this.disconnectSocks5Connections(); if (this.transport instanceof JingleInBandTransport) { this.transport.disconnect(); } this.message.setTransferable(null); - this.mJingleConnectionManager.finishConnection(this); + this.jingleConnectionManager.finishConnection(this); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received session-terminate/success while responding"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session-terminate/success while responding"); } } @@ -1041,15 +1024,15 @@ public class JingleConnection implements Transferable { this.transport.disconnect(); } sendSessionTerminate(reason); - this.mJingleConnectionManager.finishConnection(this); + this.jingleConnectionManager.finishConnection(this); if (responding()) { this.message.setTransferable(new TransferablePlaceholder(cancelled ? Transferable.STATUS_CANCELLED : Transferable.STATUS_FAILED)); if (this.file != null) { file.delete(); } - this.mJingleConnectionManager.updateConversationUi(true); + this.jingleConnectionManager.updateConversationUi(true); } else { - this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : null); + this.xmppConnectionService.markMessage(this.message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : null); this.message.setTransferable(null); } } @@ -1072,15 +1055,15 @@ public class JingleConnection implements Transferable { if (this.file != null) { file.delete(); } - this.mJingleConnectionManager.updateConversationUi(true); + this.jingleConnectionManager.updateConversationUi(true); } else { - this.mXmppConnectionService.markMessage(this.message, + this.xmppConnectionService.markMessage(this.message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage); this.message.setTransferable(null); } } - this.mJingleConnectionManager.finishConnection(this); + this.jingleConnectionManager.finishConnection(this); } private void sendSessionTerminate(String reason) { @@ -1168,7 +1151,7 @@ public class JingleConnection implements Transferable { } private void sendCandidateError() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending candidate error"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending candidate error"); JinglePacket packet = bootstrapPacket("transport-info"); Content content = new Content(this.contentCreator, this.contentName); content.setTransportId(this.transportId); @@ -1221,7 +1204,7 @@ public class JingleConnection implements Transferable { void updateProgress(int i) { this.mProgress = i; - mJingleConnectionManager.updateConversationUi(false); + jingleConnectionManager.updateConversationUi(false); } public String getTransportId() { @@ -1241,7 +1224,7 @@ public class JingleConnection implements Transferable { } public boolean start() { - if (account.getStatus() == Account.State.ONLINE) { + if (id.account.getStatus() == Account.State.ONLINE) { if (mJingleStatus == JINGLE_STATUS_INITIATED) { new Thread(this::sendAccept).start(); } @@ -1271,7 +1254,7 @@ public class JingleConnection implements Transferable { } public AbstractConnectionManager getConnectionManager() { - return this.mJingleConnectionManager; + return this.jingleConnectionManager; } interface OnProxyActivated { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInBandTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInBandTransport.java index 4182da08c..c5eaca072 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInBandTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInBandTransport.java @@ -33,7 +33,7 @@ public class JingleInBandTransport extends JingleTransport { private boolean connected = true; private DownloadableFile file; - private final JingleConnection connection; + private final JingleFileTransferConnection connection; private InputStream fileInputStream = null; private InputStream innerInputStream = null; @@ -60,10 +60,10 @@ public class JingleInBandTransport extends JingleTransport { } }; - JingleInBandTransport(final JingleConnection connection, final String sid, final int blockSize) { + JingleInBandTransport(final JingleFileTransferConnection connection, final String sid, final int blockSize) { this.connection = connection; - this.account = connection.getAccount(); - this.counterpart = connection.getCounterPart(); + this.account = connection.getId().account; + this.counterpart = connection.getId().counterPart; this.blockSize = blockSize; this.sessionId = sid; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java index e6b23ad18..2c9f11a56 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -31,8 +31,9 @@ public class JingleSocks5Transport extends JingleTransport { private static final int SOCKET_TIMEOUT_PROXY = 5000; private final JingleCandidate candidate; - private final JingleConnection connection; + private final JingleFileTransferConnection connection; private final String destination; + private final Account account; private OutputStream outputStream; private InputStream inputStream; private boolean isEstablished = false; @@ -40,7 +41,7 @@ public class JingleSocks5Transport extends JingleTransport { private ServerSocket serverSocket; private Socket socket; - JingleSocks5Transport(JingleConnection jingleConnection, JingleCandidate candidate) { + JingleSocks5Transport(JingleFileTransferConnection jingleConnection, JingleCandidate candidate) { final MessageDigest messageDigest; try { messageDigest = MessageDigest.getInstance("SHA-1"); @@ -49,19 +50,20 @@ public class JingleSocks5Transport extends JingleTransport { } this.candidate = candidate; this.connection = jingleConnection; + this.account = jingleConnection.getId().account; final StringBuilder destBuilder = new StringBuilder(); - if (jingleConnection.getFtVersion() == Content.Version.FT_3) { - Log.d(Config.LOGTAG, this.connection.getAccount().getJid().asBareJid() + ": using session Id instead of transport Id for proxy destination"); - destBuilder.append(jingleConnection.getSessionId()); + if (this.connection.getFtVersion() == Content.Version.FT_3) { + Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": using session Id instead of transport Id for proxy destination"); + destBuilder.append(this.connection.getId().sessionId); } else { - destBuilder.append(jingleConnection.getTransportId()); + destBuilder.append(this.connection.getTransportId()); } if (candidate.isOurs()) { - destBuilder.append(jingleConnection.getAccount().getJid()); - destBuilder.append(jingleConnection.getCounterPart()); + destBuilder.append(this.account.getJid()); + destBuilder.append(this.connection.getId().counterPart); } else { - destBuilder.append(jingleConnection.getCounterPart()); - destBuilder.append(jingleConnection.getAccount().getJid()); + destBuilder.append(this.connection.getId().counterPart); + destBuilder.append(this.account.getJid()); } messageDigest.reset(); this.destination = CryptoHelper.bytesToHex(messageDigest.digest(destBuilder.toString().getBytes())); @@ -130,7 +132,7 @@ public class JingleSocks5Transport extends JingleTransport { responseHeader = new byte[]{0x05, 0x00, 0x00, 0x03}; success = true; } else { - Log.d(Config.LOGTAG,connection.getAccount().getJid().asBareJid()+": destination mismatch. received "+receivedDestination+" (expected "+this.destination+")"); + Log.d(Config.LOGTAG,this.account.getJid().asBareJid()+": destination mismatch. received "+receivedDestination+" (expected "+this.destination+")"); responseHeader = new byte[]{0x05, 0x04, 0x00, 0x03}; success = false; } @@ -141,7 +143,7 @@ public class JingleSocks5Transport extends JingleTransport { outputStream.write(response.array()); outputStream.flush(); if (success) { - Log.d(Config.LOGTAG,connection.getAccount().getJid().asBareJid()+": successfully processed connection to candidate "+candidate.getHost()+":"+candidate.getPort()); + Log.d(Config.LOGTAG,this.account.getJid().asBareJid()+": successfully processed connection to candidate "+candidate.getHost()+":"+candidate.getPort()); socket.setSoTimeout(0); this.socket = socket; this.inputStream = inputStream; @@ -160,7 +162,7 @@ public class JingleSocks5Transport extends JingleTransport { new Thread(() -> { final int timeout = candidate.getType() == JingleCandidate.TYPE_DIRECT ? SOCKET_TIMEOUT_DIRECT : SOCKET_TIMEOUT_PROXY; try { - final boolean useTor = connection.getAccount().isOnion() || connection.getConnectionManager().getXmppConnectionService().useTorToConnect(); + final boolean useTor = this.account.isOnion() || connection.getConnectionManager().getXmppConnectionService().useTorToConnect(); if (useTor) { socket = SocksSocketFactory.createSocketOverTor(candidate.getHost(), candidate.getPort()); } else { @@ -185,7 +187,7 @@ public class JingleSocks5Transport extends JingleTransport { public void send(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) { new Thread(() -> { InputStream fileInputStream = null; - final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_send_" + connection.getSessionId()); + final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_send_" + connection.getId().sessionId); long transmitted = 0; try { wakeLock.acquire(); @@ -193,7 +195,7 @@ public class JingleSocks5Transport extends JingleTransport { digest.reset(); fileInputStream = connection.getFileInputStream(); if (fileInputStream == null) { - Log.d(Config.LOGTAG, connection.getAccount().getJid().asBareJid() + ": could not create input stream"); + Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": could not create input stream"); callback.onFileTransferAborted(); return; } @@ -213,7 +215,7 @@ public class JingleSocks5Transport extends JingleTransport { callback.onFileTransmitted(file); } } catch (Exception e) { - final Account account = connection.getAccount(); + final Account account = this.account; Log.d(Config.LOGTAG, account.getJid().asBareJid()+": failed sending file after "+transmitted+"/"+file.getExpectedSize()+" ("+ socket.getInetAddress()+":"+socket.getPort()+")", e); callback.onFileTransferAborted(); } finally { @@ -227,7 +229,7 @@ public class JingleSocks5Transport extends JingleTransport { public void receive(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) { new Thread(() -> { OutputStream fileOutputStream = null; - final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_receive_" + connection.getSessionId()); + final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_receive_" + connection.getId().sessionId); try { wakeLock.acquire(); MessageDigest digest = MessageDigest.getInstance("SHA-1"); @@ -237,7 +239,7 @@ public class JingleSocks5Transport extends JingleTransport { fileOutputStream = connection.getFileOutputStream(); if (fileOutputStream == null) { callback.onFileTransferAborted(); - Log.d(Config.LOGTAG, connection.getAccount().getJid().asBareJid() + ": could not create output stream"); + Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": could not create output stream"); return; } double size = file.getExpectedSize(); @@ -248,7 +250,7 @@ public class JingleSocks5Transport extends JingleTransport { count = inputStream.read(buffer); if (count == -1) { callback.onFileTransferAborted(); - Log.d(Config.LOGTAG, connection.getAccount().getJid().asBareJid() + ": file ended prematurely with " + remainingSize + " bytes remaining"); + Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": file ended prematurely with " + remainingSize + " bytes remaining"); return; } else { fileOutputStream.write(buffer, 0, count); @@ -262,7 +264,7 @@ public class JingleSocks5Transport extends JingleTransport { file.setSha1Sum(digest.digest()); callback.onFileTransmitted(file); } catch (Exception e) { - Log.d(Config.LOGTAG, connection.getAccount().getJid().asBareJid() + ": " + e.getMessage()); + Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": " + e.getMessage()); callback.onFileTransferAborted(); } finally { WakeLockHelper.release(wakeLock); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index 3696756cf..e98a7e49e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -6,6 +6,10 @@ import eu.siacs.conversations.xml.Namespace; public class Content extends Element { + + //refactor to getDescription and getTransport + //return either FileTransferDescription or GenericDescription or RtpDescription (all extend Description interface) + public enum Version { FT_3("urn:xmpp:jingle:apps:file-transfer:3"), FT_4("urn:xmpp:jingle:apps:file-transfer:4"), diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java index 53f0fd6cd..fe9a2ab56 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -7,109 +7,115 @@ import eu.siacs.conversations.xmpp.stanzas.IqPacket; import rocks.xmpp.addr.Jid; public class JinglePacket extends IqPacket { - Content content = null; - Reason reason = null; - Element checksum = null; - Element jingle = new Element("jingle"); - @Override - public Element addChild(Element child) { - if ("jingle".equals(child.getName())) { - Element contentElement = child.findChild("content"); - if (contentElement != null) { - this.content = new Content(); - this.content.setChildren(contentElement.getChildren()); - this.content.setAttributes(contentElement.getAttributes()); - } - Element reasonElement = child.findChild("reason"); - if (reasonElement != null) { - this.reason = new Reason(); - this.reason.setChildren(reasonElement.getChildren()); - this.reason.setAttributes(reasonElement.getAttributes()); - } - this.checksum = child.findChild("checksum"); - this.jingle.setAttributes(child.getAttributes()); - } - return child; - } - public JinglePacket setContent(Content content) { - this.content = content; - return this; - } + //get rid of that BS and set/get directly + Content content = null; + Reason reason = null; + Element checksum = null; + Element jingle = new Element("jingle"); - public Content getJingleContent() { - if (this.content == null) { - this.content = new Content(); - } - return this.content; - } + //get rid of what ever that is; maybe throw illegal state to ensure we are only calling setContent etc + @Override + public Element addChild(Element child) { + if ("jingle".equals(child.getName())) { + Element contentElement = child.findChild("content"); + if (contentElement != null) { + this.content = new Content(); + this.content.setChildren(contentElement.getChildren()); + this.content.setAttributes(contentElement.getAttributes()); + } + Element reasonElement = child.findChild("reason"); + if (reasonElement != null) { + this.reason = new Reason(); + this.reason.setChildren(reasonElement.getChildren()); + this.reason.setAttributes(reasonElement.getAttributes()); + } + this.checksum = child.findChild("checksum"); + this.jingle.setAttributes(child.getAttributes()); + } + return child; + } - public JinglePacket setReason(Reason reason) { - this.reason = reason; - return this; - } + public JinglePacket setContent(Content content) { //take content interface + this.content = content; + return this; + } - public Reason getReason() { - return this.reason; - } + public Content getJingleContent() { + if (this.content == null) { + this.content = new Content(); + } + return this.content; + } - public Element getChecksum() { - return this.checksum; - } + public Reason getReason() { + return this.reason; + } - private void build() { - this.children.clear(); - this.jingle.clearChildren(); - this.jingle.setAttribute("xmlns", "urn:xmpp:jingle:1"); - if (this.content != null) { - jingle.addChild(this.content); - } - if (this.reason != null) { - jingle.addChild(this.reason); - } - if (this.checksum != null) { - jingle.addChild(checksum); - } - this.children.add(jingle); - this.setAttribute("type", "set"); - } + public JinglePacket setReason(Reason reason) { + this.reason = reason; + return this; + } - public String getSessionId() { - return this.jingle.getAttribute("sid"); - } + public Element getChecksum() { + return this.checksum; + } - public void setSessionId(String sid) { - this.jingle.setAttribute("sid", sid); - } + //should be unnecessary if we set and get directly + private void build() { + this.children.clear(); + this.jingle.clearChildren(); + this.jingle.setAttribute("xmlns", "urn:xmpp:jingle:1"); + if (this.content != null) { + jingle.addChild(this.content); + } + if (this.reason != null) { + jingle.addChild(this.reason); + } + if (this.checksum != null) { + jingle.addChild(checksum); + } + this.children.add(jingle); + this.setAttribute("type", "set"); + } - @Override - public String toString() { - this.build(); - return super.toString(); - } + public String getSessionId() { + return this.jingle.getAttribute("sid"); + } - public void setAction(String action) { - this.jingle.setAttribute("action", action); - } + public void setSessionId(String sid) { + this.jingle.setAttribute("sid", sid); + } - public String getAction() { - return this.jingle.getAttribute("action"); - } + @Override + public String toString() { + this.build(); + return super.toString(); + } - public void setInitiator(final Jid initiator) { - this.jingle.setAttribute("initiator", initiator.toString()); - } + //use enum for action + public String getAction() { + return this.jingle.getAttribute("action"); + } - public boolean isAction(String action) { - return action.equalsIgnoreCase(this.getAction()); - } + public void setAction(String action) { + this.jingle.setAttribute("action", action); + } - public void addChecksum(byte[] sha1Sum, String namespace) { - this.checksum = new Element("checksum",namespace); - checksum.setAttribute("creator","initiator"); - checksum.setAttribute("name","a-file-offer"); - Element hash = checksum.addChild("file").addChild("hash","urn:xmpp:hashes:2"); - hash.setAttribute("algo","sha-1").setContent(Base64.encodeToString(sha1Sum,Base64.NO_WRAP)); - } + public void setInitiator(final Jid initiator) { + this.jingle.setAttribute("initiator", initiator.toString()); + } + + public boolean isAction(String action) { + return action.equalsIgnoreCase(this.getAction()); + } + + public void addChecksum(byte[] sha1Sum, String namespace) { + this.checksum = new Element("checksum", namespace); + checksum.setAttribute("creator", "initiator"); + checksum.setAttribute("name", "a-file-offer"); + Element hash = checksum.addChild("file").addChild("hash", "urn:xmpp:hashes:2"); + hash.setAttribute("algo", "sha-1").setContent(Base64.encodeToString(sha1Sum, Base64.NO_WRAP)); + } } From 34f42c73bc706cc6dbea71212c9f2614fe065d2c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 1 Apr 2020 13:25:52 +0200 Subject: [PATCH 017/182] cleaned JinglePacket and Content element --- .../services/XmppConnectionService.java | 12 +- .../eu/siacs/conversations/xml/Namespace.java | 1 + .../conversations/xmpp/XmppConnection.java | 21 +-- .../xmpp/jingle/JingleConnectionManager.java | 2 +- .../jingle/JingleFileTransferConnection.java | 105 ++++++------ .../xmpp/jingle/stanzas/Content.java | 104 ++++++++---- .../xmpp/jingle/stanzas/JinglePacket.java | 153 ++++++++---------- .../xmpp/jingle/stanzas/Reason.java | 13 +- 8 files changed, 218 insertions(+), 193 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index b16da4517..d68981bf6 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -221,15 +221,7 @@ public class XmppConnectionService extends Service { }; private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this); private List accounts; - private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager( - this); - private final OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() { - - @Override - public void onJinglePacketReceived(Account account, JinglePacket packet) { - mJingleConnectionManager.deliverPacket(account, packet); - } - }; + private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(this); private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(this); private AvatarService mAvatarService = new AvatarService(this); private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this); @@ -1327,7 +1319,7 @@ public class XmppConnectionService extends Service { connection.setOnStatusChangedListener(this.statusListener); connection.setOnPresencePacketReceivedListener(this.mPresenceParser); connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser); - connection.setOnJinglePacketReceivedListener(this.jingleListener); + connection.setOnJinglePacketReceivedListener(((a, jp) -> mJingleConnectionManager.deliverPacket(a, jp))); connection.setOnBindListener(this.mOnBindListener); connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService); diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 3f365642f..6d4447e38 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -33,6 +33,7 @@ public final class Namespace { public static final String PING = "urn:xmpp:ping"; public static final String PUSH = "urn:xmpp:push:0"; public static final String COMMANDS = "http://jabber.org/protocol/commands"; + public static final String JINGLE = "urn:xmpp:jingle:1"; public static final String JINGLE_ENCRYPTED_TRANSPORT = "urn:xmpp:jingle:jet:0"; public static final String JINGLE_ENCRYPTED_TRANSPORT_OMEMO = "urn:xmpp:jingle:jet-omemo:0"; public static final String MUC_USER = "http://jabber.org/protocol/muc#user"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index da0eb9656..97590e4d6 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -669,8 +669,8 @@ public class XmppConnection implements Runnable { } private @NonNull - Element processPacket(final Tag currentTag, final int packetType) throws XmlPullParserException, IOException { - Element element; + Element processPacket(final Tag currentTag, final int packetType) throws IOException { + final Element element; switch (packetType) { case PACKET_IQ: element = new IqPacket(); @@ -691,16 +691,7 @@ public class XmppConnection implements Runnable { } while (!nextTag.isEnd(element.getName())) { if (!nextTag.isNo()) { - final Element child = tagReader.readElement(nextTag); - final String type = currentTag.getAttribute("type"); - if (packetType == PACKET_IQ - && "jingle".equals(child.getName()) - && ("set".equalsIgnoreCase(type) || "get" - .equalsIgnoreCase(type))) { - element = new JinglePacket(); - element.setAttributes(currentTag.getAttributes()); - } - element.addChild(child); + element.addChild(tagReader.readElement(nextTag)); } nextTag = tagReader.readTag(); if (nextTag == null) { @@ -720,7 +711,11 @@ public class XmppConnection implements Runnable { if (Config.BACKGROUND_STANZA_LOGGING && mXmppConnectionService.checkListeners()) { Log.d(Config.LOGTAG, "[background stanza] " + element); } - return element; + if (element instanceof IqPacket && element.hasChild("jingle", Namespace.JINGLE)) { + return JinglePacket.upgrade((IqPacket) element); + } else { + return element; + } } private void processIq(final Tag currentTag) throws XmlPullParserException, IOException { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 8c2139bb0..8ba989270 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -33,7 +33,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { public void deliverPacket(final Account account, final JinglePacket packet) { final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, packet); - if (packet.isAction("session-initiate")) { //TODO check that id doesn't exist yet + if (packet.getAction() == JinglePacket.Action.SESSION_INITIATE) { //TODO check that id doesn't exist yet JingleFileTransferConnection connection = new JingleFileTransferConnection(this, id); connection.init(account, packet); connections.put(id, connection); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index 6586474da..65bc0d5ed 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -31,7 +31,6 @@ import eu.siacs.conversations.entities.TransferablePlaceholder; import eu.siacs.conversations.parser.IqParser; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.AbstractConnectionManager; -import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; @@ -70,7 +69,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private boolean proxyActivationFailed = false; private String contentName; - private String contentCreator; + private Content.Creator contentCreator; private Transport initialTransport; private boolean remoteSupportsOmemoJet; @@ -104,10 +103,15 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple @Override public void onFileTransmitted(DownloadableFile file) { if (responding()) { - if (expectedHash.length > 0 && !Arrays.equals(expectedHash, file.getSha1Sum())) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": hashes did not match"); + if (expectedHash.length > 0) { + if (Arrays.equals(expectedHash, file.getSha1Sum())) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received file matched the expected hash"); + } else { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": hashes did not match"); + } + } else { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": other party did not include file hash in file transfer"); } - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": file transmitted(). we are responding"); sendSuccess(); xmppConnectionService.getFileBackend().updateFileParams(message); xmppConnectionService.databaseBackend.createMessage(message); @@ -219,10 +223,10 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } final File parent = this.file.getParentFile(); if (parent != null && parent.mkdirs()) { - Log.d(Config.LOGTAG,"created parent directories for file "+file.getAbsolutePath()); + Log.d(Config.LOGTAG, "created parent directories for file " + file.getAbsolutePath()); } if (this.file.createNewFile()) { - Log.d(Config.LOGTAG,"created output file "+file.getAbsolutePath()); + Log.d(Config.LOGTAG, "created output file " + file.getAbsolutePath()); } this.mFileOutputStream = AbstractConnectionManager.createOutputStream(this.file, false, true); return this.mFileOutputStream; @@ -230,7 +234,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple @Override void deliverPacket(final JinglePacket packet) { - if (packet.isAction("session-terminate")) { + final JinglePacket.Action action = packet.getAction(); + //TODO switch case + if (action == JinglePacket.Action.SESSION_TERMINATE) { Reason reason = packet.getReason(); if (reason != null) { if (reason.hasChild("cancel")) { @@ -249,10 +255,10 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } else { this.fail(); } - } else if (packet.isAction("session-accept")) { + } else if (action == JinglePacket.Action.SESSION_ACCEPT) { receiveAccept(packet); - } else if (packet.isAction("session-info")) { - final Element checksum = packet.getChecksum(); + } else if (action == JinglePacket.Action.SESSION_INFO) { + final Element checksum = packet.getJingleChild("checksum"); final Element file = checksum == null ? null : checksum.findChild("file"); final Element hash = file == null ? null : file.findChild("hash", "urn:xmpp:hashes:2"); if (hash != null && "sha-1".equalsIgnoreCase(hash.getAttribute("algo"))) { @@ -263,16 +269,16 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } } respondToIq(packet, true); - } else if (packet.isAction("transport-info")) { + } else if (action == JinglePacket.Action.TRANSPORT_INFO) { receiveTransportInfo(packet); - } else if (packet.isAction("transport-replace")) { + } else if (action == JinglePacket.Action.TRANSPORT_REPLACE) { if (packet.getJingleContent().hasIbbTransport()) { receiveFallbackToIbb(packet); } else { Log.d(Config.LOGTAG, "trying to fallback to something unknown" + packet.toString()); respondToIq(packet, false); } - } else if (packet.isAction("transport-accept")) { + } else if (action == JinglePacket.Action.TRANSPORT_ACCEPT) { receiveTransportAccept(packet); } else { Log.d(Config.LOGTAG, "packet arrived in connection. action was " + packet.getAction()); @@ -317,7 +323,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void init(Message message, XmppAxolotlMessage xmppAxolotlMessage) { this.mXmppAxolotlMessage = xmppAxolotlMessage; - this.contentCreator = "initiator"; + this.contentCreator = Content.Creator.INITIATOR; this.contentName = JingleConnectionManager.nextRandomId(); this.message = message; final List remoteFeatures = getRemoteFeatures(); @@ -413,7 +419,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple this.initiator = this.id.counterPart; this.responder = this.id.account.getJid(); final Content content = packet.getJingleContent(); - this.contentCreator = content.getAttribute("creator"); + this.contentCreator = content.getCreator(); this.initialTransport = content.hasSocks5Transport() ? Transport.SOCKS : Transport.IBB; this.contentName = content.getAttribute("name"); this.transportId = content.getTransportId(); @@ -531,8 +537,8 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } private void sendInitRequest() { - JinglePacket packet = this.bootstrapPacket("session-initiate"); - Content content = new Content(this.contentCreator, this.contentName); + final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INITIATE); + final Content content = new Content(this.contentCreator, this.contentName); if (message.isFileOrImage()) { content.setTransportId(this.transportId); this.file = this.xmppConnectionService.getFileBackend().getFile(message, false); @@ -574,7 +580,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple Log.d(Config.LOGTAG, String.format("%s: sending S5B offer with %d candidates", id.account.getJid().asBareJid(), candidates.size())); content.socks5transport().setChildren(candidates); } - packet.setContent(content); + packet.setJingleContent(content); this.sendJinglePacket(packet, (account, response) -> { if (response.getType() == IqPacket.TYPE.RESULT) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": other party received offer"); @@ -593,8 +599,15 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } private void sendHash() { - JinglePacket packet = this.bootstrapPacket("session-info"); - packet.addChecksum(file.getSha1Sum(), ftVersion.getNamespace()); + + final Element checksum = new Element("checksum", ftVersion.getNamespace()); + checksum.setAttribute("creator", "initiator"); + checksum.setAttribute("name", "a-file-offer"); + Element hash = checksum.addChild("file").addChild("hash", "urn:xmpp:hashes:2"); + hash.setAttribute("algo", "sha-1").setContent(Base64.encodeToString(file.getSha1Sum(), Base64.NO_WRAP)); + + final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INFO); + packet.setJingleChild(checksum); this.sendJinglePacket(packet); } @@ -622,7 +635,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendAcceptSocks() { gatherAndConnectDirectCandidates(); this.jingleConnectionManager.getPrimaryCandidate(this.id.account, initiating(), (success, candidate) -> { - final JinglePacket packet = bootstrapPacket("session-accept"); + final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT); final Content content = new Content(contentCreator, contentName); content.setFileOffer(fileOffer, ftVersion); content.setTransportId(transportId); @@ -635,7 +648,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple public void failed() { Log.d(Config.LOGTAG, "connection to our own proxy65 candidate failed"); content.socks5transport().setChildren(getCandidatesAsElements()); - packet.setContent(content); + packet.setJingleContent(content); sendJinglePacket(packet); connectNextCandidate(); } @@ -645,7 +658,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple Log.d(Config.LOGTAG, "connected to proxy65 candidate"); mergeCandidate(candidate); content.socks5transport().setChildren(getCandidatesAsElements()); - packet.setContent(content); + packet.setJingleContent(content); sendJinglePacket(packet); connectNextCandidate(); } @@ -653,7 +666,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } else { Log.d(Config.LOGTAG, "did not find a proxy65 candidate for ourselves"); content.socks5transport().setChildren(getCandidatesAsElements()); - packet.setContent(content); + packet.setJingleContent(content); sendJinglePacket(packet); connectNextCandidate(); } @@ -662,23 +675,19 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendAcceptIbb() { this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize); - final JinglePacket packet = bootstrapPacket("session-accept"); + final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT); final Content content = new Content(contentCreator, contentName); content.setFileOffer(fileOffer, ftVersion); content.setTransportId(transportId); content.ibbTransport().setAttribute("block-size", this.ibbBlockSize); - packet.setContent(content); + packet.setJingleContent(content); this.transport.receive(file, onFileTransmissionStatusChanged); this.sendJinglePacket(packet); } - private JinglePacket bootstrapPacket(String action) { - JinglePacket packet = new JinglePacket(); - packet.setAction(action); - packet.setFrom(id.account.getJid()); + private JinglePacket bootstrapPacket(JinglePacket.Action action) { + final JinglePacket packet = new JinglePacket(action, this.id.sessionId); packet.setTo(id.counterPart); - packet.setSessionId(this.id.sessionId); - packet.setInitiator(this.initiator); return packet; } @@ -893,13 +902,13 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendFallbackToIbb() { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending fallback to ibb"); - JinglePacket packet = this.bootstrapPacket("transport-replace"); + JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.TRANSPORT_REPLACE); Content content = new Content(this.contentCreator, this.contentName); - this.transportId = this.jingleConnectionManager.nextRandomId(); + this.transportId = JingleConnectionManager.nextRandomId(); content.setTransportId(this.transportId); content.ibbTransport().setAttribute("block-size", Integer.toString(this.ibbBlockSize)); - packet.setContent(content); + packet.setJingleContent(content); this.sendJinglePacket(packet); } @@ -932,12 +941,12 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple this.transportId = packet.getJingleContent().getTransportId(); this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize); - final JinglePacket answer = bootstrapPacket("transport-accept"); + final JinglePacket answer = bootstrapPacket(JinglePacket.Action.TRANSPORT_ACCEPT); final Content content = new Content(contentCreator, contentName); content.ibbTransport().setAttribute("block-size", this.ibbBlockSize); content.ibbTransport().setAttribute("sid", this.transportId); - answer.setContent(content); + answer.setJingleContent(content); respondToIq(packet, true); @@ -1067,7 +1076,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } private void sendSessionTerminate(String reason) { - final JinglePacket packet = bootstrapPacket("session-terminate"); + final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_TERMINATE); final Reason r = new Reason(); r.addChild(reason); packet.setReason(r); @@ -1120,29 +1129,29 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } private void sendProxyActivated(String cid) { - final JinglePacket packet = bootstrapPacket("transport-info"); + final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); final Content content = new Content(this.contentCreator, this.contentName); content.setTransportId(this.transportId); content.socks5transport().addChild("activated").setAttribute("cid", cid); - packet.setContent(content); + packet.setJingleContent(content); this.sendJinglePacket(packet); } private void sendProxyError() { - final JinglePacket packet = bootstrapPacket("transport-info"); + final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); final Content content = new Content(this.contentCreator, this.contentName); content.setTransportId(this.transportId); content.socks5transport().addChild("proxy-error"); - packet.setContent(content); + packet.setJingleContent(content); this.sendJinglePacket(packet); } private void sendCandidateUsed(final String cid) { - JinglePacket packet = bootstrapPacket("transport-info"); + JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); Content content = new Content(this.contentCreator, this.contentName); content.setTransportId(this.transportId); content.socks5transport().addChild("candidate-used").setAttribute("cid", cid); - packet.setContent(content); + packet.setJingleContent(content); this.sentCandidate = true; if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) { connect(); @@ -1152,11 +1161,11 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendCandidateError() { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending candidate error"); - JinglePacket packet = bootstrapPacket("transport-info"); + JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); Content content = new Content(this.contentCreator, this.contentName); content.setTransportId(this.transportId); content.socks5transport().addChild("candidate-error"); - packet.setContent(content); + packet.setJingleContent(content); this.sentCandidate = true; this.sendJinglePacket(packet); if (receivedCandidate && mJingleStatus == JINGLE_STATUS_ACCEPTED) { @@ -1253,7 +1262,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple return this.mProgress; } - public AbstractConnectionManager getConnectionManager() { + AbstractConnectionManager getConnectionManager() { return this.jingleConnectionManager; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index e98a7e49e..5dbeef03d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -1,44 +1,44 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; +import android.support.annotation.NonNull; + +import com.google.common.base.Preconditions; + +import java.util.Locale; + import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; public class Content extends Element { + private String transportId; //refactor to getDescription and getTransport //return either FileTransferDescription or GenericDescription or RtpDescription (all extend Description interface) - public enum Version { - FT_3("urn:xmpp:jingle:apps:file-transfer:3"), - FT_4("urn:xmpp:jingle:apps:file-transfer:4"), - FT_5("urn:xmpp:jingle:apps:file-transfer:5"); - - private final String namespace; - - Version(String namespace) { - this.namespace = namespace; - } - - public String getNamespace() { - return namespace; - } - } - - private String transportId; - - public Content() { - super("content"); - } - - public Content(String creator, String name) { - super("content"); - this.setAttribute("creator", creator); - this.setAttribute("senders", creator); + public Content(final Creator creator, final String name) { + super("content", Namespace.JINGLE); + this.setAttribute("creator", creator.toString()); this.setAttribute("name", name); } + private Content() { + super("content", Namespace.JINGLE); + } + + public static Content upgrade(final Element element) { + Preconditions.checkArgument("content".equals(element.getName())); + final Content content = new Content(); + content.setAttributes(element.getAttributes()); + content.setChildren(element.getChildren()); + return content; + } + + public Creator getCreator() { + return Creator.of(getAttribute("creator")); + } + public Version getVersion() { if (hasChild("description", Version.FT_3.namespace)) { return Version.FT_3; @@ -50,10 +50,6 @@ public class Content extends Element { return null; } - public void setTransportId(String sid) { - this.transportId = sid; - } - public Element setFileOffer(DownloadableFile actualFile, boolean otr, Version version) { Element description = this.addChild("description", version.namespace); Element file; @@ -106,6 +102,10 @@ public class Content extends Element { return this.transportId; } + public void setTransportId(String sid) { + this.transportId = sid; + } + public Element socks5transport() { Element transport = this.findChild("transport", Namespace.JINGLE_TRANSPORTS_S5B); if (transport == null) { @@ -131,4 +131,48 @@ public class Content extends Element { public boolean hasIbbTransport() { return this.hasChild("transport", Namespace.JINGLE_TRANSPORTS_IBB); } + + public enum Version { + FT_3("urn:xmpp:jingle:apps:file-transfer:3"), + FT_4("urn:xmpp:jingle:apps:file-transfer:4"), + FT_5("urn:xmpp:jingle:apps:file-transfer:5"); + + private final String namespace; + + Version(String namespace) { + this.namespace = namespace; + } + + public String getNamespace() { + return namespace; + } + } + + public enum Creator { + INITIATOR, RESPONDER; + + public static Creator of(final String value) { + return Creator.valueOf(value.toUpperCase(Locale.ROOT)); + } + + @Override + @NonNull + public String toString() { + return super.toString().toLowerCase(Locale.ROOT); + } + } + + public enum Senders { + BOTH, INITIATOR, NONE, RESPONDER; + + public static Senders of(final String value) { + return Senders.valueOf(value.toUpperCase(Locale.ROOT)); + } + + @Override + @NonNull + public String toString() { + return super.toString().toLowerCase(Locale.ROOT); + } + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java index fe9a2ab56..b59ece9c2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -1,121 +1,98 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; -import android.util.Base64; +import android.support.annotation.NonNull; + +import com.google.common.base.CaseFormat; +import com.google.common.base.Preconditions; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import rocks.xmpp.addr.Jid; public class JinglePacket extends IqPacket { - - //get rid of that BS and set/get directly - Content content = null; - Reason reason = null; - Element checksum = null; - Element jingle = new Element("jingle"); - - //get rid of what ever that is; maybe throw illegal state to ensure we are only calling setContent etc - @Override - public Element addChild(Element child) { - if ("jingle".equals(child.getName())) { - Element contentElement = child.findChild("content"); - if (contentElement != null) { - this.content = new Content(); - this.content.setChildren(contentElement.getChildren()); - this.content.setAttributes(contentElement.getAttributes()); - } - Element reasonElement = child.findChild("reason"); - if (reasonElement != null) { - this.reason = new Reason(); - this.reason.setChildren(reasonElement.getChildren()); - this.reason.setAttributes(reasonElement.getAttributes()); - } - this.checksum = child.findChild("checksum"); - this.jingle.setAttributes(child.getAttributes()); - } - return child; + private JinglePacket() { + super(); } - public JinglePacket setContent(Content content) { //take content interface - this.content = content; - return this; + public JinglePacket(final Action action, final String sessionId) { + super(TYPE.SET); + final Element jingle = addChild("jingle", Namespace.JINGLE); + jingle.setAttribute("sid", sessionId); + jingle.setAttribute("action", action.toString()); + } + + public static JinglePacket upgrade(final IqPacket iqPacket) { + Preconditions.checkArgument(iqPacket.hasChild("jingle", Namespace.JINGLE)); + final JinglePacket jinglePacket = new JinglePacket(); + jinglePacket.setAttributes(iqPacket.getAttributes()); + jinglePacket.setChildren(iqPacket.getChildren()); + return jinglePacket; } public Content getJingleContent() { - if (this.content == null) { - this.content = new Content(); - } - return this.content; + final Element content = getJingleChild("content"); + return content == null ? null : Content.upgrade(content); + } + + public void setJingleContent(final Content content) { //take content interface + setJingleChild(content); } public Reason getReason() { - return this.reason; + final Element reason = getJingleChild("reason"); + return reason == null ? null : Reason.upgrade(reason); } - public JinglePacket setReason(Reason reason) { - this.reason = reason; - return this; + public void setReason(final Reason reason) { + final Element jingle = findChild("jingle", Namespace.JINGLE); + jingle.addChild(reason); } - public Element getChecksum() { - return this.checksum; + public Element getJingleChild(final String name) { + final Element jingle = findChild("jingle", Namespace.JINGLE); + return jingle == null ? null : jingle.findChild(name); } - //should be unnecessary if we set and get directly - private void build() { - this.children.clear(); - this.jingle.clearChildren(); - this.jingle.setAttribute("xmlns", "urn:xmpp:jingle:1"); - if (this.content != null) { - jingle.addChild(this.content); - } - if (this.reason != null) { - jingle.addChild(this.reason); - } - if (this.checksum != null) { - jingle.addChild(checksum); - } - this.children.add(jingle); - this.setAttribute("type", "set"); + public void setJingleChild(final Element child) { + final Element jingle = findChild("jingle", Namespace.JINGLE); + jingle.addChild(child); } public String getSessionId() { - return this.jingle.getAttribute("sid"); + return findChild("jingle", Namespace.JINGLE).getAttribute("sid"); } - public void setSessionId(String sid) { - this.jingle.setAttribute("sid", sid); + public Action getAction() { + return Action.of(findChild("jingle", Namespace.JINGLE).getAttribute("action")); } - @Override - public String toString() { - this.build(); - return super.toString(); - } + public enum Action { + CONTENT_ACCEPT, + CONTENT_ADD, + CONTENT_MODIFY, + CONTENT_REJECT, + CONTENT_REMOVE, + DESCRIPTION_INFO, + SECURITY_INFO, + SESSION_ACCEPT, + SESSION_INFO, + SESSION_INITIATE, + SESSION_TERMINATE, + TRANSPORT_ACCEPT, + TRANSPORT_INFO, + TRANSPORT_REJECT, + TRANSPORT_REPLACE; - //use enum for action - public String getAction() { - return this.jingle.getAttribute("action"); - } + public static Action of(final String value) { + //TODO handle invalid + return Action.valueOf(CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value)); + } - public void setAction(String action) { - this.jingle.setAttribute("action", action); - } - - public void setInitiator(final Jid initiator) { - this.jingle.setAttribute("initiator", initiator.toString()); - } - - public boolean isAction(String action) { - return action.equalsIgnoreCase(this.getAction()); - } - - public void addChecksum(byte[] sha1Sum, String namespace) { - this.checksum = new Element("checksum", namespace); - checksum.setAttribute("creator", "initiator"); - checksum.setAttribute("name", "a-file-offer"); - Element hash = checksum.addChild("file").addChild("hash", "urn:xmpp:hashes:2"); - hash.setAttribute("algo", "sha-1").setContent(Base64.encodeToString(sha1Sum, Base64.NO_WRAP)); + @Override + @NonNull + public String toString() { + return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, super.toString()); + } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java index 610d5e760..1eae65c5b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java @@ -1,13 +1,20 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; +import com.google.common.base.Preconditions; + import eu.siacs.conversations.xml.Element; public class Reason extends Element { - private Reason(String name) { - super(name); - } public Reason() { super("reason"); } + + public static Reason upgrade(final Element element) { + Preconditions.checkArgument("reason".equals(element.getName())); + final Reason reason = new Reason(); + reason.setAttributes(element.getAttributes()); + reason.setChildren(element.getChildren()); + return reason; + } } From 7538e387ec138cb60c2a9cab83cb992cc56c64b2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 1 Apr 2020 15:17:38 +0200 Subject: [PATCH 018/182] fixed bug in ibb delivery introduced in earlier refactoring --- .../xmpp/jingle/JingleConnectionManager.java | 12 +++++++++--- .../xmpp/jingle/JingleInBandTransport.java | 12 +++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 8ba989270..38c98082d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -122,8 +122,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { } public void deliverIbbPacket(Account account, IqPacket packet) { - String sid = null; - Element payload = null; + final String sid; + final Element payload; if (packet.hasChild("open", Namespace.IBB)) { payload = packet.findChild("open", Namespace.IBB); sid = payload.getAttribute("sid"); @@ -133,6 +133,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { } else if (packet.hasChild("close", Namespace.IBB)) { payload = packet.findChild("close", Namespace.IBB); sid = payload.getAttribute("sid"); + } else { + payload = null; + sid = null; } if (sid != null) { for (final AbstractJingleConnection connection : this.connections.values()) { @@ -140,7 +143,10 @@ public class JingleConnectionManager extends AbstractConnectionManager { final JingleFileTransferConnection fileTransfer = (JingleFileTransferConnection) connection; final JingleTransport transport = fileTransfer.getTransport(); if (transport instanceof JingleInBandTransport) { - ((JingleInBandTransport) transport).deliverPayload(packet, payload); + final JingleInBandTransport inBandTransport = (JingleInBandTransport) transport; + if (inBandTransport.matches(account, sid)) { + inBandTransport.deliverPayload(packet, payload); + } return; } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInBandTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInBandTransport.java index c5eaca072..fbc1ad95f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInBandTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInBandTransport.java @@ -3,6 +3,8 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Base64; import android.util.Log; +import com.google.common.base.Preconditions; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -77,6 +79,10 @@ public class JingleInBandTransport extends JingleTransport { this.account.getXmppConnection().sendIqPacket(iq, null); } + public boolean matches(final Account account, final String sessionId) { + return this.account == account && this.sessionId.equals(sessionId); + } + public void connect(final OnTransportConnected callback) { IqPacket iq = new IqPacket(IqPacket.TYPE.SET); iq.setTo(this.counterpart); @@ -96,7 +102,7 @@ public class JingleInBandTransport extends JingleTransport { @Override public void receive(DownloadableFile file, OnFileTransmissionStatusChanged callback) { - this.onFileTransmissionStatusChanged = callback; + this.onFileTransmissionStatusChanged = Preconditions.checkNotNull(callback); this.file = file; try { this.digest = MessageDigest.getInstance("SHA-1"); @@ -116,7 +122,7 @@ public class JingleInBandTransport extends JingleTransport { @Override public void send(DownloadableFile file, OnFileTransmissionStatusChanged callback) { - this.onFileTransmissionStatusChanged = callback; + this.onFileTransmissionStatusChanged = Preconditions.checkNotNull(callback); this.file = file; try { this.remainingSize = this.file.getExpectedSize(); @@ -205,7 +211,7 @@ public class JingleInBandTransport extends JingleTransport { connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100)); } } catch (Exception e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage()); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage(), e); FileBackend.close(fileOutputStream); this.onFileTransmissionStatusChanged.onFileTransferAborted(); } From eb22bd0499b5b00eeefe14a612788abc15575ae0 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 1 Apr 2020 18:35:36 +0200 Subject: [PATCH 019/182] create 'Description' object --- .../generator/AbstractGenerator.java | 10 +- .../xmpp/jingle/JingleConnectionManager.java | 14 +- .../jingle/JingleFileTransferConnection.java | 181 +++++++++--------- .../xmpp/jingle/JingleSocks5Transport.java | 9 +- .../xmpp/jingle/stanzas/Content.java | 75 ++------ .../stanzas/FileTransferDescription.java | 89 +++++++++ .../jingle/stanzas/GenericDescription.java | 20 ++ 7 files changed, 241 insertions(+), 157 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericDescription.java diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index a24a4ba0d..5d6c15aa9 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -20,14 +20,14 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.XmppConnection; -import eu.siacs.conversations.xmpp.jingle.stanzas.Content; +import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; public abstract class AbstractGenerator { private final String[] FEATURES = { - "urn:xmpp:jingle:1", - Content.Version.FT_3.getNamespace(), - Content.Version.FT_4.getNamespace(), - Content.Version.FT_5.getNamespace(), + Namespace.JINGLE, + FileTransferDescription.Version.FT_3.getNamespace(), + FileTransferDescription.Version.FT_4.getNamespace(), + FileTransferDescription.Version.FT_5.getNamespace(), Namespace.JINGLE_TRANSPORTS_S5B, Namespace.JINGLE_TRANSPORTS_IBB, Namespace.JINGLE_ENCRYPTED_TRANSPORT, diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 38c98082d..f63f31253 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -18,6 +18,8 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.Content; +import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import rocks.xmpp.addr.Jid; @@ -34,9 +36,17 @@ public class JingleConnectionManager extends AbstractConnectionManager { public void deliverPacket(final Account account, final JinglePacket packet) { final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, packet); if (packet.getAction() == JinglePacket.Action.SESSION_INITIATE) { //TODO check that id doesn't exist yet - JingleFileTransferConnection connection = new JingleFileTransferConnection(this, id); - connection.init(account, packet); + final Content content = packet.getJingleContent(); + final String descriptionNamespace = content == null ? null : content.getDescriptionNamespace(); + final AbstractJingleConnection connection; + if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) { + connection = new JingleFileTransferConnection(this, id); + } else { + //TODO return feature-not-implemented + return; + } connections.put(id, connection); + connection.deliverPacket(packet); } else { final AbstractJingleConnection abstractJingleConnection = connections.get(id); if (abstractJingleConnection != null) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index 65bc0d5ed..997f84b89 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -3,6 +3,8 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Base64; import android.util.Log; +import com.google.common.base.Preconditions; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -36,6 +38,7 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; +import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; import eu.siacs.conversations.xmpp.stanzas.IqPacket; @@ -50,7 +53,6 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private static final int JINGLE_STATUS_FINISHED = 4; private static final int JINGLE_STATUS_FAILED = 99; private static final int JINGLE_STATUS_OFFERED = -1; - private Content.Version ftVersion = Content.Version.FT_3; private int ibbBlockSize = 8192; @@ -63,7 +65,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private ConcurrentHashMap connections = new ConcurrentHashMap<>(); private String transportId; - private Element fileOffer; + private FileTransferDescription description; private DownloadableFile file = null; private boolean proxyActivationFailed = false; @@ -130,7 +132,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple id.account.getPgpDecryptionService().decrypt(message, true); } } else { - if (ftVersion == Content.Version.FT_5) { //older Conversations will break when receiving a session-info + if (description.getVersion() == FileTransferDescription.Version.FT_5) { //older Conversations will break when receiving a session-info sendHash(); } if (message.getEncryption() == Message.ENCRYPTION_PGP) { @@ -236,7 +238,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple void deliverPacket(final JinglePacket packet) { final JinglePacket.Action action = packet.getAction(); //TODO switch case - if (action == JinglePacket.Action.SESSION_TERMINATE) { + if (action == JinglePacket.Action.SESSION_INITIATE) { + init(packet); + } else if (action == JinglePacket.Action.SESSION_TERMINATE) { Reason reason = packet.getReason(); if (reason != null) { if (reason.hasChild("cancel")) { @@ -307,6 +311,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } public void init(final Message message) { + Preconditions.checkArgument(message.isFileOrImage()); if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { Conversation conversation = (Conversation) message.getConversation(); conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation, xmppAxolotlMessage -> { @@ -321,13 +326,13 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } } - private void init(Message message, XmppAxolotlMessage xmppAxolotlMessage) { + private void init(final Message message, final XmppAxolotlMessage xmppAxolotlMessage) { this.mXmppAxolotlMessage = xmppAxolotlMessage; this.contentCreator = Content.Creator.INITIATOR; this.contentName = JingleConnectionManager.nextRandomId(); this.message = message; final List remoteFeatures = getRemoteFeatures(); - upgradeNamespace(remoteFeatures); + final FileTransferDescription.Version remoteVersion = getAvailableFileTransferVersion(remoteFeatures); this.initialTransport = remoteFeatures.contains(Namespace.JINGLE_TRANSPORTS_S5B) ? Transport.SOCKS : Transport.IBB; this.remoteSupportsOmemoJet = remoteFeatures.contains(Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO); this.message.setTransferable(this); @@ -335,6 +340,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple this.initiator = this.id.account.getJid(); this.responder = this.id.counterPart; this.transportId = JingleConnectionManager.nextRandomId(); + this.setupDescription(remoteVersion); if (this.initialTransport == Transport.IBB) { this.sendInitRequest(); } else { @@ -386,11 +392,13 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } } - private void upgradeNamespace(List remoteFeatures) { - if (remoteFeatures.contains(Content.Version.FT_5.getNamespace())) { - this.ftVersion = Content.Version.FT_5; - } else if (remoteFeatures.contains(Content.Version.FT_4.getNamespace())) { - this.ftVersion = Content.Version.FT_4; + private FileTransferDescription.Version getAvailableFileTransferVersion(List remoteFeatures) { + if (remoteFeatures.contains(FileTransferDescription.Version.FT_5.getNamespace())) { + return FileTransferDescription.Version.FT_5; + } else if (remoteFeatures.contains(FileTransferDescription.Version.FT_4.getNamespace())) { + return FileTransferDescription.Version.FT_4; + } else { + return FileTransferDescription.Version.FT_3; } } @@ -406,11 +414,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } } - public void init(Account account, JinglePacket packet) { //should move to deliverPacket + private void init(JinglePacket packet) { //should move to deliverPacket this.mJingleStatus = JINGLE_STATUS_INITIATED; - Conversation conversation = this.xmppConnectionService - .findOrCreateConversation(account, - packet.getFrom().asBareJid(), false, false); + final Conversation conversation = this.xmppConnectionService.findOrCreateConversation(id.account, id.counterPart.asBareJid(), false, false); this.message = new Message(conversation, "", Message.ENCRYPTION_NONE); this.message.setStatus(Message.STATUS_RECEIVED); this.mStatus = Transferable.STATUS_OFFER; @@ -445,14 +451,10 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple return; } } - this.ftVersion = content.getVersion(); - if (ftVersion == null) { - respondToIq(packet, false); - this.fail(); - return; - } - this.fileOffer = content.getFileOffer(this.ftVersion); + this.description = (FileTransferDescription) content.getDescription(); + + final Element fileOffer = this.description.getFileOffer(); if (fileOffer != null) { boolean remoteIsUsingJet = false; @@ -536,71 +538,76 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } } + private void setupDescription(final FileTransferDescription.Version version) { + this.file = this.xmppConnectionService.getFileBackend().getFile(message, false); + final FileTransferDescription description; + if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { + this.file.setKey(mXmppAxolotlMessage.getInnerKey()); + this.file.setIv(mXmppAxolotlMessage.getIV()); + //legacy OMEMO encrypted file transfer reported file size of the encrypted file + //JET uses the file size of the plain text file. The difference is only 16 bytes (auth tag) + this.file.setExpectedSize(file.getSize() + (this.remoteSupportsOmemoJet ? 0 : 16)); + if (remoteSupportsOmemoJet) { + description = FileTransferDescription.of(this.file, version, null); + } else { + description = FileTransferDescription.of(this.file, version, this.mXmppAxolotlMessage); + } + } else { + this.file.setExpectedSize(file.getSize()); + description = FileTransferDescription.of(this.file, version, null); + } + this.description = description; + } + private void sendInitRequest() { final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INITIATE); final Content content = new Content(this.contentCreator, this.contentName); - if (message.isFileOrImage()) { - content.setTransportId(this.transportId); - this.file = this.xmppConnectionService.getFileBackend().getFile(message, false); - if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { - this.file.setKey(mXmppAxolotlMessage.getInnerKey()); - this.file.setIv(mXmppAxolotlMessage.getIV()); - //legacy OMEMO encrypted file transfer reported file size of the encrypted file - //JET uses the file size of the plain text file. The difference is only 16 bytes (auth tag) - this.file.setExpectedSize(file.getSize() + (this.remoteSupportsOmemoJet ? 0 : 16)); - final Element file = content.setFileOffer(this.file, false, this.ftVersion); - if (remoteSupportsOmemoJet) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote announced support for JET"); - final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT); - security.setAttribute("name", this.contentName); - security.setAttribute("cipher", JET_OMEMO_CIPHER); - security.setAttribute("type", AxolotlService.PEP_PREFIX); - security.addChild(mXmppAxolotlMessage.toElement()); - content.addChild(security); - } else { - file.addChild(mXmppAxolotlMessage.toElement()); - } - } else { - this.file.setExpectedSize(file.getSize()); - content.setFileOffer(this.file, false, this.ftVersion); - } - message.resetFileParams(); - try { - this.mFileInputStream = new FileInputStream(file); - } catch (FileNotFoundException e) { - fail(e.getMessage()); - return; - } - content.setTransportId(this.transportId); - if (this.initialTransport == Transport.IBB) { - content.ibbTransport().setAttribute("block-size", Integer.toString(this.ibbBlockSize)); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending IBB offer"); - } else { - final List candidates = getCandidatesAsElements(); - Log.d(Config.LOGTAG, String.format("%s: sending S5B offer with %d candidates", id.account.getJid().asBareJid(), candidates.size())); - content.socks5transport().setChildren(candidates); - } - packet.setJingleContent(content); - this.sendJinglePacket(packet, (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": other party received offer"); - if (mJingleStatus == JINGLE_STATUS_OFFERED) { - mJingleStatus = JINGLE_STATUS_INITIATED; - xmppConnectionService.markMessage(message, Message.STATUS_OFFERED); - } else { - Log.d(Config.LOGTAG, "received ack for offer when status was " + mJingleStatus); - } - } else { - fail(IqParser.extractErrorMessage(response)); - } - }); - + content.setTransportId(this.transportId); + if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL && remoteSupportsOmemoJet) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote announced support for JET"); + final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT); + security.setAttribute("name", this.contentName); + security.setAttribute("cipher", JET_OMEMO_CIPHER); + security.setAttribute("type", AxolotlService.PEP_PREFIX); + security.addChild(mXmppAxolotlMessage.toElement()); + content.addChild(security); } + content.setDescription(this.description); + message.resetFileParams(); + try { + this.mFileInputStream = new FileInputStream(file); + } catch (FileNotFoundException e) { + fail(e.getMessage()); + return; + } + content.setTransportId(this.transportId); + if (this.initialTransport == Transport.IBB) { + content.ibbTransport().setAttribute("block-size", Integer.toString(this.ibbBlockSize)); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending IBB offer"); + } else { + final List candidates = getCandidatesAsElements(); + Log.d(Config.LOGTAG, String.format("%s: sending S5B offer with %d candidates", id.account.getJid().asBareJid(), candidates.size())); + content.socks5transport().setChildren(candidates); + } + packet.setJingleContent(content); + this.sendJinglePacket(packet, (account, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": other party received offer"); + if (mJingleStatus == JINGLE_STATUS_OFFERED) { + mJingleStatus = JINGLE_STATUS_INITIATED; + xmppConnectionService.markMessage(message, Message.STATUS_OFFERED); + } else { + Log.d(Config.LOGTAG, "received ack for offer when status was " + mJingleStatus); + } + } else { + fail(IqParser.extractErrorMessage(response)); + } + }); + } private void sendHash() { - - final Element checksum = new Element("checksum", ftVersion.getNamespace()); + final Element checksum = new Element("checksum", description.getVersion().getNamespace()); checksum.setAttribute("creator", "initiator"); checksum.setAttribute("name", "a-file-offer"); Element hash = checksum.addChild("file").addChild("hash", "urn:xmpp:hashes:2"); @@ -637,7 +644,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple this.jingleConnectionManager.getPrimaryCandidate(this.id.account, initiating(), (success, candidate) -> { final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT); final Content content = new Content(contentCreator, contentName); - content.setFileOffer(fileOffer, ftVersion); + content.setDescription(this.description); content.setTransportId(transportId); if (success && candidate != null && !equalCandidateExists(candidate)) { final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate); @@ -677,7 +684,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize); final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT); final Content content = new Content(contentCreator, contentName); - content.setFileOffer(fileOffer, ftVersion); + content.setDescription(this.description); content.setTransportId(transportId); content.ibbTransport().setAttribute("block-size", this.ibbBlockSize); packet.setJingleContent(content); @@ -812,7 +819,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple if (connection.needsActivation()) { if (connection.getCandidate().isOurs()) { final String sid; - if (ftVersion == Content.Version.FT_3) { + if (description.getVersion() == FileTransferDescription.Version.FT_3) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": use session ID instead of transport ID to activate proxy"); sid = id.sessionId; } else { @@ -1220,12 +1227,8 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple return this.transportId; } - public Content.Version getFtVersion() { - return this.ftVersion; - } - - public boolean hasTransportId(String sid) { - return sid.equals(this.transportId); + public FileTransferDescription.Version getFtVersion() { + return this.description.getVersion(); } public JingleTransport getTransport() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java index 2c9f11a56..31c40f531 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -24,6 +24,7 @@ import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.SocksSocketFactory; import eu.siacs.conversations.utils.WakeLockHelper; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; +import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; public class JingleSocks5Transport extends JingleTransport { @@ -52,7 +53,7 @@ public class JingleSocks5Transport extends JingleTransport { this.connection = jingleConnection; this.account = jingleConnection.getId().account; final StringBuilder destBuilder = new StringBuilder(); - if (this.connection.getFtVersion() == Content.Version.FT_3) { + if (this.connection.getFtVersion() == FileTransferDescription.Version.FT_3) { Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": using session Id instead of transport Id for proxy destination"); destBuilder.append(this.connection.getId().sessionId); } else { @@ -132,7 +133,7 @@ public class JingleSocks5Transport extends JingleTransport { responseHeader = new byte[]{0x05, 0x00, 0x00, 0x03}; success = true; } else { - Log.d(Config.LOGTAG,this.account.getJid().asBareJid()+": destination mismatch. received "+receivedDestination+" (expected "+this.destination+")"); + Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": destination mismatch. received " + receivedDestination + " (expected " + this.destination + ")"); responseHeader = new byte[]{0x05, 0x04, 0x00, 0x03}; success = false; } @@ -143,7 +144,7 @@ public class JingleSocks5Transport extends JingleTransport { outputStream.write(response.array()); outputStream.flush(); if (success) { - Log.d(Config.LOGTAG,this.account.getJid().asBareJid()+": successfully processed connection to candidate "+candidate.getHost()+":"+candidate.getPort()); + Log.d(Config.LOGTAG, this.account.getJid().asBareJid() + ": successfully processed connection to candidate " + candidate.getHost() + ":" + candidate.getPort()); socket.setSoTimeout(0); this.socket = socket; this.inputStream = inputStream; @@ -216,7 +217,7 @@ public class JingleSocks5Transport extends JingleTransport { } } catch (Exception e) { final Account account = this.account; - Log.d(Config.LOGTAG, account.getJid().asBareJid()+": failed sending file after "+transmitted+"/"+file.getExpectedSize()+" ("+ socket.getInetAddress()+":"+socket.getPort()+")", e); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": failed sending file after " + transmitted + "/" + file.getExpectedSize() + " (" + socket.getInetAddress() + ":" + socket.getPort() + ")", e); callback.onFileTransferAborted(); } finally { FileBackend.close(fileInputStream); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index 5dbeef03d..34c0c706f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -39,58 +39,35 @@ public class Content extends Element { return Creator.of(getAttribute("creator")); } - public Version getVersion() { - if (hasChild("description", Version.FT_3.namespace)) { - return Version.FT_3; - } else if (hasChild("description", Version.FT_4.namespace)) { - return Version.FT_4; - } else if (hasChild("description", Version.FT_5.namespace)) { - return Version.FT_5; - } - return null; + public Senders getSenders() { + return Senders.of(getAttribute("senders")); } - public Element setFileOffer(DownloadableFile actualFile, boolean otr, Version version) { - Element description = this.addChild("description", version.namespace); - Element file; - if (version == Version.FT_3) { - Element offer = description.addChild("offer"); - file = offer.addChild("file"); - } else { - file = description.addChild("file"); - } - file.addChild("size").setContent(Long.toString(actualFile.getExpectedSize())); - if (otr) { - file.addChild("name").setContent(actualFile.getName() + ".otr"); - } else { - file.addChild("name").setContent(actualFile.getName()); - } - return file; + public void setSenders(Senders senders) { + this.setAttribute("senders", senders.toString()); } - public Element getFileOffer(Version version) { - Element description = this.findChild("description", version.namespace); + public GenericDescription getDescription() { + final Element description = this.findChild("description"); if (description == null) { return null; } - if (version == Version.FT_3) { - Element offer = description.findChild("offer"); - if (offer == null) { - return null; - } - return offer.findChild("file"); + final String xmlns = description.getNamespace(); + if (FileTransferDescription.NAMESPACES.contains(xmlns)) { + return FileTransferDescription.upgrade(description); } else { - return description.findChild("file"); + return GenericDescription.upgrade(description); } } - public void setFileOffer(Element fileOffer, Version version) { - Element description = this.addChild("description", version.namespace); - if (version == Version.FT_3) { - description.addChild("offer").addChild(fileOffer); - } else { - description.addChild(fileOffer); - } + public void setDescription(final GenericDescription description) { + Preconditions.checkNotNull(description); + this.addChild(description); + } + + public String getDescriptionNamespace() { + final Element description = this.findChild("description"); + return description == null ? null : description.getNamespace(); } public String getTransportId() { @@ -132,22 +109,6 @@ public class Content extends Element { return this.hasChild("transport", Namespace.JINGLE_TRANSPORTS_IBB); } - public enum Version { - FT_3("urn:xmpp:jingle:apps:file-transfer:3"), - FT_4("urn:xmpp:jingle:apps:file-transfer:4"), - FT_5("urn:xmpp:jingle:apps:file-transfer:5"); - - private final String namespace; - - Version(String namespace) { - this.namespace = namespace; - } - - public String getNamespace() { - return namespace; - } - } - public enum Creator { INITIATOR, RESPONDER; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java new file mode 100644 index 000000000..8e0f2ebad --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java @@ -0,0 +1,89 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import com.google.common.base.Preconditions; + +import java.util.Arrays; +import java.util.List; + +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.xml.Element; + +public class FileTransferDescription extends GenericDescription { + + public static List NAMESPACES = Arrays.asList( + Version.FT_3.namespace, + Version.FT_4.namespace, + Version.FT_5.namespace + ); + + + private FileTransferDescription(String name, String namespace) { + super(name, namespace); + } + + public Version getVersion() { + final String namespace = getNamespace(); + if (namespace.equals(Version.FT_3.namespace)) { + return Version.FT_3; + } else if (namespace.equals(Version.FT_4.namespace)) { + return Version.FT_4; + } else if (namespace.equals(Version.FT_5.namespace)) { + return Version.FT_5; + } else { + throw new IllegalStateException("Unknown namespace"); + } + } + + public Element getFileOffer() { + final Version version = getVersion(); + if (version == Version.FT_3) { + final Element offer = this.findChild("offer"); + return offer == null ? null : offer.findChild("file"); + } else { + return this.findChild("file"); + } + } + + public static FileTransferDescription of(DownloadableFile file, Version version, XmppAxolotlMessage axolotlMessage) { + final FileTransferDescription description = new FileTransferDescription("description", version.getNamespace()); + final Element fileElement; + if (version == Version.FT_3) { + Element offer = description.addChild("offer"); + fileElement = offer.addChild("file"); + } else { + fileElement = description.addChild("file"); + } + fileElement.addChild("size").setContent(Long.toString(file.getExpectedSize())); + fileElement.addChild("name").setContent(file.getName()); + if (axolotlMessage != null) { + fileElement.addChild(axolotlMessage.toElement()); + } + return description; + } + + public static FileTransferDescription upgrade(final Element element) { + Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description"); + Preconditions.checkArgument(NAMESPACES.contains(element.getNamespace()), "Element does not match a file transfer namespace"); + final FileTransferDescription description = new FileTransferDescription("description", element.getNamespace()); + description.setAttributes(element.getAttributes()); + description.setChildren(element.getChildren()); + return description; + } + + public enum Version { + FT_3("urn:xmpp:jingle:apps:file-transfer:3"), + FT_4("urn:xmpp:jingle:apps:file-transfer:4"), + FT_5("urn:xmpp:jingle:apps:file-transfer:5"); + + private final String namespace; + + Version(String namespace) { + this.namespace = namespace; + } + + public String getNamespace() { + return namespace; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericDescription.java new file mode 100644 index 000000000..0e3c5a7f1 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericDescription.java @@ -0,0 +1,20 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import com.google.common.base.Preconditions; + +import eu.siacs.conversations.xml.Element; + +public class GenericDescription extends Element { + + protected GenericDescription(String name, final String namespace) { + super(name, namespace); + } + + public static GenericDescription upgrade(final Element element) { + Preconditions.checkArgument("description".equals(element.getName())); + final GenericDescription description = new GenericDescription("description", element.getNamespace()); + description.setAttributes(element.getAttributes()); + description.setChildren(element.getChildren()); + return description; + } +} From 963ddd11c214b64e0c8138fc0deeedfbc7dc60f1 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 2 Apr 2020 10:59:25 +0200 Subject: [PATCH 020/182] refactor jingle code to use objects for TransportInfo --- .../eu/siacs/conversations/xml/Namespace.java | 1 + .../xmpp/jingle/JingleCandidate.java | 28 +-- .../jingle/JingleFileTransferConnection.java | 199 ++++++++---------- .../conversations/xmpp/jingle/Transport.java | 5 - .../xmpp/jingle/stanzas/Content.java | 54 ++--- .../jingle/stanzas/GenericDescription.java | 2 +- .../jingle/stanzas/GenericTransportInfo.java | 20 ++ .../xmpp/jingle/stanzas/IbbTransportInfo.java | 46 ++++ .../jingle/stanzas/IceUdpTransportInfo.java | 22 ++ .../xmpp/jingle/stanzas/S5BTransportInfo.java | 50 +++++ 10 files changed, 263 insertions(+), 164 deletions(-) delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/Transport.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericTransportInfo.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IbbTransportInfo.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/S5BTransportInfo.java diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 6d4447e38..755de7fa5 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -29,6 +29,7 @@ public final class Namespace { public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0"; public static final String JINGLE_TRANSPORTS_S5B = "urn:xmpp:jingle:transports:s5b:1"; public static final String JINGLE_TRANSPORTS_IBB = "urn:xmpp:jingle:transports:ibb:1"; + public static final String JINGLE_TRANSPORT_ICE_UDP = "urn:xmpp:jingle:transports:ice-udp:1"; public static final String IBB = "http://jabber.org/protocol/ibb"; public static final String PING = "urn:xmpp:ping"; public static final String PUSH = "urn:xmpp:push:0"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java index 7415c32aa..e1f4db4b0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java @@ -101,22 +101,24 @@ public class JingleCandidate { return this.type; } - public static List parse(List canditates) { - List parsedCandidates = new ArrayList<>(); - for (Element c : canditates) { - parsedCandidates.add(JingleCandidate.parse(c)); + public static List parse(final List elements) { + final List candidates = new ArrayList<>(); + for (final Element element : elements) { + if ("candidate".equals(element.getName())) { + candidates.add(JingleCandidate.parse(element)); + } } - return parsedCandidates; + return candidates; } - public static JingleCandidate parse(Element candidate) { - JingleCandidate parsedCandidate = new JingleCandidate(candidate.getAttribute("cid"), false); - parsedCandidate.setHost(candidate.getAttribute("host")); - parsedCandidate.setJid(InvalidJid.getNullForInvalid(candidate.getAttributeAsJid("jid"))); - parsedCandidate.setType(candidate.getAttribute("type")); - parsedCandidate.setPriority(Integer.parseInt(candidate.getAttribute("priority"))); - parsedCandidate.setPort(Integer.parseInt(candidate.getAttribute("port"))); - return parsedCandidate; + public static JingleCandidate parse(Element element) { + final JingleCandidate candidate = new JingleCandidate(element.getAttribute("cid"), false); + candidate.setHost(element.getAttribute("host")); + candidate.setJid(InvalidJid.getNullForInvalid(element.getAttributeAsJid("jid"))); + candidate.setType(element.getAttribute("type")); + candidate.setPriority(Integer.parseInt(element.getAttribute("priority"))); + candidate.setPort(Integer.parseInt(element.getAttribute("port"))); + return candidate; } public Element toElement() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index 997f84b89..97d6a248f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -4,6 +4,11 @@ import android.util.Base64; import android.util.Log; import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.collect.Collections2; +import com.google.common.collect.Lists; + +import org.checkerframework.checker.nullness.compatqual.NullableDecl; import java.io.File; import java.io.FileInputStream; @@ -13,6 +18,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -39,8 +45,11 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; +import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; +import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; +import eu.siacs.conversations.xmpp.jingle.stanzas.S5BTransportInfo; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import rocks.xmpp.addr.Jid; @@ -54,7 +63,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private static final int JINGLE_STATUS_FAILED = 99; private static final int JINGLE_STATUS_OFFERED = -1; - private int ibbBlockSize = 8192; + private static final int MAX_IBB_BLOCK_SIZE = 8192; + + private int ibbBlockSize = MAX_IBB_BLOCK_SIZE; private int mJingleStatus = JINGLE_STATUS_OFFERED; //migrate to enum private int mStatus = Transferable.STATUS_UNKNOWN; @@ -72,7 +83,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private String contentName; private Content.Creator contentCreator; - private Transport initialTransport; + private Class initialTransport; private boolean remoteSupportsOmemoJet; private int mProgress = 0; @@ -276,8 +287,10 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } else if (action == JinglePacket.Action.TRANSPORT_INFO) { receiveTransportInfo(packet); } else if (action == JinglePacket.Action.TRANSPORT_REPLACE) { - if (packet.getJingleContent().hasIbbTransport()) { - receiveFallbackToIbb(packet); + final Content content = packet.getJingleContent(); + final GenericTransportInfo transportInfo = content == null ? null : content.getTransport(); + if (transportInfo instanceof IbbTransportInfo) { + receiveFallbackToIbb(packet, (IbbTransportInfo) transportInfo); } else { Log.d(Config.LOGTAG, "trying to fallback to something unknown" + packet.toString()); respondToIq(packet, false); @@ -333,7 +346,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple this.message = message; final List remoteFeatures = getRemoteFeatures(); final FileTransferDescription.Version remoteVersion = getAvailableFileTransferVersion(remoteFeatures); - this.initialTransport = remoteFeatures.contains(Namespace.JINGLE_TRANSPORTS_S5B) ? Transport.SOCKS : Transport.IBB; + this.initialTransport = remoteFeatures.contains(Namespace.JINGLE_TRANSPORTS_S5B) ? S5BTransportInfo.class : IbbTransportInfo.class; this.remoteSupportsOmemoJet = remoteFeatures.contains(Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO); this.message.setTransferable(this); this.mStatus = Transferable.STATUS_UPLOADING; @@ -341,7 +354,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple this.responder = this.id.counterPart; this.transportId = JingleConnectionManager.nextRandomId(); this.setupDescription(remoteVersion); - if (this.initialTransport == Transport.IBB) { + if (this.initialTransport == IbbTransportInfo.class) { this.sendInitRequest(); } else { gatherAndConnectDirectCandidates(); @@ -425,31 +438,31 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple this.initiator = this.id.counterPart; this.responder = this.id.account.getJid(); final Content content = packet.getJingleContent(); + final GenericTransportInfo transportInfo = content.getTransport(); this.contentCreator = content.getCreator(); - this.initialTransport = content.hasSocks5Transport() ? Transport.SOCKS : Transport.IBB; this.contentName = content.getAttribute("name"); - this.transportId = content.getTransportId(); - - if (this.initialTransport == Transport.SOCKS) { - this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren())); - } else if (this.initialTransport == Transport.IBB) { - final String receivedBlockSize = content.ibbTransport().getAttribute("block-size"); - if (receivedBlockSize != null) { - try { - this.ibbBlockSize = Math.min(Integer.parseInt(receivedBlockSize), this.ibbBlockSize); - } catch (NumberFormatException e) { - Log.d(Config.LOGTAG, "number format exception " + e.getMessage()); - respondToIq(packet, false); - this.fail(); - return; - } - } else { - Log.d(Config.LOGTAG, "received block size was null"); + if (transportInfo instanceof S5BTransportInfo) { + final S5BTransportInfo s5BTransportInfo = (S5BTransportInfo) transportInfo; + this.transportId = s5BTransportInfo.getTransportId(); + this.initialTransport = s5BTransportInfo.getClass(); + this.mergeCandidates(s5BTransportInfo.getCandidates()); + } else if (transportInfo instanceof IbbTransportInfo) { + final IbbTransportInfo ibbTransportInfo = (IbbTransportInfo) transportInfo; + this.initialTransport = ibbTransportInfo.getClass(); + this.transportId = ibbTransportInfo.getTransportId(); + final int remoteBlockSize = ibbTransportInfo.getBlockSize(); + if (remoteBlockSize <= 0) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote party requested invalid ibb block size"); respondToIq(packet, false); this.fail(); - return; } + this.ibbBlockSize = Math.min(MAX_IBB_BLOCK_SIZE, ibbTransportInfo.getBlockSize()); + } else { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote tried to use unknown transport " + transportInfo.getNamespace()); + respondToIq(packet, false); + this.fail(); + return; } this.description = (FileTransferDescription) content.getDescription(); @@ -562,7 +575,6 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendInitRequest() { final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INITIATE); final Content content = new Content(this.contentCreator, this.contentName); - content.setTransportId(this.transportId); if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL && remoteSupportsOmemoJet) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote announced support for JET"); final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT); @@ -580,14 +592,13 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple fail(e.getMessage()); return; } - content.setTransportId(this.transportId); - if (this.initialTransport == Transport.IBB) { - content.ibbTransport().setAttribute("block-size", Integer.toString(this.ibbBlockSize)); + if (this.initialTransport == IbbTransportInfo.class) { + content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending IBB offer"); } else { - final List candidates = getCandidatesAsElements(); + final Collection candidates = getOurCandidates(); + content.setTransport(new S5BTransportInfo(this.transportId, candidates)); Log.d(Config.LOGTAG, String.format("%s: sending S5B offer with %d candidates", id.account.getJid().asBareJid(), candidates.size())); - content.socks5transport().setChildren(candidates); } packet.setJingleContent(content); this.sendJinglePacket(packet, (account, response) -> { @@ -618,21 +629,15 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple this.sendJinglePacket(packet); } - private List getCandidatesAsElements() { - List elements = new ArrayList<>(); - for (JingleCandidate c : this.candidates) { - if (c.isOurs()) { - elements.add(c.toElement()); - } - } - return elements; + public Collection getOurCandidates() { + return Collections2.filter(this.candidates, c -> c != null && c.isOurs()); } private void sendAccept() { mJingleStatus = JINGLE_STATUS_ACCEPTED; this.mStatus = Transferable.STATUS_DOWNLOADING; this.jingleConnectionManager.updateConversationUi(true); - if (initialTransport == Transport.SOCKS) { + if (initialTransport == S5BTransportInfo.class) { sendAcceptSocks(); } else { sendAcceptIbb(); @@ -645,7 +650,6 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT); final Content content = new Content(contentCreator, contentName); content.setDescription(this.description); - content.setTransportId(transportId); if (success && candidate != null && !equalCandidateExists(candidate)) { final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate); connections.put(candidate.getCid(), socksConnection); @@ -654,7 +658,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple @Override public void failed() { Log.d(Config.LOGTAG, "connection to our own proxy65 candidate failed"); - content.socks5transport().setChildren(getCandidatesAsElements()); + content.setTransport(new S5BTransportInfo(transportId, getOurCandidates())); packet.setJingleContent(content); sendJinglePacket(packet); connectNextCandidate(); @@ -664,7 +668,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple public void established() { Log.d(Config.LOGTAG, "connected to proxy65 candidate"); mergeCandidate(candidate); - content.socks5transport().setChildren(getCandidatesAsElements()); + content.setTransport(new S5BTransportInfo(transportId, getOurCandidates())); packet.setJingleContent(content); sendJinglePacket(packet); connectNextCandidate(); @@ -672,7 +676,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple }); } else { Log.d(Config.LOGTAG, "did not find a proxy65 candidate for ourselves"); - content.socks5transport().setChildren(getCandidatesAsElements()); + content.setTransport(new S5BTransportInfo(transportId, getOurCandidates())); packet.setJingleContent(content); sendJinglePacket(packet); connectNextCandidate(); @@ -685,8 +689,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT); final Content content = new Content(contentCreator, contentName); content.setDescription(this.description); - content.setTransportId(transportId); - content.ibbTransport().setAttribute("block-size", this.ibbBlockSize); + content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); packet.setJingleContent(content); this.transport.receive(file, onFileTransmissionStatusChanged); this.sendJinglePacket(packet); @@ -719,22 +722,21 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } this.mJingleStatus = JINGLE_STATUS_ACCEPTED; xmppConnectionService.markMessage(message, Message.STATUS_UNSEND); - Content content = packet.getJingleContent(); - if (content.hasSocks5Transport()) { + final Content content = packet.getJingleContent(); + final GenericTransportInfo transportInfo = content.getTransport(); + //TODO we want to fail if transportInfo doesn’t match our intialTransport and/or our id + if (transportInfo instanceof S5BTransportInfo) { + final S5BTransportInfo s5BTransportInfo = (S5BTransportInfo) transportInfo; respondToIq(packet, true); - mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren())); + //TODO calling merge is probably a bug because that might eliminate candidates of the other party and lead to us not sending accept/deny + //TODO: we probably just want to call add + mergeCandidates(s5BTransportInfo.getCandidates()); this.connectNextCandidate(); - } else if (content.hasIbbTransport()) { - String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size"); - if (receivedBlockSize != null) { - try { - int bs = Integer.parseInt(receivedBlockSize); - if (bs > this.ibbBlockSize) { - this.ibbBlockSize = bs; - } - } catch (Exception e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to parse block size in session-accept"); - } + } else if (transportInfo instanceof IbbTransportInfo) { + final IbbTransportInfo ibbTransportInfo = (IbbTransportInfo) transportInfo; + final int remoteBlockSize = ibbTransportInfo.getBlockSize(); + if (remoteBlockSize > 0) { + this.ibbBlockSize = Math.min(ibbBlockSize, remoteBlockSize); } respondToIq(packet, true); this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize); @@ -746,13 +748,15 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void receiveTransportInfo(JinglePacket packet) { final Content content = packet.getJingleContent(); - if (content.hasSocks5Transport()) { - if (content.socks5transport().hasChild("activated")) { + final GenericTransportInfo transportInfo = content.getTransport(); + if (transportInfo instanceof S5BTransportInfo) { + final S5BTransportInfo s5BTransportInfo = (S5BTransportInfo) transportInfo; + if (s5BTransportInfo.hasChild("activated")) { respondToIq(packet, true); if ((this.transport != null) && (this.transport instanceof JingleSocks5Transport)) { onProxyActivated.success(); } else { - String cid = content.socks5transport().findChild("activated").getAttribute("cid"); + String cid = s5BTransportInfo.findChild("activated").getAttribute("cid"); Log.d(Config.LOGTAG, "received proxy activated (" + cid + ")prior to choosing our own transport"); JingleSocks5Transport connection = this.connections.get(cid); @@ -764,18 +768,18 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple this.fail(); } } - } else if (content.socks5transport().hasChild("proxy-error")) { + } else if (s5BTransportInfo.hasChild("proxy-error")) { respondToIq(packet, true); onProxyActivated.failed(); - } else if (content.socks5transport().hasChild("candidate-error")) { + } else if (s5BTransportInfo.hasChild("candidate-error")) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received candidate error"); respondToIq(packet, true); this.receivedCandidate = true; if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) { this.connect(); } - } else if (content.socks5transport().hasChild("candidate-used")) { - String cid = content.socks5transport().findChild("candidate-used").getAttribute("cid"); + } else if (s5BTransportInfo.hasChild("candidate-used")) { + String cid = s5BTransportInfo.findChild("candidate-used").getAttribute("cid"); if (cid != null) { Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid); JingleCandidate candidate = getCandidate(cid); @@ -912,15 +916,13 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.TRANSPORT_REPLACE); Content content = new Content(this.contentCreator, this.contentName); this.transportId = JingleConnectionManager.nextRandomId(); - content.setTransportId(this.transportId); - content.ibbTransport().setAttribute("block-size", - Integer.toString(this.ibbBlockSize)); + content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); packet.setJingleContent(content); this.sendJinglePacket(packet); } - private void receiveFallbackToIbb(JinglePacket packet) { + private void receiveFallbackToIbb(final JinglePacket packet, final IbbTransportInfo transportInfo) { if (initiating()) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-replace (we were initiating)"); respondToIqWithOutOfOrder(packet); @@ -934,25 +936,19 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } this.proxyActivationFailed = false; //fallback received; now we no longer need to accept another one; Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": receiving fallback to ibb"); - final String receivedBlockSize = packet.getJingleContent().ibbTransport().getAttribute("block-size"); - if (receivedBlockSize != null) { - try { - final int bs = Integer.parseInt(receivedBlockSize); - if (bs < this.ibbBlockSize) { - this.ibbBlockSize = bs; - } - } catch (NumberFormatException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to parse block size in transport-replace"); - } + final int remoteBlockSize = transportInfo.getBlockSize(); + if (remoteBlockSize > 0) { + this.ibbBlockSize = Math.min(MAX_IBB_BLOCK_SIZE, remoteBlockSize); + } else { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to parse block size in transport-replace"); } - this.transportId = packet.getJingleContent().getTransportId(); + this.transportId = transportInfo.getTransportId(); //TODO: handle the case where this is null by the remote party this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize); final JinglePacket answer = bootstrapPacket(JinglePacket.Action.TRANSPORT_ACCEPT); final Content content = new Content(contentCreator, contentName); - content.ibbTransport().setAttribute("block-size", this.ibbBlockSize); - content.ibbTransport().setAttribute("sid", this.transportId); + content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); answer.setJingleContent(content); respondToIq(packet, true); @@ -983,20 +979,15 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple return; } this.proxyActivationFailed = false; //fallback accepted; now we no longer need to accept another one; - if (packet.getJingleContent().hasIbbTransport()) { - final Element ibbTransport = packet.getJingleContent().ibbTransport(); - final String receivedBlockSize = ibbTransport.getAttribute("block-size"); - final String sid = ibbTransport.getAttribute("sid"); - if (receivedBlockSize != null) { - try { - int bs = Integer.parseInt(receivedBlockSize); - if (bs < this.ibbBlockSize) { - this.ibbBlockSize = bs; - } - } catch (NumberFormatException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to parse block size in transport-accept"); - } + final Content content = packet.getJingleContent(); + final GenericTransportInfo transportInfo = content == null ? null : content.getTransport(); + if (transportInfo instanceof IbbTransportInfo) { + final IbbTransportInfo ibbTransportInfo = (IbbTransportInfo) transportInfo; + final int remoteBlockSize = ibbTransportInfo.getBlockSize(); + if (remoteBlockSize > 0) { + this.ibbBlockSize = Math.min(MAX_IBB_BLOCK_SIZE, remoteBlockSize); } + final String sid = ibbTransportInfo.getTransportId(); this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize); if (sid == null || !sid.equals(this.transportId)) { @@ -1138,8 +1129,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendProxyActivated(String cid) { final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); final Content content = new Content(this.contentCreator, this.contentName); - content.setTransportId(this.transportId); - content.socks5transport().addChild("activated").setAttribute("cid", cid); + content.setTransport(new S5BTransportInfo(this.transportId, new Element("activated").setAttribute("cid", cid))); packet.setJingleContent(content); this.sendJinglePacket(packet); } @@ -1147,17 +1137,15 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendProxyError() { final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); final Content content = new Content(this.contentCreator, this.contentName); - content.setTransportId(this.transportId); - content.socks5transport().addChild("proxy-error"); + content.setTransport(new S5BTransportInfo(this.transportId, new Element("proxy-error"))); packet.setJingleContent(content); this.sendJinglePacket(packet); } private void sendCandidateUsed(final String cid) { JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); - Content content = new Content(this.contentCreator, this.contentName); - content.setTransportId(this.transportId); - content.socks5transport().addChild("candidate-used").setAttribute("cid", cid); + final Content content = new Content(this.contentCreator, this.contentName); + content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-used").setAttribute("cid", cid))); packet.setJingleContent(content); this.sentCandidate = true; if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) { @@ -1170,8 +1158,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending candidate error"); JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); Content content = new Content(this.contentCreator, this.contentName); - content.setTransportId(this.transportId); - content.socks5transport().addChild("candidate-error"); + content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-error"))); packet.setJingleContent(content); this.sentCandidate = true; this.sendJinglePacket(packet); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/Transport.java deleted file mode 100644 index 4d0fb4b65..000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/Transport.java +++ /dev/null @@ -1,5 +0,0 @@ -package eu.siacs.conversations.xmpp.jingle; - -public enum Transport { - SOCKS, IBB -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index 34c0c706f..ad16041a3 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -11,11 +11,6 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; public class Content extends Element { - private String transportId; - - - //refactor to getDescription and getTransport - //return either FileTransferDescription or GenericDescription or RtpDescription (all extend Description interface) public Content(final Creator creator, final String name) { super("content", Namespace.JINGLE); @@ -70,43 +65,24 @@ public class Content extends Element { return description == null ? null : description.getNamespace(); } - public String getTransportId() { - if (hasSocks5Transport()) { - this.transportId = socks5transport().getAttribute("sid"); - } else if (hasIbbTransport()) { - this.transportId = ibbTransport().getAttribute("sid"); + public GenericTransportInfo getTransport() { + final Element transport = this.findChild("transport"); + final String namespace = transport == null ? null : transport.getNamespace(); + if (Namespace.JINGLE_TRANSPORTS_IBB.equals(namespace)) { + return IbbTransportInfo.upgrade(transport); + } else if (Namespace.JINGLE_TRANSPORTS_S5B.equals(namespace)) { + return S5BTransportInfo.upgrade(transport); + } else if (Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(namespace)) { + return IceUdpTransportInfo.upgrade(transport); + } else if (transport != null) { + return GenericTransportInfo.upgrade(transport); + } else { + return null; } - return this.transportId; } - public void setTransportId(String sid) { - this.transportId = sid; - } - - public Element socks5transport() { - Element transport = this.findChild("transport", Namespace.JINGLE_TRANSPORTS_S5B); - if (transport == null) { - transport = this.addChild("transport", Namespace.JINGLE_TRANSPORTS_S5B); - transport.setAttribute("sid", this.transportId); - } - return transport; - } - - public Element ibbTransport() { - Element transport = this.findChild("transport", Namespace.JINGLE_TRANSPORTS_IBB); - if (transport == null) { - transport = this.addChild("transport", Namespace.JINGLE_TRANSPORTS_IBB); - transport.setAttribute("sid", this.transportId); - } - return transport; - } - - public boolean hasSocks5Transport() { - return this.hasChild("transport", Namespace.JINGLE_TRANSPORTS_S5B); - } - - public boolean hasIbbTransport() { - return this.hasChild("transport", Namespace.JINGLE_TRANSPORTS_IBB); + public void setTransport(GenericTransportInfo transportInfo) { + this.addChild(transportInfo); } public enum Creator { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericDescription.java index 0e3c5a7f1..a8db0d09f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericDescription.java @@ -6,7 +6,7 @@ import eu.siacs.conversations.xml.Element; public class GenericDescription extends Element { - protected GenericDescription(String name, final String namespace) { + GenericDescription(String name, final String namespace) { super(name, namespace); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericTransportInfo.java new file mode 100644 index 000000000..4c5c77388 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/GenericTransportInfo.java @@ -0,0 +1,20 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import com.google.common.base.Preconditions; + +import eu.siacs.conversations.xml.Element; + +public class GenericTransportInfo extends Element { + + protected GenericTransportInfo(String name, String xmlns) { + super(name, xmlns); + } + + public static GenericTransportInfo upgrade(final Element element) { + Preconditions.checkArgument("transport".equals(element.getName())); + final GenericTransportInfo transport = new GenericTransportInfo("transport", element.getNamespace()); + transport.setAttributes(element.getAttributes()); + transport.setChildren(element.getChildren()); + return transport; + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IbbTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IbbTransportInfo.java new file mode 100644 index 000000000..90fb32903 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IbbTransportInfo.java @@ -0,0 +1,46 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import com.google.common.base.Preconditions; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; + +public class IbbTransportInfo extends GenericTransportInfo { + + private IbbTransportInfo(final String name, final String xmlns) { + super(name, xmlns); + } + + public IbbTransportInfo(final String transportId, final int blockSize) { + super("transport", Namespace.JINGLE_TRANSPORTS_IBB); + Preconditions.checkNotNull(transportId, "Transport ID can not be null"); + Preconditions.checkArgument(blockSize > 0, "Block size must be larger than 0"); + this.setAttribute("block-size", blockSize); + this.setAttribute("sid", transportId); + } + + public String getTransportId() { + return this.getAttribute("sid"); + } + + public int getBlockSize() { + final String blockSize = this.getAttribute("block-size"); + if (blockSize == null) { + return 0; + } + try { + return Integer.parseInt(blockSize); + } catch (NumberFormatException e) { + return 0; + } + } + + public static IbbTransportInfo upgrade(final Element element) { + Preconditions.checkArgument("transport".equals(element.getName()), "Name of provided element is not transport"); + Preconditions.checkArgument(Namespace.JINGLE_TRANSPORTS_IBB.equals(element.getNamespace()), "Element does not match ibb transport namespace"); + final IbbTransportInfo transportInfo = new IbbTransportInfo("transport", Namespace.JINGLE_TRANSPORTS_IBB); + transportInfo.setAttributes(element.getAttributes()); + transportInfo.setChildren(element.getChildren()); + return transportInfo; + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java new file mode 100644 index 000000000..00beac65c --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -0,0 +1,22 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import com.google.common.base.Preconditions; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; + +public class IceUdpTransportInfo extends GenericTransportInfo { + + private IceUdpTransportInfo(final String name, final String xmlns) { + super(name, xmlns); + } + + public static IceUdpTransportInfo upgrade(final Element element) { + Preconditions.checkArgument("transport".equals(element.getName()), "Name of provided element is not transport"); + Preconditions.checkArgument(Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(element.getNamespace()), "Element does not match ice-udp transport namespace"); + final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo("transport", Namespace.JINGLE_TRANSPORT_ICE_UDP); + transportInfo.setAttributes(element.getAttributes()); + transportInfo.setChildren(element.getChildren()); + return transportInfo; + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/S5BTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/S5BTransportInfo.java new file mode 100644 index 000000000..8f8f13416 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/S5BTransportInfo.java @@ -0,0 +1,50 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import com.google.common.base.Preconditions; + +import java.util.Collection; +import java.util.List; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.jingle.JingleCandidate; + +public class S5BTransportInfo extends GenericTransportInfo { + + private S5BTransportInfo(final String name, final String xmlns) { + super(name, xmlns); + } + + public String getTransportId() { + return this.getAttribute("sid"); + } + + public S5BTransportInfo(final String transportId, final Collection candidates) { + super("transport", Namespace.JINGLE_TRANSPORTS_S5B); + Preconditions.checkNotNull(transportId,"transport id must not be null"); + for(JingleCandidate candidate : candidates) { + this.addChild(candidate.toElement()); + } + this.setAttribute("sid", transportId); + } + + public S5BTransportInfo(final String transportId, final Element child) { + super("transport", Namespace.JINGLE_TRANSPORTS_S5B); + Preconditions.checkNotNull(transportId,"transport id must not be null"); + this.addChild(child); + this.setAttribute("sid", transportId); + } + + public List getCandidates() { + return JingleCandidate.parse(this.getChildren()); + } + + public static S5BTransportInfo upgrade(final Element element) { + Preconditions.checkArgument("transport".equals(element.getName()), "Name of provided element is not transport"); + Preconditions.checkArgument(Namespace.JINGLE_TRANSPORTS_S5B.equals(element.getNamespace()), "Element does not match s5b transport namespace"); + final S5BTransportInfo transportInfo = new S5BTransportInfo("transport", Namespace.JINGLE_TRANSPORTS_S5B); + transportInfo.setAttributes(element.getAttributes()); + transportInfo.setChildren(element.getChildren()); + return transportInfo; + } +} From f9650b95d8e1b0c30aea602d2cb1e31f5188c257 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 2 Apr 2020 11:30:16 +0200 Subject: [PATCH 021/182] create stub JingleRTPConnection --- .../generator/AbstractGenerator.java | 9 ++++++++ .../eu/siacs/conversations/xml/Namespace.java | 3 +++ .../xmpp/jingle/JingleConnectionManager.java | 2 ++ .../xmpp/jingle/JingleRtpConnection.java | 19 +++++++++++++++ .../xmpp/jingle/stanzas/RtpDescription.java | 23 +++++++++++++++++++ 5 files changed, 56 insertions(+) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index 5d6c15aa9..7be008491 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -25,6 +25,8 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; public abstract class AbstractGenerator { private final String[] FEATURES = { Namespace.JINGLE, + + //Jingle File Transfer FileTransferDescription.Version.FT_3.getNamespace(), FileTransferDescription.Version.FT_4.getNamespace(), FileTransferDescription.Version.FT_5.getNamespace(), @@ -32,6 +34,13 @@ public abstract class AbstractGenerator { Namespace.JINGLE_TRANSPORTS_IBB, Namespace.JINGLE_ENCRYPTED_TRANSPORT, Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO, + + //VoIP + Namespace.JINGLE_TRANSPORT_ICE_UDP, + Namespace.JINGLE_FEATURE_AUDIO, + Namespace.JINGLE_FEATURE_VIDEO, + Namespace.JINGLE_APP_RTP, + "http://jabber.org/protocol/muc", "jabber:x:conference", Namespace.OOB, diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 755de7fa5..a15103126 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -30,6 +30,9 @@ public final class Namespace { public static final String JINGLE_TRANSPORTS_S5B = "urn:xmpp:jingle:transports:s5b:1"; public static final String JINGLE_TRANSPORTS_IBB = "urn:xmpp:jingle:transports:ibb:1"; public static final String JINGLE_TRANSPORT_ICE_UDP = "urn:xmpp:jingle:transports:ice-udp:1"; + public static final String JINGLE_APP_RTP = "urn:xmpp:jingle:apps:rtp:1"; + public static final String JINGLE_FEATURE_AUDIO = "urn:xmpp:jingle:apps:rtp:audio"; + public static final String JINGLE_FEATURE_VIDEO = "urn:xmpp:jingle:apps:rtp:video"; public static final String IBB = "http://jabber.org/protocol/ibb"; public static final String PING = "urn:xmpp:ping"; public static final String PUSH = "urn:xmpp:push:0"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index f63f31253..d73968783 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -41,6 +41,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { final AbstractJingleConnection connection; if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) { connection = new JingleFileTransferConnection(this, id); + } else if (Namespace.JINGLE_APP_RTP.equals(descriptionNamespace)) { + connection = new JingleRtpConnection(this, id); } else { //TODO return feature-not-implemented return; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java new file mode 100644 index 000000000..fc7bd14f4 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -0,0 +1,19 @@ +package eu.siacs.conversations.xmpp.jingle; + +import android.util.Log; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; + +public class JingleRtpConnection extends AbstractJingleConnection { + + + public JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id) { + super(jingleConnectionManager, id); + } + + @Override + void deliverPacket(final JinglePacket jinglePacket) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": packet delivered to JingleRtpConnection"); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java new file mode 100644 index 000000000..5ecb6fa15 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -0,0 +1,23 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import com.google.common.base.Preconditions; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; + +public class RtpDescription extends GenericDescription { + + + private RtpDescription(String name, String namespace) { + super(name, namespace); + } + + public static RtpDescription upgrade(final Element element) { + Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description"); + Preconditions.checkArgument(Namespace.JINGLE_APP_RTP.equals(element.getNamespace()), "Element does not match the jingle rtp namespace"); + final RtpDescription description = new RtpDescription("description", Namespace.JINGLE_APP_RTP); + description.setAttributes(element.getAttributes()); + description.setChildren(element.getChildren()); + return description; + } +} From a4acfb2a190e0b652031054fa5597c45877dd924 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 2 Apr 2020 11:50:09 +0200 Subject: [PATCH 022/182] clean iq callback code in XmppConnection --- .../java/eu/siacs/conversations/xmpp/XmppConnection.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 97590e4d6..7c9374bc8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -718,7 +718,7 @@ public class XmppConnection implements Runnable { } } - private void processIq(final Tag currentTag) throws XmlPullParserException, IOException { + private void processIq(final Tag currentTag) throws IOException { final IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ); if (!packet.valid()) { Log.e(Config.LOGTAG, "encountered invalid iq from='" + packet.getFrom() + "' to='" + packet.getTo() + "'"); @@ -731,8 +731,8 @@ public class XmppConnection implements Runnable { } else { OnIqPacketReceived callback = null; synchronized (this.packetCallbacks) { - if (packetCallbacks.containsKey(packet.getId())) { - final Pair packetCallbackDuple = packetCallbacks.get(packet.getId()); + final Pair packetCallbackDuple = packetCallbacks.get(packet.getId()); + if (packetCallbackDuple != null) { // Packets to the server should have responses from the server if (packetCallbackDuple.first.toServer(account)) { if (packet.fromServer(account)) { From 385692ea289a92e61e9241343611edf09f559f04 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 2 Apr 2020 16:29:33 +0200 Subject: [PATCH 023/182] route jingle message inits --- .../generator/AbstractGenerator.java | 3 +- .../conversations/parser/MessageParser.java | 32 ++++--- .../eu/siacs/conversations/xml/Namespace.java | 90 ++++++++++--------- .../xmpp/jingle/AbstractJingleConnection.java | 18 ++-- .../xmpp/jingle/JingleConnectionManager.java | 65 ++++++++++---- .../jingle/JingleFileTransferConnection.java | 23 +++-- .../xmpp/jingle/JingleInBandTransport.java | 2 +- .../xmpp/jingle/JingleRtpConnection.java | 6 ++ .../xmpp/jingle/JingleSocks5Transport.java | 5 +- .../xmpp/jingle/stanzas/RtpDescription.java | 4 +- 10 files changed, 151 insertions(+), 97 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index 7be008491..dbb188593 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -39,7 +39,8 @@ public abstract class AbstractGenerator { Namespace.JINGLE_TRANSPORT_ICE_UDP, Namespace.JINGLE_FEATURE_AUDIO, Namespace.JINGLE_FEATURE_VIDEO, - Namespace.JINGLE_APP_RTP, + Namespace.JINGLE_APPS_RTP, + Namespace.JINGLE_APPS_DTLS, "http://jabber.org/protocol/muc", "jabber:x:conference", diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index a57f0eec8..a53613e28 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -6,6 +6,7 @@ import android.util.Pair; import java.net.URL; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.List; @@ -50,6 +51,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH); + private static final List JINGLE_MESSAGE_ELEMENT_NAMES = Arrays.asList("accept", "propose", "proceed", "reject", "retract"); + public MessageParser(XmppConnectionService service) { super(service); } @@ -136,7 +139,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status); } } else { - Log.d(Config.LOGTAG,"ignoring broken session exception because checkForDuplicates failed"); + Log.d(Config.LOGTAG, "ignoring broken session exception because checkForDuplicates failed"); return null; } } catch (NotEncryptedForThisDeviceException e) { @@ -249,13 +252,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece final Jid id = InvalidJid.getNullForInvalid(retract.getAttributeAsJid("id")); if (id != null) { account.removeBookmark(id); - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": deleted bookmark for "+id); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmark for " + id); mXmppConnectionService.processDeletedBookmark(account, id); mXmppConnectionService.updateConversationUi(); } } } else { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+" received pubsub notification for node="+node); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + " received pubsub notification for node=" + node); } } @@ -267,7 +270,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece setNick(account, from, null); } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) { account.setBookmarks(Collections.emptyMap()); - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": deleted bookmarks node"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmarks node"); } } @@ -276,7 +279,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece final String node = purge == null ? null : purge.getAttribute("node"); if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) { account.setBookmarks(Collections.emptyMap()); - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": purged bookmarks"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": purged bookmarks"); } } @@ -308,10 +311,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece final Element error = packet.findChild("error"); final boolean pingWorthyError = error != null && (error.hasChild("not-acceptable") || error.hasChild("remote-server-timeout") || error.hasChild("remote-server-not-found")); if (pingWorthyError) { - Conversation conversation = mXmppConnectionService.find(account,from); + Conversation conversation = mXmppConnectionService.find(account, from); if (conversation != null && conversation.getMode() == Conversational.MODE_MULTI) { if (conversation.getMucOptions().online()) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received ping worthy error for seemingly online muc at "+from); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ping worthy error for seemingly online muc at " + from); mXmppConnectionService.mucSelfPingAndRejoin(conversation); } } @@ -419,9 +422,9 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece final Invite invite = extractInvite(packet); if (invite != null) { if (isTypeGroupChat) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignoring invite to "+invite.jid+" because type=groupchat"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring invite to " + invite.jid + " because type=groupchat"); } else if (invite.direct && (mucUserElement != null || invite.inviter == null || mXmppConnectionService.isMuc(account, invite.inviter))) { - Log.d(Config.LOGTAG, account.getJid().asBareJid()+": ignoring direct invite to "+invite.jid+" because it was received in MUC"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring direct invite to " + invite.jid + " because it was received in MUC"); } else { invite.execute(account); return; @@ -504,7 +507,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece final boolean checkedForDuplicates = liveMessage || (serverMsgId != null && remoteMsgId != null && !conversation.possibleDuplicate(serverMsgId, remoteMsgId)); if (origin != null) { - message = parseAxolotlChat(axolotlEncrypted, origin, conversation, status, checkedForDuplicates,query != null); + message = parseAxolotlChat(axolotlEncrypted, origin, conversation, status, checkedForDuplicates, query != null); } else { Message trial = null; for (Jid fallback : fallbacksBySourceId) { @@ -809,6 +812,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } } + if (!isTypeGroupChat) { + for (Element child : packet.getChildren()) { + if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace()) && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) { + mXmppConnectionService.getJingleConnectionManager().deliverMessage(account, packet.getTo(), packet.getFrom(), child); + } + } + } } Element received = packet.findChild("received", "urn:xmpp:chat-markers:0"); @@ -944,7 +954,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece if (jid != null) { Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid, true, false); if (conversation.getMucOptions().online()) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": received invite to "+jid+" but muc is considered to be online"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received invite to " + jid + " but muc is considered to be online"); mXmppConnectionService.mucSelfPingAndRejoin(conversation); } else { conversation.getMucOptions().setPassword(password); diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index a15103126..40a89cb07 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -1,48 +1,50 @@ package eu.siacs.conversations.xml; public final class Namespace { - public static final String DISCO_ITEMS = "http://jabber.org/protocol/disco#items"; - public static final String DISCO_INFO = "http://jabber.org/protocol/disco#info"; - public static final String BLOCKING = "urn:xmpp:blocking"; - public static final String ROSTER = "jabber:iq:roster"; - public static final String REGISTER = "jabber:iq:register"; - public static final String BYTE_STREAMS = "http://jabber.org/protocol/bytestreams"; - public static final String HTTP_UPLOAD = "urn:xmpp:http:upload:0"; - public static final String HTTP_UPLOAD_LEGACY = "urn:xmpp:http:upload"; - public static final String STANZA_IDS = "urn:xmpp:sid:0"; - public static final String IDLE = "urn:xmpp:idle:1"; - public static final String DATA = "jabber:x:data"; - public static final String OOB = "jabber:x:oob"; - public static final String SASL = "urn:ietf:params:xml:ns:xmpp-sasl"; - public static final String TLS = "urn:ietf:params:xml:ns:xmpp-tls"; - public static final String PUBSUB = "http://jabber.org/protocol/pubsub"; - public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB+"#publish-options"; - public static final String PUBSUB_ERROR = PUBSUB+"#errors"; - public static final String PUBSUB_OWNER = PUBSUB+"#owner"; - public static final String NICK = "http://jabber.org/protocol/nick"; - public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline"; - public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind"; - public static final String P1_S3_FILE_TRANSFER = "p1:s3filetransfer"; - public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0"; - public static final String BOOKMARKS = "storage:bookmarks"; - public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0"; - public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0"; - public static final String JINGLE_TRANSPORTS_S5B = "urn:xmpp:jingle:transports:s5b:1"; - public static final String JINGLE_TRANSPORTS_IBB = "urn:xmpp:jingle:transports:ibb:1"; - public static final String JINGLE_TRANSPORT_ICE_UDP = "urn:xmpp:jingle:transports:ice-udp:1"; - public static final String JINGLE_APP_RTP = "urn:xmpp:jingle:apps:rtp:1"; - public static final String JINGLE_FEATURE_AUDIO = "urn:xmpp:jingle:apps:rtp:audio"; - public static final String JINGLE_FEATURE_VIDEO = "urn:xmpp:jingle:apps:rtp:video"; - public static final String IBB = "http://jabber.org/protocol/ibb"; - public static final String PING = "urn:xmpp:ping"; - public static final String PUSH = "urn:xmpp:push:0"; - public static final String COMMANDS = "http://jabber.org/protocol/commands"; - public static final String JINGLE = "urn:xmpp:jingle:1"; - public static final String JINGLE_ENCRYPTED_TRANSPORT = "urn:xmpp:jingle:jet:0"; - public static final String JINGLE_ENCRYPTED_TRANSPORT_OMEMO = "urn:xmpp:jingle:jet-omemo:0"; - public static final String MUC_USER = "http://jabber.org/protocol/muc#user"; - public static final String BOOKMARKS2 = "urn:xmpp:bookmarks:0"; - public static final String BOOKMARKS2_COMPAT = BOOKMARKS2+"#compat"; - public static final String INVITE = "urn:xmpp:invite"; - public static final String PARS = "urn:xmpp:pars:0"; + public static final String DISCO_ITEMS = "http://jabber.org/protocol/disco#items"; + public static final String DISCO_INFO = "http://jabber.org/protocol/disco#info"; + public static final String BLOCKING = "urn:xmpp:blocking"; + public static final String ROSTER = "jabber:iq:roster"; + public static final String REGISTER = "jabber:iq:register"; + public static final String BYTE_STREAMS = "http://jabber.org/protocol/bytestreams"; + public static final String HTTP_UPLOAD = "urn:xmpp:http:upload:0"; + public static final String HTTP_UPLOAD_LEGACY = "urn:xmpp:http:upload"; + public static final String STANZA_IDS = "urn:xmpp:sid:0"; + public static final String IDLE = "urn:xmpp:idle:1"; + public static final String DATA = "jabber:x:data"; + public static final String OOB = "jabber:x:oob"; + public static final String SASL = "urn:ietf:params:xml:ns:xmpp-sasl"; + public static final String TLS = "urn:ietf:params:xml:ns:xmpp-tls"; + public static final String PUBSUB = "http://jabber.org/protocol/pubsub"; + public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options"; + public static final String PUBSUB_ERROR = PUBSUB + "#errors"; + public static final String PUBSUB_OWNER = PUBSUB + "#owner"; + public static final String NICK = "http://jabber.org/protocol/nick"; + public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline"; + public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind"; + public static final String P1_S3_FILE_TRANSFER = "p1:s3filetransfer"; + public static final String BOOKMARKS_CONVERSION = "urn:xmpp:bookmarks-conversion:0"; + public static final String BOOKMARKS = "storage:bookmarks"; + public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0"; + public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0"; + public static final String JINGLE = "urn:xmpp:jingle:1"; + public static final String JINGLE_MESSAGE = "urn:xmpp:jingle-message:0"; + public static final String JINGLE_ENCRYPTED_TRANSPORT = "urn:xmpp:jingle:jet:0"; + public static final String JINGLE_ENCRYPTED_TRANSPORT_OMEMO = "urn:xmpp:jingle:jet-omemo:0"; + public static final String JINGLE_TRANSPORTS_S5B = "urn:xmpp:jingle:transports:s5b:1"; + public static final String JINGLE_TRANSPORTS_IBB = "urn:xmpp:jingle:transports:ibb:1"; + public static final String JINGLE_TRANSPORT_ICE_UDP = "urn:xmpp:jingle:transports:ice-udp:1"; + public static final String JINGLE_APPS_RTP = "urn:xmpp:jingle:apps:rtp:1"; + public static final String JINGLE_APPS_DTLS = "urn:xmpp:jingle:apps:dtls:0"; + public static final String JINGLE_FEATURE_AUDIO = "urn:xmpp:jingle:apps:rtp:audio"; + public static final String JINGLE_FEATURE_VIDEO = "urn:xmpp:jingle:apps:rtp:video"; + public static final String IBB = "http://jabber.org/protocol/ibb"; + public static final String PING = "urn:xmpp:ping"; + public static final String PUSH = "urn:xmpp:push:0"; + public static final String COMMANDS = "http://jabber.org/protocol/commands"; + public static final String MUC_USER = "http://jabber.org/protocol/muc#user"; + public static final String BOOKMARKS2 = "urn:xmpp:bookmarks:0"; + public static final String BOOKMARKS2_COMPAT = BOOKMARKS2 + "#compat"; + public static final String INVITE = "urn:xmpp:invite"; + public static final String PARS = "urn:xmpp:pars:0"; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java index 19ad2822a..4b7ecb354 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -30,14 +30,14 @@ public abstract class AbstractJingleConnection { public static class Id { public final Account account; - public final Jid counterPart; + public final Jid with; public final String sessionId; - private Id(final Account account, final Jid counterPart, final String sessionId) { - Preconditions.checkNotNull(counterPart); - Preconditions.checkArgument(counterPart.isFullJid()); + private Id(final Account account, final Jid with, final String sessionId) { + Preconditions.checkNotNull(with); + Preconditions.checkArgument(with.isFullJid()); this.account = account; - this.counterPart = counterPart; + this.with = with; this.sessionId = sessionId; } @@ -45,6 +45,10 @@ public abstract class AbstractJingleConnection { return new Id(account, jinglePacket.getFrom(), jinglePacket.getSessionId()); } + public static Id of(Account account, Jid with, final String sessionId) { + return new Id(account, with, sessionId); + } + public static Id of(Message message) { return new Id( message.getConversation().getAccount(), @@ -59,13 +63,13 @@ public abstract class AbstractJingleConnection { if (o == null || getClass() != o.getClass()) return false; Id id = (Id) o; return Objects.equal(account.getJid(), id.account.getJid()) && - Objects.equal(counterPart, id.counterPart) && + Objects.equal(with, id.with) && Objects.equal(sessionId, id.sessionId); } @Override public int hashCode() { - return Objects.hashCode(account.getJid(), counterPart, sessionId); + return Objects.hashCode(account.getJid(), with, sessionId); } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index d73968783..74082d03e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -22,6 +22,7 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; public class JingleConnectionManager extends AbstractConnectionManager { @@ -35,13 +36,16 @@ public class JingleConnectionManager extends AbstractConnectionManager { public void deliverPacket(final Account account, final JinglePacket packet) { final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, packet); - if (packet.getAction() == JinglePacket.Action.SESSION_INITIATE) { //TODO check that id doesn't exist yet + final AbstractJingleConnection existingJingleConnection = connections.get(id); + if (existingJingleConnection != null) { + existingJingleConnection.deliverPacket(packet); + } else if (packet.getAction() == JinglePacket.Action.SESSION_INITIATE) { final Content content = packet.getJingleContent(); final String descriptionNamespace = content == null ? null : content.getDescriptionNamespace(); final AbstractJingleConnection connection; if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) { connection = new JingleFileTransferConnection(this, id); - } else if (Namespace.JINGLE_APP_RTP.equals(descriptionNamespace)) { + } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace)) { connection = new JingleRtpConnection(this, id); } else { //TODO return feature-not-implemented @@ -50,22 +54,53 @@ public class JingleConnectionManager extends AbstractConnectionManager { connections.put(id, connection); connection.deliverPacket(packet); } else { - final AbstractJingleConnection abstractJingleConnection = connections.get(id); - if (abstractJingleConnection != null) { - abstractJingleConnection.deliverPacket(packet); - } else { - Log.d(Config.LOGTAG, "unable to route jingle packet: " + packet); - IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); - Element error = response.addChild("error"); - error.setAttribute("type", "cancel"); - error.addChild("item-not-found", - "urn:ietf:params:xml:ns:xmpp-stanzas"); - error.addChild("unknown-session", "urn:xmpp:jingle:errors:1"); - account.getXmppConnection().sendIqPacket(response, null); - } + Log.d(Config.LOGTAG, "unable to route jingle packet: " + packet); + final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); + final Element error = response.addChild("error"); + error.setAttribute("type", "cancel"); + error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas"); + error.addChild("unknown-session", "urn:xmpp:jingle:errors:1"); + account.getXmppConnection().sendIqPacket(response, null); } } + public void deliverMessage(final Account account, final Jid to, final Jid from, final Element message) { + Preconditions.checkArgument(Namespace.JINGLE_MESSAGE.equals(message.getNamespace())); + final String sessionId = message.getAttribute("id"); + if (sessionId == null) { + return; + } + final Jid with; + if (account.getJid().asBareJid().equals(from.asBareJid())) { + with = to; + } else { + with = from; + } + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received jingle message from " + from + " with=" + with + " " + message); + final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with, sessionId); + final AbstractJingleConnection existingJingleConnection = connections.get(id); + if (existingJingleConnection != null) { + if (existingJingleConnection instanceof JingleRtpConnection) { + ((JingleRtpConnection) existingJingleConnection).deliveryMessage(to, from, message); + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + existingJingleConnection.getClass().getName() + " does not support jingle messages"); + } + } else if ("propose".equals(message.getName())) { + final Element description = message.findChild("description"); + final String namespace = description == null ? null : description.getNamespace(); + if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { + final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id); + this.connections.put(id, rtpConnection); + rtpConnection.deliveryMessage(to, from, message); + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to react to proposed " + namespace + " session"); + } + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved out of order jingle message"); + } + + } + public void startJingleFileTransfer(final Message message) { Preconditions.checkArgument(message.isFileOrImage(), "Message is not of type file or image"); final Transferable old = message.getTransferable(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index 97d6a248f..f70bbc575 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -4,11 +4,7 @@ import android.util.Base64; import android.util.Log; import com.google.common.base.Preconditions; -import com.google.common.base.Predicate; import com.google.common.collect.Collections2; -import com.google.common.collect.Lists; - -import org.checkerframework.checker.nullness.compatqual.NullableDecl; import java.io.File; import java.io.FileInputStream; @@ -351,7 +347,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple this.message.setTransferable(this); this.mStatus = Transferable.STATUS_UPLOADING; this.initiator = this.id.account.getJid(); - this.responder = this.id.counterPart; + this.responder = this.id.with; this.transportId = JingleConnectionManager.nextRandomId(); this.setupDescription(remoteVersion); if (this.initialTransport == IbbTransportInfo.class) { @@ -416,7 +412,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } private List getRemoteFeatures() { - final Jid jid = this.id.counterPart; + 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().getPresences().get(resource); @@ -428,14 +424,15 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } private void init(JinglePacket packet) { //should move to deliverPacket + //TODO if not 'OFFERED' reply with out-of-order this.mJingleStatus = JINGLE_STATUS_INITIATED; - final Conversation conversation = this.xmppConnectionService.findOrCreateConversation(id.account, id.counterPart.asBareJid(), false, false); + final Conversation conversation = this.xmppConnectionService.findOrCreateConversation(id.account, id.with.asBareJid(), false, false); this.message = new Message(conversation, "", Message.ENCRYPTION_NONE); this.message.setStatus(Message.STATUS_RECEIVED); this.mStatus = Transferable.STATUS_OFFER; this.message.setTransferable(this); - this.message.setCounterpart(this.id.counterPart); - this.initiator = this.id.counterPart; + this.message.setCounterpart(this.id.with); + this.initiator = this.id.with; this.responder = this.id.account.getJid(); final Content content = packet.getJingleContent(); final GenericTransportInfo transportInfo = content.getTransport(); @@ -527,11 +524,11 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple respondToIq(packet, true); - if (id.account.getRoster().getContact(id.counterPart).showInContactList() + if (id.account.getRoster().getContact(id.with).showInContactList() && jingleConnectionManager.hasStoragePermission() && size < this.jingleConnectionManager.getAutoAcceptFileSize() && xmppConnectionService.isDataSaverDisabled()) { - Log.d(Config.LOGTAG, "auto accepting file from " + id.counterPart); + Log.d(Config.LOGTAG, "auto accepting file from " + id.with); this.acceptedAutomatically = true; this.sendAccept(); } else { @@ -697,7 +694,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private JinglePacket bootstrapPacket(JinglePacket.Action action) { final JinglePacket packet = new JinglePacket(action, this.id.sessionId); - packet.setTo(id.counterPart); + packet.setTo(id.with); return packet; } @@ -837,7 +834,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple activation.query("http://jabber.org/protocol/bytestreams") .setAttribute("sid", sid); activation.query().addChild("activate") - .setContent(this.id.counterPart.toEscapedString()); + .setContent(this.id.with.toEscapedString()); xmppConnectionService.sendIqPacket(this.id.account, activation, (account, response) -> { if (response.getType() != IqPacket.TYPE.RESULT) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": " + response.toString()); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInBandTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInBandTransport.java index fbc1ad95f..2dc0d9cab 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInBandTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInBandTransport.java @@ -65,7 +65,7 @@ public class JingleInBandTransport extends JingleTransport { JingleInBandTransport(final JingleFileTransferConnection connection, final String sid, final int blockSize) { this.connection = connection; this.account = connection.getId().account; - this.counterpart = connection.getId().counterPart; + this.counterpart = connection.getId().with; this.blockSize = blockSize; this.sessionId = sid; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index fc7bd14f4..571832650 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -3,7 +3,9 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Log; import eu.siacs.conversations.Config; +import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import rocks.xmpp.addr.Jid; public class JingleRtpConnection extends AbstractJingleConnection { @@ -16,4 +18,8 @@ public class JingleRtpConnection extends AbstractJingleConnection { void deliverPacket(final JinglePacket jinglePacket) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": packet delivered to JingleRtpConnection"); } + + void deliveryMessage(final Jid to, Jid from, Element message) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": delivered message to JingleRtpConnection " + message); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java index 31c40f531..4e7825c42 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -23,7 +23,6 @@ import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.SocksSocketFactory; import eu.siacs.conversations.utils.WakeLockHelper; -import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; public class JingleSocks5Transport extends JingleTransport { @@ -61,9 +60,9 @@ public class JingleSocks5Transport extends JingleTransport { } if (candidate.isOurs()) { destBuilder.append(this.account.getJid()); - destBuilder.append(this.connection.getId().counterPart); + destBuilder.append(this.connection.getId().with); } else { - destBuilder.append(this.connection.getId().counterPart); + destBuilder.append(this.connection.getId().with); destBuilder.append(this.account.getJid()); } messageDigest.reset(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index 5ecb6fa15..fd66f00ee 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -14,8 +14,8 @@ public class RtpDescription extends GenericDescription { public static RtpDescription upgrade(final Element element) { Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description"); - Preconditions.checkArgument(Namespace.JINGLE_APP_RTP.equals(element.getNamespace()), "Element does not match the jingle rtp namespace"); - final RtpDescription description = new RtpDescription("description", Namespace.JINGLE_APP_RTP); + Preconditions.checkArgument(Namespace.JINGLE_APPS_RTP.equals(element.getNamespace()), "Element does not match the jingle rtp namespace"); + final RtpDescription description = new RtpDescription("description", Namespace.JINGLE_APPS_RTP); description.setAttributes(element.getAttributes()); description.setChildren(element.getChildren()); return description; From 5b15348f13c771290c3f4f9addf50409b697d470 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 2 Apr 2020 21:12:38 +0200 Subject: [PATCH 024/182] process message inits --- .../xmpp/jingle/AbstractJingleConnection.java | 11 +++ .../xmpp/jingle/JingleRtpConnection.java | 82 ++++++++++++++++++- .../xmpp/jingle/stanzas/JinglePacket.java | 3 + 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java index 4b7ecb354..a426edd2e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -72,4 +72,15 @@ public abstract class AbstractJingleConnection { return Objects.hashCode(account.getJid(), with, sessionId); } } + + + public enum State { + NULL, //default value; nothing has been sent or received yet + PROPOSED, + ACCEPTED, + PROCEED, + SESSION_INITIALIZED, //equal to 'PENDING' + SESSION_ACCEPTED, //equal to 'ACTIVE' + TERMINATED //equal to 'ENDED' + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 571832650..c600162c2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -2,13 +2,32 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Log; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import java.util.Collection; +import java.util.Map; + import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; public class JingleRtpConnection extends AbstractJingleConnection { + private static final Map> VALID_TRANSITIONS; + + static { + final ImmutableMap.Builder> transitionBuilder = new ImmutableMap.Builder<>(); + transitionBuilder.put(State.NULL, ImmutableList.of(State.PROPOSED, State.SESSION_INITIALIZED)); + transitionBuilder.put(State.PROPOSED, ImmutableList.of(State.ACCEPTED, State.PROCEED)); + VALID_TRANSITIONS = transitionBuilder.build(); + } + + private State state = State.NULL; + public JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id) { super(jingleConnectionManager, id); @@ -17,9 +36,70 @@ public class JingleRtpConnection extends AbstractJingleConnection { @Override void deliverPacket(final JinglePacket jinglePacket) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": packet delivered to JingleRtpConnection"); + Log.d(Config.LOGTAG, jinglePacket.toString()); } - void deliveryMessage(final Jid to, Jid from, Element message) { + void deliveryMessage(final Jid to, final Jid from, final Element message) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": delivered message to JingleRtpConnection " + message); + final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); + switch (message.getName()) { + case "propose": + if (originatedFromMyself) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": saw proposal from mysql. ignoring"); + } else if (transition(State.PROPOSED)) { + //TODO start ringing or something + pickUpCall(); + } else { + Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring session proposal because already in " + state); + } + break; + default: + break; + } } + + public void pickUpCall() { + switch (this.state) { + case PROPOSED: + pickupCallFromProposed(); + break; + case SESSION_INITIALIZED: + pickupCallFromSessionInitialized(); + break; + default: + throw new IllegalStateException("Can not pick up call from " + this.state); + } + } + + private void pickupCallFromProposed() { + transitionOrThrow(State.PROCEED); + final MessagePacket messagePacket = new MessagePacket(); + messagePacket.setTo(id.with); + //Note that Movim needs 'accept' + messagePacket.addChild("proceed", Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId); + Log.d(Config.LOGTAG, messagePacket.toString()); + xmppConnectionService.sendMessagePacket(id.account, messagePacket); + } + + private void pickupCallFromSessionInitialized() { + + } + + private synchronized boolean transition(final State target) { + final Collection validTransitions = VALID_TRANSITIONS.get(this.state); + if (validTransitions != null && validTransitions.contains(target)) { + this.state = target; + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target); + return true; + } else { + return false; + } + } + + private void transitionOrThrow(final State target) { + if (!transition(target)) { + throw new IllegalStateException(String.format("Unable to transition from %s to %s", this.state, target)); + } + } + } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java index b59ece9c2..a8a366ecf 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -11,6 +11,8 @@ import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class JinglePacket extends IqPacket { + //TODO add support for groups: https://xmpp.org/extensions/xep-0338.html + private JinglePacket() { super(); } @@ -30,6 +32,7 @@ public class JinglePacket extends IqPacket { return jinglePacket; } + //TODO can have multiple contents public Content getJingleContent() { final Element content = getJingleChild("content"); return content == null ? null : Content.upgrade(content); From b2aa0e3352d7f538894d4fdf2c24349420d619f5 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 3 Apr 2020 08:16:55 +0200 Subject: [PATCH 025/182] use final varible to mark initiator once connection object has been created --- .../xmpp/jingle/AbstractJingleConnection.java | 8 ++++- .../xmpp/jingle/JingleConnectionManager.java | 13 ++++---- .../jingle/JingleFileTransferConnection.java | 33 ++++++++----------- .../xmpp/jingle/JingleRtpConnection.java | 4 +-- .../xmpp/jingle/stanzas/JinglePacket.java | 15 ++++++++- 5 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java index a426edd2e..65fce4205 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -14,11 +14,17 @@ public abstract class AbstractJingleConnection { protected final JingleConnectionManager jingleConnectionManager; protected final XmppConnectionService xmppConnectionService; protected final Id id; + protected final Jid initiator; - public AbstractJingleConnection(final JingleConnectionManager jingleConnectionManager, final Id id) { + AbstractJingleConnection(final JingleConnectionManager jingleConnectionManager, final Id id, final Jid initiator) { this.jingleConnectionManager = jingleConnectionManager; this.xmppConnectionService = jingleConnectionManager.getXmppConnectionService(); this.id = id; + this.initiator = initiator; + } + + boolean isInitiator() { + return initiator.equals(id.account.getJid()); } abstract void deliverPacket(JinglePacket jinglePacket); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 74082d03e..20fbae868 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -22,7 +22,6 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; public class JingleConnectionManager extends AbstractConnectionManager { @@ -40,13 +39,14 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (existingJingleConnection != null) { existingJingleConnection.deliverPacket(packet); } else if (packet.getAction() == JinglePacket.Action.SESSION_INITIATE) { + final Jid from = packet.getFrom(); final Content content = packet.getJingleContent(); final String descriptionNamespace = content == null ? null : content.getDescriptionNamespace(); final AbstractJingleConnection connection; if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) { - connection = new JingleFileTransferConnection(this, id); + connection = new JingleFileTransferConnection(this, id, from); } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace)) { - connection = new JingleRtpConnection(this, id); + connection = new JingleRtpConnection(this, id, from); } else { //TODO return feature-not-implemented return; @@ -89,7 +89,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { final Element description = message.findChild("description"); final String namespace = description == null ? null : description.getNamespace(); if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { - final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id); + final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, with); this.connections.put(id, rtpConnection); rtpConnection.deliveryMessage(to, from, message); } else { @@ -107,11 +107,12 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (old != null) { old.cancel(); } + final Account account = message.getConversation().getAccount(); final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(message); - final JingleFileTransferConnection connection = new JingleFileTransferConnection(this, id); + final JingleFileTransferConnection connection = new JingleFileTransferConnection(this, id, account.getJid()); mXmppConnectionService.markMessage(message, Message.STATUS_WAITING); - connection.init(message); this.connections.put(id, connection); + connection.init(message); } void finishConnection(final AbstractJingleConnection connection) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index f70bbc575..f2cb85d27 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -66,7 +66,6 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private int mJingleStatus = JINGLE_STATUS_OFFERED; //migrate to enum private int mStatus = Transferable.STATUS_UNKNOWN; private Message message; - private Jid initiator; private Jid responder; private List candidates = new ArrayList<>(); private ConcurrentHashMap connections = new ConcurrentHashMap<>(); @@ -178,7 +177,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple @Override public void success() { - if (initiator.equals(id.account.getJid())) { + if (isInitiator()) { Log.d(Config.LOGTAG, "we were initiating. sending file"); transport.send(file, onFileTransmissionStatusChanged); } else { @@ -191,14 +190,14 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple public void failed() { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": proxy activation failed"); proxyActivationFailed = true; - if (initiating()) { + if (isInitiator()) { sendFallbackToIbb(); } } }; - public JingleFileTransferConnection(JingleConnectionManager jingleConnectionManager, Id id) { - super(jingleConnectionManager, id); + public JingleFileTransferConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { + super(jingleConnectionManager, id, initiator); } private static long parseLong(final Element element, final long l) { @@ -213,13 +212,11 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } } + //TODO get rid and use isInitiator() instead private boolean responding() { return responder != null && responder.equals(id.account.getJid()); } - private boolean initiating() { - return initiator.equals(id.account.getJid()); - } InputStream getFileInputStream() { return this.mFileInputStream; @@ -346,7 +343,6 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple this.remoteSupportsOmemoJet = remoteFeatures.contains(Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO); this.message.setTransferable(this); this.mStatus = Transferable.STATUS_UPLOADING; - this.initiator = this.id.account.getJid(); this.responder = this.id.with; this.transportId = JingleConnectionManager.nextRandomId(); this.setupDescription(remoteVersion); @@ -354,7 +350,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple this.sendInitRequest(); } else { gatherAndConnectDirectCandidates(); - this.jingleConnectionManager.getPrimaryCandidate(id.account, initiating(), (success, candidate) -> { + this.jingleConnectionManager.getPrimaryCandidate(id.account, isInitiator(), (success, candidate) -> { if (success) { final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate); connections.put(candidate.getCid(), socksConnection); @@ -432,7 +428,6 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple this.mStatus = Transferable.STATUS_OFFER; this.message.setTransferable(this); this.message.setCounterpart(this.id.with); - this.initiator = this.id.with; this.responder = this.id.account.getJid(); final Content content = packet.getJingleContent(); final GenericTransportInfo transportInfo = content.getTransport(); @@ -643,7 +638,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendAcceptSocks() { gatherAndConnectDirectCandidates(); - this.jingleConnectionManager.getPrimaryCandidate(this.id.account, initiating(), (success, candidate) -> { + this.jingleConnectionManager.getPrimaryCandidate(this.id.account, isInitiator(), (success, candidate) -> { final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT); final Content content = new Content(contentCreator, contentName); content.setDescription(this.description); @@ -810,7 +805,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple if (connection == null) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": could not find suitable candidate"); this.disconnectSocks5Connections(); - if (initiating()) { + if (isInitiator()) { this.sendFallbackToIbb(); } } else { @@ -852,7 +847,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple + " was a proxy. waiting for other party to activate"); } } else { - if (initiating()) { + if (isInitiator()) { Log.d(Config.LOGTAG, "we were initiating. sending file"); connection.send(file, onFileTransmissionStatusChanged); } else { @@ -882,7 +877,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } else if (connection.getCandidate().getPriority() == currentConnection .getCandidate().getPriority()) { // Log.d(Config.LOGTAG,"found two candidates with same priority"); - if (initiating()) { + if (isInitiator()) { if (currentConnection.getCandidate().isOurs()) { connection = currentConnection; } @@ -920,7 +915,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void receiveFallbackToIbb(final JinglePacket packet, final IbbTransportInfo transportInfo) { - if (initiating()) { + if (isInitiator()) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received out of order transport-replace (we were initiating)"); respondToIqWithOutOfOrder(packet); return; @@ -950,7 +945,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple respondToIq(packet, true); - if (initiating()) { + if (isInitiator()) { this.sendJinglePacket(answer, (account, response) -> { if (response.getType() == IqPacket.TYPE.RESULT) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + " recipient ACKed our transport-accept. creating ibb"); @@ -992,7 +987,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } respondToIq(packet, true); //might be receive instead if we are not initiating - if (initiating()) { + if (isInitiator()) { this.transport.connect(onIbbTransportConnected); } } else { @@ -1002,7 +997,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } private void receiveSuccess() { - if (initiating()) { + if (isInitiator()) { this.mJingleStatus = JINGLE_STATUS_FINISHED; this.xmppConnectionService.markMessage(this.message, Message.STATUS_SEND_RECEIVED); this.disconnectSocks5Connections(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index c600162c2..1d30e22bc 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -29,8 +29,8 @@ public class JingleRtpConnection extends AbstractJingleConnection { private State state = State.NULL; - public JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id) { - super(jingleConnectionManager, id); + public JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { + super(jingleConnectionManager, id, initiator); } @Override diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java index a8a366ecf..b90333126 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -8,6 +8,7 @@ import com.google.common.base.Preconditions; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import rocks.xmpp.addr.Jid; public class JinglePacket extends IqPacket { @@ -52,9 +53,21 @@ public class JinglePacket extends IqPacket { jingle.addChild(reason); } + //RECOMMENDED for session-initiate, NOT RECOMMENDED otherwise + public void setInitiator(final Jid initiator) { + Preconditions.checkArgument(initiator.isFullJid(), "initiator should be a full JID"); + findChild("jingle", Namespace.JINGLE).setAttribute("initiator", initiator.toEscapedString()); + } + + //RECOMMENDED for session-accept, NOT RECOMMENDED otherwise + public void setResponder(Jid responder) { + Preconditions.checkArgument(responder.isFullJid(), "responder should be a full JID"); + findChild("jingle", Namespace.JINGLE).setAttribute("responder", responder.toEscapedString()); + } + public Element getJingleChild(final String name) { final Element jingle = findChild("jingle", Namespace.JINGLE); - return jingle == null ? null : jingle.findChild(name); + return jingle == null ? null : jingle.findChild(name); } public void setJingleChild(final Element child) { From 43cf1783a4b6e7db845a351671a22a1146c0f1b7 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 3 Apr 2020 10:46:42 +0200 Subject: [PATCH 026/182] support multiple jingle contents --- .../xmpp/jingle/JingleConnectionManager.java | 4 +- .../xmpp/jingle/JingleRtpConnection.java | 137 ++++++++++++++++-- .../xmpp/jingle/stanzas/Content.java | 4 + .../xmpp/jingle/stanzas/JinglePacket.java | 17 +++ 4 files changed, 147 insertions(+), 15 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 20fbae868..ae951ddfe 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -81,7 +81,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { final AbstractJingleConnection existingJingleConnection = connections.get(id); if (existingJingleConnection != null) { if (existingJingleConnection instanceof JingleRtpConnection) { - ((JingleRtpConnection) existingJingleConnection).deliveryMessage(to, from, message); + ((JingleRtpConnection) existingJingleConnection).deliveryMessage(from, message); } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + existingJingleConnection.getClass().getName() + " does not support jingle messages"); } @@ -91,7 +91,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, with); this.connections.put(id, rtpConnection); - rtpConnection.deliveryMessage(to, from, message); + rtpConnection.deliveryMessage(from, message); } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to react to proposed " + namespace + " session"); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 1d30e22bc..d178fd2f5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -2,8 +2,12 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Log; +import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; + +import org.checkerframework.checker.nullness.compatqual.NullableDecl; import java.util.Collection; import java.util.Map; @@ -11,7 +15,12 @@ import java.util.Map; import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.jingle.stanzas.Content; +import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; +import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; +import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; @@ -23,6 +32,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { final ImmutableMap.Builder> transitionBuilder = new ImmutableMap.Builder<>(); transitionBuilder.put(State.NULL, ImmutableList.of(State.PROPOSED, State.SESSION_INITIALIZED)); transitionBuilder.put(State.PROPOSED, ImmutableList.of(State.ACCEPTED, State.PROCEED)); + transitionBuilder.put(State.PROCEED, ImmutableList.of(State.SESSION_INITIALIZED)); VALID_TRANSITIONS = transitionBuilder.build(); } @@ -36,28 +46,91 @@ public class JingleRtpConnection extends AbstractJingleConnection { @Override void deliverPacket(final JinglePacket jinglePacket) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": packet delivered to JingleRtpConnection"); - Log.d(Config.LOGTAG, jinglePacket.toString()); + switch (jinglePacket.getAction()) { + case SESSION_INITIATE: + receiveSessionInitiate(jinglePacket); + break; + default: + Log.d(Config.LOGTAG, String.format("%s: received unhandled jingle action %s", id.account.getJid().asBareJid(), jinglePacket.getAction())); + break; + } } - void deliveryMessage(final Jid to, final Jid from, final Element message) { + private void receiveSessionInitiate(final JinglePacket jinglePacket) { + if (isInitiator()) { + Log.d(Config.LOGTAG, String.format("%s: received session-initiate even though we were initiating", id.account.getJid().asBareJid())); + //TODO respond with out-of-order + return; + } + final Map contents; + try { + contents = DescriptionTransport.of(jinglePacket.getJingleContents()); + } catch (IllegalArgumentException | NullPointerException e) { + Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": improperly formatted contents",e); + return; + } + Log.d(Config.LOGTAG,"processing session-init with "+contents.size()+" contents"); + final State oldState = this.state; + if (transition(State.SESSION_INITIALIZED)) { + if (oldState == State.PROCEED) { + sendSessionAccept(); + } else { + //TODO start ringing + } + } else { + Log.d(Config.LOGTAG, String.format("%s: received session-initiate while in state %s", id.account.getJid().asBareJid(), state)); + } + } + + void deliveryMessage(final Jid from, final Element message) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": delivered message to JingleRtpConnection " + message); - final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); switch (message.getName()) { case "propose": - if (originatedFromMyself) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": saw proposal from mysql. ignoring"); - } else if (transition(State.PROPOSED)) { - //TODO start ringing or something - pickUpCall(); - } else { - Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring session proposal because already in " + state); - } + receivePropose(from, message); break; + case "proceed": + receiveProceed(from, message); default: break; } } + private void receivePropose(final Jid from, final Element propose) { + final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); + if (originatedFromMyself) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": saw proposal from mysql. ignoring"); + } else if (transition(State.PROPOSED)) { + //TODO start ringing or something + pickUpCall(); + } else { + Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring session proposal because already in " + state); + } + } + + private void receiveProceed(final Jid from, final Element proceed) { + if (from.equals(id.with)) { + if (isInitiator()) { + if (transition(State.SESSION_INITIALIZED)) { + this.sendSessionInitiate(); + } else { + Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because already in %s", id.account.getJid().asBareJid(), this.state)); + } + } else { + Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because we were not initializing", id.account.getJid().asBareJid())); + } + } else { + Log.d(Config.LOGTAG, String.format("%s: ignoring proceed from %s. was expected from %s", id.account.getJid().asBareJid(), from, id.with)); + } + } + + private void sendSessionInitiate() { + + } + + private void sendSessionAccept() { + Log.d(Config.LOGTAG,"sending session-accept"); + } + public void pickUpCall() { switch (this.state) { case PROPOSED: @@ -75,8 +148,8 @@ public class JingleRtpConnection extends AbstractJingleConnection { transitionOrThrow(State.PROCEED); final MessagePacket messagePacket = new MessagePacket(); messagePacket.setTo(id.with); - //Note that Movim needs 'accept' - messagePacket.addChild("proceed", Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId); + //Note that Movim needs 'accept', correct is 'proceed' https://github.com/movim/movim/issues/916 + messagePacket.addChild("accept", Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId); Log.d(Config.LOGTAG, messagePacket.toString()); xmppConnectionService.sendMessagePacket(id.account, messagePacket); } @@ -102,4 +175,42 @@ public class JingleRtpConnection extends AbstractJingleConnection { } } + private static class DescriptionTransport { + private final RtpDescription description; + private final IceUdpTransportInfo transport; + + public DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) { + this.description = description; + this.transport = transport; + } + + public static DescriptionTransport of(final Content content) { + final GenericDescription description = content.getDescription(); + final GenericTransportInfo transportInfo = content.getTransport(); + final RtpDescription rtpDescription; + final IceUdpTransportInfo iceUdpTransportInfo; + if (description instanceof RtpDescription) { + rtpDescription = (RtpDescription) description; + } else { + throw new IllegalArgumentException("Content does not contain RtpDescription"); + } + if (transportInfo instanceof IceUdpTransportInfo) { + iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo; + } else { + throw new IllegalArgumentException("Content does not contain ICE-UDP transport"); + } + return new DescriptionTransport(rtpDescription, iceUdpTransportInfo); + } + + public static Map of(final Map contents) { + return Maps.transformValues(contents, new Function() { + @NullableDecl + @Override + public DescriptionTransport apply(@NullableDecl Content content) { + return content == null ? null : of(content); + } + }); + } + } + } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index ad16041a3..a815155dd 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -30,6 +30,10 @@ public class Content extends Element { return content; } + public String getContentName() { + return this.getAttribute("name"); + } + public Creator getCreator() { return Creator.of(getAttribute("creator")); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java index b90333126..a76c841bf 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -1,10 +1,15 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; import android.support.annotation.NonNull; +import android.util.Log; import com.google.common.base.CaseFormat; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import java.util.Map; + +import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.stanzas.IqPacket; @@ -39,6 +44,18 @@ public class JinglePacket extends IqPacket { return content == null ? null : Content.upgrade(content); } + public Map getJingleContents() { + final Element jingle = findChild("jingle", Namespace.JINGLE); + ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + for (final Element child : jingle.getChildren()) { + if ("content".equals(child.getName())) { + final Content content = Content.upgrade(child); + builder.put(content.getContentName(), content); + } + } + return builder.build(); + } + public void setJingleContent(final Content content) { //take content interface setJingleChild(content); } From 4e13893662aead99be2615ed58ce5e848c01ef01 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 3 Apr 2020 15:25:19 +0200 Subject: [PATCH 027/182] =?UTF-8?q?create=20stub=20objects=20for=20most=20?= =?UTF-8?q?of=20what=E2=80=99s=20in=20description=20and=20transport?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eu/siacs/conversations/xml/Namespace.java | 1 + .../xmpp/jingle/JingleRtpConnection.java | 25 ++- .../conversations/xmpp/jingle/SdpUtils.java | 11 ++ .../xmpp/jingle/stanzas/Content.java | 6 +- .../jingle/stanzas/IceUdpTransportInfo.java | 118 +++++++++++++ .../xmpp/jingle/stanzas/JinglePacket.java | 2 +- .../xmpp/jingle/stanzas/RtpDescription.java | 157 ++++++++++++++++++ 7 files changed, 314 insertions(+), 6 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/SdpUtils.java diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 40a89cb07..237cb4070 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -38,6 +38,7 @@ public final class Namespace { public static final String JINGLE_APPS_DTLS = "urn:xmpp:jingle:apps:dtls:0"; public static final String JINGLE_FEATURE_AUDIO = "urn:xmpp:jingle:apps:rtp:audio"; public static final String JINGLE_FEATURE_VIDEO = "urn:xmpp:jingle:apps:rtp:video"; + public static final String JINGLE_RTP_HEADER_EXTENSIONS = "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"; public static final String IBB = "http://jabber.org/protocol/ibb"; public static final String PING = "urn:xmpp:ping"; public static final String PUSH = "urn:xmpp:push:0"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index d178fd2f5..df2fb9823 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -73,6 +73,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { final State oldState = this.state; if (transition(State.SESSION_INITIALIZED)) { if (oldState == State.PROCEED) { + processContents(contents); sendSessionAccept(); } else { //TODO start ringing @@ -82,6 +83,23 @@ public class JingleRtpConnection extends AbstractJingleConnection { } } + private void processContents(final Map contents) { + for(Map.Entry content : contents.entrySet()) { + final DescriptionTransport descriptionTransport = content.getValue(); + final RtpDescription rtpDescription = descriptionTransport.description; + Log.d(Config.LOGTAG,"receive content with name "+content.getKey()+" and media="+rtpDescription.getMedia()); + for(RtpDescription.PayloadType payloadType : rtpDescription.getPayloadTypes()) { + Log.d(Config.LOGTAG,"payload type: "+payloadType.toString()); + } + for(RtpDescription.RtpHeaderExtension extension : rtpDescription.getHeaderExtensions()) { + Log.d(Config.LOGTAG,"extension: "+extension.toString()); + } + final IceUdpTransportInfo iceUdpTransportInfo = descriptionTransport.transport; + Log.d(Config.LOGTAG,"transport: "+descriptionTransport.transport); + Log.d(Config.LOGTAG,"fingerprint "+iceUdpTransportInfo.getFingerprint()); + } + } + void deliveryMessage(final Jid from, final Element message) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": delivered message to JingleRtpConnection " + message); switch (message.getName()) { @@ -175,7 +193,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { } } - private static class DescriptionTransport { + public static class DescriptionTransport { private final RtpDescription description; private final IceUdpTransportInfo transport; @@ -192,6 +210,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { if (description instanceof RtpDescription) { rtpDescription = (RtpDescription) description; } else { + Log.d(Config.LOGTAG,"description was "+description); throw new IllegalArgumentException("Content does not contain RtpDescription"); } if (transportInfo instanceof IceUdpTransportInfo) { @@ -203,13 +222,13 @@ public class JingleRtpConnection extends AbstractJingleConnection { } public static Map of(final Map contents) { - return Maps.transformValues(contents, new Function() { + return ImmutableMap.copyOf(Maps.transformValues(contents, new Function() { @NullableDecl @Override public DescriptionTransport apply(@NullableDecl Content content) { return content == null ? null : of(content); } - }); + })); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SdpUtils.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SdpUtils.java new file mode 100644 index 000000000..f70f3d299 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SdpUtils.java @@ -0,0 +1,11 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.util.Map; + +public class SdpUtils { + + public static String toSdpString(Map contents) { + return ""; + } + +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index a815155dd..f27efb1e2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -51,9 +51,11 @@ public class Content extends Element { if (description == null) { return null; } - final String xmlns = description.getNamespace(); - if (FileTransferDescription.NAMESPACES.contains(xmlns)) { + final String namespace = description.getNamespace(); + if (FileTransferDescription.NAMESPACES.contains(namespace)) { return FileTransferDescription.upgrade(description); + } else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { + return RtpDescription.upgrade(description); } else { return GenericDescription.upgrade(description); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index 00beac65c..84734b924 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -1,6 +1,9 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import java.util.List; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; @@ -11,6 +14,21 @@ public class IceUdpTransportInfo extends GenericTransportInfo { super(name, xmlns); } + public Fingerprint getFingerprint() { + final Element fingerprint = this.findChild("fingerprint", Namespace.JINGLE_APPS_DTLS); + return fingerprint == null ? null : Fingerprint.upgrade(fingerprint); + } + + public List getCandidates() { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for(final Element child : getChildren()) { + if ("candidate".equals(child.getName())) { + builder.add(Candidate.upgrade(child)); + } + } + return builder.build(); + } + public static IceUdpTransportInfo upgrade(final Element element) { Preconditions.checkArgument("transport".equals(element.getName()), "Name of provided element is not transport"); Preconditions.checkArgument(Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(element.getNamespace()), "Element does not match ice-udp transport namespace"); @@ -19,4 +37,104 @@ public class IceUdpTransportInfo extends GenericTransportInfo { transportInfo.setChildren(element.getChildren()); return transportInfo; } + + public static class Candidate extends Element { + + private Candidate() { + super("candidate"); + } + + public int getComponent() { + return getAttributeAsInt("component"); + } + + public int getFoundation() { + return getAttributeAsInt("foundation"); + } + + public int getGeneration() { + return getAttributeAsInt("generation"); + } + + public String getId() { + return getAttribute("id"); + } + + public String getIp() { + return getAttribute("ip"); + } + + public int getNetwork() { + return getAttributeAsInt("network"); + } + + public int getPort() { + return getAttributeAsInt("port"); + } + + public int getPriority() { + return getAttributeAsInt("priority"); + } + + public String getProtocol() { + return getAttribute("protocol"); + } + + public String getRelAddr() { + return getAttribute("rel-addr"); + } + + public int getRelPort() { + return getAttributeAsInt("rel-port"); + } + + public String getType() { //TODO might be converted to enum + return getAttribute("type"); + } + + private int getAttributeAsInt(final String name) { + final String value = this.getAttribute(name); + if (value == null) { + return 0; + } + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return 0; + } + } + + public static Candidate upgrade(final Element element) { + Preconditions.checkArgument("candidate".equals(element.getName())); + final Candidate candidate = new Candidate(); + candidate.setAttributes(element.getAttributes()); + candidate.setChildren(element.getChildren()); + return candidate; + } + } + + + public static class Fingerprint extends Element { + + public String getHash() { + return this.getAttribute("hash"); + } + + public String getSetup() { + return this.getAttribute("setup"); + } + + private Fingerprint() { + super("fingerprint", Namespace.JINGLE_APPS_DTLS); + } + + public static Fingerprint upgrade(final Element element) { + Preconditions.checkArgument("fingerprint".equals(element.getName())); + Preconditions.checkArgument(Namespace.JINGLE_APPS_DTLS.equals(element.getNamespace())); + final Fingerprint fingerprint = new Fingerprint(); + fingerprint.setAttributes(element.getAttributes()); + fingerprint.setContent(element.getContent()); + return fingerprint; + } + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java index a76c841bf..e4fb88ecb 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -38,7 +38,7 @@ public class JinglePacket extends IqPacket { return jinglePacket; } - //TODO can have multiple contents + //TODO deprecate this somehow and make file transfer fail if there are multiple (or something) public Content getJingleContent() { final Element content = getJingleChild("content"); return content == null ? null : Content.upgrade(content); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index fd66f00ee..15a208205 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -1,6 +1,10 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Locale; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; @@ -12,6 +16,30 @@ public class RtpDescription extends GenericDescription { super(name, namespace); } + public Media getMedia() { + return Media.of(this.getAttribute("media")); + } + + public List getPayloadTypes() { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for(Element child : getChildren()) { + if ("payload-type".equals(child.getName())) { + builder.add(PayloadType.of(child)); + } + } + return builder.build(); + } + + public List getHeaderExtensions() { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for(final Element child : getChildren()) { + if ("rtp-hdrext".equals(child.getName()) && Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(child.getNamespace())) { + builder.add(RtpHeaderExtension.upgrade(child)); + } + } + return builder.build(); + } + public static RtpDescription upgrade(final Element element) { Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description"); Preconditions.checkArgument(Namespace.JINGLE_APPS_RTP.equals(element.getNamespace()), "Element does not match the jingle rtp namespace"); @@ -20,4 +48,133 @@ public class RtpDescription extends GenericDescription { description.setChildren(element.getChildren()); return description; } + + //TODO: support for https://xmpp.org/extensions/xep-0293.html + + + public static class RtpHeaderExtension extends Element { + + private RtpHeaderExtension() { + super("rtp-hdrext", Namespace.JINGLE_RTP_HEADER_EXTENSIONS); + } + + public String getId() { + return this.getAttribute("id"); + } + + public String getUri() { + return this.getAttribute("uri"); + } + + public static RtpHeaderExtension upgrade(final Element element) { + Preconditions.checkArgument("rtp-hdrext".equals(element.getName())); + Preconditions.checkArgument(Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(element.getNamespace())); + final RtpHeaderExtension extension = new RtpHeaderExtension(); + extension.setAttributes(element.getAttributes()); + extension.setChildren(element.getChildren()); + return extension; + } + } + + public static class PayloadType extends Element { + + private PayloadType(String name, String xmlns) { + super(name, xmlns); + } + public String getId() { + return this.getAttribute("id"); + } + + public String getPayloadTypeName() { + return this.getAttribute("name"); + } + + public int getClockRate() { + final String clockRate = this.getAttribute("clockrate"); + if (clockRate == null) { + return 0; + } + try { + return Integer.parseInt(clockRate); + } catch (NumberFormatException e) { + return 0; + } + } + + public int getChannels() { + final String channels = this.getAttribute("channels"); + if (channels == null) { + return 1; // The number of channels; if omitted, it MUST be assumed to contain one channel + } + try { + return Integer.parseInt(channels); + } catch (NumberFormatException e) { + return 1; + } + } + + public List getParameters() { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for (Element child : getChildren()) { + if ("parameter".equals(child.getName())) { + builder.add(Parameter.of(child)); + } + } + return builder.build(); + } + + public static PayloadType of(final Element element) { + Preconditions.checkArgument("payload-type".equals(element.getName()), "element name must be called payload-type"); + PayloadType payloadType = new PayloadType("payload-type", Namespace.JINGLE_APPS_RTP); + payloadType.setAttributes(element.getAttributes()); + payloadType.setChildren(element.getChildren()); + return payloadType; + } + } + + public static class Parameter extends Element { + + private Parameter() { + super("parameter", Namespace.JINGLE_APPS_RTP); + } + + public Parameter(String name, String value) { + super("parameter", Namespace.JINGLE_APPS_RTP); + this.setAttribute("name", name); + this.setAttribute("value", value); + } + + public String getParameterName() { + return this.getAttribute("name"); + } + + public String getParameterValue() { + return this.getAttribute("value"); + } + + public static Parameter of(final Element element) { + Preconditions.checkArgument("parameter".equals(element.getName()), "element name must be called parameter"); + Parameter parameter = new Parameter(); + parameter.setAttributes(element.getAttributes()); + parameter.setChildren(element.getChildren()); + return parameter; + } + } + + public enum Media { + VIDEO, AUDIO, UNKNOWN; + + @Override + public String toString() { + return super.toString().toLowerCase(Locale.ROOT); + } + + public static Media of(String value) { + try { + return value == null ? UNKNOWN : Media.valueOf(value.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + return UNKNOWN; + } + } + } } From 766d1d603e29c21972e86933b75f1abf1c9d22aa Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 3 Apr 2020 16:44:29 +0200 Subject: [PATCH 028/182] show preliminary call button if contact supports it --- .../ui/ConversationFragment.java | 4 + .../xmpp/jingle/RtpCapability.java | 58 +++++++++++++ .../res/drawable-hdpi/ic_call_white_24dp.png | Bin 0 -> 340 bytes .../res/drawable-mdpi/ic_call_white_24dp.png | Bin 0 -> 246 bytes .../res/drawable-xhdpi/ic_call_white_24dp.png | Bin 0 -> 420 bytes .../drawable-xxhdpi/ic_call_white_24dp.png | Bin 0 -> 597 bytes .../drawable-xxxhdpi/ic_call_white_24dp.png | Bin 0 -> 778 bytes src/main/res/menu/fragment_conversation.xml | 76 ++++++++++-------- src/main/res/values/attrs.xml | 1 + src/main/res/values/strings.xml | 1 + src/main/res/values/themes.xml | 2 + 11 files changed, 107 insertions(+), 35 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java create mode 100644 src/main/res/drawable-hdpi/ic_call_white_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_call_white_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_call_white_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_call_white_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_call_white_24dp.png diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 9d9ebf819..11f8274eb 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -116,6 +116,7 @@ import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jingle.JingleFileTransferConnection; +import eu.siacs.conversations.xmpp.jingle.RtpCapability; import rocks.xmpp.addr.Jid; import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT; @@ -950,6 +951,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke final MenuItem menuInviteContact = menu.findItem(R.id.action_invite); final MenuItem menuMute = menu.findItem(R.id.action_mute); final MenuItem menuUnmute = menu.findItem(R.id.action_unmute); + final MenuItem menuCall = menu.findItem(R.id.action_call); if (conversation != null) { @@ -957,7 +959,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke menuContactDetails.setVisible(false); menuInviteContact.setVisible(conversation.getMucOptions().canInvite()); menuMucDetails.setTitle(conversation.getMucOptions().isPrivateAndNonAnonymous() ? R.string.action_muc_details : R.string.channel_details); + menuCall.setVisible(false); } else { + menuCall.setVisible(RtpCapability.check(conversation.getContact()) != RtpCapability.Capability.NONE); menuContactDetails.setVisible(!this.conversation.withSelf()); menuMucDetails.setVisible(false); final XmppConnectionService service = activity.xmppConnectionService; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java new file mode 100644 index 000000000..646137495 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java @@ -0,0 +1,58 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Presence; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.entities.ServiceDiscoveryResult; +import eu.siacs.conversations.xml.Namespace; + +public class RtpCapability { + + private static List BASIC_RTP_REQUIREMENTS = Arrays.asList( + Namespace.JINGLE, + Namespace.JINGLE_TRANSPORT_ICE_UDP, + Namespace.JINGLE_APPS_RTP, + Namespace.JINGLE_APPS_DTLS + ); + private static List VIDEO_REQUIREMENTS = Arrays.asList( + Namespace.JINGLE_FEATURE_AUDIO, + Namespace.JINGLE_FEATURE_VIDEO + ); + + public static Capability check(final Presence presence) { + final ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult(); + final List features = disco == null ? Collections.emptyList() : disco.getFeatures(); + if (features.containsAll(BASIC_RTP_REQUIREMENTS)) { + if (features.containsAll(VIDEO_REQUIREMENTS)) { + return Capability.VIDEO; + } + if (features.contains(Namespace.JINGLE_FEATURE_AUDIO)) { + return Capability.AUDIO; + } + } + return Capability.NONE; + } + + public static Capability check(final Contact contact) { + final Presences presences = contact.getPresences(); + Capability result = Capability.NONE; + for(Presence presence : presences.getPresences().values()) { + Capability capability = check(presence); + if (capability == Capability.VIDEO) { + result = capability; + } else if (capability == Capability.AUDIO && result == Capability.NONE) { + result = capability; + } + } + return result; + } + + public enum Capability { + NONE, AUDIO, VIDEO + } + +} diff --git a/src/main/res/drawable-hdpi/ic_call_white_24dp.png b/src/main/res/drawable-hdpi/ic_call_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4dc5065155baeba719d76845d4398431c289cde0 GIT binary patch literal 340 zcmV-a0jvIrP)2;3l0LOD$N=(0Aydxv3a1NO%A-5V49kkQ!=X zV`%)ixtjF0)a^NIzfT&=3_}@<#bU8k$;l06(wcI^K4~{{!wzW=azjYkD}h|FPQL}Y zV3vL*xgg+~w1(VpOxmv85YnrTksES+&{fCC9j7Gf82Mn8*e6T!#Wk@r`JzB#L%wJb zYcnT5dC{Hk}*!A?jJR>)4GKBfXec?}<0&9upF*X8-^I07*qoM6N<$f?IEA=l}o! literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_call_white_24dp.png b/src/main/res/drawable-xhdpi/ic_call_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ef45e933a99b720cc5f6127e6da22bc2fa679244 GIT binary patch literal 420 zcmV;V0bBlwP)u7H4zjb0wVlEO#(-afhJ{~L_owyniLh<44m_hh=0)JC~(EtGz}i`o*rY-v?y@O z7&HsExZpoDD~cS_W+YkQAr(Fol6E6IIG(V_N51l%CO?U|VOMi=rhWlQl3!NbcvwOJ O0000IqP)a~F^ihkCNXidBsWB>X=;U*@F%!%#fVZaU}txjgmm7*kE>_wfD zJ4vghX)Y+W+A>RYW}(s9ms0kgvz_-nXXic7bMt;aQYw{7rQ+a|A;-#I(=OY{5am0Y zWfwW3^~f&rM7t`x_(`-r*+q_ML$ZrWqCJ#dj1g^AcJY*GIgD&#kT@-}i3`LzE}J+^ zoB`QH3vni76Nb;kX_ZZk5a)z!;v#YO$R>6Z?LI~}VVI$`08h4Yi&6qS*+(ZU3-IIu z>zHA=08g&aOR)e??y#Lg0iN9A2}>4ekZW`hc1f;bc*&AEHpw*(5q4XyVR%khr(9zf zd6rDFUaoP2usayJMLpAmosw%DC2WCxa*ZLvKCxA9QO8@t-q9$x*vU7-#@QgZ=w^|y zaT*l^!(L9ZMYTZ>h2FDG5%3w|pEvY#jAo3IgrT28vm8(f_+%;eg;zXakY28EhGta@ z!!U(@aT=p#@W~Qqp8cv8YZ#{3JsMO1pDg7VQ}r-hr`Qx-`VXHhbwLu%tSQXU{ z2YEroR7)7z38;u_k5;bnkuu2+*3!Wc)5OWiCJb$y=Ml4%G9VW)G||O5GCXFK4}9eZ jd2$TkDV0j4QgQwO?=x#gyQpL<00000NkvXXu0mjf@K6c} literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_call_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_call_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..b0e020573d37e8b4acac23fcd3e01cc39531b5e4 GIT binary patch literal 778 zcmV+l1NHogP)pE0}6<4A=MUgDB6h!xmw?$P#D_ykG zL=hFSC`v&PZLuhB6vP_@=VGGKX8IHCrk*(yzZvID{vUkLBLzVa1VIo437F3S<}L zr)ZZ=%%^~hA`4^@1GpNLMLfgRh%BOnyC&Jg3*1eTJrr;^UH0$M-cTV>3Gwxo}CTrME zHM(UD8LF{E*6;z(an~(-ID@NuOqM-t!_8Bc$|63;MUD>HM8qB5B}b?1Vi)7f(<#eX z$ao5L$u=TxQb~b~>|-0rie%*jX7ZA9MV8AK{777pFXRuF5qDC4;R10P`GnQPT_ln} zi1>rJwekty5O<$e`GbfH#O;+&SVD=n{ma+#35SWhOPl<`Y=(&ICz3zdK)H?b2N7q9 z8>LJB;8XI%J>VnxgC5FVqgDRk80F5=B403-KPmSM&GH4Gk)zykn&k_=WsGva(xNKR z%J=lq!!$J>n@MtxRyiWeBmVObzthK;OjaYYha}heSPsZiAnp~{I7%Nqe8pU5F@+|j zhloQYc}Pa@K$Ze-MmR;g(m@mbBpGEhkt!lXfoc?4pmfkoKgoXLLnR_Zo@xy$Ei`eE zWcOI1GIWurdLv2?5xc2mKW+LSy2w)#rHf6Bk?aBMi1Y@XTphVqhBCv?_)QKhv zE4e})XyOpDiu1fDO*EF!&nPvM1 + xmlns:app="http://schemas.android.com/apk/res-auto"> + android:title="@string/action_secure" + app:showAsAction="always"> - + + android:title="@string/encryption_choice_unencrypted" /> + android:title="@string/encryption_choice_omemo" /> + android:title="@string/encryption_choice_pgp" /> @@ -25,76 +25,82 @@ android:id="@+id/action_attach_file" android:icon="?attr/icon_new_attachment" android:orderInCategory="30" - app:showAsAction="always" - android:title="@string/attach_file"> + android:title="@string/attach_file" + app:showAsAction="always"> + android:icon="?attr/ic_attach_document" + android:title="@string/choose_file" /> + android:icon="?attr/ic_attach_photo" + android:title="@string/attach_choose_picture" /> + android:icon="?attr/ic_attach_camera" + android:title="@string/attach_take_picture" /> + android:icon="?attr/ic_attach_videocam" + android:title="@string/attach_record_video" /> + android:icon="?attr/ic_attach_record" + android:title="@string/attach_record_voice" /> + android:icon="?attr/ic_attach_location" + android:title="@string/send_location" /> + + android:title="@string/action_contact_details" + app:showAsAction="never" /> + android:title="@string/action_muc_details" + app:showAsAction="never" /> + android:title="@string/invite_contact" + app:showAsAction="never" /> + android:title="@string/action_clear_history" + app:showAsAction="never" /> + android:title="@string/action_end_conversation" + app:showAsAction="never" /> + android:title="@string/disable_notifications" + app:showAsAction="never" /> + android:title="@string/enable_notifications" + app:showAsAction="never" /> \ No newline at end of file diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml index ce15d0013..3eab0fecf 100644 --- a/src/main/res/values/attrs.xml +++ b/src/main/res/values/attrs.xml @@ -85,6 +85,7 @@ + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index ef5dbbf0d..d91374ade 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -884,6 +884,7 @@ Backup About Please enable an account + Make call View %1$d Participant View %1$d Participants diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index 219fb6702..454f0ca6c 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -93,6 +93,7 @@ @drawable/ic_refresh_black_24dp @drawable/ic_attach_file_white_24dp @drawable/ic_lock_open_white_24dp + @drawable/ic_call_white_24dp @drawable/ic_delete_black_24dp @drawable/ic_search_white_24dp @drawable/ic_lock_open_white_24dp @@ -207,6 +208,7 @@ @drawable/ic_refresh_white_24dp @drawable/ic_attach_file_white_24dp @drawable/ic_lock_open_white_24dp + @drawable/ic_call_white_24dp @drawable/ic_delete_white_24dp @drawable/ic_search_white_24dp @drawable/ic_lock_open_white_24dp From e2e4390d51537b19ba6518e178a27e656a044b3d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 4 Apr 2020 10:45:42 +0200 Subject: [PATCH 029/182] untested sdp parser --- .../xmpp/jingle/MediaBuilder.java | 46 ++++++ .../conversations/xmpp/jingle/SdpUtils.java | 11 -- .../xmpp/jingle/SessionDescription.java | 145 ++++++++++++++++++ .../jingle/SessionDescriptionBuilder.java | 40 +++++ 4 files changed, 231 insertions(+), 11 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java delete mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/SdpUtils.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescriptionBuilder.java diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java new file mode 100644 index 000000000..e92c6b100 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java @@ -0,0 +1,46 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.util.List; + +public class MediaBuilder { + private String media; + private int port; + private String protocol; + private List formats; + private String connectionData; + private List attributes; + + public MediaBuilder setMedia(String media) { + this.media = media; + return this; + } + + public MediaBuilder setPort(int port) { + this.port = port; + return this; + } + + public MediaBuilder setProtocol(String protocol) { + this.protocol = protocol; + return this; + } + + public MediaBuilder setFormats(List formats) { + this.formats = formats; + return this; + } + + public MediaBuilder setConnectionData(String connectionData) { + this.connectionData = connectionData; + return this; + } + + public MediaBuilder setAttributes(List attributes) { + this.attributes = attributes; + return this; + } + + public SessionDescription.Media createMedia() { + return new SessionDescription.Media(media, port, protocol, formats, connectionData, attributes); + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SdpUtils.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SdpUtils.java deleted file mode 100644 index f70f3d299..000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SdpUtils.java +++ /dev/null @@ -1,11 +0,0 @@ -package eu.siacs.conversations.xmpp.jingle; - -import java.util.Map; - -public class SdpUtils { - - public static String toSdpString(Map contents) { - return ""; - } - -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java new file mode 100644 index 000000000..2a129b3e8 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -0,0 +1,145 @@ +package eu.siacs.conversations.xmpp.jingle; + +import android.util.Log; + +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Map; + +import eu.siacs.conversations.Config; + +public class SessionDescription { + + private final int version; + private final String name; + private final String connectionData; + private final List attributes; + private final List media; + + + public SessionDescription(int version, String name, String connectionData, List attributes, List media) { + this.version = version; + this.name = name; + this.connectionData = connectionData; + this.attributes = attributes; + this.media = media; + } + + public static SessionDescription parse(final Map contents) { + final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder(); + return sessionDescriptionBuilder.createSessionDescription(); + } + + public static SessionDescription parse(final String input) { + final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder(); + MediaBuilder currentMediaBuilder = null; + ImmutableList.Builder attributeBuilder = new ImmutableList.Builder<>(); + ImmutableList.Builder mediaBuilder = new ImmutableList.Builder<>(); + for(final String line : input.split("\n")) { + final String[] pair = line.split("=",2); + if (pair.length < 2 || pair[0].length() != 1) { + Log.d(Config.LOGTAG,"skipping sdp parsing on line "+line); + continue; + } + final char key = pair[0].charAt(0); + final String value = pair[1]; + switch (key) { + case 'v': + sessionDescriptionBuilder.setVersion(ignorantIntParser(value)); + break; + case 'c': + if (currentMediaBuilder != null) { + currentMediaBuilder.setConnectionData(value); + } else { + sessionDescriptionBuilder.setConnectionData(value); + } + break; + case 's': + sessionDescriptionBuilder.setName(value); + break; + case 'a': + attributeBuilder.add(Attribute.parse(value)); + break; + case 'm': + if (currentMediaBuilder == null) { + sessionDescriptionBuilder.setAttributes(attributeBuilder.build());; + } else { + currentMediaBuilder.setAttributes(attributeBuilder.build()); + mediaBuilder.add(currentMediaBuilder.createMedia()); + } + attributeBuilder = new ImmutableList.Builder<>(); + currentMediaBuilder = new MediaBuilder(); + final String[] parts = value.split(" "); + if (parts.length >= 3) { + currentMediaBuilder.setMedia(parts[0]); + currentMediaBuilder.setPort(ignorantIntParser(parts[1])); + currentMediaBuilder.setProtocol(parts[2]); + ImmutableList.Builder formats = new ImmutableList.Builder<>(); + for(int i = 3; i < parts.length; ++i) { + formats.add(ignorantIntParser(parts[i])); + } + currentMediaBuilder.setFormats(formats.build()); + } else { + Log.d(Config.LOGTAG,"skipping media line "+line); + } + break; + } + + } + if (currentMediaBuilder != null) { + currentMediaBuilder.setAttributes(attributeBuilder.build()); + mediaBuilder.add(currentMediaBuilder.createMedia()); + } + sessionDescriptionBuilder.setMedia(mediaBuilder.build()); + return sessionDescriptionBuilder.createSessionDescription(); + } + + private static int ignorantIntParser(final String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + return 0; + } + } + + public static class Attribute { + private final String key; + private final String value; + + public Attribute(String key, String value) { + this.key = key; + this.value = value; + } + + public static Attribute parse(String input) { + final String[] pair = input.split(":",2); + if (pair.length == 2) { + return new Attribute(pair[0],pair[1]); + } else { + return new Attribute(pair[0], null); + } + } + + + } + + public static class Media { + private final String media; + private final int port; + private final String protocol; + private final List formats; + private final String connectionData; + private final List attributes; + + public Media(String media, int port, String protocol, List formats, String connectionData, List attributes) { + this.media = media; + this.port = port; + this.protocol = protocol; + this.formats = formats; + this.connectionData = connectionData; + this.attributes = attributes; + } + } + +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescriptionBuilder.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescriptionBuilder.java new file mode 100644 index 000000000..d45c53461 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescriptionBuilder.java @@ -0,0 +1,40 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.util.List; + +public class SessionDescriptionBuilder { + private int version; + private String name; + private String connectionData; + private List attributes; + private List media; + + public SessionDescriptionBuilder setVersion(int version) { + this.version = version; + return this; + } + + public SessionDescriptionBuilder setName(String name) { + this.name = name; + return this; + } + + public SessionDescriptionBuilder setConnectionData(String connectionData) { + this.connectionData = connectionData; + return this; + } + + public SessionDescriptionBuilder setAttributes(List attributes) { + this.attributes = attributes; + return this; + } + + public SessionDescriptionBuilder setMedia(List media) { + this.media = media; + return this; + } + + public SessionDescription createSessionDescription() { + return new SessionDescription(version, name, connectionData, attributes, media); + } +} \ No newline at end of file From 3b857e6894df3297496f72ee2df2a51b6111b2b7 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 4 Apr 2020 11:31:53 +0200 Subject: [PATCH 030/182] create temporary RtpSessionPropsoal as placeholder before we can create actual session --- .../generator/MessageGenerator.java | 10 +++ .../ui/ConversationFragment.java | 26 +++++--- .../xmpp/jingle/JingleConnectionManager.java | 65 ++++++++++++++++++- .../xmpp/jingle/JingleRtpConnection.java | 8 +-- 4 files changed, 95 insertions(+), 14 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 56445e0d6..423ed9f91 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -18,6 +18,7 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.chatstate.ChatState; +import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; @@ -231,4 +232,13 @@ public class MessageGenerator extends AbstractGenerator { packet.addChild("store", "urn:xmpp:hints"); return packet; } + + public MessagePacket sessionProposal(JingleConnectionManager.RtpSessionProposal proposal) { + final MessagePacket packet = new MessagePacket(); + packet.setTo(proposal.with); + final Element propose = packet.addChild("propose", Namespace.JINGLE_MESSAGE); + propose.setAttribute("id", proposal.sessionId); + propose.addChild("description", Namespace.JINGLE_APPS_RTP); + return packet; + } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 11f8274eb..bb76d7256 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -199,7 +199,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private OnClickListener acceptJoin = new OnClickListener() { @Override public void onClick(View v) { - conversation.setAttribute("accept_non_anonymous",true); + conversation.setAttribute("accept_non_anonymous", true); activity.xmppConnectionService.updateConversation(conversation); activity.xmppConnectionService.joinMuc(conversation); } @@ -1127,7 +1127,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke showErrorMessage.setVisible(true); } final String mime = m.isFileOrImage() ? m.getMimeType() : null; - if ((m.isGeoUri() && GeoHelper.openInOsmAnd(getActivity(),m)) || (mime != null && mime.startsWith("audio/"))) { + if ((m.isGeoUri() && GeoHelper.openInOsmAnd(getActivity(), m)) || (mime != null && mime.startsWith("audio/"))) { openWith.setVisible(true); } } @@ -1232,12 +1232,20 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke BlockContactDialog.show((XmppActivity) activity, conversation); } break; + case R.id.action_call: + triggerRtpSession(); + break; default: break; } return super.onOptionsItemSelected(item); } + private void triggerRtpSession() { + final Contact contact = conversation.getContact(); + activity.xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(conversation.getAccount(), contact); + } + private void handleAttachmentSelection(MenuItem item) { switch (item.getItemId()) { case R.id.attach_choose_picture: @@ -1431,7 +1439,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } else if (message.treatAsDownloadable() || message.hasFileOnRemoteHost() || MessageUtils.unInitiatedButKnownSize(message)) { createNewConnection(message); } else { - Log.d(Config.LOGTAG,message.getConversation().getAccount()+": unable to start downloadable"); + Log.d(Config.LOGTAG, message.getConversation().getAccount() + ": unable to start downloadable"); } } @@ -1621,7 +1629,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private void openWith(final Message message) { if (message.isGeoUri()) { - GeoHelper.view(getActivity(),message); + GeoHelper.view(getActivity(), message); } else { final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); ViewUtil.view(activity, file); @@ -1641,8 +1649,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } builder.setMessage(displayError); builder.setNegativeButton(R.string.copy_to_clipboard, (dialog, which) -> { - activity.copyTextToClipboard(displayError,R.string.error_message); - Toast.makeText(activity,R.string.error_message_copied_to_clipboard, Toast.LENGTH_SHORT).show(); + activity.copyTextToClipboard(displayError, R.string.error_message); + Toast.makeText(activity, R.string.error_message_copied_to_clipboard, Toast.LENGTH_SHORT).show(); }); builder.setPositiveButton(R.string.confirm, null); builder.create().show(); @@ -2744,10 +2752,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke Log.e(Config.LOGTAG, "cleared pending photo uri"); } if (pendingConversationsUuid.clear()) { - Log.e(Config.LOGTAG,"cleared pending conversations uuid"); + Log.e(Config.LOGTAG, "cleared pending conversations uuid"); } if (pendingMediaPreviews.clear()) { - Log.e(Config.LOGTAG,"cleared pending media previews"); + Log.e(Config.LOGTAG, "cleared pending media previews"); } } @@ -2765,7 +2773,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } final PopupMenu popupMenu = new PopupMenu(getActivity(), v); final Contact contact = message.getContact(); - if (message.getStatus() <= Message.STATUS_RECEIVED && (contact == null || !contact.isSelf())) { + if (message.getStatus() <= Message.STATUS_RECEIVED && (contact == null || !contact.isSelf())) { if (message.getConversation().getMode() == Conversation.MODE_MULTI) { final Jid cp = message.getCounterpart(); if (cp == null || cp.isBareJid()) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index ae951ddfe..146892e82 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -2,15 +2,19 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Log; +import com.google.common.base.Objects; import com.google.common.base.Preconditions; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.services.AbstractConnectionManager; @@ -22,10 +26,12 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; public class JingleConnectionManager extends AbstractConnectionManager { - private Map connections = new ConcurrentHashMap<>(); + private final Set rtpSessionProposals = new HashSet<>(); + private final Map connections = new ConcurrentHashMap<>(); private HashMap primaryCandidates = new HashMap<>(); @@ -95,6 +101,22 @@ public class JingleConnectionManager extends AbstractConnectionManager { } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to react to proposed " + namespace + " session"); } + } else if ("proceed".equals(message.getName())) { + if (!with.equals(from)) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignore carbon copied proceed"); + return; + } + final RtpSessionProposal proposal = new RtpSessionProposal(account, with.asBareJid(), sessionId); + synchronized (rtpSessionProposals) { + if (rtpSessionProposals.remove(proposal)) { + final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, account.getJid()); + this.connections.put(id, rtpConnection); + rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED); + rtpConnection.deliveryMessage(from, message); + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + with); + } + } } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved out of order jingle message"); } @@ -165,6 +187,16 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } + public void proposeJingleRtpSession(final Account account, final Contact contact) { + final RtpSessionProposal proposal = RtpSessionProposal.of(account, contact.getJid().asBareJid()); + synchronized (this.rtpSessionProposals) { + this.rtpSessionProposals.add(proposal); + final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); + Log.d(Config.LOGTAG,messagePacket.toString()); + mXmppConnectionService.sendMessagePacket(account, messagePacket); + } + } + static String nextRandomId() { return UUID.randomUUID().toString(); } @@ -211,4 +243,35 @@ public class JingleConnectionManager extends AbstractConnectionManager { }*/ } } + + public static class RtpSessionProposal { + private final Account account; + public final Jid with; + public final String sessionId; + + private RtpSessionProposal(Account account, Jid with, String sessionId) { + this.account = account; + this.with = with; + this.sessionId = sessionId; + } + + public static RtpSessionProposal of(Account account, Jid with) { + return new RtpSessionProposal(account, with, UUID.randomUUID().toString()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RtpSessionProposal proposal = (RtpSessionProposal) o; + return Objects.equal(account.getJid(), proposal.account.getJid()) && + Objects.equal(with, proposal.with) && + Objects.equal(sessionId, proposal.sessionId); + } + + @Override + public int hashCode() { + return Objects.hashCode(account.getJid(), with, sessionId); + } + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index df2fb9823..399688266 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -128,7 +128,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { private void receiveProceed(final Jid from, final Element proceed) { if (from.equals(id.with)) { if (isInitiator()) { - if (transition(State.SESSION_INITIALIZED)) { + if (transition(State.PROCEED)) { this.sendSessionInitiate(); } else { Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because already in %s", id.account.getJid().asBareJid(), this.state)); @@ -142,7 +142,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { } private void sendSessionInitiate() { - + Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": sending session-initiate"); } private void sendSessionAccept() { @@ -167,7 +167,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { final MessagePacket messagePacket = new MessagePacket(); messagePacket.setTo(id.with); //Note that Movim needs 'accept', correct is 'proceed' https://github.com/movim/movim/issues/916 - messagePacket.addChild("accept", Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId); + messagePacket.addChild("proceed", Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId); Log.d(Config.LOGTAG, messagePacket.toString()); xmppConnectionService.sendMessagePacket(id.account, messagePacket); } @@ -187,7 +187,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { } } - private void transitionOrThrow(final State target) { + public void transitionOrThrow(final State target) { if (!transition(target)) { throw new IllegalStateException(String.format("Unable to transition from %s to %s", this.state, target)); } From 5b1d86d67e65c6ed233055fcdfd5f354223d63f1 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 4 Apr 2020 15:30:13 +0200 Subject: [PATCH 031/182] dummy code to get sdp out of (non-working) libwebrtc --- proguard-rules.pro | 1 + .../xmpp/jingle/JingleRtpConnection.java | 146 ++++++++++++++++-- .../xmpp/jingle/SessionDescription.java | 43 +++--- .../xmpp/jingle/stanzas/RtpDescription.java | 5 + 4 files changed, 159 insertions(+), 36 deletions(-) diff --git a/proguard-rules.pro b/proguard-rules.pro index 78cc2a0f9..d3ac8f30a 100644 --- a/proguard-rules.pro +++ b/proguard-rules.pro @@ -11,6 +11,7 @@ -keep class com.google.android.gms.** -keep class org.openintents.openpgp.* +-keep class org.webrtc.** { *; } -dontwarn org.bouncycastle.mail.** -dontwarn org.bouncycastle.x509.util.LDAPStoreHelper diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 399688266..3424b1ec7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -8,8 +8,19 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import org.checkerframework.checker.nullness.compatqual.NullableDecl; +import org.webrtc.AudioSource; +import org.webrtc.AudioTrack; +import org.webrtc.DataChannel; +import org.webrtc.IceCandidate; +import org.webrtc.MediaConstraints; +import org.webrtc.MediaStream; +import org.webrtc.PeerConnection; +import org.webrtc.PeerConnectionFactory; +import org.webrtc.RtpReceiver; +import org.webrtc.SdpObserver; import java.util.Collection; +import java.util.Collections; import java.util.Map; import eu.siacs.conversations.Config; @@ -66,10 +77,10 @@ public class JingleRtpConnection extends AbstractJingleConnection { try { contents = DescriptionTransport.of(jinglePacket.getJingleContents()); } catch (IllegalArgumentException | NullPointerException e) { - Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": improperly formatted contents",e); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); return; } - Log.d(Config.LOGTAG,"processing session-init with "+contents.size()+" contents"); + Log.d(Config.LOGTAG, "processing session-init with " + contents.size() + " contents"); final State oldState = this.state; if (transition(State.SESSION_INITIALIZED)) { if (oldState == State.PROCEED) { @@ -83,20 +94,20 @@ public class JingleRtpConnection extends AbstractJingleConnection { } } - private void processContents(final Map contents) { - for(Map.Entry content : contents.entrySet()) { + private void processContents(final Map contents) { + for (Map.Entry content : contents.entrySet()) { final DescriptionTransport descriptionTransport = content.getValue(); final RtpDescription rtpDescription = descriptionTransport.description; - Log.d(Config.LOGTAG,"receive content with name "+content.getKey()+" and media="+rtpDescription.getMedia()); - for(RtpDescription.PayloadType payloadType : rtpDescription.getPayloadTypes()) { - Log.d(Config.LOGTAG,"payload type: "+payloadType.toString()); + Log.d(Config.LOGTAG, "receive content with name " + content.getKey() + " and media=" + rtpDescription.getMedia()); + for (RtpDescription.PayloadType payloadType : rtpDescription.getPayloadTypes()) { + Log.d(Config.LOGTAG, "payload type: " + payloadType.toString()); } - for(RtpDescription.RtpHeaderExtension extension : rtpDescription.getHeaderExtensions()) { - Log.d(Config.LOGTAG,"extension: "+extension.toString()); + for (RtpDescription.RtpHeaderExtension extension : rtpDescription.getHeaderExtensions()) { + Log.d(Config.LOGTAG, "extension: " + extension.toString()); } final IceUdpTransportInfo iceUdpTransportInfo = descriptionTransport.transport; - Log.d(Config.LOGTAG,"transport: "+descriptionTransport.transport); - Log.d(Config.LOGTAG,"fingerprint "+iceUdpTransportInfo.getFingerprint()); + Log.d(Config.LOGTAG, "transport: " + descriptionTransport.transport); + Log.d(Config.LOGTAG, "fingerprint " + iceUdpTransportInfo.getFingerprint()); } } @@ -142,11 +153,12 @@ public class JingleRtpConnection extends AbstractJingleConnection { } private void sendSessionInitiate() { - Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": sending session-initiate"); + setupWebRTC(); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending session-initiate"); } private void sendSessionAccept() { - Log.d(Config.LOGTAG,"sending session-accept"); + Log.d(Config.LOGTAG, "sending session-accept"); } public void pickUpCall() { @@ -162,6 +174,110 @@ public class JingleRtpConnection extends AbstractJingleConnection { } } + private void setupWebRTC() { + PeerConnectionFactory.initialize( + PeerConnectionFactory.InitializationOptions.builder(xmppConnectionService).createInitializationOptions() + ); + final PeerConnectionFactory.Options options = new PeerConnectionFactory.Options(); + PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory(); + + final AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints()); + + final AudioTrack audioTrack = peerConnectionFactory.createAudioTrack("my-audio-track", audioSource); + final MediaStream stream = peerConnectionFactory.createLocalMediaStream("my-media-stream"); + stream.addTrack(audioTrack); + + + PeerConnection peer = peerConnectionFactory.createPeerConnection(Collections.emptyList(), new PeerConnection.Observer() { + @Override + public void onSignalingChange(PeerConnection.SignalingState signalingState) { + + } + + @Override + public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { + + } + + @Override + public void onIceConnectionReceivingChange(boolean b) { + + } + + @Override + public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { + + } + + @Override + public void onIceCandidate(IceCandidate iceCandidate) { + + } + + @Override + public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) { + + } + + @Override + public void onAddStream(MediaStream mediaStream) { + + } + + @Override + public void onRemoveStream(MediaStream mediaStream) { + + } + + @Override + public void onDataChannel(DataChannel dataChannel) { + + } + + @Override + public void onRenegotiationNeeded() { + + } + + @Override + public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { + + } + }); + + peer.addStream(stream); + + peer.createOffer(new SdpObserver() { + + @Override + public void onCreateSuccess(org.webrtc.SessionDescription description) { + final SessionDescription sessionDescription = SessionDescription.parse(description.description); + for (SessionDescription.Media media : sessionDescription.media) { + Log.d(Config.LOGTAG, "media: " + media.protocol); + for (SessionDescription.Attribute attribute : media.attributes) { + Log.d(Config.LOGTAG, "attribute key=" + attribute.key + ", value=" + attribute.value); + } + } + Log.d(Config.LOGTAG, sessionDescription.toString()); + } + + @Override + public void onSetSuccess() { + + } + + @Override + public void onCreateFailure(String s) { + + } + + @Override + public void onSetFailure(String s) { + + } + }, new MediaConstraints()); + } + private void pickupCallFromProposed() { transitionOrThrow(State.PROCEED); final MessagePacket messagePacket = new MessagePacket(); @@ -210,7 +326,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { if (description instanceof RtpDescription) { rtpDescription = (RtpDescription) description; } else { - Log.d(Config.LOGTAG,"description was "+description); + Log.d(Config.LOGTAG, "description was " + description); throw new IllegalArgumentException("Content does not contain RtpDescription"); } if (transportInfo instanceof IceUdpTransportInfo) { @@ -221,7 +337,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { return new DescriptionTransport(rtpDescription, iceUdpTransportInfo); } - public static Map of(final Map contents) { + public static Map of(final Map contents) { return ImmutableMap.copyOf(Maps.transformValues(contents, new Function() { @NullableDecl @Override diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index 2a129b3e8..8054f0a4b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -11,11 +11,11 @@ import eu.siacs.conversations.Config; public class SessionDescription { - private final int version; - private final String name; - private final String connectionData; - private final List attributes; - private final List media; + public final int version; + public final String name; + public final String connectionData; + public final List attributes; + public final List media; public SessionDescription(int version, String name, String connectionData, List attributes, List media) { @@ -36,10 +36,10 @@ public class SessionDescription { MediaBuilder currentMediaBuilder = null; ImmutableList.Builder attributeBuilder = new ImmutableList.Builder<>(); ImmutableList.Builder mediaBuilder = new ImmutableList.Builder<>(); - for(final String line : input.split("\n")) { - final String[] pair = line.split("=",2); + for (final String line : input.split("\n")) { + final String[] pair = line.split("=", 2); if (pair.length < 2 || pair[0].length() != 1) { - Log.d(Config.LOGTAG,"skipping sdp parsing on line "+line); + Log.d(Config.LOGTAG, "skipping sdp parsing on line " + line); continue; } final char key = pair[0].charAt(0); @@ -63,7 +63,8 @@ public class SessionDescription { break; case 'm': if (currentMediaBuilder == null) { - sessionDescriptionBuilder.setAttributes(attributeBuilder.build());; + sessionDescriptionBuilder.setAttributes(attributeBuilder.build()); + ; } else { currentMediaBuilder.setAttributes(attributeBuilder.build()); mediaBuilder.add(currentMediaBuilder.createMedia()); @@ -76,12 +77,12 @@ public class SessionDescription { currentMediaBuilder.setPort(ignorantIntParser(parts[1])); currentMediaBuilder.setProtocol(parts[2]); ImmutableList.Builder formats = new ImmutableList.Builder<>(); - for(int i = 3; i < parts.length; ++i) { + for (int i = 3; i < parts.length; ++i) { formats.add(ignorantIntParser(parts[i])); } currentMediaBuilder.setFormats(formats.build()); } else { - Log.d(Config.LOGTAG,"skipping media line "+line); + Log.d(Config.LOGTAG, "skipping media line " + line); } break; } @@ -104,8 +105,8 @@ public class SessionDescription { } public static class Attribute { - private final String key; - private final String value; + public final String key; + public final String value; public Attribute(String key, String value) { this.key = key; @@ -113,9 +114,9 @@ public class SessionDescription { } public static Attribute parse(String input) { - final String[] pair = input.split(":",2); + final String[] pair = input.split(":", 2); if (pair.length == 2) { - return new Attribute(pair[0],pair[1]); + return new Attribute(pair[0], pair[1]); } else { return new Attribute(pair[0], null); } @@ -125,12 +126,12 @@ public class SessionDescription { } public static class Media { - private final String media; - private final int port; - private final String protocol; - private final List formats; - private final String connectionData; - private final List attributes; + public final String media; + public final int port; + public final String protocol; + public final List formats; + public final String connectionData; + public final List attributes; public Media(String media, int port, String protocol, List formats, String connectionData, List attributes) { this.media = media; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index 15a208205..8f79022aa 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -52,6 +52,8 @@ public class RtpDescription extends GenericDescription { //TODO: support for https://xmpp.org/extensions/xep-0293.html + //XEP-0294: Jingle RTP Header Extensions Negotiation + //maps to `extmap:$id $uri` public static class RtpHeaderExtension extends Element { private RtpHeaderExtension() { @@ -76,6 +78,7 @@ public class RtpDescription extends GenericDescription { } } + //maps to `rtpmap $id $name/$clockrate/$channels` public static class PayloadType extends Element { private PayloadType(String name, String xmlns) { @@ -132,6 +135,8 @@ public class RtpDescription extends GenericDescription { } } + //map to `fmtp $id key=value;key=value + //where id is the id of the parent payload-type public static class Parameter extends Element { private Parameter() { From 18059345c874cd12ab47ee144e274306080b89f0 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 4 Apr 2020 16:51:51 +0200 Subject: [PATCH 032/182] payload-type and rtp-hdrext sdp parsing --- .../eu/siacs/conversations/xml/Namespace.java | 1 + .../xmpp/jingle/JingleRtpConnection.java | 6 +- .../xmpp/jingle/MediaBuilder.java | 6 +- .../xmpp/jingle/SessionDescription.java | 51 +++--- .../jingle/SessionDescriptionBuilder.java | 6 +- .../xmpp/jingle/stanzas/RtpDescription.java | 172 +++++++++++++++++- 6 files changed, 195 insertions(+), 47 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 237cb4070..d4d513202 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -39,6 +39,7 @@ public final class Namespace { public static final String JINGLE_FEATURE_AUDIO = "urn:xmpp:jingle:apps:rtp:audio"; public static final String JINGLE_FEATURE_VIDEO = "urn:xmpp:jingle:apps:rtp:video"; public static final String JINGLE_RTP_HEADER_EXTENSIONS = "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"; + public static final String JINGLE_RTP_FEEDBACK_NEGOTIATION = "urn:xmpp:jingle:apps:rtp:rtcp-fb:0"; public static final String IBB = "http://jabber.org/protocol/ibb"; public static final String PING = "urn:xmpp:ping"; public static final String PUSH = "urn:xmpp:push:0"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 3424b1ec7..faba08e48 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -252,11 +252,9 @@ public class JingleRtpConnection extends AbstractJingleConnection { @Override public void onCreateSuccess(org.webrtc.SessionDescription description) { final SessionDescription sessionDescription = SessionDescription.parse(description.description); + Log.d(Config.LOGTAG,"description: "+description.description); for (SessionDescription.Media media : sessionDescription.media) { - Log.d(Config.LOGTAG, "media: " + media.protocol); - for (SessionDescription.Attribute attribute : media.attributes) { - Log.d(Config.LOGTAG, "attribute key=" + attribute.key + ", value=" + attribute.value); - } + Log.d(Config.LOGTAG, RtpDescription.of(media).toString()); } Log.d(Config.LOGTAG, sessionDescription.toString()); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java index e92c6b100..67e275414 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.xmpp.jingle; +import com.google.common.collect.ArrayListMultimap; + import java.util.List; public class MediaBuilder { @@ -8,7 +10,7 @@ public class MediaBuilder { private String protocol; private List formats; private String connectionData; - private List attributes; + private ArrayListMultimap attributes; public MediaBuilder setMedia(String media) { this.media = media; @@ -35,7 +37,7 @@ public class MediaBuilder { return this; } - public MediaBuilder setAttributes(List attributes) { + public MediaBuilder setAttributes(ArrayListMultimap attributes) { this.attributes = attributes; return this; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index 8054f0a4b..4381a4816 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -1,7 +1,9 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Log; +import android.util.Pair; +import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import java.util.List; @@ -14,11 +16,11 @@ public class SessionDescription { public final int version; public final String name; public final String connectionData; - public final List attributes; + public final ArrayListMultimap attributes; public final List media; - public SessionDescription(int version, String name, String connectionData, List attributes, List media) { + public SessionDescription(int version, String name, String connectionData, ArrayListMultimap attributes, List media) { this.version = version; this.name = name; this.connectionData = connectionData; @@ -34,10 +36,10 @@ public class SessionDescription { public static SessionDescription parse(final String input) { final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder(); MediaBuilder currentMediaBuilder = null; - ImmutableList.Builder attributeBuilder = new ImmutableList.Builder<>(); + ArrayListMultimap attributeMap = ArrayListMultimap.create(); ImmutableList.Builder mediaBuilder = new ImmutableList.Builder<>(); for (final String line : input.split("\n")) { - final String[] pair = line.split("=", 2); + final String[] pair = line.trim().split("=", 2); if (pair.length < 2 || pair[0].length() != 1) { Log.d(Config.LOGTAG, "skipping sdp parsing on line " + line); continue; @@ -59,17 +61,18 @@ public class SessionDescription { sessionDescriptionBuilder.setName(value); break; case 'a': - attributeBuilder.add(Attribute.parse(value)); + final Pair attribute = parseAttribute(value); + attributeMap.put(attribute.first, attribute.second); break; case 'm': if (currentMediaBuilder == null) { - sessionDescriptionBuilder.setAttributes(attributeBuilder.build()); + sessionDescriptionBuilder.setAttributes(attributeMap); ; } else { - currentMediaBuilder.setAttributes(attributeBuilder.build()); + currentMediaBuilder.setAttributes(attributeMap); mediaBuilder.add(currentMediaBuilder.createMedia()); } - attributeBuilder = new ImmutableList.Builder<>(); + attributeMap = ArrayListMultimap.create(); currentMediaBuilder = new MediaBuilder(); final String[] parts = value.split(" "); if (parts.length >= 3) { @@ -89,14 +92,14 @@ public class SessionDescription { } if (currentMediaBuilder != null) { - currentMediaBuilder.setAttributes(attributeBuilder.build()); + currentMediaBuilder.setAttributes(attributeMap); mediaBuilder.add(currentMediaBuilder.createMedia()); } sessionDescriptionBuilder.setMedia(mediaBuilder.build()); return sessionDescriptionBuilder.createSessionDescription(); } - private static int ignorantIntParser(final String input) { + public static int ignorantIntParser(final String input) { try { return Integer.parseInt(input); } catch (NumberFormatException e) { @@ -104,25 +107,13 @@ public class SessionDescription { } } - public static class Attribute { - public final String key; - public final String value; - - public Attribute(String key, String value) { - this.key = key; - this.value = value; + public static Pair parseAttribute(final String input) { + final String[] pair = input.split(":", 2); + if (pair.length == 2) { + return new Pair<>(pair[0], pair[1]); + } else { + return new Pair<>(pair[0], ""); } - - public static Attribute parse(String input) { - final String[] pair = input.split(":", 2); - if (pair.length == 2) { - return new Attribute(pair[0], pair[1]); - } else { - return new Attribute(pair[0], null); - } - } - - } public static class Media { @@ -131,9 +122,9 @@ public class SessionDescription { public final String protocol; public final List formats; public final String connectionData; - public final List attributes; + public final ArrayListMultimap attributes; - public Media(String media, int port, String protocol, List formats, String connectionData, List attributes) { + public Media(String media, int port, String protocol, List formats, String connectionData, ArrayListMultimap attributes) { this.media = media; this.port = port; this.protocol = protocol; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescriptionBuilder.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescriptionBuilder.java index d45c53461..edee2ed76 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescriptionBuilder.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescriptionBuilder.java @@ -1,12 +1,14 @@ package eu.siacs.conversations.xmpp.jingle; +import com.google.common.collect.ArrayListMultimap; + import java.util.List; public class SessionDescriptionBuilder { private int version; private String name; private String connectionData; - private List attributes; + private ArrayListMultimap attributes; private List media; public SessionDescriptionBuilder setVersion(int version) { @@ -24,7 +26,7 @@ public class SessionDescriptionBuilder { return this; } - public SessionDescriptionBuilder setAttributes(List attributes) { + public SessionDescriptionBuilder setAttributes(ArrayListMultimap attributes) { this.attributes = attributes; return this; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index 8f79022aa..6c8a9b6d4 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -1,19 +1,23 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; +import android.util.Log; + import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import java.util.List; import java.util.Locale; +import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.jingle.SessionDescription; public class RtpDescription extends GenericDescription { - private RtpDescription(String name, String namespace) { - super(name, namespace); + private RtpDescription() { + super("description", Namespace.JINGLE_APPS_RTP); } public Media getMedia() { @@ -22,7 +26,7 @@ public class RtpDescription extends GenericDescription { public List getPayloadTypes() { final ImmutableList.Builder builder = new ImmutableList.Builder<>(); - for(Element child : getChildren()) { + for (Element child : getChildren()) { if ("payload-type".equals(child.getName())) { builder.add(PayloadType.of(child)); } @@ -30,9 +34,17 @@ public class RtpDescription extends GenericDescription { return builder.build(); } + public List getFeedbackNegotiations() { + return FeedbackNegotiation.fromChildren(this.getChildren()); + } + + public List feedbackNegotiationTrrInts() { + return FeedbackNegotiationTrrInt.fromChildren(this.getChildren()); + } + public List getHeaderExtensions() { final ImmutableList.Builder builder = new ImmutableList.Builder<>(); - for(final Element child : getChildren()) { + for (final Element child : getChildren()) { if ("rtp-hdrext".equals(child.getName()) && Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(child.getNamespace())) { builder.add(RtpHeaderExtension.upgrade(child)); } @@ -43,13 +55,82 @@ public class RtpDescription extends GenericDescription { public static RtpDescription upgrade(final Element element) { Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description"); Preconditions.checkArgument(Namespace.JINGLE_APPS_RTP.equals(element.getNamespace()), "Element does not match the jingle rtp namespace"); - final RtpDescription description = new RtpDescription("description", Namespace.JINGLE_APPS_RTP); + final RtpDescription description = new RtpDescription(); description.setAttributes(element.getAttributes()); description.setChildren(element.getChildren()); return description; } - //TODO: support for https://xmpp.org/extensions/xep-0293.html + public static class FeedbackNegotiation extends Element { + private FeedbackNegotiation() { + super("rtcp-fb", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION); + } + + public String getType() { + return this.getAttribute("type"); + } + + public String getSubType() { + return this.getAttribute("subtype"); + } + + private static FeedbackNegotiation upgrade(final Element element) { + Preconditions.checkArgument("rtcp-fb".equals(element.getName())); + Preconditions.checkArgument(Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace())); + final FeedbackNegotiation feedback = new FeedbackNegotiation(); + feedback.setAttributes(element.getAttributes()); + feedback.setChildren(element.getChildren()); + return feedback; + } + + public static List fromChildren(final List children) { + ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for (final Element child : children) { + if ("rtcp-fb".equals(child.getName()) && Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) { + builder.add(upgrade(child)); + } + } + return builder.build(); + } + + } + + public static class FeedbackNegotiationTrrInt extends Element { + private FeedbackNegotiationTrrInt() { + super("rtcp-fb-trr-int", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION); + } + + public int getValue() { + final String value = getAttribute("value"); + if (value == null) { + return 0; + } + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return 0; + } + } + + private static FeedbackNegotiationTrrInt upgrade(final Element element) { + Preconditions.checkArgument("rtcp-fb-trr-int".equals(element.getName())); + Preconditions.checkArgument(Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace())); + final FeedbackNegotiationTrrInt trr = new FeedbackNegotiationTrrInt(); + trr.setAttributes(element.getAttributes()); + trr.setChildren(element.getChildren()); + return trr; + } + + public static List fromChildren(final List children) { + ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for (final Element child : children) { + if ("rtcp-fb-trr-int".equals(child.getName()) && Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) { + builder.add(upgrade(child)); + } + } + return builder.build(); + } + } //XEP-0294: Jingle RTP Header Extensions Negotiation @@ -60,6 +141,12 @@ public class RtpDescription extends GenericDescription { super("rtp-hdrext", Namespace.JINGLE_RTP_HEADER_EXTENSIONS); } + public RtpHeaderExtension(String id, String uri) { + super("rtp-hdrext", Namespace.JINGLE_RTP_HEADER_EXTENSIONS); + this.setAttribute("id", id); + this.setAttribute("uri", uri); + } + public String getId() { return this.getAttribute("id"); } @@ -76,14 +163,36 @@ public class RtpDescription extends GenericDescription { extension.setChildren(element.getChildren()); return extension; } + + public static RtpHeaderExtension ofSdpString(final String sdp) { + final String[] pair = sdp.split(" ", 2); + if (pair.length == 2) { + final String id = pair[0]; + final String uri = pair[1]; + return new RtpHeaderExtension(id,uri); + } else { + return null; + } + } } //maps to `rtpmap $id $name/$clockrate/$channels` public static class PayloadType extends Element { - private PayloadType(String name, String xmlns) { - super(name, xmlns); + private PayloadType() { + super("payload-type", Namespace.JINGLE_APPS_RTP); } + + public PayloadType(String id, String name, int clockRate, int channels) { + super("payload-type", Namespace.JINGLE_APPS_RTP); + this.setAttribute("id",id); + this.setAttribute("name", name); + this.setAttribute("clockrate", clockRate); + if (channels != 1) { + this.setAttribute("channels", channels); + } + } + public String getId() { return this.getAttribute("id"); } @@ -126,13 +235,41 @@ public class RtpDescription extends GenericDescription { return builder.build(); } + public List getFeedbackNegotiations() { + return FeedbackNegotiation.fromChildren(this.getChildren()); + } + + public List feedbackNegotiationTrrInts() { + return FeedbackNegotiationTrrInt.fromChildren(this.getChildren()); + } + public static PayloadType of(final Element element) { Preconditions.checkArgument("payload-type".equals(element.getName()), "element name must be called payload-type"); - PayloadType payloadType = new PayloadType("payload-type", Namespace.JINGLE_APPS_RTP); + PayloadType payloadType = new PayloadType(); payloadType.setAttributes(element.getAttributes()); payloadType.setChildren(element.getChildren()); return payloadType; } + + public static PayloadType ofSdpString(final String sdp) { + final String[] pair = sdp.split(" ",2); + if (pair.length == 2) { + final String id = pair[0]; + final String[] parts = pair[1].split("/"); + if (parts.length >= 2) { + final String name = parts[0]; + final int clockRate = SessionDescription.ignorantIntParser(parts[1]); + final int channels; + if (parts.length >= 3) { + channels = SessionDescription.ignorantIntParser(parts[2]); + } else { + channels =1; + } + return new PayloadType(id,name,clockRate,channels); + } + } + return null; + } } //map to `fmtp $id key=value;key=value @@ -182,4 +319,21 @@ public class RtpDescription extends GenericDescription { } } } + + public static RtpDescription of(final SessionDescription.Media media) { + final RtpDescription rtpDescription = new RtpDescription(); + for(final String rtpmap : media.attributes.get("rtpmap")) { + final PayloadType payloadType = PayloadType.ofSdpString(rtpmap); + if (payloadType != null) { + rtpDescription.addChild(payloadType); + } + } + for(final String extmap : media.attributes.get("extmap")) { + final RtpHeaderExtension extension = RtpHeaderExtension.ofSdpString(extmap); + if (extension != null) { + rtpDescription.addChild(extension); + } + } + return rtpDescription; + } } From 28ead10ca46d2e74c1347aa51d90398339f6a0bb Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 4 Apr 2020 17:44:11 +0200 Subject: [PATCH 033/182] sdp media to description parsing --- .../xmpp/jingle/stanzas/RtpDescription.java | 101 +++++++++++++++--- 1 file changed, 89 insertions(+), 12 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index 6c8a9b6d4..b24d24d46 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -1,12 +1,16 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; import android.util.Log; +import android.util.Pair; import com.google.common.base.Preconditions; +import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; @@ -66,6 +70,14 @@ public class RtpDescription extends GenericDescription { super("rtcp-fb", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION); } + public FeedbackNegotiation(String type, String subType) { + super("rtcp-fb", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION); + this.setAttribute("type", type); + if (subType != null) { + this.setAttribute("subtype", subType); + } + } + public String getType() { return this.getAttribute("type"); } @@ -96,6 +108,13 @@ public class RtpDescription extends GenericDescription { } public static class FeedbackNegotiationTrrInt extends Element { + + private FeedbackNegotiationTrrInt(int value) { + super("rtcp-fb-trr-int", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION); + this.setAttribute("value", value); + } + + private FeedbackNegotiationTrrInt() { super("rtcp-fb-trr-int", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION); } @@ -105,11 +124,8 @@ public class RtpDescription extends GenericDescription { if (value == null) { return 0; } - try { - return Integer.parseInt(value); - } catch (NumberFormatException e) { - return 0; - } + return SessionDescription.ignorantIntParser(value); + } private static FeedbackNegotiationTrrInt upgrade(final Element element) { @@ -169,7 +185,7 @@ public class RtpDescription extends GenericDescription { if (pair.length == 2) { final String id = pair[0]; final String uri = pair[1]; - return new RtpHeaderExtension(id,uri); + return new RtpHeaderExtension(id, uri); } else { return null; } @@ -185,7 +201,7 @@ public class RtpDescription extends GenericDescription { public PayloadType(String id, String name, int clockRate, int channels) { super("payload-type", Namespace.JINGLE_APPS_RTP); - this.setAttribute("id",id); + this.setAttribute("id", id); this.setAttribute("name", name); this.setAttribute("clockrate", clockRate); if (channels != 1) { @@ -252,7 +268,7 @@ public class RtpDescription extends GenericDescription { } public static PayloadType ofSdpString(final String sdp) { - final String[] pair = sdp.split(" ",2); + final String[] pair = sdp.split(" ", 2); if (pair.length == 2) { final String id = pair[0]; final String[] parts = pair[1].split("/"); @@ -263,13 +279,25 @@ public class RtpDescription extends GenericDescription { if (parts.length >= 3) { channels = SessionDescription.ignorantIntParser(parts[2]); } else { - channels =1; + channels = 1; } - return new PayloadType(id,name,clockRate,channels); + return new PayloadType(id, name, clockRate, channels); } } return null; } + + public void addChildren(final List children) { + if (children != null) { + this.children.addAll(children); + } + } + + public void addParameters(List parameters) { + if (parameters != null) { + this.children.addAll(parameters); + } + } } //map to `fmtp $id key=value;key=value @@ -301,6 +329,23 @@ public class RtpDescription extends GenericDescription { parameter.setChildren(element.getChildren()); return parameter; } + + public static Pair> ofSdpString(final String sdp) { + final String[] pair = sdp.split(" "); + if (pair.length == 2) { + final String id = pair[0]; + ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for (final String parameter : pair[1].split(";")) { + final String[] parts = parameter.split("=", 2); + if (parts.length == 2) { + builder.add(new Parameter(parts[0], parts[1])); + } + } + return new Pair<>(id, builder.build()); + } else { + return null; + } + } } public enum Media { @@ -322,13 +367,39 @@ public class RtpDescription extends GenericDescription { public static RtpDescription of(final SessionDescription.Media media) { final RtpDescription rtpDescription = new RtpDescription(); - for(final String rtpmap : media.attributes.get("rtpmap")) { + final Map> parameterMap = new HashMap<>(); + ArrayListMultimap feedbackNegotiationMap = ArrayListMultimap.create(); + for (final String rtcpFb : media.attributes.get("rtcp-fb")) { + final String[] parts = rtcpFb.split(" "); + if (parts.length >= 2) { + final String id = parts[0]; + final String type = parts[1]; + final String subType = parts.length >= 3 ? parts[2] : null; + if ("trr-int".equals(type)) { + if (subType != null) { + feedbackNegotiationMap.put(id, new FeedbackNegotiationTrrInt(SessionDescription.ignorantIntParser(subType))); + } + } else { + feedbackNegotiationMap.put(id, new FeedbackNegotiation(type, subType)); + } + } + } + for (final String fmtp : media.attributes.get("fmtp")) { + final Pair> pair = Parameter.ofSdpString(fmtp); + if (pair != null) { + parameterMap.put(pair.first, pair.second); + } + } + rtpDescription.addChildren(feedbackNegotiationMap.get("*")); + for (final String rtpmap : media.attributes.get("rtpmap")) { final PayloadType payloadType = PayloadType.ofSdpString(rtpmap); if (payloadType != null) { + payloadType.addParameters(parameterMap.get(payloadType.getId())); + payloadType.addChildren(feedbackNegotiationMap.get(payloadType.getId())); rtpDescription.addChild(payloadType); } } - for(final String extmap : media.attributes.get("extmap")) { + for (final String extmap : media.attributes.get("extmap")) { final RtpHeaderExtension extension = RtpHeaderExtension.ofSdpString(extmap); if (extension != null) { rtpDescription.addChild(extension); @@ -336,4 +407,10 @@ public class RtpDescription extends GenericDescription { } return rtpDescription; } + + private void addChildren(List elements) { + if (elements != null) { + this.children.addAll(elements); + } + } } From ef51ec2c1d8c44997837771fb20d8f98011b6078 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 5 Apr 2020 08:23:38 +0200 Subject: [PATCH 034/182] create objects for ssma (xep-0339) --- .../eu/siacs/conversations/xml/Namespace.java | 1 + .../xmpp/jingle/stanzas/RtpDescription.java | 101 +++++++++++++++++- 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index d4d513202..fb3261ab6 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -40,6 +40,7 @@ public final class Namespace { public static final String JINGLE_FEATURE_VIDEO = "urn:xmpp:jingle:apps:rtp:video"; public static final String JINGLE_RTP_HEADER_EXTENSIONS = "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"; public static final String JINGLE_RTP_FEEDBACK_NEGOTIATION = "urn:xmpp:jingle:apps:rtp:rtcp-fb:0"; + public static final String JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES = "urn:xmpp:jingle:apps:rtp:ssma:0"; public static final String IBB = "http://jabber.org/protocol/ibb"; public static final String PING = "urn:xmpp:ping"; public static final String PUSH = "urn:xmpp:push:0"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index b24d24d46..062a58523 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -1,18 +1,17 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; -import android.util.Log; import android.util.Pair; import com.google.common.base.Preconditions; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.jingle.SessionDescription; @@ -56,6 +55,16 @@ public class RtpDescription extends GenericDescription { return builder.build(); } + public List getSources() { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for (final Element child : this.children) { + if ("source".equals(child.getName()) && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(child.getNamespace())) { + builder.add(Source.upgrade(child)); + } + } + return builder.build(); + } + public static RtpDescription upgrade(final Element element) { Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description"); Preconditions.checkArgument(Namespace.JINGLE_APPS_RTP.equals(element.getNamespace()), "Element does not match the jingle rtp namespace"); @@ -348,6 +357,78 @@ public class RtpDescription extends GenericDescription { } } + //XEP-0339: Source-Specific Media Attributes in Jingle + //maps to `a=ssrc: :` + public static class Source extends Element { + + private Source() { + super("source", Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES); + } + + public Source(String ssrcId, Collection parameters) { + super("source", Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES); + this.setAttribute("ssrc", ssrcId); + for (Parameter parameter : parameters) { + this.addChild(parameter); + } + } + + public String getSsrcId() { + return this.getAttribute("ssrc"); + } + + public List getParameters() { + ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for (Element child : this.children) { + if ("parameter".equals(child.getName())) { + builder.add(Parameter.upgrade(child)); + } + } + return builder.build(); + } + + public static Source upgrade(final Element element) { + Preconditions.checkArgument("source".equals(element.getName())); + Preconditions.checkArgument(Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(element.getNamespace())); + final Source source = new Source(); + source.setChildren(element.getChildren()); + source.setAttributes(element.getAttributes()); + return source; + } + + public static class Parameter extends Element { + + public String getParameterName() { + return this.getAttribute("name"); + } + + public String getParameterValue() { + return this.getAttribute("value"); + } + + private Parameter() { + super("parameter"); + } + + public Parameter(final String attribute, final String value) { + super("parameter"); + this.setAttribute("name", attribute); + if (value != null) { + this.setAttribute("value", value); + } + } + + public static Parameter upgrade(final Element element) { + Preconditions.checkArgument("parameter".equals(element.getName())); + Parameter parameter = new Parameter(); + parameter.setAttributes(element.getAttributes()); + parameter.setChildren(element.getChildren()); + return parameter; + } + } + + } + public enum Media { VIDEO, AUDIO, UNKNOWN; @@ -368,7 +449,8 @@ public class RtpDescription extends GenericDescription { public static RtpDescription of(final SessionDescription.Media media) { final RtpDescription rtpDescription = new RtpDescription(); final Map> parameterMap = new HashMap<>(); - ArrayListMultimap feedbackNegotiationMap = ArrayListMultimap.create(); + final ArrayListMultimap feedbackNegotiationMap = ArrayListMultimap.create(); + final ArrayListMultimap sourceParameterMap = ArrayListMultimap.create(); for (final String rtcpFb : media.attributes.get("rtcp-fb")) { final String[] parts = rtcpFb.split(" "); if (parts.length >= 2) { @@ -384,6 +466,16 @@ public class RtpDescription extends GenericDescription { } } } + for (final String ssrc : media.attributes.get(("ssrc"))) { + final String[] parts = ssrc.split(" ", 2); + if (parts.length == 2) { + final String id = parts[0]; + final String[] subParts = parts[1].split(":", 2); + final String attribute = subParts[0]; + final String value = subParts.length == 2 ? subParts[1] : null; + sourceParameterMap.put(id, new Source.Parameter(attribute, value)); + } + } for (final String fmtp : media.attributes.get("fmtp")) { final Pair> pair = Parameter.ofSdpString(fmtp); if (pair != null) { @@ -405,6 +497,9 @@ public class RtpDescription extends GenericDescription { rtpDescription.addChild(extension); } } + for (Map.Entry> source : sourceParameterMap.asMap().entrySet()) { + rtpDescription.addChild(new Source(source.getKey(), source.getValue())); + } return rtpDescription; } From b44a3aeac6312da7097fcb1f6370d00db0c1fd58 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 5 Apr 2020 10:20:34 +0200 Subject: [PATCH 035/182] parse sdp to jingle (yet w/o transport) --- .../eu/siacs/conversations/xml/Namespace.java | 1 + .../jingle/JingleFileTransferConnection.java | 24 ++-- .../xmpp/jingle/JingleRtpConnection.java | 74 +++--------- .../xmpp/jingle/RtpContentMap.java | 108 ++++++++++++++++++ .../xmpp/jingle/SessionDescription.java | 2 +- .../xmpp/jingle/stanzas/Group.java | 64 +++++++++++ .../jingle/stanzas/IceUdpTransportInfo.java | 6 +- .../xmpp/jingle/stanzas/JinglePacket.java | 17 ++- .../xmpp/jingle/stanzas/RtpDescription.java | 7 +- 9 files changed, 222 insertions(+), 81 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index fb3261ab6..a53362168 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -36,6 +36,7 @@ public final class Namespace { public static final String JINGLE_TRANSPORT_ICE_UDP = "urn:xmpp:jingle:transports:ice-udp:1"; public static final String JINGLE_APPS_RTP = "urn:xmpp:jingle:apps:rtp:1"; public static final String JINGLE_APPS_DTLS = "urn:xmpp:jingle:apps:dtls:0"; + public static final String JINGLE_APPS_GROUPING = "urn:xmpp:jingle:apps:grouping:0"; public static final String JINGLE_FEATURE_AUDIO = "urn:xmpp:jingle:apps:rtp:audio"; public static final String JINGLE_FEATURE_VIDEO = "urn:xmpp:jingle:apps:rtp:video"; public static final String JINGLE_RTP_HEADER_EXTENSIONS = "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index f2cb85d27..68964d131 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -592,7 +592,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple content.setTransport(new S5BTransportInfo(this.transportId, candidates)); Log.d(Config.LOGTAG, String.format("%s: sending S5B offer with %d candidates", id.account.getJid().asBareJid(), candidates.size())); } - packet.setJingleContent(content); + packet.addJingleContent(content); this.sendJinglePacket(packet, (account, response) -> { if (response.getType() == IqPacket.TYPE.RESULT) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": other party received offer"); @@ -617,7 +617,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple hash.setAttribute("algo", "sha-1").setContent(Base64.encodeToString(file.getSha1Sum(), Base64.NO_WRAP)); final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.SESSION_INFO); - packet.setJingleChild(checksum); + packet.addJingleChild(checksum); this.sendJinglePacket(packet); } @@ -651,7 +651,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple public void failed() { Log.d(Config.LOGTAG, "connection to our own proxy65 candidate failed"); content.setTransport(new S5BTransportInfo(transportId, getOurCandidates())); - packet.setJingleContent(content); + packet.addJingleContent(content); sendJinglePacket(packet); connectNextCandidate(); } @@ -661,7 +661,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple Log.d(Config.LOGTAG, "connected to proxy65 candidate"); mergeCandidate(candidate); content.setTransport(new S5BTransportInfo(transportId, getOurCandidates())); - packet.setJingleContent(content); + packet.addJingleContent(content); sendJinglePacket(packet); connectNextCandidate(); } @@ -669,7 +669,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } else { Log.d(Config.LOGTAG, "did not find a proxy65 candidate for ourselves"); content.setTransport(new S5BTransportInfo(transportId, getOurCandidates())); - packet.setJingleContent(content); + packet.addJingleContent(content); sendJinglePacket(packet); connectNextCandidate(); } @@ -682,7 +682,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple final Content content = new Content(contentCreator, contentName); content.setDescription(this.description); content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); - packet.setJingleContent(content); + packet.addJingleContent(content); this.transport.receive(file, onFileTransmissionStatusChanged); this.sendJinglePacket(packet); } @@ -909,7 +909,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple Content content = new Content(this.contentCreator, this.contentName); this.transportId = JingleConnectionManager.nextRandomId(); content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); - packet.setJingleContent(content); + packet.addJingleContent(content); this.sendJinglePacket(packet); } @@ -941,7 +941,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple final Content content = new Content(contentCreator, contentName); content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize)); - answer.setJingleContent(content); + answer.addJingleContent(content); respondToIq(packet, true); @@ -1122,7 +1122,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); final Content content = new Content(this.contentCreator, this.contentName); content.setTransport(new S5BTransportInfo(this.transportId, new Element("activated").setAttribute("cid", cid))); - packet.setJingleContent(content); + packet.addJingleContent(content); this.sendJinglePacket(packet); } @@ -1130,7 +1130,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); final Content content = new Content(this.contentCreator, this.contentName); content.setTransport(new S5BTransportInfo(this.transportId, new Element("proxy-error"))); - packet.setJingleContent(content); + packet.addJingleContent(content); this.sendJinglePacket(packet); } @@ -1138,7 +1138,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); final Content content = new Content(this.contentCreator, this.contentName); content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-used").setAttribute("cid", cid))); - packet.setJingleContent(content); + packet.addJingleContent(content); this.sentCandidate = true; if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) { connect(); @@ -1151,7 +1151,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO); Content content = new Content(this.contentCreator, this.contentName); content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-error"))); - packet.setJingleContent(content); + packet.addJingleContent(content); this.sentCandidate = true; this.sendJinglePacket(packet); if (receivedCandidate && mJingleStatus == JINGLE_STATUS_ACCEPTED) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index faba08e48..6165fde34 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -2,12 +2,9 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Log; -import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Maps; -import org.checkerframework.checker.nullness.compatqual.NullableDecl; import org.webrtc.AudioSource; import org.webrtc.AudioTrack; import org.webrtc.DataChannel; @@ -26,9 +23,6 @@ import java.util.Map; import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.jingle.stanzas.Content; -import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; -import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; @@ -73,18 +67,18 @@ public class JingleRtpConnection extends AbstractJingleConnection { //TODO respond with out-of-order return; } - final Map contents; + final RtpContentMap contentMap; try { - contents = DescriptionTransport.of(jinglePacket.getJingleContents()); + contentMap = RtpContentMap.of(jinglePacket); } catch (IllegalArgumentException | NullPointerException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); return; } - Log.d(Config.LOGTAG, "processing session-init with " + contents.size() + " contents"); + Log.d(Config.LOGTAG, "processing session-init with " + contentMap.contents.size() + " contents"); final State oldState = this.state; if (transition(State.SESSION_INITIALIZED)) { if (oldState == State.PROCEED) { - processContents(contents); + processContents(contentMap); sendSessionAccept(); } else { //TODO start ringing @@ -94,9 +88,9 @@ public class JingleRtpConnection extends AbstractJingleConnection { } } - private void processContents(final Map contents) { - for (Map.Entry content : contents.entrySet()) { - final DescriptionTransport descriptionTransport = content.getValue(); + private void processContents(final RtpContentMap contentMap) { + for (Map.Entry content : contentMap.contents.entrySet()) { + final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue(); final RtpDescription rtpDescription = descriptionTransport.description; Log.d(Config.LOGTAG, "receive content with name " + content.getKey() + " and media=" + rtpDescription.getMedia()); for (RtpDescription.PayloadType payloadType : rtpDescription.getPayloadTypes()) { @@ -154,7 +148,11 @@ public class JingleRtpConnection extends AbstractJingleConnection { private void sendSessionInitiate() { setupWebRTC(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending session-initiate"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate"); + } + + private void sendSessionInitiate(RtpContentMap rtpContentMap) { + Log.d(Config.LOGTAG, rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId).toString()); } private void sendSessionAccept() { @@ -252,11 +250,9 @@ public class JingleRtpConnection extends AbstractJingleConnection { @Override public void onCreateSuccess(org.webrtc.SessionDescription description) { final SessionDescription sessionDescription = SessionDescription.parse(description.description); - Log.d(Config.LOGTAG,"description: "+description.description); - for (SessionDescription.Media media : sessionDescription.media) { - Log.d(Config.LOGTAG, RtpDescription.of(media).toString()); - } - Log.d(Config.LOGTAG, sessionDescription.toString()); + Log.d(Config.LOGTAG, "description: " + description.description); + final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); + sendSessionInitiate(rtpContentMap); } @Override @@ -306,44 +302,4 @@ public class JingleRtpConnection extends AbstractJingleConnection { throw new IllegalStateException(String.format("Unable to transition from %s to %s", this.state, target)); } } - - public static class DescriptionTransport { - private final RtpDescription description; - private final IceUdpTransportInfo transport; - - public DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) { - this.description = description; - this.transport = transport; - } - - public static DescriptionTransport of(final Content content) { - final GenericDescription description = content.getDescription(); - final GenericTransportInfo transportInfo = content.getTransport(); - final RtpDescription rtpDescription; - final IceUdpTransportInfo iceUdpTransportInfo; - if (description instanceof RtpDescription) { - rtpDescription = (RtpDescription) description; - } else { - Log.d(Config.LOGTAG, "description was " + description); - throw new IllegalArgumentException("Content does not contain RtpDescription"); - } - if (transportInfo instanceof IceUdpTransportInfo) { - iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo; - } else { - throw new IllegalArgumentException("Content does not contain ICE-UDP transport"); - } - return new DescriptionTransport(rtpDescription, iceUdpTransportInfo); - } - - public static Map of(final Map contents) { - return ImmutableMap.copyOf(Maps.transformValues(contents, new Function() { - @NullableDecl - @Override - public DescriptionTransport apply(@NullableDecl Content content) { - return content == null ? null : of(content); - } - })); - } - } - } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java new file mode 100644 index 000000000..681686fb9 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -0,0 +1,108 @@ +package eu.siacs.conversations.xmpp.jingle; + +import android.util.Log; + +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; + +import org.checkerframework.checker.nullness.compatqual.NullableDecl; + +import java.util.Map; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.xmpp.jingle.stanzas.Content; +import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; +import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; +import eu.siacs.conversations.xmpp.jingle.stanzas.Group; +import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; + +public class RtpContentMap { + + public final Group group; + public final Map contents; + + private RtpContentMap(Group group, Map contents) { + this.group = group; + this.contents = contents; + } + + public static RtpContentMap of(final JinglePacket jinglePacket) { + return new RtpContentMap(jinglePacket.getGroup(), DescriptionTransport.of(jinglePacket.getJingleContents())); + } + + public static RtpContentMap of(final SessionDescription sessionDescription) { + final ImmutableMap.Builder contentMapBuilder = new ImmutableMap.Builder<>(); + for (SessionDescription.Media media : sessionDescription.media) { + final String id = Iterables.getFirst(media.attributes.get("mid"), null); + Preconditions.checkNotNull(id, "media has no mid"); + contentMapBuilder.put(id, DescriptionTransport.of(sessionDescription, media)); + } + final String groupAttribute = Iterables.getFirst(sessionDescription.attributes.get("group"), null); + final Group group = groupAttribute == null ? null : Group.ofSdpString(groupAttribute); + return new RtpContentMap(group, contentMapBuilder.build()); + } + + public JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) { + final JinglePacket jinglePacket = new JinglePacket(action, sessionId); + if (this.group != null) { + jinglePacket.addGroup(this.group); + } + for (Map.Entry entry : this.contents.entrySet()) { + final Content content = new Content(Content.Creator.INITIATOR, entry.getKey()); + content.addChild(entry.getValue().description); + content.addChild(entry.getValue().transport); + jinglePacket.addJingleContent(content); + } + return jinglePacket; + } + + public static class DescriptionTransport { + public final RtpDescription description; + public final IceUdpTransportInfo transport; + + public DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) { + this.description = description; + this.transport = transport; + } + + public static DescriptionTransport of(final Content content) { + final GenericDescription description = content.getDescription(); + final GenericTransportInfo transportInfo = content.getTransport(); + final RtpDescription rtpDescription; + final IceUdpTransportInfo iceUdpTransportInfo; + if (description instanceof RtpDescription) { + rtpDescription = (RtpDescription) description; + } else { + Log.d(Config.LOGTAG, "description was " + description); + throw new IllegalArgumentException("Content does not contain RtpDescription"); + } + if (transportInfo instanceof IceUdpTransportInfo) { + iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo; + } else { + throw new IllegalArgumentException("Content does not contain ICE-UDP transport"); + } + return new DescriptionTransport(rtpDescription, iceUdpTransportInfo); + } + + public static DescriptionTransport of(final SessionDescription sessionDescription, final SessionDescription.Media media) { + final RtpDescription rtpDescription = RtpDescription.of(media); + final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); + return new DescriptionTransport(rtpDescription, transportInfo); + } + + public static Map of(final Map contents) { + return ImmutableMap.copyOf(Maps.transformValues(contents, new Function() { + @NullableDecl + @Override + public DescriptionTransport apply(@NullableDecl Content content) { + return content == null ? null : of(content); + } + })); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index 4381a4816..3d028a439 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -28,7 +28,7 @@ public class SessionDescription { this.media = media; } - public static SessionDescription parse(final Map contents) { + public static SessionDescription parse(final Map contents) { final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder(); return sessionDescriptionBuilder.createSessionDescription(); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java new file mode 100644 index 000000000..eb5c32252 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java @@ -0,0 +1,64 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import java.util.Collection; +import java.util.List; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; + +public class Group extends Element { + + private Group() { + super("group", Namespace.JINGLE_APPS_GROUPING); + } + + public Group(final String semantics, final Collection identificationTags) { + super("group", Namespace.JINGLE_APPS_GROUPING); + this.setAttribute("semantics", semantics); + for (String tag : identificationTags) { + this.addChild(new Element("content").setAttribute("name", tag)); + } + } + + public String getSemantics() { + return this.getAttribute("semantics"); + } + + public List getIdentificationTags() { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for (final Element child : this.children) { + if ("content".equals(child.getName())) { + final String name = child.getAttribute("name"); + if (name != null) { + builder.add(name); + } + } + } + return builder.build(); + } + + public static Group ofSdpString(final String input) { + ImmutableList.Builder tagBuilder = new ImmutableList.Builder<>(); + final String[] parts = input.split(" "); + if (parts.length >= 2) { + final String semantics = parts[0]; + for(int i = 1; i < parts.length; ++i) { + tagBuilder.add(parts[i]); + } + return new Group(semantics,tagBuilder.build()); + } + return null; + } + + public static Group upgrade(final Element element) { + Preconditions.checkArgument("group".equals(element.getName())); + Preconditions.checkArgument(Namespace.JINGLE_APPS_GROUPING.equals(element.getNamespace())); + final Group group = new Group(); + group.setAttributes(element.getAttributes()); + group.setChildren(element.getChildren()); + return group; + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index 84734b924..52868f136 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -10,8 +10,8 @@ import eu.siacs.conversations.xml.Namespace; public class IceUdpTransportInfo extends GenericTransportInfo { - private IceUdpTransportInfo(final String name, final String xmlns) { - super(name, xmlns); + public IceUdpTransportInfo() { + super("transport", Namespace.JINGLE_TRANSPORT_ICE_UDP); } public Fingerprint getFingerprint() { @@ -32,7 +32,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo { public static IceUdpTransportInfo upgrade(final Element element) { Preconditions.checkArgument("transport".equals(element.getName()), "Name of provided element is not transport"); Preconditions.checkArgument(Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(element.getNamespace()), "Element does not match ice-udp transport namespace"); - final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo("transport", Namespace.JINGLE_TRANSPORT_ICE_UDP); + final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); transportInfo.setAttributes(element.getAttributes()); transportInfo.setChildren(element.getChildren()); return transportInfo; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java index e4fb88ecb..1c15af27f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -1,7 +1,6 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; import android.support.annotation.NonNull; -import android.util.Log; import com.google.common.base.CaseFormat; import com.google.common.base.Preconditions; @@ -9,7 +8,6 @@ import com.google.common.collect.ImmutableMap; import java.util.Map; -import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.stanzas.IqPacket; @@ -44,6 +42,15 @@ public class JinglePacket extends IqPacket { return content == null ? null : Content.upgrade(content); } + public Group getGroup() { + final Element group = this.findChild("group", Namespace.JINGLE_APPS_GROUPING); + return group == null ? null : Group.upgrade(group); + } + + public void addGroup(final Group group) { + this.addJingleChild(group); + } + public Map getJingleContents() { final Element jingle = findChild("jingle", Namespace.JINGLE); ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); @@ -56,8 +63,8 @@ public class JinglePacket extends IqPacket { return builder.build(); } - public void setJingleContent(final Content content) { //take content interface - setJingleChild(content); + public void addJingleContent(final Content content) { //take content interface + addJingleChild(content); } public Reason getReason() { @@ -87,7 +94,7 @@ public class JinglePacket extends IqPacket { return jingle == null ? null : jingle.findChild(name); } - public void setJingleChild(final Element child) { + public void addJingleChild(final Element child) { final Element jingle = findChild("jingle", Namespace.JINGLE); jingle.addChild(child); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index 062a58523..8b9296993 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -19,6 +19,11 @@ import eu.siacs.conversations.xmpp.jingle.SessionDescription; public class RtpDescription extends GenericDescription { + private RtpDescription(final String media) { + super("description", Namespace.JINGLE_APPS_RTP); + this.setAttribute("media", media); + } + private RtpDescription() { super("description", Namespace.JINGLE_APPS_RTP); } @@ -447,7 +452,7 @@ public class RtpDescription extends GenericDescription { } public static RtpDescription of(final SessionDescription.Media media) { - final RtpDescription rtpDescription = new RtpDescription(); + final RtpDescription rtpDescription = new RtpDescription(media.media); final Map> parameterMap = new HashMap<>(); final ArrayListMultimap feedbackNegotiationMap = ArrayListMultimap.create(); final ArrayListMultimap sourceParameterMap = ArrayListMultimap.create(); From 4d70855b4c4f74e3b7fe2869340c851b1145a9b1 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 5 Apr 2020 10:55:36 +0200 Subject: [PATCH 036/182] sdp to ice transport conversion --- .../xmpp/jingle/RtpContentMap.java | 2 +- .../jingle/stanzas/IceUdpTransportInfo.java | 48 ++++++++++++++++++- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index 681686fb9..f1dd58aba 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -91,7 +91,7 @@ public class RtpContentMap { public static DescriptionTransport of(final SessionDescription sessionDescription, final SessionDescription.Media media) { final RtpDescription rtpDescription = RtpDescription.of(media); - final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); + final IceUdpTransportInfo transportInfo = IceUdpTransportInfo.of(sessionDescription, media); return new DescriptionTransport(rtpDescription, transportInfo); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index 52868f136..275f6cd2b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -1,16 +1,19 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; import com.google.common.base.Preconditions; +import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import java.util.List; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.jingle.SessionDescription; public class IceUdpTransportInfo extends GenericTransportInfo { - public IceUdpTransportInfo() { + private IceUdpTransportInfo() { super("transport", Namespace.JINGLE_TRANSPORT_ICE_UDP); } @@ -21,7 +24,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo { public List getCandidates() { final ImmutableList.Builder builder = new ImmutableList.Builder<>(); - for(final Element child : getChildren()) { + for (final Element child : getChildren()) { if ("candidate".equals(child.getName())) { builder.add(Candidate.upgrade(child)); } @@ -38,6 +41,24 @@ public class IceUdpTransportInfo extends GenericTransportInfo { return transportInfo; } + public static IceUdpTransportInfo of(SessionDescription sessionDescription, SessionDescription.Media media) { + final String ufrag = Iterables.getFirst(media.attributes.get("ice-ufrag"), null); + final String pwd = Iterables.getFirst(media.attributes.get("ice-pwd"), null); + IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo(); + if (ufrag != null) { + iceUdpTransportInfo.setAttribute("ufrag", ufrag); + } + if (pwd != null) { + iceUdpTransportInfo.setAttribute("pwd", pwd); + } + final Fingerprint fingerprint = Fingerprint.of(sessionDescription, media); + if (fingerprint != null) { + iceUdpTransportInfo.addChild(fingerprint); + } + return iceUdpTransportInfo; + + } + public static class Candidate extends Element { private Candidate() { @@ -136,5 +157,28 @@ public class IceUdpTransportInfo extends GenericTransportInfo { fingerprint.setContent(element.getContent()); return fingerprint; } + + private static Fingerprint of(ArrayListMultimap attributes) { + final String fingerprint = Iterables.getFirst(attributes.get("fingerprint"), null); + final String setup = Iterables.getFirst(attributes.get("setup"), null); + if (setup != null && fingerprint != null) { + final String[] fingerprintParts = fingerprint.split(" ", 2); + if (fingerprintParts.length == 2) { + final String hash = fingerprintParts[0]; + final String actualFingerprint = fingerprintParts[1]; + final Fingerprint element = new Fingerprint(); + element.setAttribute("hash", hash); + element.setAttribute("setup", setup); + element.setContent(actualFingerprint); + return element; + } + } + return null; + } + + public static Fingerprint of(final SessionDescription sessionDescription, final SessionDescription.Media media) { + final Fingerprint fingerprint = of(media.attributes); + return fingerprint == null ? of(sessionDescription.attributes) : fingerprint; + } } } From 2591a96945edf67af5593ceaa822db4c0df5092e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 5 Apr 2020 13:58:05 +0200 Subject: [PATCH 037/182] sdp candidate to transport-info --- .../xmpp/jingle/JingleRtpConnection.java | 60 +++++++++++++++++-- .../xmpp/jingle/RtpContentMap.java | 16 ++++- .../jingle/stanzas/IceUdpTransportInfo.java | 42 +++++++++++++ 3 files changed, 112 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 6165fde34..2d0e953ad 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -42,6 +42,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { } private State state = State.NULL; + private RtpContentMap initialRtpContentMap; public JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { @@ -152,9 +153,32 @@ public class JingleRtpConnection extends AbstractJingleConnection { } private void sendSessionInitiate(RtpContentMap rtpContentMap) { - Log.d(Config.LOGTAG, rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId).toString()); + this.initialRtpContentMap = rtpContentMap; + final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); + Log.d(Config.LOGTAG, sessionInitiate.toString()); + send(sessionInitiate); } + private void sendTransportInfo(final String contentName, IceUdpTransportInfo.Candidate candidate) { + final RtpContentMap transportInfo; + try { + transportInfo = this.initialRtpContentMap.transportInfo(contentName, candidate); + } catch (Exception e) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to prepare transport-info from candidate for content=" + contentName); + return; + } + final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); + Log.d(Config.LOGTAG, jinglePacket.toString()); + send(jinglePacket); + } + + private void send(final JinglePacket jinglePacket) { + jinglePacket.setTo(id.with); + //TODO track errors + xmppConnectionService.sendIqPacket(id.account, jinglePacket, null); + } + + private void sendSessionAccept() { Log.d(Config.LOGTAG, "sending session-accept"); } @@ -186,7 +210,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { stream.addTrack(audioTrack); - PeerConnection peer = peerConnectionFactory.createPeerConnection(Collections.emptyList(), new PeerConnection.Observer() { + PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(Collections.emptyList(), new PeerConnection.Observer() { @Override public void onSignalingChange(PeerConnection.SignalingState signalingState) { @@ -204,11 +228,16 @@ public class JingleRtpConnection extends AbstractJingleConnection { @Override public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { - + Log.d(Config.LOGTAG, "onIceGatheringChange() " + iceGatheringState); } @Override public void onIceCandidate(IceCandidate iceCandidate) { + IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp); + Log.d(Config.LOGTAG, "onIceCandidate: " + iceCandidate.sdp); + Log.d(Config.LOGTAG, "xml: " + candidate.toString()); + Log.d(Config.LOGTAG, "mid: " + iceCandidate.sdpMid); + sendTransportInfo(iceCandidate.sdpMid, candidate); } @@ -243,9 +272,9 @@ public class JingleRtpConnection extends AbstractJingleConnection { } }); - peer.addStream(stream); + peerConnection.addStream(stream); - peer.createOffer(new SdpObserver() { + peerConnection.createOffer(new SdpObserver() { @Override public void onCreateSuccess(org.webrtc.SessionDescription description) { @@ -253,6 +282,27 @@ public class JingleRtpConnection extends AbstractJingleConnection { Log.d(Config.LOGTAG, "description: " + description.description); final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); sendSessionInitiate(rtpContentMap); + peerConnection.setLocalDescription(new SdpObserver() { + @Override + public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) { + + } + + @Override + public void onSetSuccess() { + Log.d(Config.LOGTAG, "onSetSuccess()"); + } + + @Override + public void onCreateFailure(String s) { + + } + + @Override + public void onSetFailure(String s) { + + } + }, description); } @Override diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index f1dd58aba..445d34dad 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -54,13 +54,27 @@ public class RtpContentMap { } for (Map.Entry entry : this.contents.entrySet()) { final Content content = new Content(Content.Creator.INITIATOR, entry.getKey()); - content.addChild(entry.getValue().description); + if (entry.getValue().description != null) { + content.addChild(entry.getValue().description); + } content.addChild(entry.getValue().transport); jinglePacket.addJingleContent(content); } return jinglePacket; } + public RtpContentMap transportInfo(final String contentName, final IceUdpTransportInfo.Candidate candidate) { + final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName); + final IceUdpTransportInfo transportInfo = descriptionTransport == null ? null : descriptionTransport.transport; + if (transportInfo == null) { + throw new IllegalArgumentException("Unable to find transport info for content name "+contentName); + } + final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper(); + newTransportInfo.addChild(candidate); + return new RtpContentMap(null, ImmutableMap.of(contentName, new DescriptionTransport(null,newTransportInfo))); + + } + public static class DescriptionTransport { public final RtpDescription description; public final IceUdpTransportInfo transport; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index 275f6cd2b..e4d953c83 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -1,12 +1,17 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; +import android.util.Log; + import com.google.common.base.Preconditions; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import java.util.HashMap; +import java.util.Hashtable; import java.util.List; +import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.jingle.SessionDescription; @@ -41,6 +46,12 @@ public class IceUdpTransportInfo extends GenericTransportInfo { return transportInfo; } + public IceUdpTransportInfo cloneWrapper() { + final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); + transportInfo.setAttributes(new Hashtable<>(getAttributes())); + return transportInfo; + } + public static IceUdpTransportInfo of(SessionDescription sessionDescription, SessionDescription.Media media) { final String ufrag = Iterables.getFirst(media.attributes.get("ice-ufrag"), null); final String pwd = Iterables.getFirst(media.attributes.get("ice-pwd"), null); @@ -132,6 +143,37 @@ public class IceUdpTransportInfo extends GenericTransportInfo { candidate.setChildren(element.getChildren()); return candidate; } + + // https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-5.1 + public static Candidate fromSdpAttribute(final String attribute) { + final String[] pair = attribute.split(":", 2); + if (pair.length == 2 && "candidate".equals(pair[0])) { + final String[] segments = pair[1].split(" "); + if (segments.length >= 6) { + final String foundation = segments[0]; + final String component = segments[1]; + final String transport = segments[2]; + final String priority = segments[3]; + final String connectionAddress = segments[4]; + final String port = segments[5]; + final HashMap additional = new HashMap<>(); + for (int i = 6; i < segments.length - 1; i = i + 2) { + additional.put(segments[i], segments[i + 1]); + } + final Candidate candidate = new Candidate(); + candidate.setAttribute("component", component); + candidate.setAttribute("foundation", foundation); + candidate.setAttribute("generation", additional.get("generation")); + candidate.setAttribute("ip", connectionAddress); + candidate.setAttribute("port", port); + candidate.setAttribute("priority", priority); + candidate.setAttribute("protocol", transport); + candidate.setAttribute("type", additional.get("typ")); + return candidate; + } + } + return null; + } } From b1c0e93b34531216080a30b70c476b33d05b40c7 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 5 Apr 2020 15:43:28 +0200 Subject: [PATCH 038/182] rudimentary rtpmap to session converter --- .../xmpp/jingle/JingleRtpConnection.java | 3 +- .../xmpp/jingle/SessionDescription.java | 120 +++++++++++++++++- .../xmpp/jingle/stanzas/RtpDescription.java | 26 +++- 3 files changed, 145 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 2d0e953ad..b872541a9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -156,6 +156,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { this.initialRtpContentMap = rtpContentMap; final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); Log.d(Config.LOGTAG, sessionInitiate.toString()); + Log.d(Config.LOGTAG,"here is what we think the sdp looks like"+SessionDescription.of(rtpContentMap).toString()); send(sessionInitiate); } @@ -235,8 +236,6 @@ public class JingleRtpConnection extends AbstractJingleConnection { public void onIceCandidate(IceCandidate iceCandidate) { IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp); Log.d(Config.LOGTAG, "onIceCandidate: " + iceCandidate.sdp); - Log.d(Config.LOGTAG, "xml: " + candidate.toString()); - Log.d(Config.LOGTAG, "mid: " + iceCandidate.sdpMid); sendTransportInfo(iceCandidate.sdpMid, candidate); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index 3d028a439..e9a6bf854 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -3,16 +3,28 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Log; import android.util.Pair; +import com.google.common.base.Joiner; +import com.google.common.base.Strings; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import java.util.List; +import java.util.Locale; import java.util.Map; import eu.siacs.conversations.Config; +import eu.siacs.conversations.xmpp.jingle.stanzas.Group; +import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; +import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; public class SessionDescription { + private final static String LINE_DIVIDER = "\r\n"; + private final static String HARDCODED_MEDIA_PROTOCOL = "UDP/TLS/RTP/SAVPF"; //probably only true for DTLS-SRTP aka when we have a fingerprint + private final static int HARDCODED_MEDIA_PORT = 1; + private final static String HARDCODED_ICE_OPTIONS = "trickle renomination"; + private final static String HARDCODED_CONNECTION = "IN IP4 0.0.0.0"; + public final int version; public final String name; public final String connectionData; @@ -28,6 +40,18 @@ public class SessionDescription { this.media = media; } + private static void appendAttributes(StringBuilder s, ArrayListMultimap attributes) { + for (Map.Entry attribute : attributes.entries()) { + final String key = attribute.getKey(); + final String value = attribute.getValue(); + s.append("a=").append(key); + if (!Strings.isNullOrEmpty(value)) { + s.append(':').append(value); + } + s.append(LINE_DIVIDER); + } + } + public static SessionDescription parse(final Map contents) { final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder(); return sessionDescriptionBuilder.createSessionDescription(); @@ -38,7 +62,7 @@ public class SessionDescription { MediaBuilder currentMediaBuilder = null; ArrayListMultimap attributeMap = ArrayListMultimap.create(); ImmutableList.Builder mediaBuilder = new ImmutableList.Builder<>(); - for (final String line : input.split("\n")) { + for (final String line : input.split(LINE_DIVIDER)) { final String[] pair = line.trim().split("=", 2); if (pair.length < 2 || pair[0].length() != 1) { Log.d(Config.LOGTAG, "skipping sdp parsing on line " + line); @@ -99,6 +123,86 @@ public class SessionDescription { return sessionDescriptionBuilder.createSessionDescription(); } + public static SessionDescription of(final RtpContentMap contentMap) { + final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder(); + final ArrayListMultimap attributeMap = ArrayListMultimap.create(); + final ImmutableList.Builder mediaListBuilder = new ImmutableList.Builder<>(); + final Group group = contentMap.group; + if (group != null) { + attributeMap.put("group", group.getSemantics() + " " + Joiner.on(' ').join(group.getIdentificationTags())); + } + + //random additional attributes + + + for (Map.Entry entry : contentMap.contents.entrySet()) { + final String name = entry.getKey(); + RtpContentMap.DescriptionTransport descriptionTransport = entry.getValue(); + RtpDescription description = descriptionTransport.description; + IceUdpTransportInfo transport = descriptionTransport.transport; + final ArrayListMultimap mediaAttributes = ArrayListMultimap.create(); + final String ufrag = transport.getAttribute("ufrag"); + final String pwd = transport.getAttribute("pwd"); + if (!Strings.isNullOrEmpty(ufrag)) { + mediaAttributes.put("ice-ufrag", ufrag); + } + if (!Strings.isNullOrEmpty(pwd)) { + mediaAttributes.put("ice-pwd", pwd); + } + mediaAttributes.put("ice-options", HARDCODED_ICE_OPTIONS); + final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); + if (fingerprint != null) { + mediaAttributes.put("fingerprint", fingerprint.getHash() + " " + fingerprint.getContent()); + mediaAttributes.put("setup", fingerprint.getSetup()); + } + final ImmutableList.Builder formatBuilder = new ImmutableList.Builder<>(); + for (RtpDescription.PayloadType payloadType : description.getPayloadTypes()) { + formatBuilder.add(payloadType.getIntId()); + mediaAttributes.put("rtpmap", payloadType.toSdpAttribute()); + List parameters = payloadType.getParameters(); + if (parameters.size() > 0) { + mediaAttributes.put("fmtp", RtpDescription.Parameter.toSdpString(payloadType.getId(), parameters)); + } + for (RtpDescription.FeedbackNegotiation feedbackNegotiation : payloadType.getFeedbackNegotiations()) { + mediaAttributes.put("rtcp-fb", payloadType.getId() + " " + feedbackNegotiation.getType() + (Strings.isNullOrEmpty(feedbackNegotiation.getSubType()) ? "" : " " + feedbackNegotiation.getSubType())); + } + for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : payloadType.feedbackNegotiationTrrInts()) { + mediaAttributes.put("rtcp-fb", payloadType.getId() + " trr-int " + feedbackNegotiationTrrInt.getValue()); + } + } + for (RtpDescription.FeedbackNegotiation feedbackNegotiation : description.getFeedbackNegotiations()) { + mediaAttributes.put("rtcp-fb", "* " + feedbackNegotiation.getType() + (Strings.isNullOrEmpty(feedbackNegotiation.getSubType()) ? "" : " " + feedbackNegotiation.getSubType())); + } + for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : description.feedbackNegotiationTrrInts()) { + mediaAttributes.put("rtcp-fb", "* trr-int " + feedbackNegotiationTrrInt.getValue()); + } + for (RtpDescription.RtpHeaderExtension extension : description.getHeaderExtensions()) { + mediaAttributes.put("extmap", extension.getId() + " " + extension.getUri()); + } + mediaAttributes.put("mid", name); + + //random additional attributes + mediaAttributes.put("sendrecv",""); + mediaAttributes.put("rtcp-mux",""); + + final MediaBuilder mediaBuilder = new MediaBuilder(); + mediaBuilder.setMedia(description.getMedia().toString().toLowerCase(Locale.ROOT)); + mediaBuilder.setConnectionData(HARDCODED_CONNECTION); + mediaBuilder.setPort(HARDCODED_MEDIA_PORT); + mediaBuilder.setProtocol(HARDCODED_MEDIA_PROTOCOL); + mediaBuilder.setAttributes(mediaAttributes); + mediaBuilder.setFormats(formatBuilder.build()); + mediaListBuilder.add(mediaBuilder.createMedia()); + + } + sessionDescriptionBuilder.setVersion(0); + sessionDescriptionBuilder.setName(" "); + sessionDescriptionBuilder.setMedia(mediaListBuilder.build()); + sessionDescriptionBuilder.setAttributes(attributeMap); + + return sessionDescriptionBuilder.createSessionDescription(); + } + public static int ignorantIntParser(final String input) { try { return Integer.parseInt(input); @@ -116,6 +220,20 @@ public class SessionDescription { } } + @Override + public String toString() { + final StringBuilder s = new StringBuilder() + .append("v=").append(version).append(LINE_DIVIDER) + .append("s=").append(name).append(LINE_DIVIDER); + appendAttributes(s, attributes); + for (Media media : this.media) { + s.append("m=").append(media.media).append(' ').append(media.port).append(' ').append(media.protocol).append(' ').append(Joiner.on(' ').join(media.formats)).append(LINE_DIVIDER); + s.append("c=").append(media.connectionData).append(LINE_DIVIDER); + appendAttributes(s, media.attributes); + } + return s.toString(); + } + public static class Media { public final String media; public final int port; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index 8b9296993..53621a6d8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -206,7 +206,7 @@ public class RtpDescription extends GenericDescription { } } - //maps to `rtpmap $id $name/$clockrate/$channels` + //maps to `rtpmap:$id $name/$clockrate/$channels` public static class PayloadType extends Element { private PayloadType() { @@ -223,10 +223,21 @@ public class RtpDescription extends GenericDescription { } } + public String toSdpAttribute() { + final int channels = getChannels(); + return getId()+" "+getPayloadTypeName()+"/"+getClockRate()+(channels == 1 ? "" : "/"+channels); + } + + public int getIntId() { + final String id = this.getAttribute("id"); + return id == null ? 0 : SessionDescription.ignorantIntParser(id); + } + public String getId() { return this.getAttribute("id"); } + public String getPayloadTypeName() { return this.getAttribute("name"); } @@ -344,6 +355,19 @@ public class RtpDescription extends GenericDescription { return parameter; } + public static String toSdpString(final String id, List parameters) { + final StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(id).append(' '); + for(int i = 0; i < parameters.size(); ++i) { + Parameter p = parameters.get(i); + stringBuilder.append(p.getParameterName()).append('=').append(p.getParameterValue()); + if (i != parameters.size() - 1) { + stringBuilder.append(';'); + } + } + return stringBuilder.toString(); + } + public static Pair> ofSdpString(final String sdp) { final String[] pair = sdp.split(" "); if (pair.length == 2) { From f264ef9f8ba1589c43a3ed276aeb4510d82def25 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 5 Apr 2020 16:12:44 +0200 Subject: [PATCH 039/182] create sdp string and set on peer connection --- .../xmpp/jingle/JingleRtpConnection.java | 37 +++++++++++++++++-- .../xmpp/jingle/SessionDescription.java | 8 +++- .../xmpp/jingle/stanzas/JinglePacket.java | 3 +- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index b872541a9..c82f8bcd3 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -43,6 +43,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { private State state = State.NULL; private RtpContentMap initialRtpContentMap; + private PeerConnection peerConnection; public JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { @@ -63,6 +64,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { } private void receiveSessionInitiate(final JinglePacket jinglePacket) { + Log.d(Config.LOGTAG,jinglePacket.toString()); if (isInitiator()) { Log.d(Config.LOGTAG, String.format("%s: received session-initiate even though we were initiating", id.account.getJid().asBareJid())); //TODO respond with out-of-order @@ -104,6 +106,31 @@ public class JingleRtpConnection extends AbstractJingleConnection { Log.d(Config.LOGTAG, "transport: " + descriptionTransport.transport); Log.d(Config.LOGTAG, "fingerprint " + iceUdpTransportInfo.getFingerprint()); } + setupWebRTC(); + org.webrtc.SessionDescription sessionDescription = new org.webrtc.SessionDescription(org.webrtc.SessionDescription.Type.OFFER, SessionDescription.of(contentMap).toString()); + Log.d(Config.LOGTAG, "debug print for sessionDescription:" + sessionDescription.description); + this.peerConnection.setRemoteDescription(new SdpObserver() { + @Override + public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) { + + } + + @Override + public void onSetSuccess() { + Log.d(Config.LOGTAG, "onSetSuccess() for setRemoteDescription"); + } + + @Override + public void onCreateFailure(String s) { + + } + + @Override + public void onSetFailure(String s) { + Log.d(Config.LOGTAG, "onSetFailure() for setRemoteDescription. " + s); + + } + }, sessionDescription); } void deliveryMessage(final Jid from, final Element message) { @@ -148,15 +175,16 @@ public class JingleRtpConnection extends AbstractJingleConnection { } private void sendSessionInitiate() { - setupWebRTC(); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate"); + setupWebRTC(); + createOffer(); } private void sendSessionInitiate(RtpContentMap rtpContentMap) { this.initialRtpContentMap = rtpContentMap; final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); Log.d(Config.LOGTAG, sessionInitiate.toString()); - Log.d(Config.LOGTAG,"here is what we think the sdp looks like"+SessionDescription.of(rtpContentMap).toString()); + Log.d(Config.LOGTAG, "here is what we think the sdp looks like" + SessionDescription.of(rtpContentMap).toString()); send(sessionInitiate); } @@ -211,7 +239,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { stream.addTrack(audioTrack); - PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(Collections.emptyList(), new PeerConnection.Observer() { + this.peerConnection = peerConnectionFactory.createPeerConnection(Collections.emptyList(), new PeerConnection.Observer() { @Override public void onSignalingChange(PeerConnection.SignalingState signalingState) { @@ -272,7 +300,10 @@ public class JingleRtpConnection extends AbstractJingleConnection { }); peerConnection.addStream(stream); + } + private void createOffer() { + Log.d(Config.LOGTAG, "createOffer()"); peerConnection.createOffer(new SdpObserver() { @Override diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index e9a6bf854..81f13307f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -21,7 +21,7 @@ public class SessionDescription { private final static String LINE_DIVIDER = "\r\n"; private final static String HARDCODED_MEDIA_PROTOCOL = "UDP/TLS/RTP/SAVPF"; //probably only true for DTLS-SRTP aka when we have a fingerprint - private final static int HARDCODED_MEDIA_PORT = 1; + private final static int HARDCODED_MEDIA_PORT = 9; private final static String HARDCODED_ICE_OPTIONS = "trickle renomination"; private final static String HARDCODED_CONNECTION = "IN IP4 0.0.0.0"; @@ -130,6 +130,8 @@ public class SessionDescription { final Group group = contentMap.group; if (group != null) { attributeMap.put("group", group.getSemantics() + " " + Joiner.on(' ').join(group.getIdentificationTags())); + } else { + Log.d(Config.LOGTAG,"group was null"); } //random additional attributes @@ -196,7 +198,7 @@ public class SessionDescription { } sessionDescriptionBuilder.setVersion(0); - sessionDescriptionBuilder.setName(" "); + sessionDescriptionBuilder.setName("-"); sessionDescriptionBuilder.setMedia(mediaListBuilder.build()); sessionDescriptionBuilder.setAttributes(attributeMap); @@ -224,6 +226,8 @@ public class SessionDescription { public String toString() { final StringBuilder s = new StringBuilder() .append("v=").append(version).append(LINE_DIVIDER) + .append("o=- 8770656990916039506 2 IN IP4 127.0.0.1").append(LINE_DIVIDER) //what ever that means + .append("t=0 0").append(LINE_DIVIDER) .append("s=").append(name).append(LINE_DIVIDER); appendAttributes(s, attributes); for (Media media : this.media) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java index 1c15af27f..3b4ef47d1 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -43,7 +43,8 @@ public class JinglePacket extends IqPacket { } public Group getGroup() { - final Element group = this.findChild("group", Namespace.JINGLE_APPS_GROUPING); + final Element jingle = findChild("jingle", Namespace.JINGLE); + final Element group = jingle.findChild("group", Namespace.JINGLE_APPS_GROUPING); return group == null ? null : Group.upgrade(group); } From 885ec0febec8217bd82fe2f0232475153194ebd0 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 5 Apr 2020 17:31:48 +0200 Subject: [PATCH 040/182] a couple of bug fixes for SessionDescription.toString() --- .../xmpp/jingle/JingleRtpConnection.java | 14 ----------- .../xmpp/jingle/SessionDescription.java | 23 ++++++++++++------- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index c82f8bcd3..ad2b9fdad 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -92,20 +92,6 @@ public class JingleRtpConnection extends AbstractJingleConnection { } private void processContents(final RtpContentMap contentMap) { - for (Map.Entry content : contentMap.contents.entrySet()) { - final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue(); - final RtpDescription rtpDescription = descriptionTransport.description; - Log.d(Config.LOGTAG, "receive content with name " + content.getKey() + " and media=" + rtpDescription.getMedia()); - for (RtpDescription.PayloadType payloadType : rtpDescription.getPayloadTypes()) { - Log.d(Config.LOGTAG, "payload type: " + payloadType.toString()); - } - for (RtpDescription.RtpHeaderExtension extension : rtpDescription.getHeaderExtensions()) { - Log.d(Config.LOGTAG, "extension: " + extension.toString()); - } - final IceUdpTransportInfo iceUdpTransportInfo = descriptionTransport.transport; - Log.d(Config.LOGTAG, "transport: " + descriptionTransport.transport); - Log.d(Config.LOGTAG, "fingerprint " + iceUdpTransportInfo.getFingerprint()); - } setupWebRTC(); org.webrtc.SessionDescription sessionDescription = new org.webrtc.SessionDescription(org.webrtc.SessionDescription.Type.OFFER, SessionDescription.of(contentMap).toString()); Log.d(Config.LOGTAG, "debug print for sessionDescription:" + sessionDescription.description); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index 81f13307f..c205ab0d8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -118,6 +118,8 @@ public class SessionDescription { if (currentMediaBuilder != null) { currentMediaBuilder.setAttributes(attributeMap); mediaBuilder.add(currentMediaBuilder.createMedia()); + } else { + sessionDescriptionBuilder.setAttributes(attributeMap); } sessionDescriptionBuilder.setMedia(mediaBuilder.build()); return sessionDescriptionBuilder.createSessionDescription(); @@ -130,12 +132,9 @@ public class SessionDescription { final Group group = contentMap.group; if (group != null) { attributeMap.put("group", group.getSemantics() + " " + Joiner.on(' ').join(group.getIdentificationTags())); - } else { - Log.d(Config.LOGTAG,"group was null"); } - //random additional attributes - + attributeMap.put("msid-semantic", " WMS my-media-stream"); for (Map.Entry entry : contentMap.contents.entrySet()) { final String name = entry.getKey(); @@ -172,6 +171,7 @@ public class SessionDescription { mediaAttributes.put("rtcp-fb", payloadType.getId() + " trr-int " + feedbackNegotiationTrrInt.getValue()); } } + for (RtpDescription.FeedbackNegotiation feedbackNegotiation : description.getFeedbackNegotiations()) { mediaAttributes.put("rtcp-fb", "* " + feedbackNegotiation.getType() + (Strings.isNullOrEmpty(feedbackNegotiation.getSubType()) ? "" : " " + feedbackNegotiation.getSubType())); } @@ -181,11 +181,18 @@ public class SessionDescription { for (RtpDescription.RtpHeaderExtension extension : description.getHeaderExtensions()) { mediaAttributes.put("extmap", extension.getId() + " " + extension.getUri()); } + for (RtpDescription.Source source : description.getSources()) { + for (RtpDescription.Source.Parameter parameter : source.getParameters()) { + mediaAttributes.put("ssrc", source.getSsrcId() + " " + parameter.getParameterName() + ":" + parameter.getParameterValue()); + } + } + mediaAttributes.put("mid", name); //random additional attributes - mediaAttributes.put("sendrecv",""); - mediaAttributes.put("rtcp-mux",""); + mediaAttributes.put("rtcp", "9 IN IP4 0.0.0.0"); + mediaAttributes.put("sendrecv", ""); + mediaAttributes.put("rtcp-mux", ""); final MediaBuilder mediaBuilder = new MediaBuilder(); mediaBuilder.setMedia(description.getMedia().toString().toLowerCase(Locale.ROOT)); @@ -227,8 +234,8 @@ public class SessionDescription { final StringBuilder s = new StringBuilder() .append("v=").append(version).append(LINE_DIVIDER) .append("o=- 8770656990916039506 2 IN IP4 127.0.0.1").append(LINE_DIVIDER) //what ever that means - .append("t=0 0").append(LINE_DIVIDER) - .append("s=").append(name).append(LINE_DIVIDER); + .append("s=").append(name).append(LINE_DIVIDER) + .append("t=0 0").append(LINE_DIVIDER); appendAttributes(s, attributes); for (Media media : this.media) { s.append("m=").append(media.media).append(' ').append(media.port).append(' ').append(media.protocol).append(' ').append(Joiner.on(' ').join(media.formats)).append(LINE_DIVIDER); From ac9a1a773e98cc23112c458448532cb2ee4c39a9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 6 Apr 2020 10:26:29 +0200 Subject: [PATCH 041/182] receive candidates/transport-info --- .../xmpp/jingle/JingleRtpConnection.java | 61 +++++++++++++++++-- .../xmpp/jingle/RtpContentMap.java | 15 ++++- .../jingle/stanzas/IceUdpTransportInfo.java | 47 ++++++++++++++ 3 files changed, 117 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index ad2b9fdad..ef459cb88 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -16,16 +16,19 @@ import org.webrtc.PeerConnectionFactory; import org.webrtc.RtpReceiver; import org.webrtc.SdpObserver; +import java.util.ArrayDeque; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Map; import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; -import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; @@ -45,6 +48,8 @@ public class JingleRtpConnection extends AbstractJingleConnection { private RtpContentMap initialRtpContentMap; private PeerConnection peerConnection; + private final ArrayDeque pendingIceCandidates = new ArrayDeque<>(); + public JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { super(jingleConnectionManager, id, initiator); @@ -57,14 +62,51 @@ public class JingleRtpConnection extends AbstractJingleConnection { case SESSION_INITIATE: receiveSessionInitiate(jinglePacket); break; + case TRANSPORT_INFO: + receiveTransportInfo(jinglePacket); + break; default: Log.d(Config.LOGTAG, String.format("%s: received unhandled jingle action %s", id.account.getJid().asBareJid(), jinglePacket.getAction())); break; } } + private void receiveTransportInfo(final JinglePacket jinglePacket) { + if (isInState(State.SESSION_INITIALIZED, State.SESSION_ACCEPTED)) { + final RtpContentMap contentMap; + try { + contentMap = RtpContentMap.of(jinglePacket); + } catch (IllegalArgumentException | NullPointerException e) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); + return; + } + final Group originalGroup = this.initialRtpContentMap != null ? this.initialRtpContentMap.group : null; + final List identificationTags = originalGroup == null ? Collections.emptyList() : originalGroup.getIdentificationTags(); + if (identificationTags.size() == 0) { + Log.w(Config.LOGTAG,id.account.getJid().asBareJid()+": no identification tags found in initial offer. we won't be able to calculate mLineIndices"); + } + for(final Map.Entry content : contentMap.contents.entrySet()) { + final String ufrag = content.getValue().transport.getAttribute("ufrag"); + for(final IceUdpTransportInfo.Candidate candidate : content.getValue().transport.getCandidates()) { + final String sdp = candidate.toSdpAttribute(ufrag); + final String sdpMid = content.getKey(); + final int mLineIndex = identificationTags.indexOf(sdpMid); + final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp); + Log.d(Config.LOGTAG,"received candidate: "+iceCandidate); + if (isInState(State.SESSION_ACCEPTED)) { + this.peerConnection.addIceCandidate(iceCandidate); + } else { + this.pendingIceCandidates.push(iceCandidate); + } + } + } + } else { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received transport info while in state=" + this.state); + } + } + private void receiveSessionInitiate(final JinglePacket jinglePacket) { - Log.d(Config.LOGTAG,jinglePacket.toString()); + Log.d(Config.LOGTAG, jinglePacket.toString()); if (isInitiator()) { Log.d(Config.LOGTAG, String.format("%s: received session-initiate even though we were initiating", id.account.getJid().asBareJid())); //TODO respond with out-of-order @@ -73,13 +115,15 @@ public class JingleRtpConnection extends AbstractJingleConnection { final RtpContentMap contentMap; try { contentMap = RtpContentMap.of(jinglePacket); - } catch (IllegalArgumentException | NullPointerException e) { + contentMap.requireContentDescriptions(); + } catch (IllegalArgumentException | IllegalStateException | NullPointerException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); return; } Log.d(Config.LOGTAG, "processing session-init with " + contentMap.contents.size() + " contents"); final State oldState = this.state; if (transition(State.SESSION_INITIALIZED)) { + this.initialRtpContentMap = contentMap; if (oldState == State.PROCEED) { processContents(contentMap); sendSessionAccept(); @@ -225,7 +269,10 @@ public class JingleRtpConnection extends AbstractJingleConnection { stream.addTrack(audioTrack); - this.peerConnection = peerConnectionFactory.createPeerConnection(Collections.emptyList(), new PeerConnection.Observer() { + final List iceServers = ImmutableList.of( + PeerConnection.IceServer.builder("stun:xmpp.conversations.im:3478").createIceServer() + ); + this.peerConnection = peerConnectionFactory.createPeerConnection(iceServers, new PeerConnection.Observer() { @Override public void onSignalingChange(PeerConnection.SignalingState signalingState) { @@ -249,7 +296,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { @Override public void onIceCandidate(IceCandidate iceCandidate) { IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp); - Log.d(Config.LOGTAG, "onIceCandidate: " + iceCandidate.sdp); + Log.d(Config.LOGTAG, "onIceCandidate: " + iceCandidate.sdp + " mLineIndex=" + iceCandidate.sdpMLineIndex); sendTransportInfo(iceCandidate.sdpMid, candidate); } @@ -352,6 +399,10 @@ public class JingleRtpConnection extends AbstractJingleConnection { } + private synchronized boolean isInState(State... state) { + return Arrays.asList(state).contains(this.state); + } + private synchronized boolean transition(final State target) { final Collection validTransitions = VALID_TRANSITIONS.get(this.state); if (validTransitions != null && validTransitions.contains(target)) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index 445d34dad..1ebd810b7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -47,6 +47,17 @@ public class RtpContentMap { return new RtpContentMap(group, contentMapBuilder.build()); } + public void requireContentDescriptions() { + if (this.contents.size() == 0) { + throw new IllegalStateException("No contents available"); + } + for(Map.Entry entry : this.contents.entrySet()) { + if (entry.getValue().description == null) { + throw new IllegalStateException(String.format("%s is lacking content description", entry.getKey())); + } + } + } + public JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) { final JinglePacket jinglePacket = new JinglePacket(action, sessionId); if (this.group != null) { @@ -89,7 +100,9 @@ public class RtpContentMap { final GenericTransportInfo transportInfo = content.getTransport(); final RtpDescription rtpDescription; final IceUdpTransportInfo iceUdpTransportInfo; - if (description instanceof RtpDescription) { + if (description == null) { + rtpDescription = null; + } else if (description instanceof RtpDescription) { rtpDescription = (RtpDescription) description; } else { Log.d(Config.LOGTAG, "description was " + description); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index e4d953c83..1b3fe18f2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -2,14 +2,22 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; import android.util.Log; +import com.google.common.base.Function; +import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import org.checkerframework.checker.nullness.compatqual.NullableDecl; + +import java.util.Collection; import java.util.HashMap; import java.util.Hashtable; import java.util.List; +import java.util.Map; import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; @@ -144,6 +152,43 @@ public class IceUdpTransportInfo extends GenericTransportInfo { return candidate; } + public String toSdpAttribute(final String ufrag) { + final String foundation = this.getAttribute("foundation"); + final String component = this.getAttribute("component"); + final String transport = this.getAttribute("protocol"); + final String priority = this.getAttribute("priority"); + final String connectionAddress = this.getAttribute("ip"); + final String port = this.getAttribute("port"); + final Map additionalParameter = new HashMap<>(); + final String relAddr = this.getAttribute("rel-addr"); + if (relAddr != null) { + additionalParameter.put("raddr",relAddr); + } + final String relPort = this.getAttribute("rel-port"); + if (relPort != null) { + additionalParameter.put("rport", relPort); + } + final String generation = this.getAttribute("generation"); + if (generation != null) { + additionalParameter.put("generation", generation); + } + if (ufrag != null) { + additionalParameter.put("ufrag", ufrag); + } + final String parametersString = Joiner.on(' ').join(Collections2.transform(additionalParameter.entrySet(), input -> String.format("%s %s",input.getKey(),input.getValue()))); + return String.format( + "candidate:%s %s %s %s %s %s %s", + foundation, + component, + transport, + priority, + connectionAddress, + port, + parametersString + + ); + } + // https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-5.1 public static Candidate fromSdpAttribute(final String attribute) { final String[] pair = attribute.split(":", 2); @@ -164,6 +209,8 @@ public class IceUdpTransportInfo extends GenericTransportInfo { candidate.setAttribute("component", component); candidate.setAttribute("foundation", foundation); candidate.setAttribute("generation", additional.get("generation")); + candidate.setAttribute("rel-addr", additional.get("raddr")); + candidate.setAttribute("rel-port", additional.get("rport")); candidate.setAttribute("ip", connectionAddress); candidate.setAttribute("port", port); candidate.setAttribute("priority", priority); From 9dfa9df79081ebe0051f26e27bac4bae291c9d6c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 6 Apr 2020 13:01:17 +0200 Subject: [PATCH 042/182] implement sending of session-accept --- .../xmpp/jingle/JingleRtpConnection.java | 247 +++++------------- .../xmpp/jingle/WebRTCWrapper.java | 247 ++++++++++++++++++ 2 files changed, 313 insertions(+), 181 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index ef459cb88..ca28caedb 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -5,16 +5,7 @@ import android.util.Log; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import org.webrtc.AudioSource; -import org.webrtc.AudioTrack; -import org.webrtc.DataChannel; import org.webrtc.IceCandidate; -import org.webrtc.MediaConstraints; -import org.webrtc.MediaStream; -import org.webrtc.PeerConnection; -import org.webrtc.PeerConnectionFactory; -import org.webrtc.RtpReceiver; -import org.webrtc.SdpObserver; import java.util.ArrayDeque; import java.util.Arrays; @@ -32,7 +23,7 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; -public class JingleRtpConnection extends AbstractJingleConnection { +public class JingleRtpConnection extends AbstractJingleConnection implements WebRTCWrapper.EventCallback { private static final Map> VALID_TRANSITIONS; @@ -41,14 +32,15 @@ public class JingleRtpConnection extends AbstractJingleConnection { transitionBuilder.put(State.NULL, ImmutableList.of(State.PROPOSED, State.SESSION_INITIALIZED)); transitionBuilder.put(State.PROPOSED, ImmutableList.of(State.ACCEPTED, State.PROCEED)); transitionBuilder.put(State.PROCEED, ImmutableList.of(State.SESSION_INITIALIZED)); + transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of(State.SESSION_ACCEPTED)); VALID_TRANSITIONS = transitionBuilder.build(); } - private State state = State.NULL; - private RtpContentMap initialRtpContentMap; - private PeerConnection peerConnection; - + private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this); private final ArrayDeque pendingIceCandidates = new ArrayDeque<>(); + private State state = State.NULL; + private RtpContentMap initiatorRtpContentMap; + private RtpContentMap responderRtpContentMap; public JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { @@ -80,21 +72,22 @@ public class JingleRtpConnection extends AbstractJingleConnection { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); return; } - final Group originalGroup = this.initialRtpContentMap != null ? this.initialRtpContentMap.group : null; + //TODO pick proper rtpContentMap + final Group originalGroup = this.initiatorRtpContentMap != null ? this.initiatorRtpContentMap.group : null; final List identificationTags = originalGroup == null ? Collections.emptyList() : originalGroup.getIdentificationTags(); if (identificationTags.size() == 0) { - Log.w(Config.LOGTAG,id.account.getJid().asBareJid()+": no identification tags found in initial offer. we won't be able to calculate mLineIndices"); + Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices"); } - for(final Map.Entry content : contentMap.contents.entrySet()) { + for (final Map.Entry content : contentMap.contents.entrySet()) { final String ufrag = content.getValue().transport.getAttribute("ufrag"); - for(final IceUdpTransportInfo.Candidate candidate : content.getValue().transport.getCandidates()) { + for (final IceUdpTransportInfo.Candidate candidate : content.getValue().transport.getCandidates()) { final String sdp = candidate.toSdpAttribute(ufrag); final String sdpMid = content.getKey(); final int mLineIndex = identificationTags.indexOf(sdpMid); final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp); - Log.d(Config.LOGTAG,"received candidate: "+iceCandidate); + Log.d(Config.LOGTAG, "received candidate: " + iceCandidate); if (isInState(State.SESSION_ACCEPTED)) { - this.peerConnection.addIceCandidate(iceCandidate); + this.webRTCWrapper.addIceCandidate(iceCandidate); } else { this.pendingIceCandidates.push(iceCandidate); } @@ -106,7 +99,6 @@ public class JingleRtpConnection extends AbstractJingleConnection { } private void receiveSessionInitiate(final JinglePacket jinglePacket) { - Log.d(Config.LOGTAG, jinglePacket.toString()); if (isInitiator()) { Log.d(Config.LOGTAG, String.format("%s: received session-initiate even though we were initiating", id.account.getJid().asBareJid())); //TODO respond with out-of-order @@ -123,11 +115,12 @@ public class JingleRtpConnection extends AbstractJingleConnection { Log.d(Config.LOGTAG, "processing session-init with " + contentMap.contents.size() + " contents"); final State oldState = this.state; if (transition(State.SESSION_INITIALIZED)) { - this.initialRtpContentMap = contentMap; + this.initiatorRtpContentMap = contentMap; if (oldState == State.PROCEED) { - processContents(contentMap); + Log.d(Config.LOGTAG, "automatically accepting"); sendSessionAccept(); } else { + Log.d(Config.LOGTAG, "start ringing"); //TODO start ringing } } else { @@ -135,32 +128,35 @@ public class JingleRtpConnection extends AbstractJingleConnection { } } - private void processContents(final RtpContentMap contentMap) { + private void sendSessionAccept() { + final RtpContentMap rtpContentMap = this.initiatorRtpContentMap; + if (rtpContentMap == null) { + throw new IllegalStateException("intital RTP Content Map has not been set"); + } setupWebRTC(); - org.webrtc.SessionDescription sessionDescription = new org.webrtc.SessionDescription(org.webrtc.SessionDescription.Type.OFFER, SessionDescription.of(contentMap).toString()); - Log.d(Config.LOGTAG, "debug print for sessionDescription:" + sessionDescription.description); - this.peerConnection.setRemoteDescription(new SdpObserver() { - @Override - public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) { + final org.webrtc.SessionDescription offer = new org.webrtc.SessionDescription( + org.webrtc.SessionDescription.Type.OFFER, + SessionDescription.of(rtpContentMap).toString() + ); + try { + this.webRTCWrapper.setRemoteDescription(offer).get(); + org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createAnswer().get(); + this.webRTCWrapper.setLocalDescription(webRTCSessionDescription); + final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); + final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); + sendSessionAccept(respondingRtpContentMap); + } catch (Exception e) { + Log.d(Config.LOGTAG, "unable to send session accept", e); - } + } + } - @Override - public void onSetSuccess() { - Log.d(Config.LOGTAG, "onSetSuccess() for setRemoteDescription"); - } - - @Override - public void onCreateFailure(String s) { - - } - - @Override - public void onSetFailure(String s) { - Log.d(Config.LOGTAG, "onSetFailure() for setRemoteDescription. " + s); - - } - }, sessionDescription); + private void sendSessionAccept(final RtpContentMap rtpContentMap) { + this.responderRtpContentMap = rtpContentMap; + this.transitionOrThrow(State.SESSION_ACCEPTED); + final JinglePacket sessionAccept = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId); + Log.d(Config.LOGTAG, sessionAccept.toString()); + send(sessionAccept); } void deliveryMessage(final Jid from, final Element message) { @@ -178,6 +174,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { private void receivePropose(final Jid from, final Element propose) { final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); + //TODO we can use initiator logic here if (originatedFromMyself) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": saw proposal from mysql. ignoring"); } else if (transition(State.PROPOSED)) { @@ -207,21 +204,31 @@ public class JingleRtpConnection extends AbstractJingleConnection { private void sendSessionInitiate() { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate"); setupWebRTC(); - createOffer(); + try { + org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get(); + final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); + Log.d(Config.LOGTAG, "description: " + webRTCSessionDescription.description); + final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); + sendSessionInitiate(rtpContentMap); + this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); + } catch (Exception e) { + Log.d(Config.LOGTAG, "unable to sendSessionInitiate", e); + } } private void sendSessionInitiate(RtpContentMap rtpContentMap) { - this.initialRtpContentMap = rtpContentMap; + this.initiatorRtpContentMap = rtpContentMap; + this.transitionOrThrow(State.SESSION_INITIALIZED); final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); Log.d(Config.LOGTAG, sessionInitiate.toString()); - Log.d(Config.LOGTAG, "here is what we think the sdp looks like" + SessionDescription.of(rtpContentMap).toString()); send(sessionInitiate); } private void sendTransportInfo(final String contentName, IceUdpTransportInfo.Candidate candidate) { final RtpContentMap transportInfo; try { - transportInfo = this.initialRtpContentMap.transportInfo(contentName, candidate); + //TODO when responding use responderRtpContentMap + transportInfo = this.initiatorRtpContentMap.transportInfo(contentName, candidate); } catch (Exception e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to prepare transport-info from candidate for content=" + contentName); return; @@ -238,10 +245,6 @@ public class JingleRtpConnection extends AbstractJingleConnection { } - private void sendSessionAccept() { - Log.d(Config.LOGTAG, "sending session-accept"); - } - public void pickUpCall() { switch (this.state) { case PROPOSED: @@ -256,133 +259,8 @@ public class JingleRtpConnection extends AbstractJingleConnection { } private void setupWebRTC() { - PeerConnectionFactory.initialize( - PeerConnectionFactory.InitializationOptions.builder(xmppConnectionService).createInitializationOptions() - ); - final PeerConnectionFactory.Options options = new PeerConnectionFactory.Options(); - PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory(); - - final AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints()); - - final AudioTrack audioTrack = peerConnectionFactory.createAudioTrack("my-audio-track", audioSource); - final MediaStream stream = peerConnectionFactory.createLocalMediaStream("my-media-stream"); - stream.addTrack(audioTrack); - - - final List iceServers = ImmutableList.of( - PeerConnection.IceServer.builder("stun:xmpp.conversations.im:3478").createIceServer() - ); - this.peerConnection = peerConnectionFactory.createPeerConnection(iceServers, new PeerConnection.Observer() { - @Override - public void onSignalingChange(PeerConnection.SignalingState signalingState) { - - } - - @Override - public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { - - } - - @Override - public void onIceConnectionReceivingChange(boolean b) { - - } - - @Override - public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { - Log.d(Config.LOGTAG, "onIceGatheringChange() " + iceGatheringState); - } - - @Override - public void onIceCandidate(IceCandidate iceCandidate) { - IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp); - Log.d(Config.LOGTAG, "onIceCandidate: " + iceCandidate.sdp + " mLineIndex=" + iceCandidate.sdpMLineIndex); - sendTransportInfo(iceCandidate.sdpMid, candidate); - - } - - @Override - public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) { - - } - - @Override - public void onAddStream(MediaStream mediaStream) { - - } - - @Override - public void onRemoveStream(MediaStream mediaStream) { - - } - - @Override - public void onDataChannel(DataChannel dataChannel) { - - } - - @Override - public void onRenegotiationNeeded() { - - } - - @Override - public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { - - } - }); - - peerConnection.addStream(stream); - } - - private void createOffer() { - Log.d(Config.LOGTAG, "createOffer()"); - peerConnection.createOffer(new SdpObserver() { - - @Override - public void onCreateSuccess(org.webrtc.SessionDescription description) { - final SessionDescription sessionDescription = SessionDescription.parse(description.description); - Log.d(Config.LOGTAG, "description: " + description.description); - final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); - sendSessionInitiate(rtpContentMap); - peerConnection.setLocalDescription(new SdpObserver() { - @Override - public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) { - - } - - @Override - public void onSetSuccess() { - Log.d(Config.LOGTAG, "onSetSuccess()"); - } - - @Override - public void onCreateFailure(String s) { - - } - - @Override - public void onSetFailure(String s) { - - } - }, description); - } - - @Override - public void onSetSuccess() { - - } - - @Override - public void onCreateFailure(String s) { - - } - - @Override - public void onSetFailure(String s) { - - } - }, new MediaConstraints()); + this.webRTCWrapper.setup(this.xmppConnectionService); + this.webRTCWrapper.initializePeerConnection(); } private void pickupCallFromProposed() { @@ -419,4 +297,11 @@ public class JingleRtpConnection extends AbstractJingleConnection { throw new IllegalStateException(String.format("Unable to transition from %s to %s", this.state, target)); } } + + @Override + public void onIceCandidate(final IceCandidate iceCandidate) { + final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp); + Log.d(Config.LOGTAG, "onIceCandidate: " + iceCandidate.sdp + " mLineIndex=" + iceCandidate.sdpMLineIndex); + sendTransportInfo(iceCandidate.sdpMid, candidate); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java new file mode 100644 index 000000000..67d89bcdb --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -0,0 +1,247 @@ +package eu.siacs.conversations.xmpp.jingle; + +import android.content.Context; + +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; + +import org.webrtc.AudioSource; +import org.webrtc.AudioTrack; +import org.webrtc.DataChannel; +import org.webrtc.IceCandidate; +import org.webrtc.MediaConstraints; +import org.webrtc.MediaStream; +import org.webrtc.PeerConnection; +import org.webrtc.PeerConnectionFactory; +import org.webrtc.RtpReceiver; +import org.webrtc.SdpObserver; +import org.webrtc.SessionDescription; + +import java.util.List; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class WebRTCWrapper { + + private final EventCallback eventCallback; + + private final PeerConnection.Observer peerConnectionObserver = new PeerConnection.Observer() { + @Override + public void onSignalingChange(PeerConnection.SignalingState signalingState) { + + } + + @Override + public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { + + } + + @Override + public void onIceConnectionReceivingChange(boolean b) { + + } + + @Override + public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { + + } + + @Override + public void onIceCandidate(IceCandidate iceCandidate) { + eventCallback.onIceCandidate(iceCandidate); + } + + @Override + public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) { + + } + + @Override + public void onAddStream(MediaStream mediaStream) { + + } + + @Override + public void onRemoveStream(MediaStream mediaStream) { + + } + + @Override + public void onDataChannel(DataChannel dataChannel) { + + } + + @Override + public void onRenegotiationNeeded() { + + } + + @Override + public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { + + } + }; + @Nullable + private PeerConnection peerConnection = null; + + public WebRTCWrapper(final EventCallback eventCallback) { + this.eventCallback = eventCallback; + } + + public void setup(final Context context) { + PeerConnectionFactory.initialize( + PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions() + ); + } + + public void initializePeerConnection() { + final PeerConnectionFactory.Options options = new PeerConnectionFactory.Options(); + PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory(); + + final AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints()); + + final AudioTrack audioTrack = peerConnectionFactory.createAudioTrack("my-audio-track", audioSource); + final MediaStream stream = peerConnectionFactory.createLocalMediaStream("my-media-stream"); + stream.addTrack(audioTrack); + + + final List iceServers = ImmutableList.of( + PeerConnection.IceServer.builder("stun:xmpp.conversations.im:3478").createIceServer() + ); + final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(iceServers, peerConnectionObserver); + if (peerConnection == null) { + throw new IllegalStateException("Unable to create PeerConnection"); + } + peerConnection.addStream(stream); + this.peerConnection = peerConnection; + } + + public ListenableFuture createOffer() { + return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { + final SettableFuture future = SettableFuture.create(); + peerConnection.createOffer(new CreateSdpObserver() { + @Override + public void onCreateSuccess(SessionDescription sessionDescription) { + future.set(sessionDescription); + } + + @Override + public void onCreateFailure(String s) { + future.setException(new IllegalStateException("Unable to create offer: " + s)); + } + }, new MediaConstraints()); + return future; + }, MoreExecutors.directExecutor()); + } + + public ListenableFuture createAnswer() { + return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { + final SettableFuture future = SettableFuture.create(); + peerConnection.createAnswer(new CreateSdpObserver() { + @Override + public void onCreateSuccess(SessionDescription sessionDescription) { + future.set(sessionDescription); + } + + @Override + public void onCreateFailure(String s) { + future.setException(new IllegalStateException("Unable to create answer: " + s)); + } + }, new MediaConstraints()); + return future; + }, MoreExecutors.directExecutor()); + } + + public ListenableFuture setLocalDescription(final SessionDescription sessionDescription) { + return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { + final SettableFuture future = SettableFuture.create(); + peerConnection.setLocalDescription(new SetSdpObserver() { + @Override + public void onSetSuccess() { + future.set(null); + } + + @Override + public void onSetFailure(String s) { + future.setException(new IllegalArgumentException("unable to set local session description: "+s)); + + } + }, sessionDescription); + return future; + }, MoreExecutors.directExecutor()); + } + + public ListenableFuture setRemoteDescription(final SessionDescription sessionDescription) { + return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { + final SettableFuture future = SettableFuture.create(); + peerConnection.setRemoteDescription(new SetSdpObserver() { + @Override + public void onSetSuccess() { + future.set(null); + } + + @Override + public void onSetFailure(String s) { + future.setException(new IllegalArgumentException("unable to set remote session description: "+s)); + + } + }, sessionDescription); + return future; + }, MoreExecutors.directExecutor()); + } + + @Nonnull + private ListenableFuture getPeerConnectionFuture() { + final PeerConnection peerConnection = this.peerConnection; + if (peerConnection == null) { + return Futures.immediateFailedFuture(new IllegalStateException("initialize PeerConnection first")); + } else { + return Futures.immediateFuture(peerConnection); + } + } + + public void addIceCandidate(IceCandidate iceCandidate) { + final PeerConnection peerConnection = this.peerConnection; + if (peerConnection == null) { + throw new IllegalStateException("initialize PeerConnection first"); + } + peerConnection.addIceCandidate(iceCandidate); + } + + private static abstract class SetSdpObserver implements SdpObserver { + + @Override + public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) { + throw new IllegalStateException("Not able to use SetSdpObserver"); + } + + @Override + public void onCreateFailure(String s) { + throw new IllegalStateException("Not able to use SetSdpObserver"); + } + + } + + private static abstract class CreateSdpObserver implements SdpObserver { + + + @Override + public void onSetSuccess() { + throw new IllegalStateException("Not able to use CreateSdpObserver"); + } + + + @Override + public void onSetFailure(String s) { + throw new IllegalStateException("Not able to use CreateSdpObserver"); + } + } + + public interface EventCallback { + void onIceCandidate(IceCandidate iceCandidate); + } +} From 22c755c5ce9082797d80b5f1fcb812eede9af092 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 6 Apr 2020 15:45:06 +0200 Subject: [PATCH 043/182] implement session accept --- .../xmpp/jingle/JingleRtpConnection.java | 53 +++++- .../xmpp/jingle/WebRTCWrapper.java | 26 ++- .../jingle/stanzas/IceUdpTransportInfo.java | 151 +++++++++--------- 3 files changed, 147 insertions(+), 83 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index ca28caedb..3e9759c22 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -57,6 +57,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web case TRANSPORT_INFO: receiveTransportInfo(jinglePacket); break; + case SESSION_ACCEPT: + receiveSessionAccept(jinglePacket); + break; default: Log.d(Config.LOGTAG, String.format("%s: received unhandled jingle action %s", id.account.getJid().asBareJid(), jinglePacket.getAction())); break; @@ -72,8 +75,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); return; } - //TODO pick proper rtpContentMap - final Group originalGroup = this.initiatorRtpContentMap != null ? this.initiatorRtpContentMap.group : null; + final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; + final Group originalGroup = rtpContentMap != null ? rtpContentMap.group : null; final List identificationTags = originalGroup == null ? Collections.emptyList() : originalGroup.getIdentificationTags(); if (identificationTags.size() == 0) { Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices"); @@ -128,10 +131,46 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } + private void receiveSessionAccept(final JinglePacket jinglePacket) { + if (!isInitiator()) { + Log.d(Config.LOGTAG, String.format("%s: received session-accept even though we were responding", id.account.getJid().asBareJid())); + //TODO respond with out-of-order + return; + } + final RtpContentMap contentMap; + try { + contentMap = RtpContentMap.of(jinglePacket); + contentMap.requireContentDescriptions(); + } catch (IllegalArgumentException | IllegalStateException | NullPointerException e) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); + return; + } + Log.d(Config.LOGTAG, "processing session-accept with " + contentMap.contents.size() + " contents"); + if (transition(State.SESSION_ACCEPTED)) { + receiveSessionAccept(contentMap); + } else { + Log.d(Config.LOGTAG, String.format("%s: received session-accept while in state %s", id.account.getJid().asBareJid(), state)); + //TODO out-of-order + } + } + + private void receiveSessionAccept(final RtpContentMap contentMap) { + this.responderRtpContentMap = contentMap; + org.webrtc.SessionDescription answer = new org.webrtc.SessionDescription( + org.webrtc.SessionDescription.Type.ANSWER, + SessionDescription.of(contentMap).toString() + ); + try { + this.webRTCWrapper.setRemoteDescription(answer).get(); + } catch (Exception e) { + Log.d(Config.LOGTAG, "unable to receive session accept", e); + } + } + private void sendSessionAccept() { final RtpContentMap rtpContentMap = this.initiatorRtpContentMap; if (rtpContentMap == null) { - throw new IllegalStateException("intital RTP Content Map has not been set"); + throw new IllegalStateException("initiator RTP Content Map has not been set"); } setupWebRTC(); final org.webrtc.SessionDescription offer = new org.webrtc.SessionDescription( @@ -141,10 +180,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { this.webRTCWrapper.setRemoteDescription(offer).get(); org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createAnswer().get(); - this.webRTCWrapper.setLocalDescription(webRTCSessionDescription); final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); sendSessionAccept(respondingRtpContentMap); + this.webRTCWrapper.setLocalDescription(webRTCSessionDescription); } catch (Exception e) { Log.d(Config.LOGTAG, "unable to send session accept", e); @@ -227,8 +266,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void sendTransportInfo(final String contentName, IceUdpTransportInfo.Candidate candidate) { final RtpContentMap transportInfo; try { - //TODO when responding use responderRtpContentMap - transportInfo = this.initiatorRtpContentMap.transportInfo(contentName, candidate); + final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; + transportInfo = rtpContentMap.transportInfo(contentName, candidate); } catch (Exception e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to prepare transport-info from candidate for content=" + contentName); return; @@ -301,7 +340,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web @Override public void onIceCandidate(final IceCandidate iceCandidate) { final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp); - Log.d(Config.LOGTAG, "onIceCandidate: " + iceCandidate.sdp + " mLineIndex=" + iceCandidate.sdpMLineIndex); + Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate.toString()); sendTransportInfo(iceCandidate.sdpMid, candidate); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 67d89bcdb..53210e78c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.xmpp.jingle; import android.content.Context; +import android.util.Log; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; @@ -25,6 +26,8 @@ import java.util.List; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import eu.siacs.conversations.Config; + public class WebRTCWrapper { private final EventCallback eventCallback; @@ -32,9 +35,15 @@ public class WebRTCWrapper { private final PeerConnection.Observer peerConnectionObserver = new PeerConnection.Observer() { @Override public void onSignalingChange(PeerConnection.SignalingState signalingState) { + Log.d(Config.LOGTAG, "onSignalingChange(" + signalingState + ")"); } + @Override + public void onConnectionChange(PeerConnection.PeerConnectionState newState) { + Log.d(Config.LOGTAG, "onConnectionChange(" + newState + ")"); + } + @Override public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { @@ -62,7 +71,10 @@ public class WebRTCWrapper { @Override public void onAddStream(MediaStream mediaStream) { - + Log.d(Config.LOGTAG, "onAddStream"); + for(AudioTrack audioTrack : mediaStream.audioTracks) { + Log.d(Config.LOGTAG,"remote? - audioTrack enabled:"+audioTrack.enabled()+" state="+audioTrack.state()); + } } @Override @@ -82,6 +94,7 @@ public class WebRTCWrapper { @Override public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { + Log.d(Config.LOGTAG, "onAddTrack()"); } }; @@ -105,6 +118,7 @@ public class WebRTCWrapper { final AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints()); final AudioTrack audioTrack = peerConnectionFactory.createAudioTrack("my-audio-track", audioSource); + Log.d(Config.LOGTAG,"audioTrack enabled:"+audioTrack.enabled()+" state="+audioTrack.state()); final MediaStream stream = peerConnectionFactory.createLocalMediaStream("my-media-stream"); stream.addTrack(audioTrack); @@ -117,6 +131,8 @@ public class WebRTCWrapper { throw new IllegalStateException("Unable to create PeerConnection"); } peerConnection.addStream(stream); + peerConnection.setAudioPlayout(true); + peerConnection.setAudioRecording(true); this.peerConnection = peerConnection; } @@ -167,7 +183,7 @@ public class WebRTCWrapper { @Override public void onSetFailure(String s) { - future.setException(new IllegalArgumentException("unable to set local session description: "+s)); + future.setException(new IllegalArgumentException("unable to set local session description: " + s)); } }, sessionDescription); @@ -186,7 +202,7 @@ public class WebRTCWrapper { @Override public void onSetFailure(String s) { - future.setException(new IllegalArgumentException("unable to set remote session description: "+s)); + future.setException(new IllegalArgumentException("unable to set remote session description: " + s)); } }, sessionDescription); @@ -212,6 +228,10 @@ public class WebRTCWrapper { peerConnection.addIceCandidate(iceCandidate); } + public PeerConnection.PeerConnectionState getState() { + return this.peerConnection.connectionState(); + } + private static abstract class SetSdpObserver implements SdpObserver { @Override diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index 1b3fe18f2..467a25490 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -16,6 +16,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableDecl; import java.util.Collection; import java.util.HashMap; import java.util.Hashtable; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -30,21 +31,6 @@ public class IceUdpTransportInfo extends GenericTransportInfo { super("transport", Namespace.JINGLE_TRANSPORT_ICE_UDP); } - public Fingerprint getFingerprint() { - final Element fingerprint = this.findChild("fingerprint", Namespace.JINGLE_APPS_DTLS); - return fingerprint == null ? null : Fingerprint.upgrade(fingerprint); - } - - public List getCandidates() { - final ImmutableList.Builder builder = new ImmutableList.Builder<>(); - for (final Element child : getChildren()) { - if ("candidate".equals(child.getName())) { - builder.add(Candidate.upgrade(child)); - } - } - return builder.build(); - } - public static IceUdpTransportInfo upgrade(final Element element) { Preconditions.checkArgument("transport".equals(element.getName()), "Name of provided element is not transport"); Preconditions.checkArgument(Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(element.getNamespace()), "Element does not match ice-udp transport namespace"); @@ -54,12 +40,6 @@ public class IceUdpTransportInfo extends GenericTransportInfo { return transportInfo; } - public IceUdpTransportInfo cloneWrapper() { - final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); - transportInfo.setAttributes(new Hashtable<>(getAttributes())); - return transportInfo; - } - public static IceUdpTransportInfo of(SessionDescription sessionDescription, SessionDescription.Media media) { final String ufrag = Iterables.getFirst(media.attributes.get("ice-ufrag"), null); final String pwd = Iterables.getFirst(media.attributes.get("ice-pwd"), null); @@ -78,12 +58,74 @@ public class IceUdpTransportInfo extends GenericTransportInfo { } + public Fingerprint getFingerprint() { + final Element fingerprint = this.findChild("fingerprint", Namespace.JINGLE_APPS_DTLS); + return fingerprint == null ? null : Fingerprint.upgrade(fingerprint); + } + + public List getCandidates() { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for (final Element child : getChildren()) { + if ("candidate".equals(child.getName())) { + builder.add(Candidate.upgrade(child)); + } + } + return builder.build(); + } + + public IceUdpTransportInfo cloneWrapper() { + final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); + transportInfo.setAttributes(new Hashtable<>(getAttributes())); + return transportInfo; + } + public static class Candidate extends Element { private Candidate() { super("candidate"); } + public static Candidate upgrade(final Element element) { + Preconditions.checkArgument("candidate".equals(element.getName())); + final Candidate candidate = new Candidate(); + candidate.setAttributes(element.getAttributes()); + candidate.setChildren(element.getChildren()); + return candidate; + } + + // https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-5.1 + public static Candidate fromSdpAttribute(final String attribute) { + final String[] pair = attribute.split(":", 2); + if (pair.length == 2 && "candidate".equals(pair[0])) { + final String[] segments = pair[1].split(" "); + if (segments.length >= 6) { + final String foundation = segments[0]; + final String component = segments[1]; + final String transport = segments[2]; + final String priority = segments[3]; + final String connectionAddress = segments[4]; + final String port = segments[5]; + final HashMap additional = new HashMap<>(); + for (int i = 6; i < segments.length - 1; i = i + 2) { + additional.put(segments[i], segments[i + 1]); + } + final Candidate candidate = new Candidate(); + candidate.setAttribute("component", component); + candidate.setAttribute("foundation", foundation); + candidate.setAttribute("generation", additional.get("generation")); + candidate.setAttribute("rel-addr", additional.get("raddr")); + candidate.setAttribute("rel-port", additional.get("rport")); + candidate.setAttribute("ip", connectionAddress); + candidate.setAttribute("port", port); + candidate.setAttribute("priority", priority); + candidate.setAttribute("protocol", transport); + candidate.setAttribute("type", additional.get("typ")); + return candidate; + } + } + return null; + } + public int getComponent() { return getAttributeAsInt("component"); } @@ -144,14 +186,6 @@ public class IceUdpTransportInfo extends GenericTransportInfo { } } - public static Candidate upgrade(final Element element) { - Preconditions.checkArgument("candidate".equals(element.getName())); - final Candidate candidate = new Candidate(); - candidate.setAttributes(element.getAttributes()); - candidate.setChildren(element.getChildren()); - return candidate; - } - public String toSdpAttribute(final String ufrag) { final String foundation = this.getAttribute("foundation"); final String component = this.getAttribute("component"); @@ -159,10 +193,14 @@ public class IceUdpTransportInfo extends GenericTransportInfo { final String priority = this.getAttribute("priority"); final String connectionAddress = this.getAttribute("ip"); final String port = this.getAttribute("port"); - final Map additionalParameter = new HashMap<>(); + final Map additionalParameter = new LinkedHashMap<>(); final String relAddr = this.getAttribute("rel-addr"); + final String type = this.getAttribute("type"); + if (type != null) { + additionalParameter.put("typ", type); + } if (relAddr != null) { - additionalParameter.put("raddr",relAddr); + additionalParameter.put("raddr", relAddr); } final String relPort = this.getAttribute("rel-port"); if (relPort != null) { @@ -175,7 +213,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo { if (ufrag != null) { additionalParameter.put("ufrag", ufrag); } - final String parametersString = Joiner.on(' ').join(Collections2.transform(additionalParameter.entrySet(), input -> String.format("%s %s",input.getKey(),input.getValue()))); + final String parametersString = Joiner.on(' ').join(Collections2.transform(additionalParameter.entrySet(), input -> String.format("%s %s", input.getKey(), input.getValue()))); return String.format( "candidate:%s %s %s %s %s %s %s", foundation, @@ -188,52 +226,11 @@ public class IceUdpTransportInfo extends GenericTransportInfo { ); } - - // https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-5.1 - public static Candidate fromSdpAttribute(final String attribute) { - final String[] pair = attribute.split(":", 2); - if (pair.length == 2 && "candidate".equals(pair[0])) { - final String[] segments = pair[1].split(" "); - if (segments.length >= 6) { - final String foundation = segments[0]; - final String component = segments[1]; - final String transport = segments[2]; - final String priority = segments[3]; - final String connectionAddress = segments[4]; - final String port = segments[5]; - final HashMap additional = new HashMap<>(); - for (int i = 6; i < segments.length - 1; i = i + 2) { - additional.put(segments[i], segments[i + 1]); - } - final Candidate candidate = new Candidate(); - candidate.setAttribute("component", component); - candidate.setAttribute("foundation", foundation); - candidate.setAttribute("generation", additional.get("generation")); - candidate.setAttribute("rel-addr", additional.get("raddr")); - candidate.setAttribute("rel-port", additional.get("rport")); - candidate.setAttribute("ip", connectionAddress); - candidate.setAttribute("port", port); - candidate.setAttribute("priority", priority); - candidate.setAttribute("protocol", transport); - candidate.setAttribute("type", additional.get("typ")); - return candidate; - } - } - return null; - } } public static class Fingerprint extends Element { - public String getHash() { - return this.getAttribute("hash"); - } - - public String getSetup() { - return this.getAttribute("setup"); - } - private Fingerprint() { super("fingerprint", Namespace.JINGLE_APPS_DTLS); } @@ -269,5 +266,13 @@ public class IceUdpTransportInfo extends GenericTransportInfo { final Fingerprint fingerprint = of(media.attributes); return fingerprint == null ? of(sessionDescription.attributes) : fingerprint; } + + public String getHash() { + return this.getAttribute("hash"); + } + + public String getSetup() { + return this.getAttribute("setup"); + } } } From f8c032841612baf4b85f06c76c71b7d9838b7613 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 7 Apr 2020 11:36:28 +0200 Subject: [PATCH 044/182] dummy Jingle activity --- src/main/AndroidManifest.xml | 2 + .../generator/MessageGenerator.java | 5 +- .../conversations/parser/MessageParser.java | 29 +++++-- .../conversations/ui/RtpSessionActivity.java | 46 +++++++++++ .../xmpp/jingle/AbstractJingleConnection.java | 2 + .../xmpp/jingle/JingleConnectionManager.java | 27 ++++++- .../xmpp/jingle/JingleRtpConnection.java | 14 +++- .../xmpp/jingle/WebRTCWrapper.java | 68 +++++++++++++++- .../drawable-hdpi/ic_call_end_white_48dp.png | Bin 0 -> 553 bytes .../res/drawable-hdpi/ic_call_white_48dp.png | Bin 0 -> 597 bytes .../drawable-mdpi/ic_call_end_white_48dp.png | Bin 0 -> 389 bytes .../res/drawable-mdpi/ic_call_white_48dp.png | Bin 0 -> 420 bytes .../drawable-xhdpi/ic_call_end_white_48dp.png | Bin 0 -> 712 bytes .../res/drawable-xhdpi/ic_call_white_48dp.png | Bin 0 -> 778 bytes .../ic_call_end_white_48dp.png | Bin 0 -> 1039 bytes .../drawable-xxhdpi/ic_call_white_48dp.png | Bin 0 -> 1134 bytes .../ic_call_end_white_48dp.png | Bin 0 -> 1355 bytes .../drawable-xxxhdpi/ic_call_white_48dp.png | Bin 0 -> 1529 bytes src/main/res/layout/activity_rtp_session.xml | 75 ++++++++++++++++++ src/main/res/values/colors.xml | 1 + src/main/res/values/styles.xml | 7 +- src/main/res/values/themes.xml | 1 + 22 files changed, 263 insertions(+), 14 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java create mode 100644 src/main/res/drawable-hdpi/ic_call_end_white_48dp.png create mode 100644 src/main/res/drawable-hdpi/ic_call_white_48dp.png create mode 100644 src/main/res/drawable-mdpi/ic_call_end_white_48dp.png create mode 100644 src/main/res/drawable-mdpi/ic_call_white_48dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_call_end_white_48dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_call_white_48dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_call_end_white_48dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_call_white_48dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_call_end_white_48dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_call_white_48dp.png create mode 100644 src/main/res/layout/activity_rtp_session.xml diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 5516758fa..b1ac0dc4e 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ + @@ -286,6 +287,7 @@ + diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 423ed9f91..393cec5d4 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -19,6 +19,7 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; +import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; @@ -233,12 +234,14 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket sessionProposal(JingleConnectionManager.RtpSessionProposal proposal) { + public MessagePacket sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) { final MessagePacket packet = new MessagePacket(); packet.setTo(proposal.with); + packet.setId(JingleRtpConnection.JINGLE_MESSAGE_ID_PREFIX+proposal.sessionId); final Element propose = packet.addChild("propose", Namespace.JINGLE_MESSAGE); propose.setAttribute("id", proposal.sessionId); propose.addChild("description", Namespace.JINGLE_APPS_RTP); + packet.addChild("request", "urn:xmpp:receipts"); return packet; } } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index a53613e28..4b15b8b72 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -43,6 +43,8 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.OnMessagePacketReceived; import eu.siacs.conversations.xmpp.chatstate.ChatState; +import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; +import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; @@ -301,11 +303,18 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece private boolean handleErrorMessage(Account account, MessagePacket packet) { if (packet.getType() == MessagePacket.TYPE_ERROR) { - Jid from = packet.getFrom(); - if (from != null) { + final Jid from = packet.getFrom(); + final String id = packet.getId(); + if (from != null && id != null) { + if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_ID_PREFIX)) { + final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_ID_PREFIX.length()); + mXmppConnectionService.getJingleConnectionManager() + .updateProposedSessionDiscovered(account, from, sessionId, JingleConnectionManager.DeviceDiscoveryState.FAILED); + return true; + } mXmppConnectionService.markMessage(account, from.asBareJid(), - packet.getId(), + id, Message.STATUS_SEND_FAILED, extractErrorMessage(packet)); final Element error = packet.findChild("error"); @@ -815,7 +824,11 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece if (!isTypeGroupChat) { for (Element child : packet.getChildren()) { if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace()) && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) { + if (!account.getJid().asBareJid().equals(from.asBareJid())) { + processMessageReceipts(account, packet, query); + } mXmppConnectionService.getJingleConnectionManager().deliverMessage(account, packet.getTo(), packet.getFrom(), child); + break; } } } @@ -831,8 +844,14 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece if (query != null && id != null && packet.getTo() != null) { query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id)); } - } else { - mXmppConnectionService.markMessage(account, from.asBareJid(), received.getAttribute("id"), Message.STATUS_SEND_RECEIVED); + } else if (id != null) { + if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_ID_PREFIX)) { + final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_ID_PREFIX.length()); + mXmppConnectionService.getJingleConnectionManager() + .updateProposedSessionDiscovered(account, from, sessionId, JingleConnectionManager.DeviceDiscoveryState.DISCOVERED); + } else { + mXmppConnectionService.markMessage(account, from.asBareJid(), id, Message.STATUS_SEND_RECEIVED); + } } } Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0"); diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java new file mode 100644 index 000000000..2fdf2ac65 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -0,0 +1,46 @@ +package eu.siacs.conversations.ui; + +import android.databinding.DataBindingUtil; +import android.os.Bundle; +import android.view.View; +import android.view.WindowManager; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.ActivityRtpSessionBinding; + +public class RtpSessionActivity extends XmppActivity { + + public static final String EXTRA_WITH = "with"; + + private ActivityRtpSessionBinding binding; + + public void onCreate(Bundle savedInstanceState) { + getWindow().addFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); + super.onCreate(savedInstanceState); + this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session); + this.binding.acceptCall.setOnClickListener(this::acceptCall); + this.binding.rejectCall.setOnClickListener(this::rejectCall); + } + + private void rejectCall(View view) { + + } + + private void acceptCall(View view) { + + } + + @Override + protected void refreshUiReal() { + + } + + @Override + void onBackendConnected() { + + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java index 65fce4205..603371394 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -11,6 +11,8 @@ import rocks.xmpp.addr.Jid; public abstract class AbstractJingleConnection { + public static final String JINGLE_MESSAGE_ID_PREFIX = "jm-propose-"; + protected final JingleConnectionManager jingleConnectionManager; protected final XmppConnectionService xmppConnectionService; protected final Id id; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 146892e82..bc9e25f2e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -30,7 +30,7 @@ import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; public class JingleConnectionManager extends AbstractConnectionManager { - private final Set rtpSessionProposals = new HashSet<>(); + private final HashMap rtpSessionProposals = new HashMap<>(); private final Map connections = new ConcurrentHashMap<>(); private HashMap primaryCandidates = new HashMap<>(); @@ -108,7 +108,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { } final RtpSessionProposal proposal = new RtpSessionProposal(account, with.asBareJid(), sessionId); synchronized (rtpSessionProposals) { - if (rtpSessionProposals.remove(proposal)) { + if (rtpSessionProposals.remove(proposal) != null) { final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, account.getJid()); this.connections.put(id, rtpConnection); rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED); @@ -190,7 +190,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { public void proposeJingleRtpSession(final Account account, final Contact contact) { final RtpSessionProposal proposal = RtpSessionProposal.of(account, contact.getJid().asBareJid()); synchronized (this.rtpSessionProposals) { - this.rtpSessionProposals.add(proposal); + this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING); final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); Log.d(Config.LOGTAG,messagePacket.toString()); mXmppConnectionService.sendMessagePacket(account, messagePacket); @@ -244,6 +244,23 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } + public void updateProposedSessionDiscovered(Account account, Jid from, String sessionId, final DeviceDiscoveryState target) { + final RtpSessionProposal sessionProposal = new RtpSessionProposal(account,from.asBareJid(),sessionId); + synchronized (this.rtpSessionProposals) { + final DeviceDiscoveryState currentState = rtpSessionProposals.get(sessionProposal); + if (currentState == null) { + Log.d(Config.LOGTAG,"unable to find session proposal for session id "+sessionId); + return; + } + if (currentState == DeviceDiscoveryState.DISCOVERED) { + Log.d(Config.LOGTAG,"session proposal already at discovered. not going to fall back"); + return; + } + this.rtpSessionProposals.put(sessionProposal, target); + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": flagging session "+sessionId+" as "+target); + } + } + public static class RtpSessionProposal { private final Account account; public final Jid with; @@ -274,4 +291,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { return Objects.hashCode(account.getJid(), with, sessionId); } } + + public enum DeviceDiscoveryState { + SEARCHING, DISCOVERED, FAILED + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 3e9759c22..f18212df8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.xmpp.jingle; +import android.content.Intent; import android.util.Log; import com.google.common.collect.ImmutableList; @@ -15,6 +16,7 @@ import java.util.List; import java.util.Map; import eu.siacs.conversations.Config; +import eu.siacs.conversations.ui.RtpSessionActivity; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; @@ -217,13 +219,21 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (originatedFromMyself) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": saw proposal from mysql. ignoring"); } else if (transition(State.PROPOSED)) { - //TODO start ringing or something - pickUpCall(); + startRinging(); } else { Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring session proposal because already in " + state); } } + private void startRinging() { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received call from " + id.with + ". start ringing"); + final Intent intent = new Intent(xmppConnectionService, RtpSessionActivity.class); + intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString()); + intent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString()); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + xmppConnectionService.startActivity(intent); + } + private void receiveProceed(final Jid from, final Element proceed) { if (from.equals(id.with)) { if (isInitiator()) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 53210e78c..f4b03a062 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -11,6 +11,9 @@ import com.google.common.util.concurrent.SettableFuture; import org.webrtc.AudioSource; import org.webrtc.AudioTrack; +import org.webrtc.Camera1Capturer; +import org.webrtc.Camera1Enumerator; +import org.webrtc.CameraVideoCapturer; import org.webrtc.DataChannel; import org.webrtc.IceCandidate; import org.webrtc.MediaConstraints; @@ -20,6 +23,9 @@ import org.webrtc.PeerConnectionFactory; import org.webrtc.RtpReceiver; import org.webrtc.SdpObserver; import org.webrtc.SessionDescription; +import org.webrtc.VideoCapturer; +import org.webrtc.VideoSource; +import org.webrtc.VideoTrack; import java.util.List; @@ -30,6 +36,9 @@ import eu.siacs.conversations.Config; public class WebRTCWrapper { + private VideoTrack localVideoTrack = null; + private VideoTrack remoteVideoTrack = null; + private final EventCallback eventCallback; private final PeerConnection.Observer peerConnectionObserver = new PeerConnection.Observer() { @@ -75,6 +84,11 @@ public class WebRTCWrapper { for(AudioTrack audioTrack : mediaStream.audioTracks) { Log.d(Config.LOGTAG,"remote? - audioTrack enabled:"+audioTrack.enabled()+" state="+audioTrack.state()); } + final List videoTracks = mediaStream.videoTracks; + if (videoTracks.size() > 0) { + Log.d(Config.LOGTAG, "more than zero remote video tracks found. using first"); + remoteVideoTrack = videoTracks.get(0); + } } @Override @@ -112,15 +126,65 @@ public class WebRTCWrapper { } public void initializePeerConnection() { - final PeerConnectionFactory.Options options = new PeerConnectionFactory.Options(); PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory(); + CameraVideoCapturer capturer = null; + Camera1Enumerator camera1Enumerator = new Camera1Enumerator(); + for(String deviceName : camera1Enumerator.getDeviceNames()) { + Log.d(Config.LOGTAG,"camera device name: "+deviceName); + if (camera1Enumerator.isFrontFacing(deviceName)) { + capturer = camera1Enumerator.createCapturer(deviceName, new CameraVideoCapturer.CameraEventsHandler() { + @Override + public void onCameraError(String s) { + + } + + @Override + public void onCameraDisconnected() { + + } + + @Override + public void onCameraFreezed(String s) { + + } + + @Override + public void onCameraOpening(String s) { + Log.d(Config.LOGTAG,"onCameraOpening"); + } + + @Override + public void onFirstFrameAvailable() { + Log.d(Config.LOGTAG,"onFirstFrameAvailable"); + } + + @Override + public void onCameraClosed() { + + } + }); + } + } + + /*if (capturer != null) { + capturer.initialize(); + Log.d(Config.LOGTAG,"start capturing"); + capturer.startCapture(800,600,30); + }*/ + + final VideoSource videoSource = peerConnectionFactory.createVideoSource(false); + final VideoTrack videoTrack = peerConnectionFactory.createVideoTrack("my-video-track", videoSource); + final AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints()); final AudioTrack audioTrack = peerConnectionFactory.createAudioTrack("my-audio-track", audioSource); Log.d(Config.LOGTAG,"audioTrack enabled:"+audioTrack.enabled()+" state="+audioTrack.state()); final MediaStream stream = peerConnectionFactory.createLocalMediaStream("my-media-stream"); stream.addTrack(audioTrack); + //stream.addTrack(videoTrack); + + this.localVideoTrack = videoTrack; final List iceServers = ImmutableList.of( @@ -136,6 +200,8 @@ public class WebRTCWrapper { this.peerConnection = peerConnection; } + + public ListenableFuture createOffer() { return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { final SettableFuture future = SettableFuture.create(); diff --git a/src/main/res/drawable-hdpi/ic_call_end_white_48dp.png b/src/main/res/drawable-hdpi/ic_call_end_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..e1831d7afd086dcfc741a496d058af3d0308da99 GIT binary patch literal 553 zcmV+^0@nSBP)Ksbir@OQBE-g|t&k+Ykz+ zv_rwEC`K3YWfDSS5J7RvWL_XcKqYFppN>MvIXStxmwO?<=lKUdap+5>QmIrbl}e>j zGRKXZG-J-9Wt+BCRc+a{Y|)qQxnSHenb_~4{1ro{d|_SL)_iW#>#}--+KstyO|7k% z^}hD{fL?Las(SQ?8(uyLC^P13^={64vi*ZH6XrFlAAKO(59m$bE9xH$9-8v0VFP+~ zsHo`BYrtDhy5x~1|0(JlL*+odW>xLpwjW$JqEnQ(%e$_bmsizYJ$r#Zu_nI}QN!c% zTk&B&=mn1z_eM>icNF(P_imuq{7zFq{AzF~=!Cy%8i+rA{68pj1)2(ii!u?~%+L}b zZmWdrvtU;=W`?d^6guO|>85IXU^O5DfI3g62s-|^{I^FF~F-L?V(yCEW=O=9B zb417|EjTI~@JoKlw1WKR;N}vQvpae>w!v}f>M-hs<`NyOS00000NkvXXu0mjfwpIZY literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_call_white_48dp.png b/src/main/res/drawable-hdpi/ic_call_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..90ead2e4551b165530bd2430b3d69c34263c5c4e GIT binary patch literal 597 zcmV-b0;>IqP)a~F^ihkCNXidBsWB>X=;U*@F%!%#fVZaU}txjgmm7*kE>_wfD zJ4vghX)Y+W+A>RYW}(s9ms0kgvz_-nXXic7bMt;aQYw{7rQ+a|A;-#I(=OY{5am0Y zWfwW3^~f&rM7t`x_(`-r*+q_ML$ZrWqCJ#dj1g^AcJY*GIgD&#kT@-}i3`LzE}J+^ zoB`QH3vni76Nb;kX_ZZk5a)z!;v#YO$R>6Z?LI~}VVI$`08h4Yi&6qS*+(ZU3-IIu z>zHA=08g&aOR)e??y#Lg0iN9A2}>4ekZW`hc1f;bc*&AEHpw*(5q4XyVR%khr(9zf zd6rDFUaoP2usayJMLpAmosw%DC2WCxa*ZLvKCxA9QO8@t-q9$x*vU7-#@QgZ=w^|y zaT*l^!(L9ZMYTZ>h2FDG5%3w|pEvY#jAo3IgrT28vm8(f_+%;eg;zXakY28EhGta@ z!!U(@aT=p#@W~Qqp8cv8YZ#{3JsMO1pDg7VQ}r-hr`Qx-`VXHhbwLu%tSQXU{ z2YEroR7)7z38;u_k5;bnkuu2+*3!Wc)5OWiCJb$y=Ml4%G9VW)G||O5GCXFK4}9eZ jd2$TkDV0j4QgQwO?=x#gyQpL<00000NkvXXu0mjf@K6c} literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_call_end_white_48dp.png b/src/main/res/drawable-mdpi/ic_call_end_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a4fe6889d159cac861cac4f885ae3ec28cd9ca44 GIT binary patch literal 389 zcmV;00eb$4P)Gh#=i2 zxUHW#j;ZlmFB~yTpC3Sx}?~Nic9mw!VXE%t4-O|}Tsh_|+P54s)>dbrr zidFQ20(vVbVGTk9pdd6u1?s{B?p^l`+zDx(acj#Hu#H<)l7cYeNZ1sJc>*koG@S1d z!I?N=0P(RwPMz)%p_8y#Kp{S-Sj5)v5kV&^WRQT%Q2D;s0U^mDyCjevmSBfHh7cVH jZ3Zxa0SsV(e*t^~=a|RM72~P_00000NkvXXu0mjf^Vpp> literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_call_white_48dp.png b/src/main/res/drawable-mdpi/ic_call_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ef45e933a99b720cc5f6127e6da22bc2fa679244 GIT binary patch literal 420 zcmV;V0bBlwP)u7H4zjb0wVlEO#(-afhJ{~L_owyniLh<44m_hh=0)JC~(EtGz}i`o*rY-v?y@O z7&HsExZpoDD~cS_W+YkQAr(Fol6E6IIG(V_N51l%CO?U|VOMi=rhWlQl3!NbcvwOJ O0000E%2g55>;f$a!bnJ3^a-8Q&a-BDj#(M~HGj;g!I zhT#AT6Q-T;yKcMWxVr6%K>-T8JnM{J*Dz;?qQ8JmrhTEGeXj2M{s8XwmOl-mWk>Al zC7>{A!Js?kent0y!j#j7)u$d%bOX4{$I5r!N%LksY_|!UjT==ctTS%23A^nzW8O*U zmG474I|JO{4P{z(%A6@TiZ*tWDRVyYht{on)phLwYWDj8L-itux0`9nrkixxLt!a58z9;H3Llg znMwfo(S8319JESh0Q}{te*uN(pi%&sQ;0C)bt(zquv(a*vHpE0}6<4A=MUgDB6h!xmw?$P#D_ykG zL=hFSC`v&PZLuhB6vP_@=VGGKX8IHCrk*(yzZvID{vUkLBLzVa1VIo437F3S<}L zr)ZZ=%%^~hA`4^@1GpNLMLfgRh%BOnyC&Jg3*1eTJrr;^UH0$M-cTV>3Gwxo}CTrME zHM(UD8LF{E*6;z(an~(-ID@NuOqM-t!_8Bc$|63;MUD>HM8qB5B}b?1Vi)7f(<#eX z$ao5L$u=TxQb~b~>|-0rie%*jX7ZA9MV8AK{777pFXRuF5qDC4;R10P`GnQPT_ln} zi1>rJwekty5O<$e`GbfH#O;+&SVD=n{ma+#35SWhOPl<`Y=(&ICz3zdK)H?b2N7q9 z8>LJB;8XI%J>VnxgC5FVqgDRk80F5=B403-KPmSM&GH4Gk)zykn&k_=WsGva(xNKR z%J=lq!!$J>n@MtxRyiWeBmVObzthK;OjaYYha}heSPsZiAnp~{I7%Nqe8pU5F@+|j zhloQYc}Pa@K$Ze-MmR;g(m@mbBpGEhkt!lXfoc?4pmfkoKgoXLLnR_Zo@xy$Ei`eE zWcOI1GIWurdLv2?5xc2mKW+LSy2w)#rHf6Bk?aBMi1Y@XTphVqhBCv?_)QKhv zE4e})XyOpDiu1fDO*EF!&nPvM1*4F5EbdU*GfFjIneHf!T&u3Z-OsM5Qb8 zN9#sXwu~_BPAVY?l86#YGl}X+VTcbxx}8}LN{TF!!Usi2NR9HQ(!i)Dh@fn#$LZ#) zN0DM4^n1SN>>PLApa0?|ZVN5bX`PL>>#;|#yg`HVdhO9;yN%X)$1)4e zahAIR$6SncC99BF@LgwAZ-Ucr9La5>9$Ez zuPzT5D@wRA?y=flY1-`i;(?mHDP1obA3|xo zfHNKlUe$2AKvVq8umNI=Nv95!cu1aM1;hdOof=T1P8fDT@VW+tfsE75umxh1=0ZTz z>}FU4vCB0l1G-;7!ybr3=AH=!mJJ%$w#&_B5WEVVoGEHG9nw#g_k@h0lZjz@&7h&H=XRLEL&XTu{x z7DNX;p-xoL>MXFocpedoC)(g5QGpxpi__$369s`%5FN5WPE_a;kIEOrBSJAm+e{G^ zzR9+v=Mf=2(Q1vN3f8DIEsqH4hz30_5mmCpV+IQF+$;(Jr6B5ao2atgs;@9l3ZRn^ z?Q*%O!d<4PFi#qwaJ#%?`^AN#N;lq5j;&8y7Dt?ue5fDn_L7mJig&i9{s4X6GCsRI z>9b7GA}3V;66b36zsh>*{kt7HAOQdX01)*5bvpv_fm;d$2@)hokRU;V5F|*DAVGoz z2||z{L4pJc5+n#gf&>W?BuJ1TK?o8gNRS{wf&?K*kRU;mAOhFlPsF?ZQhopc002ov JPDHLkV1l-t*)jkC literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_call_white_48dp.png b/src/main/res/drawable-xxhdpi/ic_call_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a8e295a42f97bc49fb454a0584f77450d4e12a16 GIT binary patch literal 1134 zcmV-!1d;oRP)t zm_R;bE-sGKCciO~vsCH?6XZX3P?-}n#RC^Jz))pKDL&Z6&}2y~PPm<+ zI7PeSg;sKu>lE#ZAKs=s-!MsW#C*!}Eggy{5@aalJ0>f>SVwWaOjevRjzPlpGDUI6 z8-)3lsWMzem^{;DBt9a{J2DcB2scxPfTM)jPawk$gu6qAfJ21&fmRt75$**U0`?Ln z&kPx^A>0>?lHqm2ZIEF+e-h?Avt?LMxC~a1+?xc7JLXUz+&waM60V=C6?e39 znsB|eDehQ6F`trD+_8ybHWMh`XrhZ^9#hN9U_Z~XoXZ%YC?lYgQnt{fiqKA$Qu^p(CmVTyo0-crCeTWfgrb53 zTPbB9<1}QnlcjP4bg_nJ)gL3-Nhzn9t|1{!7I!^#sQyT@n^O9@pFm|IO_u8Dp;`6E zXm(TDc3M>i(qyTg)rta=?4-0)T&ogDlck!DC>j{aR!YnBG)aXbO@`{qDJn?NNjbWi zqYENUhN@F^5U`R0r4{JpBK;33GSq;gg{9;u$B*1aLjNK~hMG{+FpD#k=MZ!B4^m{P z5k(Iv4pOdNTtSKqHKQmZ$;*^0&rj5lqKXyt5dn%Wrg4uqK^6WQb&qD z#_|#c>O_r!S$suZs8NvQA^u?4sgW?A%@i0`YBWse6NZr*4*?4}z%Wr`BH(&HXBen) z5wL&{s4+DJ9N}kifzx#XN^L+37z2EuYm;FS=MQzbpp;^brBL0}%#vUm%q9ysx5xMcm1OL&|>S| zf*lEs@S5w#PjRi=WAJ0S&siy6i8uu2p^Oc%xMqHspb7J-gy$W>o0E*nG;R8~0Q;p< zVKuXxnn)UH!A@sEwv`z1<3B3$IZ;l>tzri!Qss1c>S)o1dbM#wzSErZR4!^5O5)gu zx{YOqlQoUWw-Y)F_k22Ah5eA}bJsPF13K5#?-~21_&UJ>2d`?+L*bWkYNp|y~9USG;yG$j-dMB@rgeFND)Oyic zrHF>uCD&J-AE+zAd}O9?aqKW=|1}4@6tUo|n)S&|0Q<*GE6D@?{5e!??n^_`hu1ew z_N^CpGOWxgkIA9p;%?sGVqQJQLfQn|TUxNfq7q#Vq!^nN@(-@@&6U?SvBll=LJ3@z z`wkSbu)-%!X7Lqop6)e*~YP9^g^-8Nb6PVgPDu72zZL@hlQuJDef4aO!_6C-y}B2TK+ z=q4D0K!hFQ?eyv}ow%>s?;hV|Q;BrAE$X34Z?B_2J{YO;uw~+^dV6MaY@fE~J3Evk zu_H-cExy#XTMlCsC%W!PFj)!mN-hQ4WGuSIV&ZdiQPd=&@9T>YTj;#@+B9uuZ9 z1`fi#NpG{<891}kK65ksF!tW-dbLLWw)@1T{O)V*e};tkbr6w`OQ6e68Oi2nRWJOUai{tT~Jw*mcy4u&3+JaH9LFcG5$(75z0# zod4y5i&865GS(wY*fX?Wje9e67#^&sAnaMH__X_0|1!S)np%I>l2GH46Gu;n*Vu6f zs+ZGozNRhT?A*02j564xmIq|j1RQ6thH=!2z_3mwkp7?4e%pBEl&Cnt}9X{@J zdMJj<$+C!f=SkP(6t;uy`ex>fB;yv{Elu4gXf(wjO8zB&*f6bk{^hreT(>?PizW!& za;BYiw`X<^yKw_OcpBJ9ylEPzdrtG0000HRNklTuSk4>40&Th@PWX9m^c?n z7GYu3juNLKcGOiQ`&X2%aR4>t!MM0dZ`Qh2S3I=#hnB19ALZ z7J{3IV^kJ`&k@H2vt;SGl2|U1rDGAXd`6az5>?{3O%{T~#PNtM1Urc1m@EX}C6;%| z((!d-StU!yDq{JzEFBjR%ZMx;2`7kSk`7rq_7ck)Svs~7%Qjg$ZX}jJlgQF=Iq|HJ zrQsY-6U(<`>DWUo`()|ZL@Z;nbgUwlS7hnvB$g3b2o4d)CRqsXBZf)dBMZSb#BjGP z3>B*6Z3nYuq39=H1C(XqxS2fdq%6yVrQ~OjvMd`CUM3&AsK~No6M5K8MOF*CsoxM4 zS#3x-LVcd5T~<53Nj>(^E~^KZP`ka%mDPuYL)2!Nxw884HEQt;^JIBoA+M3uGt86a zg&&j6v&@s_hfk8lK00K1qJ`fRYJ?71-nfG>&oN(?KhC8}y62fMi-SJWy-yYk%Sg9P z77ve;W|AecSoj#}9+AaD!U57AYmWrHp^n+ zYSK;8EsKMMr%Cq3@qj(A=b;{;7f$4(k+XD5_<@7lulU;ETKw>L0V-oa0_9! z$YP+xPQu(Ei-ASFN{H9^ge(Tu5oVm_vKV-PFk@UO%OCChmN17|D9ay9I7yga(<#dz zYYFuN9kTqfiBQ9|%ksu71_(7whb(U_;25D^pi`DNmU5g>zhR*wVj17zm;9Yq+0Wfv zK}jZoz(ZWMy zv6WWkfC@w8Yl0*6vRp+&E02-IE*7XxRM<@{4^mOFFoyxMIL30-feO2bX_$(Ng$jGg zqRLGqstFZ#5nG>%h7R_U&5xL?%qTNRBXp~1sL)R~NBEF3q0AtS(WByF4v&$|8SbK0 zfhaRbql~JEXyqZYdX6pypv+Dhr>bJ2g$Ky$G`G^Kw?&x&8cIb)!UnSY6_@EPQD%S! zQ;~5U6J#~XJ5Vc~Rm9PB6`>xp6)}VOmr3V>L&awlotkhg%tETGC9x zd2DAI(oDi-3^C1UW+CC@Jj*nqnTLdx>}Q(L%tXR!_A?D=<|5%Le#R8jOvh5TQDw@= zBo=Zz$Cwhb|DeRxJj?_QCi`E!n_D6nW^C)rQ%u;EQZwh~w014`o@sNLb8DzQR_X z;1H(>)k|5HHxka}!+e(Y+{R|U$K#A}keB!aV;tw7Oj2c(`?*wRW@ct)W@ct)W@ct) fW@ct)X0Q7fzBJy1v=0Z&00000NkvXXu0mjfo3+PT literal 0 HcmV?d00001 diff --git a/src/main/res/layout/activity_rtp_session.xml b/src/main/res/layout/activity_rtp_session.xml new file mode 100644 index 000000000..dc7dc4eec --- /dev/null +++ b/src/main/res/layout/activity_rtp_session.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/res/values/colors.xml b/src/main/res/values/colors.xml index 074b5ae30..4332d1907 100644 --- a/src/main/res/values/colors.xml +++ b/src/main/res/values/colors.xml @@ -19,6 +19,7 @@ #ff424242 #ff282828 #fff44336 + #ffD32F2F #ffd50000 #ffff8a80 #ffc62828 diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml index 432f256c0..00ed0b3b5 100644 --- a/src/main/res/values/styles.xml +++ b/src/main/res/values/styles.xml @@ -1,11 +1,14 @@ - + + diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index 454f0ca6c..054194fbe 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -31,6 +31,7 @@ 14sp 16sp 20sp + 45sp 16sp 5sp 18sp From a9a35fb74bda76f68480e77dbb690dd03d7f35e5 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 7 Apr 2020 13:15:24 +0200 Subject: [PATCH 045/182] show status in RtpSessionActivity --- .../services/XmppConnectionService.java | 43 +++++++++- .../conversations/ui/RtpSessionActivity.java | 80 ++++++++++++++++++- .../siacs/conversations/ui/XmppActivity.java | 15 +++- .../xmpp/jingle/JingleConnectionManager.java | 10 +++ .../xmpp/jingle/JingleRtpConnection.java | 57 +++++++++++-- .../xmpp/jingle/RtpEndUserState.java | 12 +++ .../xmpp/jingle/WebRTCWrapper.java | 3 +- src/main/res/values/strings.xml | 4 + 8 files changed, 209 insertions(+), 15 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index d68981bf6..5951b7116 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -143,6 +143,7 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; +import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.mam.MamReference; import eu.siacs.conversations.xmpp.pep.Avatar; @@ -267,6 +268,7 @@ public class XmppConnectionService extends Service { private final Set mOnUpdateBlocklist = Collections.newSetFromMap(new WeakHashMap()); private final Set mOnMucRosterUpdate = Collections.newSetFromMap(new WeakHashMap()); private final Set mOnKeyStatusUpdated = Collections.newSetFromMap(new WeakHashMap()); + private final Set onJingleRtpConnectionUpdate = Collections.newSetFromMap(new WeakHashMap()); private final Object LISTENER_LOCK = new Object(); @@ -2467,6 +2469,30 @@ public class XmppConnectionService extends Service { } } + public void setOnRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) { + final boolean remainingListeners; + synchronized (LISTENER_LOCK) { + remainingListeners = checkListeners(); + if (!this.onJingleRtpConnectionUpdate.add(listener)) { + Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnJingleRtpConnectionUpdate"); + } + } + if (remainingListeners) { + switchToForeground(); + } + } + + public void removeRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) { + final boolean remainingListeners; + synchronized (LISTENER_LOCK) { + this.onJingleRtpConnectionUpdate.remove(listener); + remainingListeners = checkListeners(); + } + if (remainingListeners) { + switchToBackground(); + } + } + public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) { final boolean remainingListeners; synchronized (LISTENER_LOCK) { @@ -2499,6 +2525,7 @@ public class XmppConnectionService extends Service { && this.mOnMucRosterUpdate.size() == 0 && this.mOnUpdateBlocklist.size() == 0 && this.mOnShowErrorToasts.size() == 0 + && this.onJingleRtpConnectionUpdate.size() == 0 && this.mOnKeyStatusUpdated.size() == 0); } @@ -3943,6 +3970,12 @@ public class XmppConnectionService extends Service { } } + public void notifyJingleRtpConnectionUpdate(final Account account, final Jid with, final RtpEndUserState state) { + for(OnJingleRtpConnectionUpdate listener : threadSafeList(this.onJingleRtpConnectionUpdate)) { + listener.onJingleRtpConnectionUpdate(account, with, state); + } + } + public void updateAccountUi() { for (OnAccountUpdate listener : threadSafeList(this.mOnAccountUpdates)) { listener.onAccountUpdate(); @@ -3986,9 +4019,9 @@ public class XmppConnectionService extends Service { } } - public Account findAccountByJid(final Jid accountJid) { - for (Account account : this.accounts) { - if (account.getJid().asBareJid().equals(accountJid.asBareJid())) { + public Account findAccountByJid(final Jid jid) { + for (final Account account : this.accounts) { + if (account.getJid().asBareJid().equals(jid.asBareJid())) { return account; } } @@ -4620,6 +4653,10 @@ public class XmppConnectionService extends Service { void onConversationUpdate(); } + public interface OnJingleRtpConnectionUpdate { + void onJingleRtpConnectionUpdate(final Account account, final Jid with, final RtpEndUserState state); + } + public interface OnAccountUpdate { void onAccountUpdate(); } diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 2fdf2ac65..b1cc321e0 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -1,16 +1,31 @@ package eu.siacs.conversations.ui; +import android.content.Intent; import android.databinding.DataBindingUtil; import android.os.Bundle; +import android.util.Log; import android.view.View; import android.view.WindowManager; +import java.lang.ref.WeakReference; + +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityRtpSessionBinding; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; +import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; +import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; +import rocks.xmpp.addr.Jid; -public class RtpSessionActivity extends XmppActivity { +public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate { public static final String EXTRA_WITH = "with"; + public static final String EXTRA_SESSION_ID = "session_id"; + + private WeakReference rtpConnectionReference; private ActivityRtpSessionBinding binding; @@ -27,11 +42,12 @@ public class RtpSessionActivity extends XmppActivity { } private void rejectCall(View view) { - + requireRtpConnection().rejectCall(); + finish(); } private void acceptCall(View view) { - + requireRtpConnection().acceptCall(); } @Override @@ -41,6 +57,64 @@ public class RtpSessionActivity extends XmppActivity { @Override void onBackendConnected() { + final Intent intent = getIntent(); + final Account account = extractAccount(intent); + final String with = intent.getStringExtra(EXTRA_WITH); + final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID); + if (with != null && sessionId != null) { + final WeakReference reference = xmppConnectionService.getJingleConnectionManager() + .findJingleRtpConnection(account, Jid.ofEscaped(with), sessionId); + if (reference == null || reference.get() == null) { + finish(); + return; + } + this.rtpConnectionReference = reference; + binding.with.setText(getWith().getDisplayName()); + showState(requireRtpConnection().getEndUserState()); + } + } + + private void showState(final RtpEndUserState state) { + switch (state) { + case INCOMING_CALL: + binding.status.setText(R.string.rtp_state_incoming_call); + break; + case CONNECTING: + binding.status.setText(R.string.rtp_state_connecting); + break; + case CONNECTED: + binding.status.setText(R.string.rtp_state_connected); + break; + case ACCEPTING_CALL: + binding.status.setText(R.string.rtp_state_accepting_call); + break; + } + } + + private Contact getWith() { + final AbstractJingleConnection.Id id = requireRtpConnection().getId(); + final Account account = id.account; + return account.getRoster().getContact(id.with); + } + + private JingleRtpConnection requireRtpConnection() { + final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + if (connection == null) { + throw new IllegalStateException("No RTP connection found"); + } + return connection; + } + + @Override + public void onJingleRtpConnectionUpdate(Account account, Jid with, RtpEndUserState state) { + final AbstractJingleConnection.Id id = requireRtpConnection().getId(); + if (account == id.account && id.with.equals(with)) { + runOnUiThread(()->{ + showState(state); + }); + } else { + Log.d(Config.LOGTAG,"received update for other rtp session"); + } } } diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index f476736fa..9ed35190b 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -305,6 +305,9 @@ public abstract class XmppActivity extends ActionBarActivity { if (this instanceof OnKeyStatusUpdated) { this.xmppConnectionService.setOnKeyStatusUpdatedListener((OnKeyStatusUpdated) this); } + if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) { + this.xmppConnectionService.setOnRtpConnectionUpdateListener((XmppConnectionService.OnJingleRtpConnectionUpdate) this); + } } protected void unregisterListeners() { @@ -332,6 +335,9 @@ public abstract class XmppActivity extends ActionBarActivity { if (this instanceof OnKeyStatusUpdated) { this.xmppConnectionService.removeOnNewKeysAvailableListener((OnKeyStatusUpdated) this); } + if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) { + this.xmppConnectionService.removeRtpConnectionUpdateListener((XmppConnectionService.OnJingleRtpConnectionUpdate) this); + } } @Override @@ -388,13 +394,18 @@ public abstract class XmppActivity extends ActionBarActivity { } } + @SuppressLint("UnsupportedChromeOsCameraSystemFeature") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); metrics = getResources().getDisplayMetrics(); ExceptionHelper.init(getApplicationContext()); new EmojiService(this).init(); - this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); + } else { + this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA); + } this.mTheme = findTheme(); setTheme(this.mTheme); @@ -851,7 +862,7 @@ public abstract class XmppActivity extends ActionBarActivity { } protected Account extractAccount(Intent intent) { - String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null; + final String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null; try { return jid != null ? xmppConnectionService.findAccountByJid(Jid.of(jid)) : null; } catch (IllegalArgumentException e) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index bc9e25f2e..982dc00b4 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -5,6 +5,7 @@ import android.util.Log; import com.google.common.base.Objects; import com.google.common.base.Preconditions; +import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -244,6 +245,15 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } + public WeakReference findJingleRtpConnection(Account account, Jid with, String sessionId) { + final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, Jid.ofEscaped(with), sessionId); + final AbstractJingleConnection connection = connections.get(id); + if (connection instanceof JingleRtpConnection) { + return new WeakReference<>((JingleRtpConnection) connection); + } + return null; + } + public void updateProposedSessionDiscovered(Account account, Jid from, String sessionId, final DeviceDiscoveryState target) { final RtpSessionProposal sessionProposal = new RtpSessionProposal(account,from.asBareJid(),sessionId); synchronized (this.rtpSessionProposals) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index f18212df8..8ea923804 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -7,6 +7,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import org.webrtc.IceCandidate; +import org.webrtc.PeerConnection; import java.util.ArrayDeque; import java.util.Arrays; @@ -230,6 +231,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final Intent intent = new Intent(xmppConnectionService, RtpSessionActivity.class); intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString()); intent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString()); + intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); xmppConnectionService.startActivity(intent); } @@ -293,26 +295,59 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web xmppConnectionService.sendIqPacket(id.account, jinglePacket, null); } - - public void pickUpCall() { + public RtpEndUserState getEndUserState() { switch (this.state) { case PROPOSED: - pickupCallFromProposed(); + if (isInitiator()) { + return RtpEndUserState.RINGING; + } else { + return RtpEndUserState.INCOMING_CALL; + } + case PROCEED: + if (isInitiator()) { + return RtpEndUserState.CONNECTING; + } else { + return RtpEndUserState.ACCEPTING_CALL; + } + case SESSION_INITIALIZED: + return RtpEndUserState.CONNECTING; + case SESSION_ACCEPTED: + final PeerConnection.PeerConnectionState state = webRTCWrapper.getState(); + if (state == PeerConnection.PeerConnectionState.CONNECTED) { + return RtpEndUserState.CONNECTED; + } else if (state == PeerConnection.PeerConnectionState.NEW || state == PeerConnection.PeerConnectionState.CONNECTING) { + return RtpEndUserState.CONNECTING; + } else { + return RtpEndUserState.FAILED; + } + } + return RtpEndUserState.FAILED; + } + + + public void acceptCall() { + switch (this.state) { + case PROPOSED: + acceptCallFromProposed(); break; case SESSION_INITIALIZED: - pickupCallFromSessionInitialized(); + acceptCallFromSessionInitialized(); break; default: throw new IllegalStateException("Can not pick up call from " + this.state); } } + public void rejectCall() { + Log.d(Config.LOGTAG, "todo rejecting call"); + } + private void setupWebRTC() { this.webRTCWrapper.setup(this.xmppConnectionService); this.webRTCWrapper.initializePeerConnection(); } - private void pickupCallFromProposed() { + private void acceptCallFromProposed() { transitionOrThrow(State.PROCEED); final MessagePacket messagePacket = new MessagePacket(); messagePacket.setTo(id.with); @@ -322,7 +357,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web xmppConnectionService.sendMessagePacket(id.account, messagePacket); } - private void pickupCallFromSessionInitialized() { + private void acceptCallFromSessionInitialized() { } @@ -335,6 +370,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (validTransitions != null && validTransitions.contains(target)) { this.state = target; Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target); + updateEndUserState(); return true; } else { return false; @@ -353,4 +389,13 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate.toString()); sendTransportInfo(iceCandidate.sdpMid, candidate); } + + @Override + public void onConnectionChange(PeerConnection.PeerConnectionState newState) { + updateEndUserState(); + } + + private void updateEndUserState() { + xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, getEndUserState()); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java new file mode 100644 index 000000000..bfae8e53b --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java @@ -0,0 +1,12 @@ +package eu.siacs.conversations.xmpp.jingle; + +public enum RtpEndUserState { + INCOMING_CALL, //received a 'propose' message + CONNECTING, //session-initiate or session-accepted but no webrtc peer connection yet + CONNECTED, //session-accepted and webrtc peer connection is connected + FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet + RINGING, //'propose' has been sent out and it has been 184 acked + ACCEPTED_ON_OTHER_DEVICE, //received 'accept' from one of our own devices + ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received + FAILED //something went wrong. TODO needs more concrete error states +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index f4b03a062..2fdd99003 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -50,7 +50,7 @@ public class WebRTCWrapper { @Override public void onConnectionChange(PeerConnection.PeerConnectionState newState) { - Log.d(Config.LOGTAG, "onConnectionChange(" + newState + ")"); + eventCallback.onConnectionChange(newState); } @Override @@ -329,5 +329,6 @@ public class WebRTCWrapper { public interface EventCallback { void onIceCandidate(IceCandidate iceCandidate); + void onConnectionChange(PeerConnection.PeerConnectionState newState); } } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index d91374ade..b7de99684 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -885,6 +885,10 @@ About Please enable an account Make call + Incoming call + Connecting + Connected + Accepting call View %1$d Participant View %1$d Participants From 0e88b56eb4bae4305b3317dfb1ed310e15d967b5 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 7 Apr 2020 14:22:12 +0200 Subject: [PATCH 046/182] display status information in ui --- src/main/AndroidManifest.xml | 1 + .../conversations/ui/RtpSessionActivity.java | 35 ++++++++++++++++--- .../xmpp/jingle/JingleRtpConnection.java | 11 ++++++ .../xmpp/jingle/RtpEndUserState.java | 1 + .../xmpp/jingle/WebRTCWrapper.java | 32 ++++++++++------- src/main/res/layout/activity_rtp_session.xml | 21 +++++++++-- src/main/res/values/strings.xml | 1 + 7 files changed, 84 insertions(+), 18 deletions(-) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index b1ac0dc4e..66c4f5bcb 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -32,6 +32,7 @@ + { - showState(state); + updateStateDisplay(state); + updateButtonConfiguration(state); }); } else { Log.d(Config.LOGTAG,"received update for other rtp session"); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 8ea923804..3d346dfd5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -317,6 +317,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return RtpEndUserState.CONNECTED; } else if (state == PeerConnection.PeerConnectionState.NEW || state == PeerConnection.PeerConnectionState.CONNECTING) { return RtpEndUserState.CONNECTING; + } else if (state == PeerConnection.PeerConnectionState.CLOSED) { + return RtpEndUserState.ENDING_CALL; } else { return RtpEndUserState.FAILED; } @@ -342,6 +344,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web Log.d(Config.LOGTAG, "todo rejecting call"); } + public void endCall() { + if (isInState(State.SESSION_INITIALIZED, State.SESSION_ACCEPTED)) { + webRTCWrapper.close(); + } else { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": called 'endCall' while in state " + this.state); + } + } + private void setupWebRTC() { this.webRTCWrapper.setup(this.xmppConnectionService); this.webRTCWrapper.initializePeerConnection(); @@ -392,6 +402,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web @Override public void onConnectionChange(PeerConnection.PeerConnectionState newState) { + Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": PeerConnectionState changed to "+newState); updateEndUserState(); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java index bfae8e53b..30055a3d6 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java @@ -8,5 +8,6 @@ public enum RtpEndUserState { RINGING, //'propose' has been sent out and it has been 184 acked ACCEPTED_ON_OTHER_DEVICE, //received 'accept' from one of our own devices ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received + ENDING_CALL, //libwebrt says 'closed' but session-terminate hasnt gone through FAILED //something went wrong. TODO needs more concrete error states } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 2fdd99003..f7d79771e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -81,8 +81,8 @@ public class WebRTCWrapper { @Override public void onAddStream(MediaStream mediaStream) { Log.d(Config.LOGTAG, "onAddStream"); - for(AudioTrack audioTrack : mediaStream.audioTracks) { - Log.d(Config.LOGTAG,"remote? - audioTrack enabled:"+audioTrack.enabled()+" state="+audioTrack.state()); + for (AudioTrack audioTrack : mediaStream.audioTracks) { + Log.d(Config.LOGTAG, "remote? - audioTrack enabled:" + audioTrack.enabled() + " state=" + audioTrack.state()); } final List videoTracks = mediaStream.videoTracks; if (videoTracks.size() > 0) { @@ -130,8 +130,8 @@ public class WebRTCWrapper { CameraVideoCapturer capturer = null; Camera1Enumerator camera1Enumerator = new Camera1Enumerator(); - for(String deviceName : camera1Enumerator.getDeviceNames()) { - Log.d(Config.LOGTAG,"camera device name: "+deviceName); + for (String deviceName : camera1Enumerator.getDeviceNames()) { + Log.d(Config.LOGTAG, "camera device name: " + deviceName); if (camera1Enumerator.isFrontFacing(deviceName)) { capturer = camera1Enumerator.createCapturer(deviceName, new CameraVideoCapturer.CameraEventsHandler() { @Override @@ -151,12 +151,12 @@ public class WebRTCWrapper { @Override public void onCameraOpening(String s) { - Log.d(Config.LOGTAG,"onCameraOpening"); + Log.d(Config.LOGTAG, "onCameraOpening"); } @Override public void onFirstFrameAvailable() { - Log.d(Config.LOGTAG,"onFirstFrameAvailable"); + Log.d(Config.LOGTAG, "onFirstFrameAvailable"); } @Override @@ -179,7 +179,7 @@ public class WebRTCWrapper { final AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints()); final AudioTrack audioTrack = peerConnectionFactory.createAudioTrack("my-audio-track", audioSource); - Log.d(Config.LOGTAG,"audioTrack enabled:"+audioTrack.enabled()+" state="+audioTrack.state()); + Log.d(Config.LOGTAG, "audioTrack enabled:" + audioTrack.enabled() + " state=" + audioTrack.state()); final MediaStream stream = peerConnectionFactory.createLocalMediaStream("my-media-stream"); stream.addTrack(audioTrack); //stream.addTrack(videoTrack); @@ -200,6 +200,9 @@ public class WebRTCWrapper { this.peerConnection = peerConnection; } + public void close() { + requirePeerConnection().close(); + } public ListenableFuture createOffer() { @@ -287,15 +290,19 @@ public class WebRTCWrapper { } public void addIceCandidate(IceCandidate iceCandidate) { + requirePeerConnection().addIceCandidate(iceCandidate); + } + + public PeerConnection.PeerConnectionState getState() { + return requirePeerConnection().connectionState(); + } + + private PeerConnection requirePeerConnection() { final PeerConnection peerConnection = this.peerConnection; if (peerConnection == null) { throw new IllegalStateException("initialize PeerConnection first"); } - peerConnection.addIceCandidate(iceCandidate); - } - - public PeerConnection.PeerConnectionState getState() { - return this.peerConnection.connectionState(); + return peerConnection; } private static abstract class SetSdpObserver implements SdpObserver { @@ -329,6 +336,7 @@ public class WebRTCWrapper { public interface EventCallback { void onIceCandidate(IceCandidate iceCandidate); + void onConnectionChange(PeerConnection.PeerConnectionState newState); } } diff --git a/src/main/res/layout/activity_rtp_session.xml b/src/main/res/layout/activity_rtp_session.xml index dc7dc4eec..28d954313 100644 --- a/src/main/res/layout/activity_rtp_session.xml +++ b/src/main/res/layout/activity_rtp_session.xml @@ -54,7 +54,22 @@ app:backgroundTint="@color/red700" app:elevation="4dp" app:fabCustomSize="72dp" - app:maxImageSize="36dp" /> + app:maxImageSize="36dp" + android:visibility="gone" + tools:visibility="visible"/> + + + app:maxImageSize="36dp" + android:visibility="gone" + tools:visibility="visible"/> diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index b7de99684..3d4c35661 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -889,6 +889,7 @@ Connecting Connected Accepting call + Ending call View %1$d Participant View %1$d Participants From 4c6ee9693ad1acdd48004b143dd4758527cc0845 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 7 Apr 2020 16:17:29 +0200 Subject: [PATCH 047/182] use appbarlayout in RtpSessionActivity --- .../conversations/ui/RtpSessionActivity.java | 12 ++--- src/main/res/layout/activity_rtp_session.xml | 44 +++++++++---------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 4fcab1157..32a0d03ef 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -29,12 +29,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private ActivityRtpSessionBinding binding; + @Override public void onCreate(Bundle savedInstanceState) { - getWindow().addFlags( - WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON - | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD - | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED - | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); super.onCreate(savedInstanceState); this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session); this.binding.rejectCall.setOnClickListener(this::rejectCall); @@ -42,6 +38,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.binding.acceptCall.setOnClickListener(this::acceptCall); } + @Override + public void onStart() { + super.onStart(); + Log.d(Config.LOGTAG,"RtpSessionActivity.onStart()"); + } + private void endCall(View view) { requireRtpConnection().endCall(); } diff --git a/src/main/res/layout/activity_rtp_session.xml b/src/main/res/layout/activity_rtp_session.xml index 28d954313..c21cec4ff 100644 --- a/src/main/res/layout/activity_rtp_session.xml +++ b/src/main/res/layout/activity_rtp_session.xml @@ -7,33 +7,33 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + android:paddingRight="16dp" + android:paddingBottom="32dp"> + + android:textAppearance="@style/TextAppearance.Conversations.Title" + android:textColor="@color/white" + tools:text="Incoming call" /> + + android:textColor="@color/white" + tools:text="Juliet Capulet" /> + + - + tools:visibility="visible" /> + app:maxImageSize="36dp" /> + tools:visibility="visible" /> From ccfc55e9b6a581849bf31c99f7d1f681ec5e4455 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 7 Apr 2020 18:50:39 +0200 Subject: [PATCH 048/182] show proper notification on incoming call --- src/main/AndroidManifest.xml | 3 +- .../services/NotificationService.java | 99 ++++++++++++++++--- .../services/XmppConnectionService.java | 6 ++ .../conversations/ui/RtpSessionActivity.java | 19 +++- .../xmpp/jingle/JingleRtpConnection.java | 12 +-- src/main/res/values/strings.xml | 4 + 6 files changed, 119 insertions(+), 24 deletions(-) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 66c4f5bcb..dac289003 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -10,7 +10,6 @@ - @@ -33,6 +32,8 @@ + + > notifications = new LinkedHashMap<>(); private final HashMap mBacklogMessageCounter = new HashMap<>(); @@ -100,6 +103,14 @@ public class NotificationService { return Pattern.compile("(?<=(^|\\s))" + Pattern.quote(nick) + "(?=\\s|$|\\p{Punct})"); } + private static boolean isImageMessage(Message message) { + return message.getType() != Message.TYPE_TEXT + && message.getTransferable() == null + && !message.isDeleted() + && message.getEncryption() != Message.ENCRYPTION_PGP + && message.getFileParams().height > 0; + } + @RequiresApi(api = Build.VERSION_CODES.O) void initializeChannels() { final Context c = mXmppConnectionService; @@ -112,6 +123,7 @@ public class NotificationService { notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("status", c.getString(R.string.notification_group_status_information))); notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("chats", c.getString(R.string.notification_group_messages))); + notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("calls", c.getString(R.string.notification_group_calls))); final NotificationChannel foregroundServiceChannel = new NotificationChannel("foreground", c.getString(R.string.foreground_service_channel_name), NotificationManager.IMPORTANCE_MIN); @@ -141,6 +153,20 @@ public class NotificationService { exportChannel.setGroup("status"); notificationManager.createNotificationChannel(exportChannel); + final NotificationChannel incomingCallsChannel = new NotificationChannel("incoming_calls", + c.getString(R.string.incoming_calls_channel_name), + NotificationManager.IMPORTANCE_HIGH); + incomingCallsChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE), new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) + .build()); + incomingCallsChannel.setShowBadge(false); + incomingCallsChannel.setLightColor(LED_COLOR); + incomingCallsChannel.enableLights(true); + incomingCallsChannel.setGroup("calls"); + notificationManager.createNotificationChannel(incomingCallsChannel); + + final NotificationChannel messagesChannel = new NotificationChannel("messages", c.getString(R.string.messages_channel_name), NotificationManager.IMPORTANCE_HIGH); @@ -300,6 +326,53 @@ public class NotificationService { } } + public void showIncomingCallNotification(AbstractJingleConnection.Id id) { + final Intent fullScreenIntent = new Intent(mXmppConnectionService, RtpSessionActivity.class); + fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString()); + fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString()); + fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId); + fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + final PendingIntent pendingIntent = PendingIntent.getActivity(mXmppConnectionService, 101, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT); + final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "incoming_calls"); + builder.setSmallIcon(R.drawable.ic_call_white_24dp); + builder.setContentTitle(mXmppConnectionService.getString(R.string.rtp_state_incoming_call)); + builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName()); + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + builder.setPriority(NotificationCompat.PRIORITY_HIGH); + builder.setCategory(NotificationCompat.CATEGORY_CALL); + builder.setFullScreenIntent(createPendingRtpSession(id, Intent.ACTION_VIEW, 101), true); + builder.setOngoing(true); + builder.addAction(new NotificationCompat.Action.Builder( + R.drawable.ic_call_end_white_48dp, + mXmppConnectionService.getString(R.string.dismiss_call), + createDismissCall(id.sessionId, 102)) + .build()); + builder.addAction(new NotificationCompat.Action.Builder( + R.drawable.ic_call_white_24dp, + mXmppConnectionService.getString(R.string.answer_call), + createPendingRtpSession(id, RtpSessionActivity.ACTION_ACCEPT, 103)) + .build()); + final Notification notification = builder.build(); + notification.flags = notification.flags | Notification.FLAG_INSISTENT; + notify(INCOMING_CALL_NOTIFICATION_ID, builder.build()); + } + + private PendingIntent createPendingRtpSession(final AbstractJingleConnection.Id id, final String action, final int requestCode) { + final Intent fullScreenIntent = new Intent(mXmppConnectionService, RtpSessionActivity.class); + fullScreenIntent.setAction(action); + fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString()); + fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString()); + fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId); + fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + return PendingIntent.getActivity(mXmppConnectionService, requestCode, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + public void cancelIncomingCallNotification() { + cancel(INCOMING_CALL_NOTIFICATION_ID); + } + private void pushNow(final Message message) { mXmppConnectionService.updateUnreadCountBadge(); if (!notify(message)) { @@ -467,7 +540,7 @@ public class NotificationService { private Builder buildMultipleConversation(final boolean notify, final boolean quietHours) { final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService, quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages")); final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); - style.setBigContentTitle(mXmppConnectionService.getString(R.string.x_unread_conversations,notifications.size())); + style.setBigContentTitle(mXmppConnectionService.getString(R.string.x_unread_conversations, notifications.size())); final StringBuilder names = new StringBuilder(); Conversation conversation = null; for (final ArrayList messages : notifications.values()) { @@ -652,7 +725,7 @@ public class NotificationService { return builder.build(); } - private void modifyForTextOnly(final Builder builder, final ArrayList messages) { + private void modifyForTextOnly(final Builder builder, final ArrayList messages) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { final Conversation conversation = (Conversation) messages.get(0).getConversation(); final Person.Builder meBuilder = new Person.Builder().setName(mXmppConnectionService.getString(R.string.me)); @@ -668,7 +741,7 @@ public class NotificationService { for (Message message : messages) { final Person sender = message.getStatus() == Message.STATUS_RECEIVED ? getPerson(message) : null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isImageMessage(message)) { - final Uri dataUri = FileBackend.getMediaUri(mXmppConnectionService,mXmppConnectionService.getFileBackend().getFile(message)); + final Uri dataUri = FileBackend.getMediaUri(mXmppConnectionService, mXmppConnectionService.getFileBackend().getFile(message)); NotificationCompat.MessagingStyle.Message imageMessage = new NotificationCompat.MessagingStyle.Message(UIHelper.getMessagePreview(mXmppConnectionService, message).first, message.getTimeSent(), sender); if (dataUri != null) { imageMessage.setData(message.getMimeType(), dataUri); @@ -683,7 +756,7 @@ public class NotificationService { } else { if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) { builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages))); - final CharSequence preview = UIHelper.getMessagePreview(mXmppConnectionService, messages.get(messages.size()-1)).first; + final CharSequence preview = UIHelper.getMessagePreview(mXmppConnectionService, messages.get(messages.size() - 1)).first; builder.setContentText(preview); builder.setTicker(preview); builder.setNumber(messages.size()); @@ -726,14 +799,6 @@ public class NotificationService { return image; } - private static boolean isImageMessage(Message message) { - return message.getType() != Message.TYPE_TEXT - && message.getTransferable() == null - && !message.isDeleted() - && message.getEncryption() != Message.ENCRYPTION_PGP - && message.getFileParams().height > 0; - } - private Message getFirstDownloadableMessage(final Iterable messages) { for (final Message message : messages) { if (message.getTransferable() != null || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) { @@ -834,6 +899,14 @@ public class NotificationService { return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 16), intent, PendingIntent.FLAG_UPDATE_CURRENT); } + private PendingIntent createDismissCall(String sessionId, int requestCode) { + final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); + intent.setAction(XmppConnectionService.ACTION_DISMISS_CALL); + intent.setPackage(mXmppConnectionService.getPackageName()); + intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId); + return PendingIntent.getService(mXmppConnectionService, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + private PendingIntent createSnoozeIntent(Conversation conversation) { final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); intent.setAction(XmppConnectionService.ACTION_SNOOZE); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 5951b7116..1ac96354e 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -107,6 +107,7 @@ import eu.siacs.conversations.parser.PresenceParser; import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.ui.ChooseAccountForProfilePictureActivity; +import eu.siacs.conversations.ui.RtpSessionActivity; import eu.siacs.conversations.ui.SettingsActivity; import eu.siacs.conversations.ui.UiCallback; import eu.siacs.conversations.ui.interfaces.OnAvatarPublication; @@ -165,6 +166,7 @@ public class XmppConnectionService extends Service { public static final String ACTION_IDLE_PING = "idle_ping"; public static final String ACTION_FCM_TOKEN_REFRESH = "fcm_token_refresh"; public static final String ACTION_FCM_MESSAGE_RECEIVED = "fcm_message_received"; + public static final String ACTION_DISMISS_CALL = "dismiss_call"; private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE"; private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp"; @@ -638,6 +640,10 @@ public class XmppConnectionService extends Service { } }); break; + case ACTION_DISMISS_CALL: + final String sessionId = intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID); + Log.d(Config.LOGTAG,"received intent to dismiss call with session id "+sessionId); + break; case ACTION_DISMISS_ERROR_NOTIFICATIONS: dismissErrorNotifications(); break; diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 32a0d03ef..2464f81b7 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -25,6 +25,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe public static final String EXTRA_WITH = "with"; public static final String EXTRA_SESSION_ID = "session_id"; + public static final String ACTION_ACCEPT = "accept"; + private WeakReference rtpConnectionReference; private ActivityRtpSessionBinding binding; @@ -32,6 +34,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON) + ; + Log.d(Config.LOGTAG, "RtpSessionActivity.onCreate()"); this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session); this.binding.rejectCall.setOnClickListener(this::rejectCall); this.binding.endCall.setOnClickListener(this::endCall); @@ -41,7 +49,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override public void onStart() { super.onStart(); - Log.d(Config.LOGTAG,"RtpSessionActivity.onStart()"); + Log.d(Config.LOGTAG, "RtpSessionActivity.onStart()"); } private void endCall(View view) { @@ -78,8 +86,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.rtpConnectionReference = reference; binding.with.setText(getWith().getDisplayName()); final RtpEndUserState currentState = requireRtpConnection().getEndUserState(); + final String action = intent.getAction(); updateStateDisplay(currentState); updateButtonConfiguration(currentState); + if (ACTION_ACCEPT.equals(action)) { + Log.d(Config.LOGTAG,"intent action was accept"); + requireRtpConnection().acceptCall(); + } } } @@ -137,12 +150,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe public void onJingleRtpConnectionUpdate(Account account, Jid with, RtpEndUserState state) { final AbstractJingleConnection.Id id = requireRtpConnection().getId(); if (account == id.account && id.with.equals(with)) { - runOnUiThread(()->{ + runOnUiThread(() -> { updateStateDisplay(state); updateButtonConfiguration(state); }); } else { - Log.d(Config.LOGTAG,"received update for other rtp session"); + Log.d(Config.LOGTAG, "received update for other rtp session"); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 3d346dfd5..56991635f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -228,12 +228,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void startRinging() { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received call from " + id.with + ". start ringing"); - final Intent intent = new Intent(xmppConnectionService, RtpSessionActivity.class); - intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString()); - intent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString()); - intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - xmppConnectionService.startActivity(intent); + xmppConnectionService.getNotificationService().showIncomingCallNotification(id); } private void receiveProceed(final Jid from, final Element proceed) { @@ -342,6 +337,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web public void rejectCall() { Log.d(Config.LOGTAG, "todo rejecting call"); + xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); } public void endCall() { @@ -359,6 +355,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void acceptCallFromProposed() { transitionOrThrow(State.PROCEED); + xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); final MessagePacket messagePacket = new MessagePacket(); messagePacket.setTo(id.with); //Note that Movim needs 'accept', correct is 'proceed' https://github.com/movim/movim/issues/916 @@ -368,7 +365,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void acceptCallFromSessionInitialized() { - + xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); + throw new IllegalStateException("accepting from this state has not been implemented yet"); } private synchronized boolean isInState(State... state) { diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 3d4c35661..e7d20d98f 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -748,7 +748,9 @@ Connectivity Problems This notification category is used to display a notification in case there is a problem connecting to an account. Messages + Calls Messages + Incoming calls Silent messages This notification group is used to display notifications that should not trigger any sound. For example when being active on another device (Grace Period). Notification Settings @@ -890,6 +892,8 @@ Connected Accepting call Ending call + Answer + Dismiss View %1$d Participant View %1$d Participants From e2f1cec2e541501400ec1e86d976a700b06bf194 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 7 Apr 2020 21:26:51 +0200 Subject: [PATCH 049/182] prepare more state transitions --- src/main/AndroidManifest.xml | 3 +- .../java/eu/siacs/conversations/Config.java | 2 +- .../services/NotificationService.java | 4 +- .../services/XmppConnectionService.java | 1 + .../conversations/ui/RtpSessionActivity.java | 13 +++ .../xmpp/jingle/AbstractJingleConnection.java | 7 +- .../xmpp/jingle/JingleConnectionManager.java | 10 +++ .../jingle/JingleFileTransferConnection.java | 41 ++++----- .../xmpp/jingle/JingleRtpConnection.java | 85 +++++++++++++++++-- .../xmpp/jingle/RtpEndUserState.java | 4 +- .../xmpp/jingle/stanzas/JinglePacket.java | 12 ++- .../xmpp/jingle/stanzas/Reason.java | 27 +++--- 12 files changed, 157 insertions(+), 52 deletions(-) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index dac289003..9ab4073b2 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -289,7 +289,8 @@ - + diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index d12de90b7..777953735 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -105,7 +105,7 @@ public final class Config { public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true; - public static final boolean DISABLE_HTTP_UPLOAD = true; + public static final boolean DISABLE_HTTP_UPLOAD = false; public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts public static final boolean BACKGROUND_STANZA_LOGGING = false; //log all stanzas that were received while the app is in background public static final boolean RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE = true; //setting to true might increase power consumption diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 8a521ee9d..9a51393a9 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -364,8 +364,8 @@ public class NotificationService { fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString()); fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString()); fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId); - fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + //fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + //fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); return PendingIntent.getActivity(mXmppConnectionService, requestCode, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 1ac96354e..22df52035 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -643,6 +643,7 @@ public class XmppConnectionService extends Service { case ACTION_DISMISS_CALL: final String sessionId = intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID); Log.d(Config.LOGTAG,"received intent to dismiss call with session id "+sessionId); + mJingleConnectionManager.rejectRtpSession(sessionId); break; case ACTION_DISMISS_ERROR_NOTIFICATIONS: dismissErrorNotifications(); diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 2464f81b7..5778a3de9 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -70,6 +70,15 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } + @Override + public void onNewIntent(final Intent intent) { + super.onNewIntent(intent); + if (ACTION_ACCEPT.equals(intent.getAction())) { + Log.d(Config.LOGTAG,"accepting through onNewIntent()"); + requireRtpConnection().acceptCall(); + } + } + @Override void onBackendConnected() { final Intent intent = getIntent(); @@ -150,6 +159,10 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe public void onJingleRtpConnectionUpdate(Account account, Jid with, RtpEndUserState state) { final AbstractJingleConnection.Id id = requireRtpConnection().getId(); if (account == id.account && id.with.equals(with)) { + if (state == RtpEndUserState.ENDED) { + finish(); + return; + } runOnUiThread(() -> { updateStateDisplay(state); updateButtonConfiguration(state); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java index 603371394..5b60479b8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -87,8 +87,13 @@ public abstract class AbstractJingleConnection { PROPOSED, ACCEPTED, PROCEED, + REJECTED, + RETRACTED, SESSION_INITIALIZED, //equal to 'PENDING' SESSION_ACCEPTED, //equal to 'ACTIVE' - TERMINATED //equal to 'ENDED' + TERMINATED_SUCCESS, //equal to 'ENDED' (after successful call) ui will just close + TERMINATED_DECLINED_OR_BUSY, //equal to 'ENDED' (after other party declined the call) + TERMINATED_CONNECTIVITY_ERROR, //equal to 'ENDED' (but after network failures; ui will display retry button) + TERMINATED_CANCEL_OR_TIMEOUT //more or less the same as retracted; caller pressed end call before session was accepted } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 982dc00b4..404cb0cc4 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -271,6 +271,16 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } + public void rejectRtpSession(final String sessionId) { + for(final AbstractJingleConnection connection : this.connections.values()) { + if (connection.getId().sessionId.equals(sessionId)) { + if (connection instanceof JingleRtpConnection) { + ((JingleRtpConnection) connection).rejectCall(); + } + } + } + } + public static class RtpSessionProposal { private final Account account; public final Jid with; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index 68964d131..e309e4289 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -156,7 +156,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple @Override public void onFileTransferAborted() { - JingleFileTransferConnection.this.sendSessionTerminate("connectivity-error"); + JingleFileTransferConnection.this.sendSessionTerminate(Reason.CONNECTIVITY_ERROR); JingleFileTransferConnection.this.fail(); } }; @@ -245,23 +245,20 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple if (action == JinglePacket.Action.SESSION_INITIATE) { init(packet); } else if (action == JinglePacket.Action.SESSION_TERMINATE) { - Reason reason = packet.getReason(); - if (reason != null) { - if (reason.hasChild("cancel")) { + final Reason reason = packet.getReason(); + switch (reason) { + case CANCEL: this.cancelled = true; this.fail(); - } else if (reason.hasChild("success")) { + break; + case SUCCESS: this.receiveSuccess(); - } else { - final List children = reason.getChildren(); - if (children.size() == 1) { - this.fail(children.get(0).getName()); - } else { - this.fail(); - } - } - } else { - this.fail(); + break; + default: + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session-terminate with reason " + reason); + this.fail(); + break; + } } else if (action == JinglePacket.Action.SESSION_ACCEPT) { receiveAccept(packet); @@ -756,7 +753,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple connection.setActivated(true); } else { Log.d(Config.LOGTAG, "activated connection not found"); - sendSessionTerminate("failed-transport"); + sendSessionTerminate(Reason.FAILED_TRANSPORT); this.fail(); } } @@ -894,7 +891,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } private void sendSuccess() { - sendSessionTerminate("success"); + sendSessionTerminate(Reason.SUCCESS); this.disconnectSocks5Connections(); this.mJingleStatus = JINGLE_STATUS_FINISHED; this.message.setStatus(Message.STATUS_RECEIVED); @@ -1014,10 +1011,10 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple @Override public void cancel() { this.cancelled = true; - abort("cancel"); + abort(Reason.CANCEL); } - void abort(final String reason) { + void abort(final Reason reason) { this.disconnectSocks5Connections(); if (this.transport instanceof JingleInBandTransport) { this.transport.disconnect(); @@ -1065,11 +1062,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple this.jingleConnectionManager.finishConnection(this); } - private void sendSessionTerminate(String reason) { + private void sendSessionTerminate(Reason reason) { final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_TERMINATE); - final Reason r = new Reason(); - r.addChild(reason); - packet.setReason(r); + packet.setReason(reason); this.sendJinglePacket(packet); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 56991635f..9b73b131d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -23,6 +23,7 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; @@ -33,7 +34,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web static { final ImmutableMap.Builder> transitionBuilder = new ImmutableMap.Builder<>(); transitionBuilder.put(State.NULL, ImmutableList.of(State.PROPOSED, State.SESSION_INITIALIZED)); - transitionBuilder.put(State.PROPOSED, ImmutableList.of(State.ACCEPTED, State.PROCEED)); + transitionBuilder.put(State.PROPOSED, ImmutableList.of(State.ACCEPTED, State.PROCEED, State.REJECTED)); transitionBuilder.put(State.PROCEED, ImmutableList.of(State.SESSION_INITIALIZED)); transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of(State.SESSION_ACCEPTED)); VALID_TRANSITIONS = transitionBuilder.build(); @@ -63,12 +64,36 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web case SESSION_ACCEPT: receiveSessionAccept(jinglePacket); break; + case SESSION_TERMINATE: + receiveSessionTerminate(jinglePacket); + break; default: Log.d(Config.LOGTAG, String.format("%s: received unhandled jingle action %s", id.account.getJid().asBareJid(), jinglePacket.getAction())); break; } } + private void receiveSessionTerminate(final JinglePacket jinglePacket) { + final Reason reason = jinglePacket.getReason(); + switch (reason) { + case SUCCESS: + transitionOrThrow(State.TERMINATED_SUCCESS); + break; + case DECLINE: + case BUSY: + transitionOrThrow(State.TERMINATED_DECLINED_OR_BUSY); + break; + case CANCEL: + case TIMEOUT: + transitionOrThrow(State.TERMINATED_CANCEL_OR_TIMEOUT); + break; + default: + transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR); + break; + } + jingleConnectionManager.finishConnection(this); + } + private void receiveTransportInfo(final JinglePacket jinglePacket) { if (isInState(State.SESSION_INITIALIZED, State.SESSION_ACCEPTED)) { final RtpContentMap contentMap; @@ -315,10 +340,24 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } else if (state == PeerConnection.PeerConnectionState.CLOSED) { return RtpEndUserState.ENDING_CALL; } else { - return RtpEndUserState.FAILED; + return RtpEndUserState.ENDING_CALL; } + case REJECTED: + case TERMINATED_DECLINED_OR_BUSY: + if (isInitiator()) { + return RtpEndUserState.DECLINED_OR_BUSY; + } else { + return RtpEndUserState.ENDED; + } + case TERMINATED_SUCCESS: + case ACCEPTED: + case RETRACTED: + case TERMINATED_CANCEL_OR_TIMEOUT: + return RtpEndUserState.ENDED; + case TERMINATED_CONNECTIVITY_ERROR: + return RtpEndUserState.CONNECTIVITY_ERROR; } - return RtpEndUserState.FAILED; + throw new IllegalStateException(String.format("%s has no equivalent EndUserState", this.state)); } @@ -331,19 +370,34 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web acceptCallFromSessionInitialized(); break; default: - throw new IllegalStateException("Can not pick up call from " + this.state); + throw new IllegalStateException("Can not accept call from " + this.state); } } public void rejectCall() { - Log.d(Config.LOGTAG, "todo rejecting call"); - xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); + switch (this.state) { + case PROPOSED: + rejectCallFromProposed(); + break; + default: + throw new IllegalStateException("Can not reject call from " + this.state); + } } public void endCall() { + + //TODO from `propose` we call `retract` + if (isInState(State.SESSION_INITIALIZED, State.SESSION_ACCEPTED)) { + //TODO during session_initialized we might not have a peer connection yet (if the session was initialized directly) + + //TODO from session_initialized we call `cancel` + + //TODO from session_accepted we call `success` + webRTCWrapper.close(); } else { + //TODO during earlier stages we want to retract the proposal etc Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": called 'endCall' while in state " + this.state); } } @@ -356,10 +410,23 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void acceptCallFromProposed() { transitionOrThrow(State.PROCEED); xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); + //Note that Movim needs 'accept', correct is 'proceed' https://github.com/movim/movim/issues/916 + this.sendJingleMessage("proceed"); + + //TODO send `accept` to self + } + + private void rejectCallFromProposed() { + transitionOrThrow(State.REJECTED); + xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); + this.sendJingleMessage("reject"); + jingleConnectionManager.finishConnection(this); + } + + private void sendJingleMessage(final String action) { final MessagePacket messagePacket = new MessagePacket(); messagePacket.setTo(id.with); - //Note that Movim needs 'accept', correct is 'proceed' https://github.com/movim/movim/issues/916 - messagePacket.addChild("proceed", Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId); + messagePacket.addChild(action, Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId); Log.d(Config.LOGTAG, messagePacket.toString()); xmppConnectionService.sendMessagePacket(id.account, messagePacket); } @@ -400,7 +467,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web @Override public void onConnectionChange(PeerConnection.PeerConnectionState newState) { - Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": PeerConnectionState changed to "+newState); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); updateEndUserState(); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java index 30055a3d6..e8d8bd5d2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java @@ -9,5 +9,7 @@ public enum RtpEndUserState { ACCEPTED_ON_OTHER_DEVICE, //received 'accept' from one of our own devices ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received ENDING_CALL, //libwebrt says 'closed' but session-terminate hasnt gone through - FAILED //something went wrong. TODO needs more concrete error states + ENDED, //close UI + DECLINED_OR_BUSY, //other party declined; no retry button + CONNECTIVITY_ERROR //network error; retry button } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java index 3b4ef47d1..25ca3d5fd 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -70,12 +70,20 @@ public class JinglePacket extends IqPacket { public Reason getReason() { final Element reason = getJingleChild("reason"); - return reason == null ? null : Reason.upgrade(reason); + if (reason == null) { + return Reason.UNKNOWN; + } + for(Element child : reason.getChildren()) { + if (!"text".equals(child.getName())) { + return Reason.of(child.getName()); + } + } + return Reason.UNKNOWN; } public void setReason(final Reason reason) { final Element jingle = findChild("jingle", Namespace.JINGLE); - jingle.addChild(reason); + jingle.addChild(new Element("reason").addChild(reason.toString())); } //RECOMMENDED for session-initiate, NOT RECOMMENDED otherwise diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java index 1eae65c5b..070f77226 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java @@ -1,20 +1,23 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; -import com.google.common.base.Preconditions; +import android.support.annotation.NonNull; -import eu.siacs.conversations.xml.Element; +import com.google.common.base.CaseFormat; -public class Reason extends Element { +public enum Reason { + SUCCESS, DECLINE, BUSY, CANCEL, CONNECTIVITY_ERROR, FAILED_TRANSPORT, TIMEOUT, UNKNOWN; - public Reason() { - super("reason"); + public static Reason of(final String value) { + try { + return Reason.valueOf(CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value)); + } catch (Exception e) { + return UNKNOWN; + } } - public static Reason upgrade(final Element element) { - Preconditions.checkArgument("reason".equals(element.getName())); - final Reason reason = new Reason(); - reason.setAttributes(element.getAttributes()); - reason.setChildren(element.getChildren()); - return reason; + @Override + @NonNull + public String toString() { + return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, super.toString()); } -} +} \ No newline at end of file From 7909a72d43822853908202e6a9254d71bf61cfe5 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 8 Apr 2020 09:42:06 +0200 Subject: [PATCH 050/182] make retract jingle messages work --- .../generator/MessageGenerator.java | 9 ++ .../services/NotificationService.java | 2 +- .../services/XmppConnectionService.java | 6 +- .../ui/ConversationFragment.java | 6 +- .../conversations/ui/RtpSessionActivity.java | 107 ++++++++++++++---- .../xmpp/jingle/JingleConnectionManager.java | 70 ++++++++++-- .../xmpp/jingle/JingleRtpConnection.java | 25 +++- src/main/res/values/strings.xml | 2 + 8 files changed, 187 insertions(+), 40 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 393cec5d4..c0acf50b7 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -244,4 +244,13 @@ public class MessageGenerator extends AbstractGenerator { packet.addChild("request", "urn:xmpp:receipts"); return packet; } + + public MessagePacket sessionRetract(final JingleConnectionManager.RtpSessionProposal proposal) { + final MessagePacket packet = new MessagePacket(); + packet.setTo(proposal.with); + final Element propose = packet.addChild("retract", Namespace.JINGLE_MESSAGE); + propose.setAttribute("id", proposal.sessionId); + propose.addChild("description", Namespace.JINGLE_APPS_RTP); + return packet; + } } diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 9a51393a9..da7028f35 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -351,7 +351,7 @@ public class NotificationService { builder.addAction(new NotificationCompat.Action.Builder( R.drawable.ic_call_white_24dp, mXmppConnectionService.getString(R.string.answer_call), - createPendingRtpSession(id, RtpSessionActivity.ACTION_ACCEPT, 103)) + createPendingRtpSession(id, RtpSessionActivity.ACTION_ACCEPT_CALL, 103)) .build()); final Notification notification = builder.build(); notification.flags = notification.flags | Notification.FLAG_INSISTENT; diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 22df52035..14e94951f 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -3977,9 +3977,9 @@ public class XmppConnectionService extends Service { } } - public void notifyJingleRtpConnectionUpdate(final Account account, final Jid with, final RtpEndUserState state) { + public void notifyJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state) { for(OnJingleRtpConnectionUpdate listener : threadSafeList(this.onJingleRtpConnectionUpdate)) { - listener.onJingleRtpConnectionUpdate(account, with, state); + listener.onJingleRtpConnectionUpdate(account, with, sessionId, state); } } @@ -4661,7 +4661,7 @@ public class XmppConnectionService extends Service { } public interface OnJingleRtpConnectionUpdate { - void onJingleRtpConnectionUpdate(final Account account, final Jid with, final RtpEndUserState state); + void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state); } public interface OnAccountUpdate { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index bb76d7256..ba531cc45 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1243,7 +1243,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private void triggerRtpSession() { final Contact contact = conversation.getContact(); - activity.xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(conversation.getAccount(), contact); + final Intent intent = new Intent(activity, RtpSessionActivity.class); + intent.setAction(RtpSessionActivity.ACTION_MAKE_VOICE_CALL); + intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, contact.getAccount().getJid().toEscapedString()); + intent.putExtra(RtpSessionActivity.EXTRA_WITH, contact.getJid().asBareJid().toEscapedString()); + startActivity(intent); } private void handleAttachmentSelection(MenuItem item) { diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 5778a3de9..b70dab72f 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -8,6 +8,7 @@ import android.view.View; import android.view.WindowManager; import java.lang.ref.WeakReference; +import java.util.Arrays; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -20,12 +21,16 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; import rocks.xmpp.addr.Jid; +import static java.util.Arrays.asList; + public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate { public static final String EXTRA_WITH = "with"; public static final String EXTRA_SESSION_ID = "session_id"; - public static final String ACTION_ACCEPT = "accept"; + public static final String ACTION_ACCEPT_CALL = "action_accept_call"; + public static final String ACTION_MAKE_VOICE_CALL = "action_make_voice_call"; + public static final String ACTION_MAKE_VIDEO_CALL = "action_make_video_call"; private WeakReference rtpConnectionReference; @@ -53,7 +58,15 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void endCall(View view) { - requireRtpConnection().endCall(); + if (this.rtpConnectionReference == null) { + final Intent intent = getIntent(); + final Account account = extractAccount(intent); + final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH)); + xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid()); + finish(); + } else { + requireRtpConnection().endCall(); + } } private void rejectCall(View view) { @@ -73,8 +86,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override public void onNewIntent(final Intent intent) { super.onNewIntent(intent); - if (ACTION_ACCEPT.equals(intent.getAction())) { - Log.d(Config.LOGTAG,"accepting through onNewIntent()"); + if (ACTION_ACCEPT_CALL.equals(intent.getAction())) { + Log.d(Config.LOGTAG, "accepting through onNewIntent()"); requireRtpConnection().acceptCall(); } } @@ -83,28 +96,50 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe void onBackendConnected() { final Intent intent = getIntent(); final Account account = extractAccount(intent); - final String with = intent.getStringExtra(EXTRA_WITH); + final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH)); final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID); - if (with != null && sessionId != null) { - final WeakReference reference = xmppConnectionService.getJingleConnectionManager() - .findJingleRtpConnection(account, Jid.ofEscaped(with), sessionId); - if (reference == null || reference.get() == null) { - finish(); - return; - } - this.rtpConnectionReference = reference; - binding.with.setText(getWith().getDisplayName()); - final RtpEndUserState currentState = requireRtpConnection().getEndUserState(); - final String action = intent.getAction(); - updateStateDisplay(currentState); - updateButtonConfiguration(currentState); - if (ACTION_ACCEPT.equals(action)) { - Log.d(Config.LOGTAG,"intent action was accept"); + if (sessionId != null) { + initializeActivityWithRunningRapSession(account, with, sessionId); + if (ACTION_ACCEPT_CALL.equals(intent.getAction())) { + Log.d(Config.LOGTAG, "intent action was accept"); requireRtpConnection().acceptCall(); } + } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(intent.getAction())) { + xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with); + binding.with.setText(account.getRoster().getContact(with).getDisplayName()); } } + + private void initializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) { + final WeakReference reference = xmppConnectionService.getJingleConnectionManager() + .findJingleRtpConnection(account, with, sessionId); + if (reference == null || reference.get() == null) { + finish(); + return; + } + this.rtpConnectionReference = reference; + final RtpEndUserState currentState = requireRtpConnection().getEndUserState(); + if (currentState == RtpEndUserState.ENDED) { + finish(); + return; + } + binding.with.setText(getWith().getDisplayName()); + updateStateDisplay(currentState); + updateButtonConfiguration(currentState); + } + + private void reInitializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) { + runOnUiThread(() -> { + initializeActivityWithRunningRapSession(account, with, sessionId); + }); + final Intent intent = new Intent(Intent.ACTION_VIEW); + intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString()); + intent.putExtra(EXTRA_WITH, with.toEscapedString()); + intent.putExtra(EXTRA_SESSION_ID, sessionId); + setIntent(intent); + } + private void updateStateDisplay(final RtpEndUserState state) { switch (state) { case INCOMING_CALL: @@ -122,6 +157,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe case ENDING_CALL: binding.status.setText(R.string.rtp_state_ending_call); break; + case FINDING_DEVICE: + binding.status.setText(R.string.rtp_state_finding_device); + break; + case RINGING: + binding.status.setText(R.string.rtp_state_ringing); } } @@ -156,9 +196,19 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } @Override - public void onJingleRtpConnectionUpdate(Account account, Jid with, RtpEndUserState state) { + public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) { + Log.d(Config.LOGTAG,"onJingleRtpConnectionUpdate("+state+")"); + if (with.isBareJid()) { + updateRtpSessionProposalState(with, state); + return; + } + if (this.rtpConnectionReference == null) { + //this happens when going from proposed session to actual session + reInitializeActivityWithRunningRapSession(account, with, sessionId); + return; + } final AbstractJingleConnection.Id id = requireRtpConnection().getId(); - if (account == id.account && id.with.equals(with)) { + if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) { if (state == RtpEndUserState.ENDED) { finish(); return; @@ -170,6 +220,19 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } else { Log.d(Config.LOGTAG, "received update for other rtp session"); } + } + private void updateRtpSessionProposalState(Jid with, RtpEndUserState state) { + final Intent intent = getIntent(); + final String intentExtraWith = intent == null ? null : intent.getStringExtra(EXTRA_WITH); + if (intentExtraWith == null) { + return; + } + if (Jid.ofEscaped(intentExtraWith).asBareJid().equals(with)) { + runOnUiThread(() -> { + updateStateDisplay(state); + updateButtonConfiguration(state); + }); + } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 404cb0cc4..2c306816f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -188,12 +188,52 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } - public void proposeJingleRtpSession(final Account account, final Contact contact) { - final RtpSessionProposal proposal = RtpSessionProposal.of(account, contact.getJid().asBareJid()); + public void retractSessionProposal(final Account account, final Jid with) { synchronized (this.rtpSessionProposals) { + RtpSessionProposal matchingProposal = null; + for (RtpSessionProposal proposal : this.rtpSessionProposals.keySet()) { + if (proposal.account == account && with.asBareJid().equals(proposal.with)) { + matchingProposal = proposal; + break; + } + } + if (matchingProposal != null) { + this.rtpSessionProposals.remove(matchingProposal); + final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(matchingProposal); + Log.d(Config.LOGTAG, messagePacket.toString()); + mXmppConnectionService.sendMessagePacket(account, messagePacket); + + } + } + } + + public void proposeJingleRtpSession(final Account account, final Jid with) { + synchronized (this.rtpSessionProposals) { + for (Map.Entry entry : this.rtpSessionProposals.entrySet()) { + RtpSessionProposal proposal = entry.getKey(); + if (proposal.account == account && with.asBareJid().equals(proposal.with)) { + final DeviceDiscoveryState preexistingState = entry.getValue(); + if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) { + mXmppConnectionService.notifyJingleRtpConnectionUpdate( + account, + with, + proposal.sessionId, + preexistingState.toEndUserState() + ); + return; + } + } + } + final RtpSessionProposal proposal = RtpSessionProposal.of(account, with.asBareJid()); this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING); + mXmppConnectionService.notifyJingleRtpConnectionUpdate( + account, + proposal.with, + proposal.sessionId, + RtpEndUserState.FINDING_DEVICE + ); final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); - Log.d(Config.LOGTAG,messagePacket.toString()); + Log.d(Config.LOGTAG, messagePacket.toString()); mXmppConnectionService.sendMessagePacket(account, messagePacket); } } @@ -255,24 +295,25 @@ public class JingleConnectionManager extends AbstractConnectionManager { } public void updateProposedSessionDiscovered(Account account, Jid from, String sessionId, final DeviceDiscoveryState target) { - final RtpSessionProposal sessionProposal = new RtpSessionProposal(account,from.asBareJid(),sessionId); + final RtpSessionProposal sessionProposal = new RtpSessionProposal(account, from.asBareJid(), sessionId); synchronized (this.rtpSessionProposals) { final DeviceDiscoveryState currentState = rtpSessionProposals.get(sessionProposal); if (currentState == null) { - Log.d(Config.LOGTAG,"unable to find session proposal for session id "+sessionId); + Log.d(Config.LOGTAG, "unable to find session proposal for session id " + sessionId); return; } if (currentState == DeviceDiscoveryState.DISCOVERED) { - Log.d(Config.LOGTAG,"session proposal already at discovered. not going to fall back"); + Log.d(Config.LOGTAG, "session proposal already at discovered. not going to fall back"); return; } this.rtpSessionProposals.put(sessionProposal, target); - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": flagging session "+sessionId+" as "+target); + mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, sessionProposal.with, sessionProposal.sessionId, target.toEndUserState()); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": flagging session " + sessionId + " as " + target); } } public void rejectRtpSession(final String sessionId) { - for(final AbstractJingleConnection connection : this.connections.values()) { + for (final AbstractJingleConnection connection : this.connections.values()) { if (connection.getId().sessionId.equals(sessionId)) { if (connection instanceof JingleRtpConnection) { ((JingleRtpConnection) connection).rejectCall(); @@ -313,6 +354,17 @@ public class JingleConnectionManager extends AbstractConnectionManager { } public enum DeviceDiscoveryState { - SEARCHING, DISCOVERED, FAILED + SEARCHING, DISCOVERED, FAILED; + + public RtpEndUserState toEndUserState() { + switch (this) { + case SEARCHING: + return RtpEndUserState.FINDING_DEVICE; + case DISCOVERED: + return RtpEndUserState.RINGING; + default: + return RtpEndUserState.CONNECTIVITY_ERROR; + } + } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 9b73b131d..28e06ed3a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.xmpp.jingle; -import android.content.Intent; import android.util.Log; import com.google.common.collect.ImmutableList; @@ -17,7 +16,6 @@ import java.util.List; import java.util.Map; import eu.siacs.conversations.Config; -import eu.siacs.conversations.ui.RtpSessionActivity; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; @@ -34,7 +32,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web static { final ImmutableMap.Builder> transitionBuilder = new ImmutableMap.Builder<>(); transitionBuilder.put(State.NULL, ImmutableList.of(State.PROPOSED, State.SESSION_INITIALIZED)); - transitionBuilder.put(State.PROPOSED, ImmutableList.of(State.ACCEPTED, State.PROCEED, State.REJECTED)); + transitionBuilder.put(State.PROPOSED, ImmutableList.of(State.ACCEPTED, State.PROCEED, State.REJECTED, State.RETRACTED)); transitionBuilder.put(State.PROCEED, ImmutableList.of(State.SESSION_INITIALIZED)); transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of(State.SESSION_ACCEPTED)); VALID_TRANSITIONS = transitionBuilder.build(); @@ -234,6 +232,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web break; case "proceed": receiveProceed(from, message); + break; + case "retract": + receiveRetract(from, message); + break; default: break; } @@ -272,6 +274,21 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } + private void receiveRetract(final Jid from, final Element retract) { + if (from.equals(id.with)) { + if (transition(State.RETRACTED)) { + xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": session with " + id.with + " has been retracted"); + //TODO create missed call notification/message + jingleConnectionManager.finishConnection(this); + } else { + Log.d(Config.LOGTAG, "ignoring retract because already in " + this.state); + } + } else { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received retract from " + from + ". expected retract from" + id.with + ". ignoring"); + } + } + private void sendSessionInitiate() { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate"); setupWebRTC(); @@ -472,6 +489,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void updateEndUserState() { - xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, getEndUserState()); + xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, getEndUserState()); } } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index e7d20d98f..47e635368 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -894,6 +894,8 @@ Ending call Answer Dismiss + Locating devices + Ringing View %1$d Participant View %1$d Participants From a11d506bf0f448d420f6e0ea96e71a35beed2d2d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 8 Apr 2020 10:46:26 +0200 Subject: [PATCH 051/182] support reject --- .../conversations/ui/RtpSessionActivity.java | 18 +++++++++++++++++- .../xmpp/jingle/JingleConnectionManager.java | 18 ++++++++++++++++-- .../res/drawable-hdpi/ic_clear_white_48dp.png | Bin 0 -> 347 bytes .../res/drawable-mdpi/ic_clear_white_48dp.png | Bin 0 -> 257 bytes .../drawable-xhdpi/ic_clear_white_48dp.png | Bin 0 -> 436 bytes .../drawable-xxhdpi/ic_clear_white_48dp.png | Bin 0 -> 524 bytes .../drawable-xxxhdpi/ic_clear_white_48dp.png | Bin 0 -> 702 bytes src/main/res/values/strings.xml | 1 + 8 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 src/main/res/drawable-hdpi/ic_clear_white_48dp.png create mode 100644 src/main/res/drawable-mdpi/ic_clear_white_48dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_clear_white_48dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_clear_white_48dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_clear_white_48dp.png diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index b70dab72f..2e6436b3d 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -162,6 +162,10 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe break; case RINGING: binding.status.setText(R.string.rtp_state_ringing); + break; + case DECLINED_OR_BUSY: + binding.status.setText(R.string.rtp_state_declined_or_busy); + break; } } @@ -174,13 +178,25 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.binding.rejectCall.hide(); this.binding.endCall.hide(); this.binding.acceptCall.hide(); + } else if (state == RtpEndUserState.DECLINED_OR_BUSY) { + this.binding.rejectCall.hide(); + this.binding.endCall.setImageResource(R.drawable.ic_clear_white_48dp); + this.binding.endCall.show(); + this.binding.endCall.setOnClickListener(this::exit); + this.binding.acceptCall.hide(); } else { this.binding.rejectCall.hide(); + this.binding.endCall.setImageResource(R.drawable.ic_call_end_white_48dp); this.binding.endCall.show(); + this.binding.endCall.setOnClickListener(this::endCall); this.binding.acceptCall.hide(); } } + private void exit(View view) { + finish(); + } + private Contact getWith() { final AbstractJingleConnection.Id id = requireRtpConnection().getId(); final Account account = id.account; @@ -197,7 +213,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) { - Log.d(Config.LOGTAG,"onJingleRtpConnectionUpdate("+state+")"); + Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")"); if (with.isBareJid()) { updateRtpSessionProposalState(with, state); return; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 2c306816f..f0928dfb7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -77,6 +77,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (sessionId == null) { return; } + final boolean carbonCopy = from.asBareJid().equals(account.getJid().asBareJid()); final Jid with; if (account.getJid().asBareJid().equals(from.asBareJid())) { with = to; @@ -103,7 +104,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to react to proposed " + namespace + " session"); } } else if ("proceed".equals(message.getName())) { - if (!with.equals(from)) { + if (carbonCopy) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignore carbon copied proceed"); return; } @@ -115,7 +116,20 @@ public class JingleConnectionManager extends AbstractConnectionManager { rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED); rtpConnection.deliveryMessage(from, message); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + with); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + with + " to deliver proceed"); + } + } + } else if ("reject".equals(message.getName())) { + if (carbonCopy) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignore carbon copied reject"); + return; + } + final RtpSessionProposal proposal = new RtpSessionProposal(account, with.asBareJid(), sessionId); + synchronized (rtpSessionProposals) { + if (rtpSessionProposals.remove(proposal) != null) { + mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, proposal.with, proposal.sessionId, RtpEndUserState.DECLINED_OR_BUSY); + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + with + " to deliver reject"); } } } else { diff --git a/src/main/res/drawable-hdpi/ic_clear_white_48dp.png b/src/main/res/drawable-hdpi/ic_clear_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..6b717e0dda8649aa3b5f1d6851ba0dd20cc4ea66 GIT binary patch literal 347 zcmV-h0i^zkP)vMkH8EXyh{JTRaptiwcq@D(P;=ehLbl?EMKm*m7(@7_siS}}k zNFtnck{HL4mc!ypcyUoqJV~4rM^fS3C#iAnkyJU3biCmDy`VZLOv=LXld^HVq zOA5m<3jYBNi%A&<@ZO?$P9};P4Y5CjG5$M&YXI45J{s}~# zf|&?x1_gn4B7+hS@X!l}&!voFhmZP^sujifL@~PKMMM~{6xH}^g$q7WOzwCQ5vHTU z6`v~H@rlA8e;CUh_(b84zg=+ih`wG<)HiJjzSlQx5#CnjMR;A)R^jtaTa9;7rSy)7O%~`cm?ZjXImW?6TYRT<;U^@VKiSj`soFk00000NkvXX Hu0mjfhD&W| literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_clear_white_48dp.png b/src/main/res/drawable-xhdpi/ic_clear_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..39641921925f090e33df2767a4ee5e6d5911194f GIT binary patch literal 436 zcmV;l0ZaagP)32ETvhYyB39D^lU%DgQgv&#U%8l^-CA%qY@2qA=!B5=cm zUgehujQG*l{{`@rPr!f|fEk^>KI9WteE@ilV6HEl&_rJ@p_zU*;T}RirIc{5NocNLm*7JGdV(AM zYYDFOvk5~8{Z*t_>U=!XvoehUSEh=adIga45oe)B9kGgT}7UT3Cirmr(oHPv^YQ1-p=Hlh5u;xggf zX{&yw+El-OrrKQJRl@b7x{HLmNkj95`a#LLnW{Veb2C+!`ppt#r)=g4@?c8RY{yJj~W@W|h^mU4q`i)2y~Rw@J`jIh$1%|In!~{Tb{nMqaxlgb+dq eA%qY@zJ@mriVM?qfwL0;0000u<8%L#D-{7p<|%4@n9yOn_`BhIN5Se>F5&yX=g3#NJr`MF8+b%+$B8BmnCIPndHJicwnxNiHsOkk&+*iSjJjmJe1$X)E`JjJT7)$8wm+rVMFtHWj=GUBk= z=ke_WBcq(Ni(Gt*T>KFvWMaekQTMd-+4Z${JvAkNz8{`iZ@=$N<$){y-?y+S-FBWF zc%ND6E)u%gY+Qf)aIpRD!%Emtu>HZuoi`qLflzYyjmJ==_n}6?N_~C6*PuJQ&feR6 zYOCY7g5y!UB2LPQOzopr0M`r!;Q#;t literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_clear_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_clear_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..bb21ce03a954236e431072961e57a9af317a68ee GIT binary patch literal 702 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE8Azrw%`pX1#sNMdu0Z-fiD3KGM@>L&0wqCy z!3?}ShmRdO`suT!jkVQ|?c1(&>uh9TVAAq*aSW-5dwa(*PbyG=?IF*RV-kihZ5`K%4WRLhrBZ|F1`7uibhLg{ZBIk7Kr&_g(j2|L0$mnjU|z+@1e% z{_2El_4Br`m;D!N@TR><^S_qFH|K*>>P3Bi{};6}`w@O{jy%`0KLG|0+M76ml3&-? zf<;^8xsLr^op5ab;s?KN7A73n|1I6-!KZJ@2XChy;+OYxY&^ivnr1J4<8MsD_u|I8 z=UL0(oIB@P+mD>LUf}ThIn(a>+;8giHvHas;2j#Ldp`HQdX>HMKz%R%#ykKTa{oN* zn{x3D_hS##RWkni%KGHH_=NlM40ZL5ch1LueXv~&ELG0<`z~BvHRI2}Fm?Z!esN5c z=lUh}w!P<1%!R`-{g>Z7Z~7PXZTY;v*R@4%{1vaTc7MP8d-tC@jSIgQI=pXZ+AELh zx+isd7H{h6tdFBR7R?*?a|M6wg*yVpML^D9);Hh9Z4TPUe30S)u`Accm+AO6Hmvuct~^wI jTJ;u6{1-oD!MDismiss Locating devices Ringing + Busy View %1$d Participant View %1$d Participants From 9edadc9835ddfc04193ae4691a5f861a441e29be Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 8 Apr 2020 11:29:01 +0200 Subject: [PATCH 052/182] process retract jingle messages --- .../generator/MessageGenerator.java | 2 ++ .../conversations/ui/RtpSessionActivity.java | 3 ++- .../xmpp/jingle/JingleRtpConnection.java | 26 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index c0acf50b7..7e93303cd 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -236,6 +236,7 @@ public class MessageGenerator extends AbstractGenerator { public MessagePacket sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) { final MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those packet.setTo(proposal.with); packet.setId(JingleRtpConnection.JINGLE_MESSAGE_ID_PREFIX+proposal.sessionId); final Element propose = packet.addChild("propose", Namespace.JINGLE_MESSAGE); @@ -247,6 +248,7 @@ public class MessageGenerator extends AbstractGenerator { public MessagePacket sessionRetract(final JingleConnectionManager.RtpSessionProposal proposal) { final MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those packet.setTo(proposal.with); final Element propose = packet.addChild("retract", Namespace.JINGLE_MESSAGE); propose.setAttribute("id", proposal.sessionId); diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 2e6436b3d..1aef98afc 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -8,7 +8,6 @@ import android.view.View; import android.view.WindowManager; import java.lang.ref.WeakReference; -import java.util.Arrays; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -23,6 +22,8 @@ import rocks.xmpp.addr.Jid; import static java.util.Arrays.asList; +//TODO if last state was BUSY (or RETRY); we want to reset action to view or something so we don’t automatically call again on recreate + public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate { public static final String EXTRA_WITH = "with"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 28e06ed3a..1a66d41b9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -236,11 +236,29 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web case "retract": receiveRetract(from, message); break; + case "reject": + receiveReject(from, message); + break; default: break; } } + private void receiveReject(Jid from, Element message) { + final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); + //reject from another one of my clients + if (originatedFromMyself) { + if (transition(State.REJECTED)) { + this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); + this.jingleConnectionManager.finishConnection(this); + } else { + Log.d(Config.LOGTAG,"not able to transition into REJECTED because already in "+this.state); + } + } else { + Log.d(Config.LOGTAG,id.account.getJid()+": ignoring reject from "+from+" for session with "+id.with); + } + } + private void receivePropose(final Jid from, final Element propose) { final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); //TODO we can use initiator logic here @@ -269,7 +287,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } else { Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because we were not initializing", id.account.getJid().asBareJid())); } + } else if (from.asBareJid().equals(id.account.getJid().asBareJid())) { + if (transition(State.ACCEPTED)) { + Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": moved session with "+id.with+" into state accepted after received carbon copied procced"); + this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); + this.jingleConnectionManager.finishConnection(this); + } } else { + //TODO a carbon copied proceed from another client of mine has the same logic as `accept` Log.d(Config.LOGTAG, String.format("%s: ignoring proceed from %s. was expected from %s", id.account.getJid().asBareJid(), from, id.with)); } } @@ -442,6 +467,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void sendJingleMessage(final String action) { final MessagePacket messagePacket = new MessagePacket(); + messagePacket.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those messagePacket.setTo(id.with); messagePacket.addChild(action, Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId); Log.d(Config.LOGTAG, messagePacket.toString()); From f7d1e02d4b9824edc11a94feeb16b182569fcd71 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 8 Apr 2020 12:17:46 +0200 Subject: [PATCH 053/182] parse 'accept' messages --- .../xmpp/jingle/JingleConnectionManager.java | 13 +++++++ .../xmpp/jingle/JingleRtpConnection.java | 35 +++++++++++++++---- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index f0928dfb7..5e2e59b29 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -77,6 +77,19 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (sessionId == null) { return; } + if ("accept".equals(message.getName())) { + for (AbstractJingleConnection connection : connections.values()) { + if (connection instanceof JingleRtpConnection) { + final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection; + final AbstractJingleConnection.Id id = connection.getId(); + if (id.account == account && id.sessionId.equals(sessionId)) { + rtpConnection.deliveryMessage(from, message); + return; + } + } + } + return; + } final boolean carbonCopy = from.asBareJid().equals(account.getJid().asBareJid()); final Jid with; if (account.getJid().asBareJid().equals(from.asBareJid())) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 1a66d41b9..70ad0a37b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -239,11 +239,28 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web case "reject": receiveReject(from, message); break; + case "accept": + receiveAccept(from, message); + break; default: break; } } + private void receiveAccept(Jid from, Element message) { + final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); + if (originatedFromMyself) { + if (transition(State.ACCEPTED)) { + this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); + this.jingleConnectionManager.finishConnection(this); + } else { + Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": unable to transition to accept because already in state="+this.state); + } + } else { + Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": ignoring 'accept' from "+from); + } + } + private void receiveReject(Jid from, Element message) { final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); //reject from another one of my clients @@ -252,10 +269,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); this.jingleConnectionManager.finishConnection(this); } else { - Log.d(Config.LOGTAG,"not able to transition into REJECTED because already in "+this.state); + Log.d(Config.LOGTAG, "not able to transition into REJECTED because already in " + this.state); } } else { - Log.d(Config.LOGTAG,id.account.getJid()+": ignoring reject from "+from+" for session with "+id.with); + Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring reject from " + from + " for session with " + id.with); } } @@ -289,7 +306,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } else if (from.asBareJid().equals(id.account.getJid().asBareJid())) { if (transition(State.ACCEPTED)) { - Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": moved session with "+id.with+" into state accepted after received carbon copied procced"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": moved session with " + id.with + " into state accepted after received carbon copied procced"); this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); this.jingleConnectionManager.finishConnection(this); } @@ -381,6 +398,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return RtpEndUserState.CONNECTING; } else if (state == PeerConnection.PeerConnectionState.CLOSED) { return RtpEndUserState.ENDING_CALL; + } else if (state == PeerConnection.PeerConnectionState.FAILED) { + return RtpEndUserState.CONNECTIVITY_ERROR; } else { return RtpEndUserState.ENDING_CALL; } @@ -452,10 +471,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void acceptCallFromProposed() { transitionOrThrow(State.PROCEED); xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); - //Note that Movim needs 'accept', correct is 'proceed' https://github.com/movim/movim/issues/916 + this.sendJingleMessage("accept", id.account.getJid().asBareJid()); this.sendJingleMessage("proceed"); - - //TODO send `accept` to self } private void rejectCallFromProposed() { @@ -466,9 +483,13 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void sendJingleMessage(final String action) { + sendJingleMessage(action, id.with); + } + + private void sendJingleMessage(final String action, final Jid to) { final MessagePacket messagePacket = new MessagePacket(); messagePacket.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those - messagePacket.setTo(id.with); + messagePacket.setTo(to); messagePacket.addChild(action, Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId); Log.d(Config.LOGTAG, messagePacket.toString()); xmppConnectionService.sendMessagePacket(id.account, messagePacket); From 00f273b0c058f8701fe271edb555cdb786d4802d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 8 Apr 2020 13:30:12 +0200 Subject: [PATCH 054/182] show retry button after failed call --- .../conversations/ui/RtpSessionActivity.java | 26 ++++++++++++++---- .../xmpp/jingle/WebRTCWrapper.java | 7 +++++ .../drawable-hdpi/ic_replay_white_48dp.png | Bin 0 -> 675 bytes .../drawable-mdpi/ic_replay_white_48dp.png | Bin 0 -> 457 bytes .../drawable-xhdpi/ic_replay_white_48dp.png | Bin 0 -> 908 bytes .../drawable-xxhdpi/ic_replay_white_48dp.png | Bin 0 -> 1390 bytes .../drawable-xxxhdpi/ic_replay_white_48dp.png | Bin 0 -> 1885 bytes src/main/res/values/strings.xml | 1 + 8 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 src/main/res/drawable-hdpi/ic_replay_white_48dp.png create mode 100644 src/main/res/drawable-mdpi/ic_replay_white_48dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_replay_white_48dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_replay_white_48dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_replay_white_48dp.png diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 1aef98afc..7c4024fe0 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.ui; import android.content.Intent; +import android.content.res.ColorStateList; import android.databinding.DataBindingUtil; import android.os.Bundle; import android.util.Log; @@ -47,9 +48,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe ; Log.d(Config.LOGTAG, "RtpSessionActivity.onCreate()"); this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session); - this.binding.rejectCall.setOnClickListener(this::rejectCall); - this.binding.endCall.setOnClickListener(this::endCall); - this.binding.acceptCall.setOnClickListener(this::acceptCall); } @Override @@ -166,14 +164,20 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe break; case DECLINED_OR_BUSY: binding.status.setText(R.string.rtp_state_declined_or_busy); + case CONNECTIVITY_ERROR: + binding.status.setText(R.string.rtp_state_connectivity_error); break; } } private void updateButtonConfiguration(final RtpEndUserState state) { if (state == RtpEndUserState.INCOMING_CALL) { + this.binding.rejectCall.setOnClickListener(this::rejectCall); + this.binding.rejectCall.setImageResource(R.drawable.ic_call_end_white_48dp); this.binding.rejectCall.show(); this.binding.endCall.hide(); + this.binding.acceptCall.setOnClickListener(this::acceptCall); + this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp); this.binding.acceptCall.show(); } else if (state == RtpEndUserState.ENDING_CALL) { this.binding.rejectCall.hide(); @@ -181,19 +185,31 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.binding.acceptCall.hide(); } else if (state == RtpEndUserState.DECLINED_OR_BUSY) { this.binding.rejectCall.hide(); + this.binding.endCall.setOnClickListener(this::exit); this.binding.endCall.setImageResource(R.drawable.ic_clear_white_48dp); this.binding.endCall.show(); - this.binding.endCall.setOnClickListener(this::exit); this.binding.acceptCall.hide(); + } else if (state == RtpEndUserState.CONNECTIVITY_ERROR) { + this.binding.rejectCall.setOnClickListener(this::exit); + this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp); + this.binding.rejectCall.show(); + this.binding.endCall.hide(); + this.binding.acceptCall.setOnClickListener(this::retry); + this.binding.acceptCall.setImageResource(R.drawable.ic_replay_white_48dp); + this.binding.acceptCall.show(); } else { this.binding.rejectCall.hide(); + this.binding.endCall.setOnClickListener(this::endCall); this.binding.endCall.setImageResource(R.drawable.ic_call_end_white_48dp); this.binding.endCall.show(); - this.binding.endCall.setOnClickListener(this::endCall); this.binding.acceptCall.hide(); } } + private void retry(View view) { + + } + private void exit(View view) { finish(); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index f7d79771e..f8e7947b7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -14,6 +14,7 @@ import org.webrtc.AudioTrack; import org.webrtc.Camera1Capturer; import org.webrtc.Camera1Enumerator; import org.webrtc.CameraVideoCapturer; +import org.webrtc.CandidatePairChangeEvent; import org.webrtc.DataChannel; import org.webrtc.IceCandidate; import org.webrtc.MediaConstraints; @@ -58,6 +59,12 @@ public class WebRTCWrapper { } + @Override + public void onSelectedCandidatePairChanged(CandidatePairChangeEvent event) { + Log.d(Config.LOGTAG, "remote candidate selected: " + event.remote); + Log.d(Config.LOGTAG, "local candidate selected: " + event.local); + } + @Override public void onIceConnectionReceivingChange(boolean b) { diff --git a/src/main/res/drawable-hdpi/ic_replay_white_48dp.png b/src/main/res/drawable-hdpi/ic_replay_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..fcddcf02ddb58ee1680889e7315757d76419b4b2 GIT binary patch literal 675 zcmV;U0$lxxP)i^4O#T$_9m82|5VvpF*wP4OP zM$UZi@^>7mIp;Yu=T~<=&tBi0Ip_J;@t6l6eCkO56W1~dg3&=h1q zQ;-2oK?XDh8PF7DKvR$bO+kjAeDJ~pr|dFMzEwenMqRT?w%I|3a4PK3rz1$XY>?9M z$o%dYv=53msmx64U;8dHjIzKUk9_ChB>7>S;1G`>C)ufEh%>(Om`-|fb*SR^#z4}` zk1=8_ANv5(l+5L85*Xb37G?llB;BT5>A(ui94pbtcshM*TjCYGR_ z-6MNIjB5B%pCT4Cy!N|@y;y<{>^_Y1h{sr_j%UPd_@F#~6P7u_$u$?{J9LdWv<^B( zTt-`dtRg&yta3*15X;emvQ!XfRzVjCmrT?J+75nB>W0d9%nhW=(93#VAeC5$p_bT;=ksFy7&G{R+*d}}W7vaE1LouAaX zWSLB>FM)fQW1AE1DN&FTt0oJ-PWqJo#>PDsS z`vtg-Yj!6pz7!(!0)U7tJ0c*i|=W=`GD{Rhrh*S+VSbLJ}_yb|Y} z2=nyPP+9@g9^_WikSR{kYsy-hy zEku#&*XNV6oCC6@Y0~GT-ej9>Czv`dtH=_DbXwRliC~qaN~fj83SwBz>9x4qN(Rvr ze!Z60m#~`DYlFi_98nBIuPvr=tUC1CrC;Y$3W(^FgzQC`p+k!}pOw6jBq=iPfkh0TWmS)dUz^Bf7(L-mV&-4}J$XOF7sBlc6Scm>hhDc(^Uj{LXd-%JWIiC2AM`rMs#_tFGOcEm> zQ6{J?t)HM6bg;rDU%4Peu#|sCR!~qE~FTC?knj22pV}>5;)E~ds^l}5-05`x5 ia0A=`H^2>0aQz3T@7w=K#~w`p0000BgE0Bosd;&l!5{GB}Nw&aWDv$#(+==h%zxz{0{`dg+v3oNsZEs1O#b7TU=z3 zN+U>6X{oItF;)nPJ2M7_WMc9%Um4!^-ghT8^Ukw#zTbcF!Mpd}bI-XHnCAtLxJ@r7 z*+WWFkh~r@MJBk;QR;*R$?I{j$^*{OAS_5;j{}dp93n0(NM4VF=X6pdEJ$9DgITg9 zR8TMzWb_kr@e}pBNC)$pcHQP(8I2%dRM20TlCA~t5IK}Qkj#7O)Q^3r9eOSQH4a|6K{m=!i&(jnw*l3S} zAo;|Y#mrR|c`AQ73Aj>W`=NK1ad*g$rgcHjacN zsE$?43`9cEp#0VI1I#Q^C$1+=0W+iFH7}gO##wPe=dsZho}dOi%*>M#)$=KDF|$HC z%=VYN*tj4n=n^*Wgxi952pdam7t`|<%h)&=uAn&2u`wd{|1*k>Nn)W2a<^>@q#HZO z!*3+7VHO(;>=dXW7O*kRCla0ZS?o+w8>*fZ|6u2s%rHxW3G57$2u;t&j9}+4Hb_nT zWC$WhETqMGkRZO19H<*2h`taM#pox9o3bOB^}HsC9El2lT!REL$7j+rtL+3cT*2i! z#R$QC8-=m(9KlS|RH2RiIl)|u#=`OrwtD`Z#efJ`%0Sgyv2>j;V3?^)IpJQ zC~$)oVryJ;_A*F;awzhBRG;YTz@uDdxXb~P?psnaT;V0<;<@!m_9(Wo#9CP45&fK^ zm0dKDWCIB{)4(oTIZZ#0Sz#>{I2`q70$a!<#VdblmKEeqBTtav5BJ6{FUU`l0m9~* z`?oM!L%nU}35z-RnM+)dA!%+A1~=V*R#Jl8+0QtYYudtsG%831sUQ`k wf>e+SQb8(6qk>eB3Q|ETNCl}N6{IobUk;_uiK)$-$N&HU07*qoM6N<$g8NE^b^rhX literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_replay_white_48dp.png b/src/main/res/drawable-xxxhdpi/ic_replay_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..04cbde9af1926410c77e666492a8c5407d938f2b GIT binary patch literal 1885 zcmV-j2cr0iP)G0000LhNkl?*|(>Q-}x=BZLQ4&YpE&mJ&MS+o(x;tb+l&fx65xW;&^oP6*;;qZ`277j0&B;g2y13QyVxcB2x=EDkM`q92JwP7LF>&R0l^@ zWU7IqT37j=K`wEM_jsFaJi{v5DanN4NXjtpvz;}}lo=0>dN|9gbkZzyEgbbU#4aA8 zQRXK&((omlnJeIN0diU~(n^z$bb4~{H3$n7c`99eROtxQ(2;K-J9 ztWdGw$ePz^QIX)tno}%Rk>JRh5uQ|$;K-g=Xi#zB$etcrRU9~~)5H{JvygjP&5P`! zk0Gk{A=6bHII5uQP@;q9+0P)AJIgE;2abwqTr_YWuQNbpF3_&xz)>Lu(9CLjC==fW zW~n%ER78=mfZdD{*IA~kIB--zkujGyxI#Srw5m98#HLv2;281r(4gYL5tCw~#8Z4l z99=3B9I>bZ+{!z|u}(#TBL-E7XBj1`5f-UPa73nxaTn)^>JwU2Bsd~c6_`pNQEgYT z;D|(3p_Ri#Q|2KR3yug>B^v1_n$Ngd#eyS9Rii{V(fmV2gCjsSKqH5V;xfNi(cth> zP0&goQ5;b5;P6n5(8hV9SgGQ{F)q~%cQZ=Z2^A5Jaj1rPo+utw5#hKY)fD>(>sK-1 zxB=A^Q}~*&2UScsuA>^`X~KF{R5*S^HAad55K?BIiVDXyRC9DNM#xJlE*!t21h9jU zFPNm_!to0wfVqqj^00~w$Ip}qx(Rtp#fIZYN(2ij<2OK)iVepPln{CeS*fDKffB+R zLb_ydN(e0s;&(s}pO7bHl0ySS_;tx7i39i@lSvXU;5R6fB<>_+woGy; zF^pfQOp@rwZ@o;C=*Dl0Op@4$-(Hy{u@=80GD%`7ekWy;Lqo1 zGD)HtzcHnatN1l3$7sfHOljkL{H7?!Xv1$rX=4z-*~&5I;P;2zgmqK_?+S${7w4vO-xwGeh{5S)d$Y5oP=aXi|2ti;#aRN7zHiPGt#?5Hd!) za)S9>C8SeXLL*-i@`iGPorGLslCp)(gj`{RC89@{O zCW=w!Df`$;G`lG&11PbFXf`Pin9Mn%*-fKTMiYC8=2Kdf53C@n!?Y?z+`_+zrpyD% z4_+gxKH8KLrttw$btzA1;WSa5=WeBdd-$BFKBigu!eT~ zNgVr_qB>zJ?-Ivqp|mlQH7yW3uv%>6|65F}hix8e$Q97$dI#F-`f$EG`gVnO@e=qAJnCIu24M zzVpn~#6UY2sLUWcxSs}9fJrRlO$MpVdFE*1U>0Yo+zyvz!kWGch&c!DmDF+?1L9Az7iGef3Um`x|^ z*}`6qaFUA*Fv1vPjPNZNImr?BvW4|@a+{1X#u#IaF~%5Uj4{R-V~jDz7-NhvCN=*D X>#M$jzV5xv00000NkvXXu0mjfXY*@a literal 0 HcmV?d00001 diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 0a1d6f78c..487ee434b 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -897,6 +897,7 @@ Locating devices Ringing Busy + Unable to connect call View %1$d Participant View %1$d Participants From 859bc0bef397385e866c74d3d4ae8e28a6a21c1c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 8 Apr 2020 15:27:17 +0200 Subject: [PATCH 055/182] send and receive session terminates --- .../conversations/ui/RtpSessionActivity.java | 1 + .../xmpp/jingle/AbstractJingleConnection.java | 1 + .../xmpp/jingle/JingleRtpConnection.java | 105 +++++++++++------- .../xmpp/jingle/WebRTCWrapper.java | 9 +- .../xmpp/jingle/stanzas/JinglePacket.java | 2 +- 5 files changed, 74 insertions(+), 44 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 7c4024fe0..8d46f0591 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -164,6 +164,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe break; case DECLINED_OR_BUSY: binding.status.setText(R.string.rtp_state_declined_or_busy); + break; case CONNECTIVITY_ERROR: binding.status.setText(R.string.rtp_state_connectivity_error); break; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java index 5b60479b8..32a52fe79 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -90,6 +90,7 @@ public abstract class AbstractJingleConnection { REJECTED, RETRACTED, SESSION_INITIALIZED, //equal to 'PENDING' + SESSION_INITIALIZED_PRE_APPROVED, SESSION_ACCEPTED, //equal to 'ACTIVE' TERMINATED_SUCCESS, //equal to 'ENDED' (after successful call) ui will just close TERMINATED_DECLINED_OR_BUSY, //equal to 'ENDED' (after other party declined the call) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 70ad0a37b..85008ef56 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -33,8 +33,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final ImmutableMap.Builder> transitionBuilder = new ImmutableMap.Builder<>(); transitionBuilder.put(State.NULL, ImmutableList.of(State.PROPOSED, State.SESSION_INITIALIZED)); transitionBuilder.put(State.PROPOSED, ImmutableList.of(State.ACCEPTED, State.PROCEED, State.REJECTED, State.RETRACTED)); - transitionBuilder.put(State.PROCEED, ImmutableList.of(State.SESSION_INITIALIZED)); - transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of(State.SESSION_ACCEPTED)); + transitionBuilder.put(State.PROCEED, ImmutableList.of(State.SESSION_INITIALIZED_PRE_APPROVED)); + transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of(State.SESSION_ACCEPTED, State.TERMINATED_CANCEL_OR_TIMEOUT)); + transitionBuilder.put(State.SESSION_INITIALIZED_PRE_APPROVED, ImmutableList.of(State.SESSION_ACCEPTED, State.TERMINATED_CANCEL_OR_TIMEOUT)); + transitionBuilder.put(State.SESSION_ACCEPTED, ImmutableList.of(State.TERMINATED_SUCCESS, State.TERMINATED_CONNECTIVITY_ERROR)); VALID_TRANSITIONS = transitionBuilder.build(); } @@ -73,27 +75,33 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void receiveSessionTerminate(final JinglePacket jinglePacket) { final Reason reason = jinglePacket.getReason(); - switch (reason) { - case SUCCESS: - transitionOrThrow(State.TERMINATED_SUCCESS); - break; - case DECLINE: - case BUSY: - transitionOrThrow(State.TERMINATED_DECLINED_OR_BUSY); - break; - case CANCEL: - case TIMEOUT: - transitionOrThrow(State.TERMINATED_CANCEL_OR_TIMEOUT); - break; - default: - transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR); - break; + final State previous = this.state; + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session terminate reason=" + reason + " while in state " + previous); + webRTCWrapper.close(); + transitionOrThrow(reasonToState(reason)); + if (previous == State.PROPOSED || previous == State.SESSION_INITIALIZED) { + xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); } jingleConnectionManager.finishConnection(this); } + private static State reasonToState(Reason reason) { + switch (reason) { + case SUCCESS: + return State.TERMINATED_SUCCESS; + case DECLINE: + case BUSY: + return State.TERMINATED_DECLINED_OR_BUSY; + case CANCEL: + case TIMEOUT: + return State.TERMINATED_CANCEL_OR_TIMEOUT; + default: + return State.TERMINATED_CONNECTIVITY_ERROR; + } + } + private void receiveTransportInfo(final JinglePacket jinglePacket) { - if (isInState(State.SESSION_INITIALIZED, State.SESSION_ACCEPTED)) { + if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { final RtpContentMap contentMap; try { contentMap = RtpContentMap.of(jinglePacket); @@ -142,10 +150,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return; } Log.d(Config.LOGTAG, "processing session-init with " + contentMap.contents.size() + " contents"); - final State oldState = this.state; - if (transition(State.SESSION_INITIALIZED)) { + final State target; + if (this.state == State.PROCEED) { + target = State.SESSION_INITIALIZED_PRE_APPROVED; + } else { + target = State.SESSION_INITIALIZED; + } + if (transition(target)) { this.initiatorRtpContentMap = contentMap; - if (oldState == State.PROCEED) { + if (target == State.SESSION_INITIALIZED_PRE_APPROVED) { Log.d(Config.LOGTAG, "automatically accepting"); sendSessionAccept(); } else { @@ -254,10 +267,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); this.jingleConnectionManager.finishConnection(this); } else { - Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": unable to transition to accept because already in state="+this.state); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to transition to accept because already in state=" + this.state); } } else { - Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": ignoring 'accept' from "+from); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring 'accept' from " + from); } } @@ -297,7 +310,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (from.equals(id.with)) { if (isInitiator()) { if (transition(State.PROCEED)) { - this.sendSessionInitiate(); + this.sendSessionInitiate(State.SESSION_INITIALIZED_PRE_APPROVED); } else { Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because already in %s", id.account.getJid().asBareJid(), this.state)); } @@ -331,7 +344,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - private void sendSessionInitiate() { + private void sendSessionInitiate(final State targetState) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate"); setupWebRTC(); try { @@ -339,21 +352,31 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); Log.d(Config.LOGTAG, "description: " + webRTCSessionDescription.description); final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); - sendSessionInitiate(rtpContentMap); + sendSessionInitiate(rtpContentMap, targetState); this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); } catch (Exception e) { Log.d(Config.LOGTAG, "unable to sendSessionInitiate", e); } } - private void sendSessionInitiate(RtpContentMap rtpContentMap) { + private void sendSessionInitiate(RtpContentMap rtpContentMap, final State targetState) { this.initiatorRtpContentMap = rtpContentMap; - this.transitionOrThrow(State.SESSION_INITIALIZED); + this.transitionOrThrow(targetState); final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); Log.d(Config.LOGTAG, sessionInitiate.toString()); send(sessionInitiate); } + private void sendSessionTerminate(final Reason reason) { + final State target = reasonToState(reason); + transitionOrThrow(target); + final JinglePacket jinglePacket = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); + jinglePacket.setReason(reason); + send(jinglePacket); + Log.d(Config.LOGTAG, jinglePacket.toString()); + jingleConnectionManager.finishConnection(this); + } + private void sendTransportInfo(final String contentName, IceUdpTransportInfo.Candidate candidate) { final RtpContentMap transportInfo; try { @@ -377,6 +400,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web public RtpEndUserState getEndUserState() { switch (this.state) { case PROPOSED: + case SESSION_INITIALIZED: if (isInitiator()) { return RtpEndUserState.RINGING; } else { @@ -388,7 +412,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } else { return RtpEndUserState.ACCEPTING_CALL; } - case SESSION_INITIALIZED: + case SESSION_INITIALIZED_PRE_APPROVED: return RtpEndUserState.CONNECTING; case SESSION_ACCEPTED: final PeerConnection.PeerConnectionState state = webRTCWrapper.getState(); @@ -446,20 +470,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } public void endCall() { - - //TODO from `propose` we call `retract` - - if (isInState(State.SESSION_INITIALIZED, State.SESSION_ACCEPTED)) { - //TODO during session_initialized we might not have a peer connection yet (if the session was initialized directly) - - //TODO from session_initialized we call `cancel` - - //TODO from session_accepted we call `success` - + if (isInitiator() && isInState(State.SESSION_INITIALIZED)) { webRTCWrapper.close(); + sendSessionTerminate(Reason.CANCEL); + } else if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { + webRTCWrapper.close(); + sendSessionTerminate(Reason.SUCCESS); } else { - //TODO during earlier stages we want to retract the proposal etc - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": called 'endCall' while in state " + this.state); + throw new IllegalStateException("called 'endCall' while in state " + this.state); } } @@ -530,9 +548,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } @Override - public void onConnectionChange(PeerConnection.PeerConnectionState newState) { + public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); updateEndUserState(); + if (newState == PeerConnection.PeerConnectionState.FAILED) { //TODO guard this in isState(initiated,initated_approved,accepted) otherwise it might fire too late + sendSessionTerminate(Reason.CONNECTIVITY_ERROR); + } } private void updateEndUserState() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index f8e7947b7..397ddbbcd 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -207,10 +207,17 @@ public class WebRTCWrapper { this.peerConnection = peerConnection; } - public void close() { + public void closeOrThrow() { requirePeerConnection().close(); } + public void close() { + final PeerConnection peerConnection = this.peerConnection; + if (peerConnection != null) { + peerConnection.close(); + } + } + public ListenableFuture createOffer() { return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java index 25ca3d5fd..90867ed25 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -83,7 +83,7 @@ public class JinglePacket extends IqPacket { public void setReason(final Reason reason) { final Element jingle = findChild("jingle", Namespace.JINGLE); - jingle.addChild(new Element("reason").addChild(reason.toString())); + jingle.addChild("reason").addChild(reason.toString()); } //RECOMMENDED for session-initiate, NOT RECOMMENDED otherwise From ca9b95fc9c6e20eedf09c05ca1351507ff56ff1d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 8 Apr 2020 17:52:47 +0200 Subject: [PATCH 056/182] discover stun server --- .../eu/siacs/conversations/xml/Namespace.java | 1 + .../conversations/xmpp/XmppConnection.java | 4 + .../xmpp/jingle/JingleRtpConnection.java | 140 ++++++++++++------ .../xmpp/jingle/WebRTCWrapper.java | 6 +- 4 files changed, 103 insertions(+), 48 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index a53362168..31b3420dd 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -3,6 +3,7 @@ package eu.siacs.conversations.xml; public final class Namespace { public static final String DISCO_ITEMS = "http://jabber.org/protocol/disco#items"; public static final String DISCO_INFO = "http://jabber.org/protocol/disco#info"; + public static final String EXTERNAL_SERVICE_DISCOVERY = "urn:xmpp:extdisco:2"; public static final String BLOCKING = "urn:xmpp:blocking"; public static final String ROSTER = "jabber:iq:roster"; public static final String REGISTER = "jabber:iq:register"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 7c9374bc8..61a156f69 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1902,5 +1902,9 @@ public class XmppConnection implements Runnable { public boolean bookmarks2() { return Config.USE_BOOKMARKS2 /* || hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT)*/; } + + public boolean extendedServiceDiscovery() { + return hasDiscoFeature(Jid.of(account.getServer()),Namespace.EXTERNAL_SERVICE_DISCOVERY); + } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 85008ef56..da9e99908 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -16,12 +16,15 @@ import java.util.List; import java.util.Map; import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; @@ -51,6 +54,21 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web super(jingleConnectionManager, id, initiator); } + private static State reasonToState(Reason reason) { + switch (reason) { + case SUCCESS: + return State.TERMINATED_SUCCESS; + case DECLINE: + case BUSY: + return State.TERMINATED_DECLINED_OR_BUSY; + case CANCEL: + case TIMEOUT: + return State.TERMINATED_CANCEL_OR_TIMEOUT; + default: + return State.TERMINATED_CONNECTIVITY_ERROR; + } + } + @Override void deliverPacket(final JinglePacket jinglePacket) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": packet delivered to JingleRtpConnection"); @@ -85,21 +103,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web jingleConnectionManager.finishConnection(this); } - private static State reasonToState(Reason reason) { - switch (reason) { - case SUCCESS: - return State.TERMINATED_SUCCESS; - case DECLINE: - case BUSY: - return State.TERMINATED_DECLINED_OR_BUSY; - case CANCEL: - case TIMEOUT: - return State.TERMINATED_CANCEL_OR_TIMEOUT; - default: - return State.TERMINATED_CONNECTIVITY_ERROR; - } - } - private void receiveTransportInfo(final JinglePacket jinglePacket) { if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { final RtpContentMap contentMap; @@ -211,22 +214,24 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (rtpContentMap == null) { throw new IllegalStateException("initiator RTP Content Map has not been set"); } - setupWebRTC(); - final org.webrtc.SessionDescription offer = new org.webrtc.SessionDescription( - org.webrtc.SessionDescription.Type.OFFER, - SessionDescription.of(rtpContentMap).toString() - ); - try { - this.webRTCWrapper.setRemoteDescription(offer).get(); - org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createAnswer().get(); - final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); - final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); - sendSessionAccept(respondingRtpContentMap); - this.webRTCWrapper.setLocalDescription(webRTCSessionDescription); - } catch (Exception e) { - Log.d(Config.LOGTAG, "unable to send session accept", e); + discoverIceServers(iceServers -> { + setupWebRTC(iceServers); + final org.webrtc.SessionDescription offer = new org.webrtc.SessionDescription( + org.webrtc.SessionDescription.Type.OFFER, + SessionDescription.of(rtpContentMap).toString() + ); + try { + this.webRTCWrapper.setRemoteDescription(offer).get(); + org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createAnswer().get(); + final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); + final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); + sendSessionAccept(respondingRtpContentMap); + this.webRTCWrapper.setLocalDescription(webRTCSessionDescription); + } catch (Exception e) { + Log.d(Config.LOGTAG, "unable to send session accept", e); - } + } + }); } private void sendSessionAccept(final RtpContentMap rtpContentMap) { @@ -346,17 +351,19 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void sendSessionInitiate(final State targetState) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate"); - setupWebRTC(); - try { - org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get(); - final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); - Log.d(Config.LOGTAG, "description: " + webRTCSessionDescription.description); - final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); - sendSessionInitiate(rtpContentMap, targetState); - this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); - } catch (Exception e) { - Log.d(Config.LOGTAG, "unable to sendSessionInitiate", e); - } + discoverIceServers(iceServers -> { + setupWebRTC(iceServers); + try { + org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get(); + final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); + Log.d(Config.LOGTAG, "description: " + webRTCSessionDescription.description); + final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); + sendSessionInitiate(rtpContentMap, targetState); + this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); + } catch (Exception e) { + Log.d(Config.LOGTAG, "unable to sendSessionInitiate", e); + } + }); } private void sendSessionInitiate(RtpContentMap rtpContentMap, final State targetState) { @@ -481,9 +488,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - private void setupWebRTC() { + private void setupWebRTC(final List iceServers) { this.webRTCWrapper.setup(this.xmppConnectionService); - this.webRTCWrapper.initializePeerConnection(); + this.webRTCWrapper.initializePeerConnection(iceServers); } private void acceptCallFromProposed() { @@ -559,4 +566,51 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void updateEndUserState() { xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, getEndUserState()); } + + private void discoverIceServers(final OnIceServersDiscovered onIceServersDiscovered) { + if (id.account.getXmppConnection().getFeatures().extendedServiceDiscovery()) { + final IqPacket request = new IqPacket(IqPacket.TYPE.GET); + request.setTo(Jid.of(id.account.getJid().getDomain())); + request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); + xmppConnectionService.sendIqPacket(id.account, request, (account, response) -> { + ImmutableList.Builder listBuilder = new ImmutableList.Builder<>(); + if (response.getType() == IqPacket.TYPE.RESULT) { + final Element services = response.findChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); + final List children = services == null ? Collections.emptyList() : services.getChildren(); + for (final Element child : children) { + if ("service".equals(child.getName())) { + final String type = child.getAttribute("type"); + final String host = child.getAttribute("host"); + final String port = child.getAttribute("port"); + final String transport = child.getAttribute("transport"); + final String username = child.getAttribute("username"); + final String password = child.getAttribute("password"); + if (Arrays.asList("stun", "type").contains(type) && host != null && port != null && "udp".equals(transport)) { + PeerConnection.IceServer.Builder iceServerBuilder = PeerConnection.IceServer.builder(String.format("%s:%s:%s", type, host, port)); + if (username != null && password != null) { + iceServerBuilder.setUsername(username); + iceServerBuilder.setPassword(password); + } + final PeerConnection.IceServer iceServer = iceServerBuilder.createIceServer(); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": discovered ICE Server: " + iceServer); + listBuilder.add(iceServer); + } + } + } + } + List iceServers = listBuilder.build(); + if (iceServers.size() == 0) { + Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no ICE server found " + response); + } + onIceServersDiscovered.onIceServersDiscovered(iceServers); + }); + } else { + Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": has no external service discovery"); + onIceServersDiscovered.onIceServersDiscovered(Collections.emptyList()); + } + } + + private interface OnIceServersDiscovered { + void onIceServersDiscovered(List iceServers); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 397ddbbcd..1688a8ecb 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -132,7 +132,7 @@ public class WebRTCWrapper { ); } - public void initializePeerConnection() { + public void initializePeerConnection(final List iceServers) { PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory(); CameraVideoCapturer capturer = null; @@ -193,10 +193,6 @@ public class WebRTCWrapper { this.localVideoTrack = videoTrack; - - final List iceServers = ImmutableList.of( - PeerConnection.IceServer.builder("stun:xmpp.conversations.im:3478").createIceServer() - ); final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(iceServers, peerConnectionObserver); if (peerConnection == null) { throw new IllegalStateException("Unable to create PeerConnection"); From 0bf991d95c48adabd4084a0aa110cb1f81996099 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 9 Apr 2020 07:38:12 +0200 Subject: [PATCH 057/182] make jingle->sdp parsing fail on some obvious errors --- .../xmpp/jingle/JingleRtpConnection.java | 18 ++++- .../xmpp/jingle/SessionDescription.java | 73 +++++++++++++++++-- .../xmpp/jingle/stanzas/RtpDescription.java | 43 +++++++++++ 3 files changed, 125 insertions(+), 9 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index da9e99908..546c35f48 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -214,14 +214,26 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (rtpContentMap == null) { throw new IllegalStateException("initiator RTP Content Map has not been set"); } + final SessionDescription offer; + try { + offer = SessionDescription.of(rtpContentMap); + } catch (final IllegalArgumentException e) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to process offer", e); + //TODO terminate session with application error + return; + } + sendSessionAccept(offer); + } + + private void sendSessionAccept(SessionDescription offer) { discoverIceServers(iceServers -> { setupWebRTC(iceServers); - final org.webrtc.SessionDescription offer = new org.webrtc.SessionDescription( + final org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription( org.webrtc.SessionDescription.Type.OFFER, - SessionDescription.of(rtpContentMap).toString() + offer.toString() ); try { - this.webRTCWrapper.setRemoteDescription(offer).get(); + this.webRTCWrapper.setRemoteDescription(sdp).get(); org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createAnswer().get(); final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index c205ab0d8..587fb513c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -158,32 +158,80 @@ public class SessionDescription { } final ImmutableList.Builder formatBuilder = new ImmutableList.Builder<>(); for (RtpDescription.PayloadType payloadType : description.getPayloadTypes()) { + final String id = payloadType.getId(); + if (Strings.isNullOrEmpty(id)) { + throw new IllegalArgumentException("Payload type is missing id"); + } + if (!isInt(id)) { + throw new IllegalArgumentException("Payload id is not numeric"); + } formatBuilder.add(payloadType.getIntId()); mediaAttributes.put("rtpmap", payloadType.toSdpAttribute()); List parameters = payloadType.getParameters(); if (parameters.size() > 0) { - mediaAttributes.put("fmtp", RtpDescription.Parameter.toSdpString(payloadType.getId(), parameters)); + mediaAttributes.put("fmtp", RtpDescription.Parameter.toSdpString(id, parameters)); } for (RtpDescription.FeedbackNegotiation feedbackNegotiation : payloadType.getFeedbackNegotiations()) { - mediaAttributes.put("rtcp-fb", payloadType.getId() + " " + feedbackNegotiation.getType() + (Strings.isNullOrEmpty(feedbackNegotiation.getSubType()) ? "" : " " + feedbackNegotiation.getSubType())); + final String type = feedbackNegotiation.getType(); + final String subtype = feedbackNegotiation.getSubType(); + if (Strings.isNullOrEmpty(type)) { + throw new IllegalArgumentException("a feedback for payload-type " + id + " negotiation is missing type"); + } + mediaAttributes.put("rtcp-fb", id + " " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); } for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : payloadType.feedbackNegotiationTrrInts()) { - mediaAttributes.put("rtcp-fb", payloadType.getId() + " trr-int " + feedbackNegotiationTrrInt.getValue()); + mediaAttributes.put("rtcp-fb", id + " trr-int " + feedbackNegotiationTrrInt.getValue()); } } for (RtpDescription.FeedbackNegotiation feedbackNegotiation : description.getFeedbackNegotiations()) { - mediaAttributes.put("rtcp-fb", "* " + feedbackNegotiation.getType() + (Strings.isNullOrEmpty(feedbackNegotiation.getSubType()) ? "" : " " + feedbackNegotiation.getSubType())); + final String type = feedbackNegotiation.getType(); + final String subtype = feedbackNegotiation.getSubType(); + if (Strings.isNullOrEmpty(type)) { + throw new IllegalArgumentException("a feedback negotiation is missing type"); + } + mediaAttributes.put("rtcp-fb", "* " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); } for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : description.feedbackNegotiationTrrInts()) { mediaAttributes.put("rtcp-fb", "* trr-int " + feedbackNegotiationTrrInt.getValue()); } for (RtpDescription.RtpHeaderExtension extension : description.getHeaderExtensions()) { - mediaAttributes.put("extmap", extension.getId() + " " + extension.getUri()); + final String id = extension.getId(); + final String uri = extension.getUri(); + if (Strings.isNullOrEmpty(id)) { + throw new IllegalArgumentException("A header extension is missing id"); + } + if (Strings.isNullOrEmpty(uri)) { + throw new IllegalArgumentException("A header extension is missing uri"); + } + mediaAttributes.put("extmap", id + " " + uri); + } + for (RtpDescription.SourceGroup sourceGroup : description.getSourceGroups()) { + final String semantics = sourceGroup.getSemantics(); + final List groups = sourceGroup.getSsrcs(); + if (Strings.isNullOrEmpty(semantics)) { + throw new IllegalArgumentException("A SSRC group is missing semantics attribute"); + } + if (groups.size() == 0) { + throw new IllegalArgumentException("A SSRC group is missing SSRC ids"); + } + mediaAttributes.put("ssrc-group", String.format("%s %s", semantics, Joiner.on(' ').join(groups))); } for (RtpDescription.Source source : description.getSources()) { for (RtpDescription.Source.Parameter parameter : source.getParameters()) { - mediaAttributes.put("ssrc", source.getSsrcId() + " " + parameter.getParameterName() + ":" + parameter.getParameterValue()); + final String id = source.getSsrcId(); + final String parameterName = parameter.getParameterName(); + final String parameterValue = parameter.getParameterValue(); + if (Strings.isNullOrEmpty(id)) { + throw new IllegalArgumentException("A source specific media attribute is missing the id"); + } + if (Strings.isNullOrEmpty(parameterName)) { + throw new IllegalArgumentException("A source specific media attribute is missing its name"); + } + if (Strings.isNullOrEmpty(parameterValue)) { + throw new IllegalArgumentException("A source specific media attribute is missing its value"); + } + mediaAttributes.put("ssrc", id + " " + parameter.getParameterName() + ":" + parameter.getParameterValue()); } } @@ -220,6 +268,18 @@ public class SessionDescription { } } + public static boolean isInt(final String input) { + if (input == null) { + return false; + } + try { + Integer.parseInt(input); + return true; + } catch (NumberFormatException e) { + return false; + } + } + public static Pair parseAttribute(final String input) { final String[] pair = input.split(":", 2); if (pair.length == 2) { @@ -233,6 +293,7 @@ public class SessionDescription { public String toString() { final StringBuilder s = new StringBuilder() .append("v=").append(version).append(LINE_DIVIDER) + //TODO randomize or static .append("o=- 8770656990916039506 2 IN IP4 127.0.0.1").append(LINE_DIVIDER) //what ever that means .append("s=").append(name).append(LINE_DIVIDER) .append("t=0 0").append(LINE_DIVIDER); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index 53621a6d8..59881b089 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -70,6 +70,16 @@ public class RtpDescription extends GenericDescription { return builder.build(); } + public List getSourceGroups() { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for (final Element child : this.children) { + if ("ssrc-group".equals(child.getName()) && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(child.getNamespace())) { + builder.add(SourceGroup.upgrade(child)); + } + } + return builder.build(); + } + public static RtpDescription upgrade(final Element element) { Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description"); Preconditions.checkArgument(Namespace.JINGLE_APPS_RTP.equals(element.getNamespace()), "Element does not match the jingle rtp namespace"); @@ -458,6 +468,39 @@ public class RtpDescription extends GenericDescription { } + public static class SourceGroup extends Element { + + private SourceGroup() { + super("ssrc-group", Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES); + } + + public String getSemantics() { + return this.getAttribute("semantics"); + } + + public List getSsrcs() { + ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for(Element child : this.children) { + if ("source".equals(child.getName())) { + final String ssrc = child.getAttribute("ssrc"); + if (ssrc != null) { + builder.add(ssrc); + } + } + } + return builder.build(); + } + + public static SourceGroup upgrade(final Element element) { + Preconditions.checkArgument("ssrc-group".equals(element.getName())); + Preconditions.checkArgument(Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(element.getNamespace())); + final SourceGroup group = new SourceGroup(); + group.setChildren(element.getChildren()); + group.setAttributes(element.getAttributes()); + return group; + } + } + public enum Media { VIDEO, AUDIO, UNKNOWN; From 3e5e4e813b77d7f4861aa159b293eef22a2d2fc4 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 9 Apr 2020 09:04:59 +0200 Subject: [PATCH 058/182] reject call from proceed state; and deal with direct inits --- .../xmpp/jingle/JingleRtpConnection.java | 51 +++++++++++++------ 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 546c35f48..0f51d0e13 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -16,10 +16,8 @@ import java.util.List; import java.util.Map; import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; @@ -36,9 +34,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final ImmutableMap.Builder> transitionBuilder = new ImmutableMap.Builder<>(); transitionBuilder.put(State.NULL, ImmutableList.of(State.PROPOSED, State.SESSION_INITIALIZED)); transitionBuilder.put(State.PROPOSED, ImmutableList.of(State.ACCEPTED, State.PROCEED, State.REJECTED, State.RETRACTED)); - transitionBuilder.put(State.PROCEED, ImmutableList.of(State.SESSION_INITIALIZED_PRE_APPROVED)); - transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of(State.SESSION_ACCEPTED, State.TERMINATED_CANCEL_OR_TIMEOUT)); - transitionBuilder.put(State.SESSION_INITIALIZED_PRE_APPROVED, ImmutableList.of(State.SESSION_ACCEPTED, State.TERMINATED_CANCEL_OR_TIMEOUT)); + transitionBuilder.put(State.PROCEED, ImmutableList.of(State.SESSION_INITIALIZED_PRE_APPROVED, State.TERMINATED_SUCCESS)); + transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of(State.SESSION_ACCEPTED, State.TERMINATED_CANCEL_OR_TIMEOUT, State.TERMINATED_DECLINED_OR_BUSY)); + transitionBuilder.put(State.SESSION_INITIALIZED_PRE_APPROVED, ImmutableList.of(State.SESSION_ACCEPTED, State.TERMINATED_CANCEL_OR_TIMEOUT, State.TERMINATED_DECLINED_OR_BUSY)); transitionBuilder.put(State.SESSION_ACCEPTED, ImmutableList.of(State.TERMINATED_SUCCESS, State.TERMINATED_CONNECTIVITY_ERROR)); VALID_TRANSITIONS = transitionBuilder.build(); } @@ -130,6 +128,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.webRTCWrapper.addIceCandidate(iceCandidate); } else { this.pendingIceCandidates.push(iceCandidate); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": put ICE candidate on backlog"); } } } @@ -162,11 +161,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (transition(target)) { this.initiatorRtpContentMap = contentMap; if (target == State.SESSION_INITIALIZED_PRE_APPROVED) { - Log.d(Config.LOGTAG, "automatically accepting"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate"); sendSessionAccept(); } else { - Log.d(Config.LOGTAG, "start ringing"); - //TODO start ringing + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received not pre-approved session-initiate. start ringing"); + startRinging(); } } else { Log.d(Config.LOGTAG, String.format("%s: received session-initiate while in state %s", id.account.getJid().asBareJid(), state)); @@ -427,12 +426,16 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } case PROCEED: if (isInitiator()) { - return RtpEndUserState.CONNECTING; + return RtpEndUserState.RINGING; } else { return RtpEndUserState.ACCEPTING_CALL; } case SESSION_INITIALIZED_PRE_APPROVED: - return RtpEndUserState.CONNECTING; + if (isInitiator()) { + return RtpEndUserState.RINGING; + } else { + return RtpEndUserState.CONNECTING; + } case SESSION_ACCEPTED: final PeerConnection.PeerConnectionState state = webRTCWrapper.getState(); if (state == PeerConnection.PeerConnectionState.CONNECTED) { @@ -483,21 +486,33 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web case PROPOSED: rejectCallFromProposed(); break; + case SESSION_INITIALIZED: + rejectCallFromSessionInitiate(); + break; default: throw new IllegalStateException("Can not reject call from " + this.state); } } public void endCall() { - if (isInitiator() && isInState(State.SESSION_INITIALIZED)) { + if (isInState(State.PROCEED)) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ending call while in state PROCEED just means ending the connection"); + webRTCWrapper.close(); + jingleConnectionManager.finishConnection(this); + transitionOrThrow(State.TERMINATED_SUCCESS); //arguably this wasn't success; but not a real failure either + return; + } + if (isInitiator() && isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED)) { webRTCWrapper.close(); sendSessionTerminate(Reason.CANCEL); - } else if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { + return; + } + if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { webRTCWrapper.close(); sendSessionTerminate(Reason.SUCCESS); - } else { - throw new IllegalStateException("called 'endCall' while in state " + this.state); + return; } + throw new IllegalStateException("called 'endCall' while in state " + this.state); } private void setupWebRTC(final List iceServers) { @@ -519,6 +534,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web jingleConnectionManager.finishConnection(this); } + private void rejectCallFromSessionInitiate() { + webRTCWrapper.close(); + sendSessionTerminate(Reason.DECLINE); + xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); + } + private void sendJingleMessage(final String action) { sendJingleMessage(action, id.with); } @@ -534,7 +555,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void acceptCallFromSessionInitialized() { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); - throw new IllegalStateException("accepting from this state has not been implemented yet"); + sendSessionAccept(); } private synchronized boolean isInState(State... state) { From 845b3d8a0eab6a3cdb70e5a0b0cf3c6988d2b3ba Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 9 Apr 2020 09:29:07 +0200 Subject: [PATCH 059/182] properly parse transport info and apply ice candidates after direct init --- .../xmpp/jingle/JingleRtpConnection.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 0f51d0e13..4b76b8991 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -110,7 +110,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); return; } - final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; + final RtpContentMap rtpContentMap = isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap; final Group originalGroup = rtpContentMap != null ? rtpContentMap.group : null; final List identificationTags = originalGroup == null ? Collections.emptyList() : originalGroup.getIdentificationTags(); if (identificationTags.size() == 0) { @@ -123,11 +123,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final String sdpMid = content.getKey(); final int mLineIndex = identificationTags.indexOf(sdpMid); final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp); - Log.d(Config.LOGTAG, "received candidate: " + iceCandidate); if (isInState(State.SESSION_ACCEPTED)) { + Log.d(Config.LOGTAG, "received candidate: " + iceCandidate); this.webRTCWrapper.addIceCandidate(iceCandidate); } else { - this.pendingIceCandidates.push(iceCandidate); + this.pendingIceCandidates.offer(iceCandidate); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": put ICE candidate on backlog"); } } @@ -233,6 +233,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web ); try { this.webRTCWrapper.setRemoteDescription(sdp).get(); + addIceCandidatesFromBlackLog(); org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createAnswer().get(); final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); @@ -245,6 +246,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web }); } + private void addIceCandidatesFromBlackLog() { + while (!this.pendingIceCandidates.isEmpty()) { + final IceCandidate iceCandidate = this.pendingIceCandidates.poll(); + this.webRTCWrapper.addIceCandidate(iceCandidate); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added ICE candidate from back log " + iceCandidate); + } + } + private void sendSessionAccept(final RtpContentMap rtpContentMap) { this.responderRtpContentMap = rtpContentMap; this.transitionOrThrow(State.SESSION_ACCEPTED); From 15a2491d7bf6e23f6471388c3de68c792482e12e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 9 Apr 2020 13:27:13 +0200 Subject: [PATCH 060/182] correctly parse turn server --- .../conversations/ui/RtpSessionActivity.java | 1 + .../xmpp/jingle/JingleRtpConnection.java | 23 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 8d46f0591..7eb4908fb 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -85,6 +85,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override public void onNewIntent(final Intent intent) { super.onNewIntent(intent); + //TODO reinitialize if (ACTION_ACCEPT_CALL.equals(intent.getAction())) { Log.d(Config.LOGTAG, "accepting through onNewIntent()"); requireRtpConnection().acceptCall(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 4b76b8991..bc32c3838 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -2,8 +2,10 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Log; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.Ints; import org.webrtc.IceCandidate; import org.webrtc.PeerConnection; @@ -600,7 +602,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); updateEndUserState(); - if (newState == PeerConnection.PeerConnectionState.FAILED) { //TODO guard this in isState(initiated,initated_approved,accepted) otherwise it might fire too late + if (newState == PeerConnection.PeerConnectionState.FAILED) { //TODO guard this in isState(initiated,initiated_approved,accepted) otherwise it might fire too late sendSessionTerminate(Reason.CONNECTIVITY_ERROR); } } @@ -623,15 +625,28 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if ("service".equals(child.getName())) { final String type = child.getAttribute("type"); final String host = child.getAttribute("host"); - final String port = child.getAttribute("port"); + final String sport = child.getAttribute("port"); + final Integer port = sport == null ? null : Ints.tryParse(sport); final String transport = child.getAttribute("transport"); final String username = child.getAttribute("username"); final String password = child.getAttribute("password"); - if (Arrays.asList("stun", "type").contains(type) && host != null && port != null && "udp".equals(transport)) { - PeerConnection.IceServer.Builder iceServerBuilder = PeerConnection.IceServer.builder(String.format("%s:%s:%s", type, host, port)); + if (Strings.isNullOrEmpty(host) || port == null) { + continue; + } + if (port < 0 || port > 65535) { + continue; + } + if (Arrays.asList("stun", "turn").contains(type) || Arrays.asList("udp", "tcp").contains(transport)) { + //TODO wrap ipv6 addresses + PeerConnection.IceServer.Builder iceServerBuilder = PeerConnection.IceServer.builder(String.format("%s:%s:%s?transport=%s", type, host, port, transport)); if (username != null && password != null) { iceServerBuilder.setUsername(username); iceServerBuilder.setPassword(password); + } else if (Arrays.asList("turn", "turns").contains(type)) { + //The WebRTC spec requires throwing an InvalidAccessError when username (from libwebrtc source coder) + //https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": skipping " + type + "/" + transport + " without username and password"); + continue; } final PeerConnection.IceServer iceServer = iceServerBuilder.createIceServer(); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": discovered ICE Server: " + iceServer); From 268eedad8902dcbdab391395fe8d8d2d445a4ea9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 9 Apr 2020 15:22:03 +0200 Subject: [PATCH 061/182] proper iq tracing (handling of errors); responding to all iqs --- .../conversations/ui/RtpSessionActivity.java | 5 +- .../xmpp/jingle/AbstractJingleConnection.java | 3 +- .../xmpp/jingle/JingleConnectionManager.java | 19 ++- .../xmpp/jingle/JingleRtpConnection.java | 154 +++++++++++++++--- .../xmpp/jingle/RtpEndUserState.java | 4 +- .../xmpp/jingle/WebRTCWrapper.java | 11 +- .../xmpp/jingle/stanzas/Reason.java | 2 +- .../AbstractAcknowledgeableStanza.java | 12 ++ src/main/res/values/strings.xml | 1 + 9 files changed, 174 insertions(+), 37 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 7eb4908fb..232ceb8b2 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -169,6 +169,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe case CONNECTIVITY_ERROR: binding.status.setText(R.string.rtp_state_connectivity_error); break; + case APPLICATION_ERROR: + binding.status.setText(R.string.rtp_state_application_failure); + break; } } @@ -191,7 +194,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.binding.endCall.setImageResource(R.drawable.ic_clear_white_48dp); this.binding.endCall.show(); this.binding.acceptCall.hide(); - } else if (state == RtpEndUserState.CONNECTIVITY_ERROR) { + } else if (state == RtpEndUserState.CONNECTIVITY_ERROR || state == RtpEndUserState.APPLICATION_ERROR) { this.binding.rejectCall.setOnClickListener(this::exit); this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp); this.binding.rejectCall.show(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java index 32a52fe79..6cf7d2a91 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -95,6 +95,7 @@ public abstract class AbstractJingleConnection { TERMINATED_SUCCESS, //equal to 'ENDED' (after successful call) ui will just close TERMINATED_DECLINED_OR_BUSY, //equal to 'ENDED' (after other party declined the call) TERMINATED_CONNECTIVITY_ERROR, //equal to 'ENDED' (but after network failures; ui will display retry button) - TERMINATED_CANCEL_OR_TIMEOUT //more or less the same as retracted; caller pressed end call before session was accepted + TERMINATED_CANCEL_OR_TIMEOUT, //more or less the same as retracted; caller pressed end call before session was accepted + TERMINATED_APPLICATION_FAILURE } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 5e2e59b29..88ce13d6e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -55,22 +55,27 @@ public class JingleConnectionManager extends AbstractConnectionManager { } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace)) { connection = new JingleRtpConnection(this, id, from); } else { - //TODO return feature-not-implemented + respondWithJingleError(account, packet, "unsupported-info", "feature-not-implemented", "cancel"); return; } connections.put(id, connection); connection.deliverPacket(packet); } else { Log.d(Config.LOGTAG, "unable to route jingle packet: " + packet); - final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); - final Element error = response.addChild("error"); - error.setAttribute("type", "cancel"); - error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas"); - error.addChild("unknown-session", "urn:xmpp:jingle:errors:1"); - account.getXmppConnection().sendIqPacket(response, null); + respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel"); + } } + public void respondWithJingleError(final Account account, final IqPacket original, String jingleCondition, String condition, String conditionType) { + final IqPacket response = original.generateResponse(IqPacket.TYPE.ERROR); + final Element error = response.addChild("error"); + error.setAttribute("type", conditionType); + error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas"); + error.addChild(jingleCondition, "urn:xmpp:jingle:errors:1"); + account.getXmppConnection().sendIqPacket(response, null); + } + public void deliverMessage(final Account account, final Jid to, final Jid from, final Element message) { Preconditions.checkArgument(Namespace.JINGLE_MESSAGE.equals(message.getNamespace())); final String sessionId = message.getAttribute("id"); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index bc32c3838..153b0172f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -34,12 +34,40 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web static { final ImmutableMap.Builder> transitionBuilder = new ImmutableMap.Builder<>(); - transitionBuilder.put(State.NULL, ImmutableList.of(State.PROPOSED, State.SESSION_INITIALIZED)); - transitionBuilder.put(State.PROPOSED, ImmutableList.of(State.ACCEPTED, State.PROCEED, State.REJECTED, State.RETRACTED)); - transitionBuilder.put(State.PROCEED, ImmutableList.of(State.SESSION_INITIALIZED_PRE_APPROVED, State.TERMINATED_SUCCESS)); - transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of(State.SESSION_ACCEPTED, State.TERMINATED_CANCEL_OR_TIMEOUT, State.TERMINATED_DECLINED_OR_BUSY)); - transitionBuilder.put(State.SESSION_INITIALIZED_PRE_APPROVED, ImmutableList.of(State.SESSION_ACCEPTED, State.TERMINATED_CANCEL_OR_TIMEOUT, State.TERMINATED_DECLINED_OR_BUSY)); - transitionBuilder.put(State.SESSION_ACCEPTED, ImmutableList.of(State.TERMINATED_SUCCESS, State.TERMINATED_CONNECTIVITY_ERROR)); + transitionBuilder.put(State.NULL, ImmutableList.of( + State.PROPOSED, + State.SESSION_INITIALIZED, + State.TERMINATED_APPLICATION_FAILURE + )); + transitionBuilder.put(State.PROPOSED, ImmutableList.of( + State.ACCEPTED, + State.PROCEED, + State.REJECTED, + State.RETRACTED, + State.TERMINATED_APPLICATION_FAILURE + )); + transitionBuilder.put(State.PROCEED, ImmutableList.of( + State.SESSION_INITIALIZED_PRE_APPROVED, + State.TERMINATED_SUCCESS, + State.TERMINATED_APPLICATION_FAILURE + )); + transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of( + State.SESSION_ACCEPTED, + State.TERMINATED_CANCEL_OR_TIMEOUT, + State.TERMINATED_DECLINED_OR_BUSY, + State.TERMINATED_APPLICATION_FAILURE + )); + transitionBuilder.put(State.SESSION_INITIALIZED_PRE_APPROVED, ImmutableList.of( + State.SESSION_ACCEPTED, + State.TERMINATED_CANCEL_OR_TIMEOUT, + State.TERMINATED_DECLINED_OR_BUSY, + State.TERMINATED_APPLICATION_FAILURE + )); + transitionBuilder.put(State.SESSION_ACCEPTED, ImmutableList.of( + State.TERMINATED_SUCCESS, + State.TERMINATED_CONNECTIVITY_ERROR, + State.TERMINATED_APPLICATION_FAILURE + )); VALID_TRANSITIONS = transitionBuilder.build(); } @@ -64,6 +92,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web case CANCEL: case TIMEOUT: return State.TERMINATED_CANCEL_OR_TIMEOUT; + case FAILED_APPLICATION: + return State.TERMINATED_APPLICATION_FAILURE; default: return State.TERMINATED_CONNECTIVITY_ERROR; } @@ -86,12 +116,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web receiveSessionTerminate(jinglePacket); break; default: + respondOk(jinglePacket); Log.d(Config.LOGTAG, String.format("%s: received unhandled jingle action %s", id.account.getJid().asBareJid(), jinglePacket.getAction())); break; } } private void receiveSessionTerminate(final JinglePacket jinglePacket) { + respondOk(jinglePacket); final Reason reason = jinglePacket.getReason(); final State previous = this.state; Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session terminate reason=" + reason + " while in state " + previous); @@ -105,11 +137,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void receiveTransportInfo(final JinglePacket jinglePacket) { if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { + respondOk(jinglePacket); final RtpContentMap contentMap; try { contentMap = RtpContentMap.of(jinglePacket); } catch (IllegalArgumentException | NullPointerException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents; ignoring", e); return; } final RtpContentMap rtpContentMap = isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap; @@ -136,13 +169,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } else { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received transport info while in state=" + this.state); + respondWithOutOfOrder(jinglePacket); } } private void receiveSessionInitiate(final JinglePacket jinglePacket) { if (isInitiator()) { Log.d(Config.LOGTAG, String.format("%s: received session-initiate even though we were initiating", id.account.getJid().asBareJid())); - //TODO respond with out-of-order + respondWithOutOfOrder(jinglePacket); return; } final RtpContentMap contentMap; @@ -150,6 +184,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web contentMap = RtpContentMap.of(jinglePacket); contentMap.requireContentDescriptions(); } catch (IllegalArgumentException | IllegalStateException | NullPointerException e) { + respondOk(jinglePacket); + sendSessionTerminate(Reason.FAILED_APPLICATION); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); return; } @@ -161,6 +197,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web target = State.SESSION_INITIALIZED; } if (transition(target)) { + respondOk(jinglePacket); this.initiatorRtpContentMap = contentMap; if (target == State.SESSION_INITIALIZED_PRE_APPROVED) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate"); @@ -171,13 +208,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } else { Log.d(Config.LOGTAG, String.format("%s: received session-initiate while in state %s", id.account.getJid().asBareJid(), state)); + respondWithOutOfOrder(jinglePacket); } } private void receiveSessionAccept(final JinglePacket jinglePacket) { if (!isInitiator()) { Log.d(Config.LOGTAG, String.format("%s: received session-accept even though we were responding", id.account.getJid().asBareJid())); - //TODO respond with out-of-order + respondWithOutOfOrder(jinglePacket); return; } final RtpContentMap contentMap; @@ -185,28 +223,43 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web contentMap = RtpContentMap.of(jinglePacket); contentMap.requireContentDescriptions(); } catch (IllegalArgumentException | IllegalStateException | NullPointerException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); + respondOk(jinglePacket); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents in session-accept", e); + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION); return; } Log.d(Config.LOGTAG, "processing session-accept with " + contentMap.contents.size() + " contents"); if (transition(State.SESSION_ACCEPTED)) { + respondOk(jinglePacket); receiveSessionAccept(contentMap); } else { Log.d(Config.LOGTAG, String.format("%s: received session-accept while in state %s", id.account.getJid().asBareJid(), state)); - //TODO out-of-order + respondOk(jinglePacket); } } private void receiveSessionAccept(final RtpContentMap contentMap) { this.responderRtpContentMap = contentMap; + final SessionDescription sessionDescription; + try { + sessionDescription = SessionDescription.of(contentMap); + } catch (IllegalArgumentException e) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable convert offer from session-accept to SDP", e); + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION); + return; + } org.webrtc.SessionDescription answer = new org.webrtc.SessionDescription( org.webrtc.SessionDescription.Type.ANSWER, - SessionDescription.of(contentMap).toString() + sessionDescription.toString() ); try { this.webRTCWrapper.setRemoteDescription(answer).get(); } catch (Exception e) { - Log.d(Config.LOGTAG, "unable to receive session accept", e); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", e); + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION); } } @@ -219,8 +272,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { offer = SessionDescription.of(rtpContentMap); } catch (final IllegalArgumentException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to process offer", e); - //TODO terminate session with application error + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable convert offer from session-initiate to SDP", e); + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION); + ; return; } sendSessionAccept(offer); @@ -228,7 +283,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void sendSessionAccept(SessionDescription offer) { discoverIceServers(iceServers -> { - setupWebRTC(iceServers); + try { + setupWebRTC(iceServers); + } catch (WebRTCWrapper.InitializationException e) { + sendSessionTerminate(Reason.FAILED_APPLICATION); + return; + } final org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription( org.webrtc.SessionDescription.Type.OFFER, offer.toString() @@ -351,7 +411,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.jingleConnectionManager.finishConnection(this); } } else { - //TODO a carbon copied proceed from another client of mine has the same logic as `accept` Log.d(Config.LOGTAG, String.format("%s: ignoring proceed from %s. was expected from %s", id.account.getJid().asBareJid(), from, id.with)); } } @@ -374,7 +433,13 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void sendSessionInitiate(final State targetState) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate"); discoverIceServers(iceServers -> { - setupWebRTC(iceServers); + try { + setupWebRTC(iceServers); + } catch (WebRTCWrapper.InitializationException e) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to initialize webrtc"); + transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE); + return; + } try { org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get(); final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); @@ -382,8 +447,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); sendSessionInitiate(rtpContentMap, targetState); this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); - } catch (Exception e) { - Log.d(Config.LOGTAG, "unable to sendSessionInitiate", e); + } catch (final Exception e) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", e); + webRTCWrapper.close(); + if (isInState(targetState)) { + sendSessionTerminate(Reason.FAILED_APPLICATION); + } else { + transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE); + } } }); } @@ -422,8 +493,43 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void send(final JinglePacket jinglePacket) { jinglePacket.setTo(id.with); - //TODO track errors - xmppConnectionService.sendIqPacket(id.account, jinglePacket, null); + xmppConnectionService.sendIqPacket(id.account, jinglePacket, (account, response) -> { + if (response.getType() == IqPacket.TYPE.ERROR) { + final String errorCondition = response.getErrorCondition(); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ-error from " + response.getFrom() + " in RTP session. " + errorCondition); + this.webRTCWrapper.close(); + final State target; + if (Arrays.asList( + "service-unavailable", + "recipient-unavailable", + "remote-server-not-found", + "remote-server-timeout" + ).contains(errorCondition)) { + target = State.TERMINATED_CONNECTIVITY_ERROR; + } else { + target = State.TERMINATED_APPLICATION_FAILURE; + } + if (transition(target)) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": terminated session with " + id.with); + } else { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not transitioning because already at state=" + this.state); + } + + } else if (response.getType() == IqPacket.TYPE.TIMEOUT) { + this.webRTCWrapper.close(); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ timeout in RTP session with " + id.with + ". terminating with connectivity error"); + transition(State.TERMINATED_CONNECTIVITY_ERROR); + this.jingleConnectionManager.finishConnection(this); + } + }); + } + + private void respondWithOutOfOrder(final JinglePacket jinglePacket) { + jingleConnectionManager.respondWithJingleError(id.account, jinglePacket, "out-of-order", "unexpected-request", "wait"); + } + + private void respondOk(final JinglePacket jinglePacket) { + xmppConnectionService.sendIqPacket(id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null); } public RtpEndUserState getEndUserState() { @@ -474,6 +580,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return RtpEndUserState.ENDED; case TERMINATED_CONNECTIVITY_ERROR: return RtpEndUserState.CONNECTIVITY_ERROR; + case TERMINATED_APPLICATION_FAILURE: + return RtpEndUserState.APPLICATION_ERROR; } throw new IllegalStateException(String.format("%s has no equivalent EndUserState", this.state)); } @@ -526,7 +634,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web throw new IllegalStateException("called 'endCall' while in state " + this.state); } - private void setupWebRTC(final List iceServers) { + private void setupWebRTC(final List iceServers) throws WebRTCWrapper.InitializationException { this.webRTCWrapper.setup(this.xmppConnectionService); this.webRTCWrapper.initializePeerConnection(iceServers); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java index e8d8bd5d2..4baa0019d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java @@ -6,10 +6,10 @@ public enum RtpEndUserState { CONNECTED, //session-accepted and webrtc peer connection is connected FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet RINGING, //'propose' has been sent out and it has been 184 acked - ACCEPTED_ON_OTHER_DEVICE, //received 'accept' from one of our own devices ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received ENDING_CALL, //libwebrt says 'closed' but session-terminate hasnt gone through ENDED, //close UI DECLINED_OR_BUSY, //other party declined; no retry button - CONNECTIVITY_ERROR //network error; retry button + CONNECTIVITY_ERROR, //network error; retry button + APPLICATION_ERROR //something rather bad happened; libwebrtc failed or we got in IQ-error } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 1688a8ecb..8eb46fe1e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -132,7 +132,7 @@ public class WebRTCWrapper { ); } - public void initializePeerConnection(final List iceServers) { + public void initializePeerConnection(final List iceServers) throws InitializationException { PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory(); CameraVideoCapturer capturer = null; @@ -195,7 +195,7 @@ public class WebRTCWrapper { final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(iceServers, peerConnectionObserver); if (peerConnection == null) { - throw new IllegalStateException("Unable to create PeerConnection"); + throw new InitializationException("Unable to create PeerConnection"); } peerConnection.addStream(stream); peerConnection.setAudioPlayout(true); @@ -344,6 +344,13 @@ public class WebRTCWrapper { } } + public static class InitializationException extends Exception { + + private InitializationException(String message) { + super(message); + } + } + public interface EventCallback { void onIceCandidate(IceCandidate iceCandidate); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java index 070f77226..635f26d54 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java @@ -5,7 +5,7 @@ import android.support.annotation.NonNull; import com.google.common.base.CaseFormat; public enum Reason { - SUCCESS, DECLINE, BUSY, CANCEL, CONNECTIVITY_ERROR, FAILED_TRANSPORT, TIMEOUT, UNKNOWN; + SUCCESS, DECLINE, BUSY, CANCEL, CONNECTIVITY_ERROR, FAILED_TRANSPORT, FAILED_APPLICATION, TIMEOUT, UNKNOWN; public static Reason of(final String value) { try { diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java index 095075616..552f40598 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java @@ -31,6 +31,18 @@ abstract public class AbstractAcknowledgeableStanza extends AbstractStanza { return null; } + public String getErrorCondition() { + Element error = findChild("error"); + if (error != null) { + for(Element element : error.getChildren()) { + if (!element.getName().equals("text")) { + return element.getName(); + } + } + } + return null; + } + public boolean valid() { return InvalidJid.isValid(getFrom()) && InvalidJid.isValid(getTo()); } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 487ee434b..39b9a3337 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -898,6 +898,7 @@ Ringing Busy Unable to connect call + Application failure View %1$d Participant View %1$d Participants From 7749a7ce220b7e0e1b9b7fc508681a4713290fa2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 9 Apr 2020 17:37:21 +0200 Subject: [PATCH 062/182] fixed rotation issues in RtpSessionActivity --- .../conversations/ui/RtpSessionActivity.java | 40 +++++++++++++++---- .../xmpp/jingle/JingleRtpConnection.java | 2 + 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 232ceb8b2..7890bfdbe 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -29,6 +29,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe public static final String EXTRA_WITH = "with"; public static final String EXTRA_SESSION_ID = "session_id"; + public static final String EXTRA_LAST_REPORTED_STATE = "last_reported_state"; public static final String ACTION_ACCEPT_CALL = "action_accept_call"; public static final String ACTION_MAKE_VOICE_CALL = "action_make_voice_call"; @@ -107,6 +108,15 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(intent.getAction())) { xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with); binding.with.setText(account.getRoster().getContact(with).getDisplayName()); + } else if (Intent.ACTION_VIEW.equals(intent.getAction())) { + final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE); + if (extraLastState != null) { + Log.d(Config.LOGTAG, "restored last state from intent extra"); + RtpEndUserState state = RtpEndUserState.valueOf(extraLastState); + updateButtonConfiguration(state); + updateStateDisplay(state); + } + binding.with.setText(account.getRoster().getContact(with).getDisplayName()); } } @@ -172,6 +182,10 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe case APPLICATION_ERROR: binding.status.setText(R.string.rtp_state_application_failure); break; + case ENDED: + throw new IllegalStateException("Activity should have called finish()"); + default: + throw new IllegalStateException(String.format("State %s has not been handled in UI", state)); } } @@ -212,7 +226,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void retry(View view) { - + Log.d(Config.LOGTAG,"attempting retry"); } private void exit(View view) { @@ -237,7 +251,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) { Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")"); if (with.isBareJid()) { - updateRtpSessionProposalState(with, state); + updateRtpSessionProposalState(account, with, state); return; } if (this.rtpConnectionReference == null) { @@ -250,6 +264,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe if (state == RtpEndUserState.ENDED) { finish(); return; + } else if (asList(RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.CONNECTIVITY_ERROR).contains(state)) { + resetIntent(account, with, state); } runOnUiThread(() -> { updateStateDisplay(state); @@ -260,17 +276,27 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } - private void updateRtpSessionProposalState(Jid with, RtpEndUserState state) { - final Intent intent = getIntent(); - final String intentExtraWith = intent == null ? null : intent.getStringExtra(EXTRA_WITH); - if (intentExtraWith == null) { + private void updateRtpSessionProposalState(final Account account, final Jid with, final RtpEndUserState state) { + final Intent currentIntent = getIntent(); + final String withExtra = currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH); + if (withExtra == null) { return; } - if (Jid.ofEscaped(intentExtraWith).asBareJid().equals(with)) { + if (Jid.ofEscaped(withExtra).asBareJid().equals(with)) { runOnUiThread(() -> { updateStateDisplay(state); updateButtonConfiguration(state); }); + resetIntent(account, with, state); } } + + private void resetIntent(final Account account, Jid with, final RtpEndUserState state) { + Log.d(Config.LOGTAG, "resetting intent"); + 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_LAST_REPORTED_STATE, state.toString()); + setIntent(intent); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 153b0172f..e023d6b4e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -183,6 +183,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { contentMap = RtpContentMap.of(jinglePacket); contentMap.requireContentDescriptions(); + //TODO requireTransportWithDtls(); } catch (IllegalArgumentException | IllegalStateException | NullPointerException e) { respondOk(jinglePacket); sendSessionTerminate(Reason.FAILED_APPLICATION); @@ -222,6 +223,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { contentMap = RtpContentMap.of(jinglePacket); contentMap.requireContentDescriptions(); + //TODO requireTransportWithDtls(); } catch (IllegalArgumentException | IllegalStateException | NullPointerException e) { respondOk(jinglePacket); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents in session-accept", e); From 6a1df0538e0c49a9a063fa8e1d88d8986b24760d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 9 Apr 2020 19:45:21 +0200 Subject: [PATCH 063/182] request recording permission when making or accepting audio calls --- .../ui/ConversationFragment.java | 15 +++++- .../conversations/ui/RtpSessionActivity.java | 51 +++++++++++++++++-- .../conversations/utils/PermissionUtils.java | 26 ++++++++++ 3 files changed, 85 insertions(+), 7 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index ba531cc45..e1638b6db 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -109,6 +109,7 @@ import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.NickValidityChecker; import eu.siacs.conversations.utils.Patterns; +import eu.siacs.conversations.utils.PermissionUtils; import eu.siacs.conversations.utils.QuickLoader; import eu.siacs.conversations.utils.StylingHelper; import eu.siacs.conversations.utils.TimeframeUtils; @@ -138,6 +139,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke public static final int REQUEST_START_DOWNLOAD = 0x0210; public static final int REQUEST_ADD_EDITOR_CONTENT = 0x0211; public static final int REQUEST_COMMIT_ATTACHMENTS = 0x0212; + public static final int REQUEST_START_AUDIO_CALL = 0x213; public static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301; public static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302; public static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303; @@ -1233,7 +1235,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } break; case R.id.action_call: - triggerRtpSession(); + checkPermissionAndTriggerRtpSession(); break; default: break; @@ -1241,6 +1243,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke return super.onOptionsItemSelected(item); } + private void checkPermissionAndTriggerRtpSession() { + if (hasPermissions(REQUEST_START_AUDIO_CALL, Manifest.permission.RECORD_AUDIO)) { + triggerRtpSession(); + } + } + private void triggerRtpSession() { final Contact contact = conversation.getContact(); final Intent intent = new Intent(activity, RtpSessionActivity.class); @@ -1383,7 +1391,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (grantResults.length > 0) { if (allGranted(grantResults)) { switch (requestCode) { @@ -1400,6 +1408,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke case REQUEST_COMMIT_ATTACHMENTS: commitAttachments(); break; + case REQUEST_START_AUDIO_CALL: + triggerRtpSession(); + break; default: attachFile(requestCode); break; diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 7890bfdbe..fd7614e99 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -1,14 +1,24 @@ package eu.siacs.conversations.ui; +import android.Manifest; +import android.app.Activity; import android.content.Intent; -import android.content.res.ColorStateList; +import android.content.pm.PackageManager; import android.databinding.DataBindingUtil; +import android.os.Build; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.StringRes; +import android.support.v4.app.ActivityCompat; import android.util.Log; import android.view.View; import android.view.WindowManager; +import android.widget.Toast; + +import com.google.common.collect.ImmutableList; import java.lang.ref.WeakReference; +import java.util.List; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -16,17 +26,21 @@ import eu.siacs.conversations.databinding.ActivityRtpSessionBinding; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.PermissionUtils; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; import rocks.xmpp.addr.Jid; +import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied; import static java.util.Arrays.asList; //TODO if last state was BUSY (or RETRY); we want to reset action to view or something so we don’t automatically call again on recreate public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate { + private static final int REQUEST_ACCEPT_CALL = 0x1111; + public static final String EXTRA_WITH = "with"; public static final String EXTRA_SESSION_ID = "session_id"; public static final String EXTRA_LAST_REPORTED_STATE = "last_reported_state"; @@ -75,7 +89,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void acceptCall(View view) { - requireRtpConnection().acceptCall(); + requestPermissionsAndAcceptCall(); + } + + private void requestPermissionsAndAcceptCall() { + if (PermissionUtils.hasPermission(this, ImmutableList.of(Manifest.permission.RECORD_AUDIO), REQUEST_ACCEPT_CALL)) { + requireRtpConnection().acceptCall(); + } } @Override @@ -89,7 +109,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe //TODO reinitialize if (ACTION_ACCEPT_CALL.equals(intent.getAction())) { Log.d(Config.LOGTAG, "accepting through onNewIntent()"); - requireRtpConnection().acceptCall(); + requestPermissionsAndAcceptCall(); } } @@ -103,7 +123,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe initializeActivityWithRunningRapSession(account, with, sessionId); if (ACTION_ACCEPT_CALL.equals(intent.getAction())) { Log.d(Config.LOGTAG, "intent action was accept"); - requireRtpConnection().acceptCall(); + requestPermissionsAndAcceptCall(); } } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(intent.getAction())) { xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with); @@ -120,6 +140,27 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (PermissionUtils.allGranted(grantResults)) { + if (requestCode == REQUEST_ACCEPT_CALL) { + requireRtpConnection().acceptCall(); + } + } else { + @StringRes int res; + final String firstDenied = getFirstDenied(grantResults, permissions); + if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) { + res = R.string.no_microphone_permission; + } else if (Manifest.permission.CAMERA.equals(firstDenied)) { + res = R.string.no_camera_permission; + } else { + throw new IllegalStateException("Invalid permission result request"); + } + Toast.makeText(this, res, Toast.LENGTH_SHORT).show(); + } + } + private void initializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) { final WeakReference reference = xmppConnectionService.getJingleConnectionManager() @@ -226,7 +267,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void retry(View view) { - Log.d(Config.LOGTAG,"attempting retry"); + Log.d(Config.LOGTAG, "attempting retry"); } private void exit(View view) { diff --git a/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java b/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java index 706b6c2f8..852dedc00 100644 --- a/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java @@ -1,7 +1,14 @@ package eu.siacs.conversations.utils; import android.Manifest; +import android.app.Activity; import android.content.pm.PackageManager; +import android.os.Build; +import android.support.v4.app.ActivityCompat; + +import com.google.common.collect.ImmutableList; + +import java.util.List; public class PermissionUtils { @@ -31,4 +38,23 @@ public class PermissionUtils { } return null; } + + public static boolean hasPermission(final Activity activity, final List permissions, final int requestCode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + final ImmutableList.Builder missingPermissions = new ImmutableList.Builder<>(); + for (final String permission : permissions) { + if (ActivityCompat.checkSelfPermission(activity, permission) != PackageManager.PERMISSION_GRANTED) { + missingPermissions.add(permission); + } + } + final ImmutableList missing = missingPermissions.build(); + if (missing.size() == 0) { + return true; + } + ActivityCompat.requestPermissions(activity, missing.toArray(new String[0]), requestCode); + return false; + } else { + return true; + } + } } From 39e3791345eed83f1c381a71ab53c47722ee0ea3 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 9 Apr 2020 20:35:44 +0200 Subject: [PATCH 064/182] incude human readable text in some session-terminates --- .../xmpp/jingle/JingleFileTransferConnection.java | 2 +- .../xmpp/jingle/JingleRtpConnection.java | 13 ++++++++----- .../xmpp/jingle/stanzas/JinglePacket.java | 9 +++++++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index e309e4289..dab5b8381 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -1064,7 +1064,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple private void sendSessionTerminate(Reason reason) { final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_TERMINATE); - packet.setReason(reason); + packet.setReason(reason, null); this.sendJinglePacket(packet); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index e023d6b4e..7c1c6c8c5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -246,10 +246,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final SessionDescription sessionDescription; try { sessionDescription = SessionDescription.of(contentMap); - } catch (IllegalArgumentException e) { + } catch (final IllegalArgumentException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable convert offer from session-accept to SDP", e); webRTCWrapper.close(); - sendSessionTerminate(Reason.FAILED_APPLICATION); + sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); return; } org.webrtc.SessionDescription answer = new org.webrtc.SessionDescription( @@ -276,8 +276,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } catch (final IllegalArgumentException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable convert offer from session-initiate to SDP", e); webRTCWrapper.close(); - sendSessionTerminate(Reason.FAILED_APPLICATION); - ; + sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); return; } sendSessionAccept(offer); @@ -470,10 +469,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void sendSessionTerminate(final Reason reason) { + sendSessionTerminate(reason, null); + } + + private void sendSessionTerminate(final Reason reason, final String text) { final State target = reasonToState(reason); transitionOrThrow(target); final JinglePacket jinglePacket = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); - jinglePacket.setReason(reason); + jinglePacket.setReason(reason, text); send(jinglePacket); Log.d(Config.LOGTAG, jinglePacket.toString()); jingleConnectionManager.finishConnection(this); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java index 90867ed25..ef769be8e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -4,6 +4,7 @@ import android.support.annotation.NonNull; import com.google.common.base.CaseFormat; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import java.util.Map; @@ -81,9 +82,13 @@ public class JinglePacket extends IqPacket { return Reason.UNKNOWN; } - public void setReason(final Reason reason) { + public void setReason(final Reason reason, final String text) { final Element jingle = findChild("jingle", Namespace.JINGLE); - jingle.addChild("reason").addChild(reason.toString()); + final Element reasonElement = jingle.addChild("reason"); + reasonElement.addChild(reason.toString()); + if (!Strings.isNullOrEmpty(text)) { + reasonElement.addChild("text").setContent(text); + } } //RECOMMENDED for session-initiate, NOT RECOMMENDED otherwise From 0661c1bd374be28ddeb7f0a56f54c38f7e6c8535 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 10 Apr 2020 07:07:22 +0200 Subject: [PATCH 065/182] add state transitions for iq service-unavailable errors and timeouts --- .../xmpp/jingle/JingleRtpConnection.java | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 7c1c6c8c5..5a8359de7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -49,19 +49,22 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web transitionBuilder.put(State.PROCEED, ImmutableList.of( State.SESSION_INITIALIZED_PRE_APPROVED, State.TERMINATED_SUCCESS, - State.TERMINATED_APPLICATION_FAILURE + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_CONNECTIVITY_ERROR //at this state used for error bounces of the proceed message )); transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of( State.SESSION_ACCEPTED, State.TERMINATED_CANCEL_OR_TIMEOUT, State.TERMINATED_DECLINED_OR_BUSY, - State.TERMINATED_APPLICATION_FAILURE + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_CONNECTIVITY_ERROR //at this state used for IQ errors and IQ timeouts )); transitionBuilder.put(State.SESSION_INITIALIZED_PRE_APPROVED, ImmutableList.of( State.SESSION_ACCEPTED, State.TERMINATED_CANCEL_OR_TIMEOUT, State.TERMINATED_DECLINED_OR_BUSY, - State.TERMINATED_APPLICATION_FAILURE + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_CONNECTIVITY_ERROR //at this state used for IQ errors and IQ timeouts )); transitionBuilder.put(State.SESSION_ACCEPTED, ImmutableList.of( State.TERMINATED_SUCCESS, @@ -169,14 +172,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } else { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received transport info while in state=" + this.state); - respondWithOutOfOrder(jinglePacket); + terminateWithOutOfOrder(jinglePacket); } } private void receiveSessionInitiate(final JinglePacket jinglePacket) { if (isInitiator()) { Log.d(Config.LOGTAG, String.format("%s: received session-initiate even though we were initiating", id.account.getJid().asBareJid())); - respondWithOutOfOrder(jinglePacket); + terminateWithOutOfOrder(jinglePacket); return; } final RtpContentMap contentMap; @@ -209,14 +212,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } else { Log.d(Config.LOGTAG, String.format("%s: received session-initiate while in state %s", id.account.getJid().asBareJid(), state)); - respondWithOutOfOrder(jinglePacket); + terminateWithOutOfOrder(jinglePacket); } } private void receiveSessionAccept(final JinglePacket jinglePacket) { if (!isInitiator()) { Log.d(Config.LOGTAG, String.format("%s: received session-accept even though we were responding", id.account.getJid().asBareJid())); - respondWithOutOfOrder(jinglePacket); + terminateWithOutOfOrder(jinglePacket); return; } final RtpContentMap contentMap; @@ -529,6 +532,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web }); } + private void terminateWithOutOfOrder(final JinglePacket jinglePacket) { + Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": terminating session with out-of-order"); + webRTCWrapper.close(); + transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE); + respondWithOutOfOrder(jinglePacket); + jingleConnectionManager.finishConnection(this); + } + private void respondWithOutOfOrder(final JinglePacket jinglePacket) { jingleConnectionManager.respondWithJingleError(id.account, jinglePacket, "out-of-order", "unexpected-request", "wait"); } From 6884e427ef4ced8ded52e3e210d5fc465e9cf1ba Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 10 Apr 2020 07:45:23 +0200 Subject: [PATCH 066/182] require dtls and ensure procceds get tracked --- .../generator/MessageGenerator.java | 2 +- .../conversations/parser/MessageParser.java | 13 +++++++---- .../xmpp/jingle/AbstractJingleConnection.java | 3 ++- .../xmpp/jingle/JingleConnectionManager.java | 8 +++++++ .../xmpp/jingle/JingleRtpConnection.java | 22 ++++++++++++++----- .../xmpp/jingle/RtpContentMap.java | 22 +++++++++++++++---- 6 files changed, 55 insertions(+), 15 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 7e93303cd..d61e42b22 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -238,7 +238,7 @@ public class MessageGenerator extends AbstractGenerator { final MessagePacket packet = new MessagePacket(); packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those packet.setTo(proposal.with); - packet.setId(JingleRtpConnection.JINGLE_MESSAGE_ID_PREFIX+proposal.sessionId); + packet.setId(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX +proposal.sessionId); final Element propose = packet.addChild("propose", Namespace.JINGLE_MESSAGE); propose.setAttribute("id", proposal.sessionId); propose.addChild("description", Namespace.JINGLE_APPS_RTP); diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 4b15b8b72..f7c40269e 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -306,12 +306,17 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece final Jid from = packet.getFrom(); final String id = packet.getId(); if (from != null && id != null) { - if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_ID_PREFIX)) { - final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_ID_PREFIX.length()); + if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) { + final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length()); mXmppConnectionService.getJingleConnectionManager() .updateProposedSessionDiscovered(account, from, sessionId, JingleConnectionManager.DeviceDiscoveryState.FAILED); return true; } + if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX)) { + final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX.length()); + mXmppConnectionService.getJingleConnectionManager().failProceed(account, from, sessionId); + return true; + } mXmppConnectionService.markMessage(account, from.asBareJid(), id, @@ -845,8 +850,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id)); } } else if (id != null) { - if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_ID_PREFIX)) { - final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_ID_PREFIX.length()); + if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) { + final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length()); mXmppConnectionService.getJingleConnectionManager() .updateProposedSessionDiscovered(account, from, sessionId, JingleConnectionManager.DeviceDiscoveryState.DISCOVERED); } else { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java index 6cf7d2a91..bea185902 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -11,7 +11,8 @@ import rocks.xmpp.addr.Jid; public abstract class AbstractJingleConnection { - public static final String JINGLE_MESSAGE_ID_PREFIX = "jm-propose-"; + 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; protected final XmppConnectionService xmppConnectionService; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 88ce13d6e..f5fb9c1dc 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -354,6 +354,14 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } + public void failProceed(Account account, final Jid with, String sessionId) { + final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with, sessionId); + final AbstractJingleConnection existingJingleConnection = connections.get(id); + if (existingJingleConnection instanceof JingleRtpConnection) { + ((JingleRtpConnection) existingJingleConnection).deliverFailedProceed(); + } + } + public static class RtpSessionProposal { private final Account account; public final Jid with; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 5a8359de7..946d021ab 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -81,7 +81,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private RtpContentMap responderRtpContentMap; - public JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { + JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { super(jingleConnectionManager, id, initiator); } @@ -186,8 +186,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { contentMap = RtpContentMap.of(jinglePacket); contentMap.requireContentDescriptions(); - //TODO requireTransportWithDtls(); - } catch (IllegalArgumentException | IllegalStateException | NullPointerException e) { + contentMap.requireDTLSFingerprint(); + } catch (final IllegalArgumentException | IllegalStateException | NullPointerException e) { respondOk(jinglePacket); sendSessionTerminate(Reason.FAILED_APPLICATION); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); @@ -226,7 +226,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { contentMap = RtpContentMap.of(jinglePacket); contentMap.requireContentDescriptions(); - //TODO requireTransportWithDtls(); + contentMap.requireDTLSFingerprint(); } catch (IllegalArgumentException | IllegalStateException | NullPointerException e) { respondOk(jinglePacket); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents in session-accept", e); @@ -351,6 +351,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } + void deliverFailedProceed() { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": receive message error for proceed message"); + if (transition(State.TERMINATED_CONNECTIVITY_ERROR)) { + webRTCWrapper.close(); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into connectivity error"); + this.jingleConnectionManager.finishConnection(this); + } + } + private void receiveAccept(Jid from, Element message) { final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); if (originatedFromMyself) { @@ -533,7 +542,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void terminateWithOutOfOrder(final JinglePacket jinglePacket) { - Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": terminating session with out-of-order"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": terminating session with out-of-order"); webRTCWrapper.close(); transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE); respondWithOutOfOrder(jinglePacket); @@ -681,6 +690,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void sendJingleMessage(final String action, final Jid to) { final MessagePacket messagePacket = new MessagePacket(); + if ("proceed".equals(action)) { + messagePacket.setId(JINGLE_MESSAGE_PROCEED_ID_PREFIX + id.sessionId); + } messagePacket.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those messagePacket.setTo(to); messagePacket.addChild(action, Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index 1ebd810b7..fcac3d729 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -4,6 +4,7 @@ import android.util.Log; import com.google.common.base.Function; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; @@ -51,13 +52,26 @@ public class RtpContentMap { if (this.contents.size() == 0) { throw new IllegalStateException("No contents available"); } - for(Map.Entry entry : this.contents.entrySet()) { + for (Map.Entry entry : this.contents.entrySet()) { if (entry.getValue().description == null) { throw new IllegalStateException(String.format("%s is lacking content description", entry.getKey())); } } } + public void requireDTLSFingerprint() { + if (this.contents.size() == 0) { + throw new IllegalStateException("No contents available"); + } + for (Map.Entry entry : this.contents.entrySet()) { + final IceUdpTransportInfo transport = entry.getValue().transport; + final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); + if (fingerprint == null || Strings.isNullOrEmpty(fingerprint.getContent()) || Strings.isNullOrEmpty(fingerprint.getHash())) { + throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s", entry.getKey())); + } + } + } + public JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) { final JinglePacket jinglePacket = new JinglePacket(action, sessionId); if (this.group != null) { @@ -75,14 +89,14 @@ public class RtpContentMap { } public RtpContentMap transportInfo(final String contentName, final IceUdpTransportInfo.Candidate candidate) { - final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName); + final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName); final IceUdpTransportInfo transportInfo = descriptionTransport == null ? null : descriptionTransport.transport; if (transportInfo == null) { - throw new IllegalArgumentException("Unable to find transport info for content name "+contentName); + throw new IllegalArgumentException("Unable to find transport info for content name " + contentName); } final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper(); newTransportInfo.addChild(candidate); - return new RtpContentMap(null, ImmutableMap.of(contentName, new DescriptionTransport(null,newTransportInfo))); + return new RtpContentMap(null, ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo))); } From 2ba84bd32e268facf8c8d1bdcb740c4469f9078d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 10 Apr 2020 07:53:29 +0200 Subject: [PATCH 067/182] no need to be careful about Int parsing in session description; just fail --- .../siacs/conversations/xmpp/jingle/JingleRtpConnection.java | 4 ++-- .../siacs/conversations/xmpp/jingle/SessionDescription.java | 3 +++ .../conversations/xmpp/jingle/stanzas/RtpDescription.java | 5 +---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 946d021ab..52e595b99 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -249,7 +249,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final SessionDescription sessionDescription; try { sessionDescription = SessionDescription.of(contentMap); - } catch (final IllegalArgumentException e) { + } catch (final IllegalArgumentException | NullPointerException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable convert offer from session-accept to SDP", e); webRTCWrapper.close(); sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); @@ -276,7 +276,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final SessionDescription offer; try { offer = SessionDescription.of(rtpContentMap); - } catch (final IllegalArgumentException e) { + } catch (final IllegalArgumentException | NullPointerException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable convert offer from session-initiate to SDP", e); webRTCWrapper.close(); sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index 587fb513c..f6f12513d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -137,6 +137,9 @@ public class SessionDescription { attributeMap.put("msid-semantic", " WMS my-media-stream"); for (Map.Entry entry : contentMap.contents.entrySet()) { + + //TODO sprinkle in a few noWhiteSpaces checks into various parameters and types + final String name = entry.getKey(); RtpContentMap.DescriptionTransport descriptionTransport = entry.getValue(); RtpDescription description = descriptionTransport.description; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index 59881b089..00b2ddc60 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -145,10 +145,7 @@ public class RtpDescription extends GenericDescription { public int getValue() { final String value = getAttribute("value"); - if (value == null) { - return 0; - } - return SessionDescription.ignorantIntParser(value); + return Integer.parseInt(value); } From f5c4de877090fe36f7cf38fe1ee492bd68accf09 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 10 Apr 2020 09:31:39 +0200 Subject: [PATCH 068/182] make retry work --- .../conversations/ui/RtpSessionActivity.java | 59 +++++++++++-------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index fd7614e99..1822fef19 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -1,15 +1,12 @@ package eu.siacs.conversations.ui; import android.Manifest; -import android.app.Activity; +import android.annotation.SuppressLint; import android.content.Intent; -import android.content.pm.PackageManager; import android.databinding.DataBindingUtil; -import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.StringRes; -import android.support.v4.app.ActivityCompat; import android.util.Log; import android.view.View; import android.view.WindowManager; @@ -18,7 +15,6 @@ import android.widget.Toast; import com.google.common.collect.ImmutableList; import java.lang.ref.WeakReference; -import java.util.List; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -106,10 +102,18 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override public void onNewIntent(final Intent intent) { super.onNewIntent(intent); - //TODO reinitialize - if (ACTION_ACCEPT_CALL.equals(intent.getAction())) { - Log.d(Config.LOGTAG, "accepting through onNewIntent()"); - requestPermissionsAndAcceptCall(); + final Account account = extractAccount(intent); + final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH)); + final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID); + if (sessionId != null) { + Log.d(Config.LOGTAG, "reinitializing from onNewIntent()"); + initializeActivityWithRunningRapSession(account, with, sessionId); + if (ACTION_ACCEPT_CALL.equals(intent.getAction())) { + Log.d(Config.LOGTAG, "accepting call from onNewIntent()"); + requestPermissionsAndAcceptCall(); + } + } else { + throw new IllegalStateException("received onNewIntent without sessionId"); } } @@ -230,44 +234,50 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } + @SuppressLint("RestrictedApi") private void updateButtonConfiguration(final RtpEndUserState state) { if (state == RtpEndUserState.INCOMING_CALL) { this.binding.rejectCall.setOnClickListener(this::rejectCall); this.binding.rejectCall.setImageResource(R.drawable.ic_call_end_white_48dp); - this.binding.rejectCall.show(); - this.binding.endCall.hide(); + this.binding.rejectCall.setVisibility(View.VISIBLE); + this.binding.endCall.setVisibility(View.INVISIBLE); this.binding.acceptCall.setOnClickListener(this::acceptCall); this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp); - this.binding.acceptCall.show(); + this.binding.acceptCall.setVisibility(View.VISIBLE); } else if (state == RtpEndUserState.ENDING_CALL) { - this.binding.rejectCall.hide(); - this.binding.endCall.hide(); - this.binding.acceptCall.hide(); + this.binding.rejectCall.setVisibility(View.INVISIBLE); + this.binding.endCall.setVisibility(View.INVISIBLE); + this.binding.acceptCall.setVisibility(View.INVISIBLE); } else if (state == RtpEndUserState.DECLINED_OR_BUSY) { - this.binding.rejectCall.hide(); + this.binding.rejectCall.setVisibility(View.INVISIBLE); this.binding.endCall.setOnClickListener(this::exit); this.binding.endCall.setImageResource(R.drawable.ic_clear_white_48dp); - this.binding.endCall.show(); - this.binding.acceptCall.hide(); + this.binding.endCall.setVisibility(View.VISIBLE); + this.binding.acceptCall.setVisibility(View.INVISIBLE); } else if (state == RtpEndUserState.CONNECTIVITY_ERROR || state == RtpEndUserState.APPLICATION_ERROR) { this.binding.rejectCall.setOnClickListener(this::exit); this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp); - this.binding.rejectCall.show(); - this.binding.endCall.hide(); + this.binding.rejectCall.setVisibility(View.VISIBLE); + this.binding.endCall.setVisibility(View.INVISIBLE); this.binding.acceptCall.setOnClickListener(this::retry); this.binding.acceptCall.setImageResource(R.drawable.ic_replay_white_48dp); - this.binding.acceptCall.show(); + this.binding.acceptCall.setVisibility(View.VISIBLE); } else { - this.binding.rejectCall.hide(); + this.binding.rejectCall.setVisibility(View.INVISIBLE); this.binding.endCall.setOnClickListener(this::endCall); this.binding.endCall.setImageResource(R.drawable.ic_call_end_white_48dp); - this.binding.endCall.show(); - this.binding.acceptCall.hide(); + this.binding.endCall.setVisibility(View.VISIBLE); + this.binding.acceptCall.setVisibility(View.INVISIBLE); } } private void retry(View view) { Log.d(Config.LOGTAG, "attempting retry"); + final Intent intent = getIntent(); + final Account account = extractAccount(intent); + final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH)); + this.rtpConnectionReference = null; + xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with); } private void exit(View view) { @@ -314,6 +324,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe }); } else { Log.d(Config.LOGTAG, "received update for other rtp session"); + //TODO if we only ever have one; we might just switch over? Maybe! } } From 0302eacac1b00b9585cde5a9b86d26e55bf3506a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 10 Apr 2020 10:35:00 +0200 Subject: [PATCH 069/182] back button rejects or ends call --- .../siacs/conversations/ui/RtpSessionActivity.java | 10 ++++++++++ .../xmpp/jingle/JingleRtpConnection.java | 12 ++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 1822fef19..47ec38024 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -68,6 +68,10 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void endCall(View view) { + endCall(); + } + + private void endCall() { if (this.rtpConnectionReference == null) { final Intent intent = getIntent(); final Account account = extractAccount(intent); @@ -165,6 +169,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } + @Override + public void onBackPressed() { + endCall(); + super.onBackPressed(); + } + private void initializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) { final WeakReference reference = xmppConnectionService.getJingleConnectionManager() diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 52e595b99..b1a2e01e7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -639,6 +639,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } public void endCall() { + if (isInState(State.PROPOSED) && !isInitiator()) { + rejectCallFromProposed(); + return; + } if (isInState(State.PROCEED)) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ending call while in state PROCEED just means ending the connection"); webRTCWrapper.close(); @@ -651,12 +655,16 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web sendSessionTerminate(Reason.CANCEL); return; } - if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { + if (isInState(State.SESSION_INITIALIZED)) { + rejectCallFromSessionInitiate(); + return; + } + if (isInState(State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { webRTCWrapper.close(); sendSessionTerminate(Reason.SUCCESS); return; } - throw new IllegalStateException("called 'endCall' while in state " + this.state); + throw new IllegalStateException("called 'endCall' while in state " + this.state + ". isInitiator=" + isInitiator()); } private void setupWebRTC(final List iceServers) throws WebRTCWrapper.InitializationException { From 14d008d89d7b4fefe43428d0d220ca3d6a786cf7 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 10 Apr 2020 11:16:54 +0200 Subject: [PATCH 070/182] turn screen off during call --- .../conversations/ui/RtpSessionActivity.java | 68 ++++++++++++++++--- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 47ec38024..f86856493 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -2,11 +2,15 @@ package eu.siacs.conversations.ui; import android.Manifest; import android.annotation.SuppressLint; +import android.content.Context; import android.content.Intent; import android.databinding.DataBindingUtil; +import android.os.Build; import android.os.Bundle; +import android.os.PowerManager; import android.support.annotation.NonNull; import android.support.annotation.StringRes; +import android.support.v4.content.ContextCompat; import android.util.Log; import android.view.View; import android.view.WindowManager; @@ -15,6 +19,7 @@ import android.widget.Toast; import com.google.common.collect.ImmutableList; import java.lang.ref.WeakReference; +import java.util.Arrays; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -35,6 +40,8 @@ import static java.util.Arrays.asList; public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate { + private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session"; + private static final int REQUEST_ACCEPT_CALL = 0x1111; public static final String EXTRA_WITH = "with"; @@ -48,6 +55,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private WeakReference rtpConnectionReference; private ActivityRtpSessionBinding binding; + private PowerManager.WakeLock mProximityWakeLock; @Override public void onCreate(Bundle savedInstanceState) { @@ -55,8 +63,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED - | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON) - ; + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); Log.d(Config.LOGTAG, "RtpSessionActivity.onCreate()"); this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session); } @@ -77,7 +84,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final Account account = extractAccount(intent); final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH)); xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid()); - finish(); + finishAndReleaseWakeLock(); } else { requireRtpConnection().endCall(); } @@ -85,7 +92,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void rejectCall(View view) { requireRtpConnection().rejectCall(); - finish(); + finishAndReleaseWakeLock(); } private void acceptCall(View view) { @@ -94,10 +101,41 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void requestPermissionsAndAcceptCall() { if (PermissionUtils.hasPermission(this, ImmutableList.of(Manifest.permission.RECORD_AUDIO), REQUEST_ACCEPT_CALL)) { + putScreenInCallMode(); requireRtpConnection().acceptCall(); } } + @SuppressLint("WakelockTimeout") + private void putScreenInCallMode() { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); + if (powerManager == null) { + Log.e(Config.LOGTAG, "power manager not available"); + return; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + this.mProximityWakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, PROXIMITY_WAKE_LOCK_TAG); + if (!this.mProximityWakeLock.isHeld()) { + Log.d(Config.LOGTAG, "acquiring wake lock"); + this.mProximityWakeLock.acquire(); + } + } + } + + private void finishAndReleaseWakeLock() { + releaseWakeLock(); + finish(); + } + + private void releaseWakeLock() { + if (this.mProximityWakeLock != null && mProximityWakeLock.isHeld()) { + Log.d(Config.LOGTAG, "releasing wake lock"); + this.mProximityWakeLock.release(); + this.mProximityWakeLock = null; + } + } + @Override protected void refreshUiReal() { @@ -134,7 +172,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe requestPermissionsAndAcceptCall(); } } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(intent.getAction())) { - xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with); + proposeJingleRtpSession(account, with); binding.with.setText(account.getRoster().getContact(with).getDisplayName()); } else if (Intent.ACTION_VIEW.equals(intent.getAction())) { final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE); @@ -148,6 +186,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } + private void proposeJingleRtpSession(final Account account, final Jid with) { + xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with); + putScreenInCallMode(); + } + @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); @@ -180,13 +223,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final WeakReference reference = xmppConnectionService.getJingleConnectionManager() .findJingleRtpConnection(account, with, sessionId); if (reference == null || reference.get() == null) { - finish(); + finishAndReleaseWakeLock(); return; } this.rtpConnectionReference = reference; final RtpEndUserState currentState = requireRtpConnection().getEndUserState(); if (currentState == RtpEndUserState.ENDED) { - finish(); + finishAndReleaseWakeLock(); return; } binding.with.setText(getWith().getDisplayName()); @@ -238,7 +281,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe binding.status.setText(R.string.rtp_state_application_failure); break; case ENDED: - throw new IllegalStateException("Activity should have called finish()"); + throw new IllegalStateException("Activity should have called finishAndReleaseWakeLock();"); default: throw new IllegalStateException(String.format("State %s has not been handled in UI", state)); } @@ -287,11 +330,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final Account account = extractAccount(intent); final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH)); this.rtpConnectionReference = null; - xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with); + proposeJingleRtpSession(account, with); } private void exit(View view) { - finish(); + finishAndReleaseWakeLock(); } private Contact getWith() { @@ -310,6 +353,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) { + if (Arrays.asList(RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.DECLINED_OR_BUSY).contains(state)) { + releaseWakeLock(); + } Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")"); if (with.isBareJid()) { updateRtpSessionProposalState(account, with, state); @@ -323,7 +369,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final AbstractJingleConnection.Id id = requireRtpConnection().getId(); if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) { if (state == RtpEndUserState.ENDED) { - finish(); + finishAndReleaseWakeLock(); return; } else if (asList(RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.CONNECTIVITY_ERROR).contains(state)) { resetIntent(account, with, state); From 2e8b91665b80823df82ecd7a9ca728e267ece26e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 10 Apr 2020 13:13:20 +0200 Subject: [PATCH 071/182] improvements to RtpSessionActivity --- .../conversations/ui/RtpSessionActivity.java | 34 ++++++++++++------- .../xmpp/jingle/JingleRtpConnection.java | 4 +++ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index f86856493..ba3fae92e 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -84,7 +84,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final Account account = extractAccount(intent); final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH)); xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid()); - finishAndReleaseWakeLock(); + finish(); } else { requireRtpConnection().endCall(); } @@ -92,7 +92,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void rejectCall(View view) { requireRtpConnection().rejectCall(); - finishAndReleaseWakeLock(); + finish(); } private void acceptCall(View view) { @@ -123,11 +123,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } - private void finishAndReleaseWakeLock() { - releaseWakeLock(); - finish(); - } - private void releaseWakeLock() { if (this.mProximityWakeLock != null && mProximityWakeLock.isHeld()) { Log.d(Config.LOGTAG, "releasing wake lock"); @@ -153,6 +148,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe if (ACTION_ACCEPT_CALL.equals(intent.getAction())) { Log.d(Config.LOGTAG, "accepting call from onNewIntent()"); requestPermissionsAndAcceptCall(); + resetIntent(intent.getExtras()); } } else { throw new IllegalStateException("received onNewIntent without sessionId"); @@ -170,6 +166,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe if (ACTION_ACCEPT_CALL.equals(intent.getAction())) { Log.d(Config.LOGTAG, "intent action was accept"); requestPermissionsAndAcceptCall(); + resetIntent(intent.getExtras()); } } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(intent.getAction())) { proposeJingleRtpSession(account, with); @@ -212,6 +209,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } + @Override + public void onStop() { + if (!isChangingConfigurations()) { + releaseWakeLock(); + } + super.onStop(); + } + @Override public void onBackPressed() { endCall(); @@ -223,13 +228,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final WeakReference reference = xmppConnectionService.getJingleConnectionManager() .findJingleRtpConnection(account, with, sessionId); if (reference == null || reference.get() == null) { - finishAndReleaseWakeLock(); + finish(); return; } this.rtpConnectionReference = reference; final RtpEndUserState currentState = requireRtpConnection().getEndUserState(); if (currentState == RtpEndUserState.ENDED) { - finishAndReleaseWakeLock(); + finish(); return; } binding.with.setText(getWith().getDisplayName()); @@ -334,7 +339,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void exit(View view) { - finishAndReleaseWakeLock(); + finish(); } private Contact getWith() { @@ -369,7 +374,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final AbstractJingleConnection.Id id = requireRtpConnection().getId(); if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) { if (state == RtpEndUserState.ENDED) { - finishAndReleaseWakeLock(); + finish(); return; } else if (asList(RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.CONNECTIVITY_ERROR).contains(state)) { resetIntent(account, with, state); @@ -399,8 +404,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } + private void resetIntent(final Bundle extras) { + final Intent intent = new Intent(Intent.ACTION_VIEW); + intent.putExtras(extras); + setIntent(intent); + } + private void resetIntent(final Account account, Jid with, final RtpEndUserState state) { - Log.d(Config.LOGTAG, "resetting intent"); final Intent intent = new Intent(Intent.ACTION_VIEW); intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString()); intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString()); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index b1a2e01e7..e35a713c7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -664,6 +664,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web sendSessionTerminate(Reason.SUCCESS); return; } + if (isInState(State.TERMINATED_APPLICATION_FAILURE, State.TERMINATED_CONNECTIVITY_ERROR, State.TERMINATED_DECLINED_OR_BUSY)) { + Log.d(Config.LOGTAG, "ignoring request to end call because already in state " + this.state); + return; + } throw new IllegalStateException("called 'endCall' while in state " + this.state + ". isInitiator=" + isInitiator()); } From d19b5e06345a421de955b300a0a5eca14771fcae Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 10 Apr 2020 15:19:56 +0200 Subject: [PATCH 072/182] show notification during ongoing call --- .../services/NotificationService.java | 39 +++++++++++++++++-- .../services/XmppConnectionService.java | 14 +++++-- .../conversations/ui/RtpSessionActivity.java | 3 ++ .../xmpp/jingle/JingleConnectionManager.java | 22 ++++++++--- .../xmpp/jingle/JingleRtpConnection.java | 20 ++++++++++ src/main/res/values/strings.xml | 3 ++ 6 files changed, 88 insertions(+), 13 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index da7028f35..bd8e0dabb 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -76,6 +76,7 @@ public class NotificationService { private static final int NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 2; private static final int ERROR_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 6; private static final int INCOMING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 8; + private static final int ONGOING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 10; private final XmppConnectionService mXmppConnectionService; private final LinkedHashMap> notifications = new LinkedHashMap<>(); private final HashMap mBacklogMessageCounter = new HashMap<>(); @@ -166,6 +167,13 @@ public class NotificationService { incomingCallsChannel.setGroup("calls"); notificationManager.createNotificationChannel(incomingCallsChannel); + final NotificationChannel ongoingCallsChannel = new NotificationChannel("ongoing_calls", + c.getString(R.string.ongoing_calls_channel_name), + NotificationManager.IMPORTANCE_LOW); + ongoingCallsChannel.setShowBadge(false); + ongoingCallsChannel.setGroup("calls"); + notificationManager.createNotificationChannel(ongoingCallsChannel); + final NotificationChannel messagesChannel = new NotificationChannel("messages", c.getString(R.string.messages_channel_name), @@ -333,7 +341,6 @@ public class NotificationService { fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId); fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - final PendingIntent pendingIntent = PendingIntent.getActivity(mXmppConnectionService, 101, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT); final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "incoming_calls"); builder.setSmallIcon(R.drawable.ic_call_white_24dp); builder.setContentTitle(mXmppConnectionService.getString(R.string.rtp_state_incoming_call)); @@ -346,7 +353,7 @@ public class NotificationService { builder.addAction(new NotificationCompat.Action.Builder( R.drawable.ic_call_end_white_48dp, mXmppConnectionService.getString(R.string.dismiss_call), - createDismissCall(id.sessionId, 102)) + createCallAction(id.sessionId, XmppConnectionService.ACTION_DISMISS_CALL, 102)) .build()); builder.addAction(new NotificationCompat.Action.Builder( R.drawable.ic_call_white_24dp, @@ -358,6 +365,26 @@ public class NotificationService { notify(INCOMING_CALL_NOTIFICATION_ID, builder.build()); } + public void showOngoingCallNotification(final AbstractJingleConnection.Id id) { + final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls"); + builder.setSmallIcon(R.drawable.ic_call_white_24dp); + builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call)); + builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName()); + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + builder.setPriority(NotificationCompat.PRIORITY_HIGH); + builder.setCategory(NotificationCompat.CATEGORY_CALL); + builder.setContentIntent(createPendingRtpSession(id, Intent.ACTION_VIEW, 101)); + builder.setOngoing(true); + builder.addAction(new NotificationCompat.Action.Builder( + R.drawable.ic_call_end_white_48dp, + mXmppConnectionService.getString(R.string.hang_up), + createCallAction(id.sessionId, XmppConnectionService.ACTION_END_CALL, 104)) + .build()); + final Notification notification = builder.build(); + notification.flags = notification.flags | Notification.FLAG_INSISTENT; + notify(ONGOING_CALL_NOTIFICATION_ID, builder.build()); + } + private PendingIntent createPendingRtpSession(final AbstractJingleConnection.Id id, final String action, final int requestCode) { final Intent fullScreenIntent = new Intent(mXmppConnectionService, RtpSessionActivity.class); fullScreenIntent.setAction(action); @@ -373,6 +400,10 @@ public class NotificationService { cancel(INCOMING_CALL_NOTIFICATION_ID); } + public void cancelOngoingCallNotification() { + cancel(ONGOING_CALL_NOTIFICATION_ID); + } + private void pushNow(final Message message) { mXmppConnectionService.updateUnreadCountBadge(); if (!notify(message)) { @@ -899,9 +930,9 @@ public class NotificationService { return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 16), intent, PendingIntent.FLAG_UPDATE_CURRENT); } - private PendingIntent createDismissCall(String sessionId, int requestCode) { + private PendingIntent createCallAction(String sessionId, final String action, int requestCode) { final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_DISMISS_CALL); + intent.setAction(action); intent.setPackage(mXmppConnectionService.getPackageName()); intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId); return PendingIntent.getService(mXmppConnectionService, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 14e94951f..ed2e5ba5a 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -167,6 +167,7 @@ public class XmppConnectionService extends Service { public static final String ACTION_FCM_TOKEN_REFRESH = "fcm_token_refresh"; public static final String ACTION_FCM_MESSAGE_RECEIVED = "fcm_message_received"; public static final String ACTION_DISMISS_CALL = "dismiss_call"; + public static final String ACTION_END_CALL = "end_call"; private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE"; private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp"; @@ -640,10 +641,17 @@ public class XmppConnectionService extends Service { } }); break; - case ACTION_DISMISS_CALL: + case ACTION_DISMISS_CALL: { final String sessionId = intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID); - Log.d(Config.LOGTAG,"received intent to dismiss call with session id "+sessionId); + Log.d(Config.LOGTAG, "received intent to dismiss call with session id " + sessionId); mJingleConnectionManager.rejectRtpSession(sessionId); + } + break; + case ACTION_END_CALL: { + final String sessionId = intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID); + Log.d(Config.LOGTAG, "received intent to end call with session id " + sessionId); + mJingleConnectionManager.endRtpSession(sessionId); + } break; case ACTION_DISMISS_ERROR_NOTIFICATIONS: dismissErrorNotifications(); @@ -3978,7 +3986,7 @@ public class XmppConnectionService extends Service { } public void notifyJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state) { - for(OnJingleRtpConnectionUpdate listener : threadSafeList(this.onJingleRtpConnectionUpdate)) { + for (OnJingleRtpConnectionUpdate listener : threadSafeList(this.onJingleRtpConnectionUpdate)) { listener.onJingleRtpConnectionUpdate(account, with, sessionId, state); } } diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index ba3fae92e..0aaf1ef2a 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -237,6 +237,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe finish(); return; } + if (JingleRtpConnection.STATES_SHOWING_ONGOING_CALL.contains(requireRtpConnection().getState())) { + putScreenInCallMode(); + } binding.with.setText(getWith().getDisplayName()); updateStateDisplay(currentState); updateButtonConfiguration(currentState); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index f5fb9c1dc..f6f1abd91 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -1,21 +1,19 @@ package eu.siacs.conversations.xmpp.jingle; +import android.util.Base64; import android.util.Log; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import java.lang.ref.WeakReference; +import java.security.SecureRandom; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; -import java.util.Set; -import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.services.AbstractConnectionManager; @@ -271,7 +269,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { } static String nextRandomId() { - return UUID.randomUUID().toString(); + final byte[] id = new byte[16]; + new SecureRandom().nextBytes(id); + return Base64.encodeToString(id, Base64.NO_WRAP | Base64.NO_PADDING); } public void deliverIbbPacket(Account account, IqPacket packet) { @@ -354,6 +354,16 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } + public void endRtpSession(final String sessionId) { + for (final AbstractJingleConnection connection : this.connections.values()) { + if (connection.getId().sessionId.equals(sessionId)) { + if (connection instanceof JingleRtpConnection) { + ((JingleRtpConnection) connection).endCall(); + } + } + } + } + public void failProceed(Account account, final Jid with, String sessionId) { final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with, sessionId); final AbstractJingleConnection existingJingleConnection = connections.get(id); @@ -374,7 +384,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { } public static RtpSessionProposal of(Account account, Jid with) { - return new RtpSessionProposal(account, with, UUID.randomUUID().toString()); + return new RtpSessionProposal(account, with, nextRandomId()); } @Override diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index e35a713c7..6cc3f69a7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -74,6 +74,13 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web VALID_TRANSITIONS = transitionBuilder.build(); } + public static final List STATES_SHOWING_ONGOING_CALL = Arrays.asList( + State.PROCEED, + State.SESSION_INITIALIZED, + State.SESSION_INITIALIZED_PRE_APPROVED, + State.SESSION_ACCEPTED + ); + private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this); private final ArrayDeque pendingIceCandidates = new ArrayDeque<>(); private State state = State.NULL; @@ -727,6 +734,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.state = target; Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target); updateEndUserState(); + updateOngoingCallNotification(); return true; } else { return false; @@ -759,6 +767,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, getEndUserState()); } + private void updateOngoingCallNotification() { + if (STATES_SHOWING_ONGOING_CALL.contains(this.state)) { + xmppConnectionService.getNotificationService().showOngoingCallNotification(id); + } else { + xmppConnectionService.getNotificationService().cancelOngoingCallNotification(); + } + } + private void discoverIceServers(final OnIceServersDiscovered onIceServersDiscovered) { if (id.account.getXmppConnection().getFeatures().extendedServiceDiscovery()) { final IqPacket request = new IqPacket(IqPacket.TYPE.GET); @@ -815,6 +831,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } + public State getState() { + return this.state; + } + private interface OnIceServersDiscovered { void onIceServersDiscovered(List iceServers); } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 39b9a3337..7bf491ed5 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -751,6 +751,7 @@ Calls Messages Incoming calls + Ongoing calls Silent messages This notification group is used to display notifications that should not trigger any sound. For example when being active on another device (Grace Period). Notification Settings @@ -899,6 +900,8 @@ Busy Unable to connect call Application failure + Hang up + Ongoing call View %1$d Participant View %1$d Participants From 9d83981f2c578ffbc9e7b7777f80624d624c8c09 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 10 Apr 2020 16:02:01 +0200 Subject: [PATCH 073/182] respond with busy if there is anthor rtp session --- .../generator/MessageGenerator.java | 10 ++ .../conversations/ui/RtpSessionActivity.java | 1 + .../xmpp/jingle/JingleConnectionManager.java | 99 ++++++++++++------- 3 files changed, 74 insertions(+), 36 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index d61e42b22..3eb3c1aa6 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -255,4 +255,14 @@ public class MessageGenerator extends AbstractGenerator { propose.addChild("description", Namespace.JINGLE_APPS_RTP); return packet; } + + public MessagePacket sessionReject(final Jid with, final String sessionId) { + final MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those + packet.setTo(with); + final Element propose = packet.addChild("reject", Namespace.JINGLE_MESSAGE); + propose.setAttribute("id", sessionId); + propose.addChild("description", Namespace.JINGLE_APPS_RTP); + return packet; + } } diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 0aaf1ef2a..c98d55cb6 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -213,6 +213,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe public void onStop() { if (!isChangingConfigurations()) { releaseWakeLock(); + //TODO maybe we want to finish if call had ended } super.onStop(); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index f6f1abd91..0a4336c3f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -18,12 +18,14 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.util.Attachment; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; @@ -38,7 +40,14 @@ public class JingleConnectionManager extends AbstractConnectionManager { super(service); } + static String nextRandomId() { + final byte[] id = new byte[16]; + new SecureRandom().nextBytes(id); + return Base64.encodeToString(id, Base64.NO_WRAP | Base64.NO_PADDING); + } + public void deliverPacket(final Account account, final JinglePacket packet) { + //TODO check that sessionId is not null final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, packet); final AbstractJingleConnection existingJingleConnection = connections.get(id); if (existingJingleConnection != null) { @@ -50,7 +59,15 @@ public class JingleConnectionManager extends AbstractConnectionManager { final AbstractJingleConnection connection; if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) { connection = new JingleFileTransferConnection(this, id, from); - } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace)) { + } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace)) { //and not using Tor + if (isBusy()) { + mXmppConnectionService.sendIqPacket(account, packet.generateResponse(IqPacket.TYPE.RESULT), null); + final JinglePacket sessionTermination = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); + sessionTermination.setTo(id.with); + sessionTermination.setReason(Reason.BUSY, null); + mXmppConnectionService.sendIqPacket(account, sessionTermination, null); + return; + } connection = new JingleRtpConnection(this, id, from); } else { respondWithJingleError(account, packet, "unsupported-info", "feature-not-implemented", "cancel"); @@ -65,6 +82,17 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } + private boolean isBusy() { + for (AbstractJingleConnection connection : this.connections.values()) { + if (connection instanceof JingleRtpConnection) { + return true; + } + } + synchronized (this.rtpSessionProposals) { + return this.rtpSessionProposals.containsValue(DeviceDiscoveryState.DISCOVERED) || this.rtpSessionProposals.containsValue(DeviceDiscoveryState.SEARCHING); + } + } + public void respondWithJingleError(final Account account, final IqPacket original, String jingleCondition, String condition, String conditionType) { final IqPacket response = original.generateResponse(IqPacket.TYPE.ERROR); final Element error = response.addChild("error"); @@ -109,21 +137,30 @@ public class JingleConnectionManager extends AbstractConnectionManager { } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + existingJingleConnection.getClass().getName() + " does not support jingle messages"); } - } else if ("propose".equals(message.getName())) { + return; + } + if (carbonCopy) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignore jingle message from self"); + return; + } + + if ("propose".equals(message.getName())) { final Element description = message.findChild("description"); final String namespace = description == null ? null : description.getNamespace(); - if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { - final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, with); - this.connections.put(id, rtpConnection); - rtpConnection.deliveryMessage(from, message); + if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { //and not using Tor + if (isBusy()) { + final MessagePacket reject = mXmppConnectionService.getMessageGenerator().sessionReject(from, sessionId); + mXmppConnectionService.sendMessagePacket(account, reject); + } else { + final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, with); + this.connections.put(id, rtpConnection); + rtpConnection.deliveryMessage(from, message); + } } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to react to proposed " + namespace + " session"); } } else if ("proceed".equals(message.getName())) { - if (carbonCopy) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignore carbon copied proceed"); - return; - } + final RtpSessionProposal proposal = new RtpSessionProposal(account, with.asBareJid(), sessionId); synchronized (rtpSessionProposals) { if (rtpSessionProposals.remove(proposal) != null) { @@ -136,10 +173,6 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } } else if ("reject".equals(message.getName())) { - if (carbonCopy) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignore carbon copied reject"); - return; - } final RtpSessionProposal proposal = new RtpSessionProposal(account, with.asBareJid(), sessionId); synchronized (rtpSessionProposals) { if (rtpSessionProposals.remove(proposal) != null) { @@ -268,12 +301,6 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } - static String nextRandomId() { - final byte[] id = new byte[16]; - new SecureRandom().nextBytes(id); - return Base64.encodeToString(id, Base64.NO_WRAP | Base64.NO_PADDING); - } - public void deliverIbbPacket(Account account, IqPacket packet) { final String sid; final Element payload; @@ -372,10 +399,25 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } + public enum DeviceDiscoveryState { + SEARCHING, DISCOVERED, FAILED; + + public RtpEndUserState toEndUserState() { + switch (this) { + case SEARCHING: + return RtpEndUserState.FINDING_DEVICE; + case DISCOVERED: + return RtpEndUserState.RINGING; + default: + return RtpEndUserState.CONNECTIVITY_ERROR; + } + } + } + public static class RtpSessionProposal { - private final Account account; public final Jid with; public final String sessionId; + private final Account account; private RtpSessionProposal(Account account, Jid with, String sessionId) { this.account = account; @@ -402,19 +444,4 @@ public class JingleConnectionManager extends AbstractConnectionManager { return Objects.hashCode(account.getJid(), with, sessionId); } } - - public enum DeviceDiscoveryState { - SEARCHING, DISCOVERED, FAILED; - - public RtpEndUserState toEndUserState() { - switch (this) { - case SEARCHING: - return RtpEndUserState.FINDING_DEVICE; - case DISCOVERED: - return RtpEndUserState.RINGING; - default: - return RtpEndUserState.CONNECTIVITY_ERROR; - } - } - } } From 07e671d7c31c5ab5deef76b37cf0f4a168e51136 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 10 Apr 2020 16:28:15 +0200 Subject: [PATCH 074/182] do not offer jingle calls when using Tor --- .../generator/AbstractGenerator.java | 17 +++++++++-------- .../conversations/ui/ConversationFragment.java | 5 +++++ .../xmpp/jingle/JingleConnectionManager.java | 8 ++++++-- src/main/res/values/strings.xml | 1 + 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index dbb188593..d478a252e 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -34,14 +34,6 @@ public abstract class AbstractGenerator { Namespace.JINGLE_TRANSPORTS_IBB, Namespace.JINGLE_ENCRYPTED_TRANSPORT, Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO, - - //VoIP - Namespace.JINGLE_TRANSPORT_ICE_UDP, - Namespace.JINGLE_FEATURE_AUDIO, - Namespace.JINGLE_FEATURE_VIDEO, - Namespace.JINGLE_APPS_RTP, - Namespace.JINGLE_APPS_DTLS, - "http://jabber.org/protocol/muc", "jabber:x:conference", Namespace.OOB, @@ -63,6 +55,14 @@ public abstract class AbstractGenerator { private final String[] PRIVACY_SENSITIVE = { "urn:xmpp:time" //XEP-0202: Entity Time leaks time zone }; + + private final String[] VOIP_NAMESPACES = { + Namespace.JINGLE_TRANSPORT_ICE_UDP, + Namespace.JINGLE_FEATURE_AUDIO, + Namespace.JINGLE_FEATURE_VIDEO, + Namespace.JINGLE_APPS_RTP, + Namespace.JINGLE_APPS_DTLS, + }; private String mVersion = null; private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); @@ -132,6 +132,7 @@ public abstract class AbstractGenerator { } if (!mXmppConnectionService.useTorToConnect() && !account.isOnion()) { features.addAll(Arrays.asList(PRIVACY_SENSITIVE)); + features.addAll(Arrays.asList(VOIP_NAMESPACES)); } if (mXmppConnectionService.broadcastLastActivity()) { features.add(Namespace.IDLE); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index e1638b6db..aec25048e 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1244,11 +1244,16 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } private void checkPermissionAndTriggerRtpSession() { + if (activity.xmppConnectionService.useTorToConnect() || conversation.getAccount().isOnion()) { + Toast.makeText(activity, R.string.disable_tor_to_make_call, Toast.LENGTH_SHORT).show(); + return; + } if (hasPermissions(REQUEST_START_AUDIO_CALL, Manifest.permission.RECORD_AUDIO)) { triggerRtpSession(); } } + private void triggerRtpSession() { final Contact contact = conversation.getContact(); final Intent intent = new Intent(activity, RtpSessionActivity.class); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 0a4336c3f..30bcf66a4 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -59,7 +59,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { final AbstractJingleConnection connection; if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) { connection = new JingleFileTransferConnection(this, id, from); - } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace)) { //and not using Tor + } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace) && !usesTor(account)) { if (isBusy()) { mXmppConnectionService.sendIqPacket(account, packet.generateResponse(IqPacket.TYPE.RESULT), null); final JinglePacket sessionTermination = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); @@ -82,6 +82,10 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } + private boolean usesTor(final Account account) { + return account.isOnion() || mXmppConnectionService.useTorToConnect(); + } + private boolean isBusy() { for (AbstractJingleConnection connection : this.connections.values()) { if (connection instanceof JingleRtpConnection) { @@ -147,7 +151,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { if ("propose".equals(message.getName())) { final Element description = message.findChild("description"); final String namespace = description == null ? null : description.getNamespace(); - if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { //and not using Tor + if (Namespace.JINGLE_APPS_RTP.equals(namespace) && !usesTor(account)) { if (isBusy()) { final MessagePacket reject = mXmppConnectionService.getMessageGenerator().sessionReject(from, sessionId); mXmppConnectionService.sendMessagePacket(account, reject); diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 7bf491ed5..ef2267a4a 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -902,6 +902,7 @@ Application failure Hang up Ongoing call + Disable Tor to make calls View %1$d Participant View %1$d Participants From 7b382d2ba5ec828a786c58a4e9bd1703a3333657 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 10 Apr 2020 19:22:29 +0200 Subject: [PATCH 075/182] include more human readable text in application errors --- .../xmpp/jingle/JingleConnectionManager.java | 8 +++++--- .../xmpp/jingle/JingleRtpConnection.java | 17 ++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 30bcf66a4..99f20f7f9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -18,7 +18,6 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.util.Attachment; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.OnIqPacketReceived; @@ -47,7 +46,11 @@ public class JingleConnectionManager extends AbstractConnectionManager { } public void deliverPacket(final Account account, final JinglePacket packet) { - //TODO check that sessionId is not null + final String sessionId = packet.getSessionId(); + if (sessionId == null) { + respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel"); + return; + } final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, packet); final AbstractJingleConnection existingJingleConnection = connections.get(id); if (existingJingleConnection != null) { @@ -78,7 +81,6 @@ public class JingleConnectionManager extends AbstractConnectionManager { } else { Log.d(Config.LOGTAG, "unable to route jingle packet: " + packet); respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel"); - } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 6cc3f69a7..d03505fd8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -30,6 +30,12 @@ import rocks.xmpp.addr.Jid; public class JingleRtpConnection extends AbstractJingleConnection implements WebRTCWrapper.EventCallback { + public static final List STATES_SHOWING_ONGOING_CALL = Arrays.asList( + State.PROCEED, + State.SESSION_INITIALIZED, + State.SESSION_INITIALIZED_PRE_APPROVED, + State.SESSION_ACCEPTED + ); private static final Map> VALID_TRANSITIONS; static { @@ -74,13 +80,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web VALID_TRANSITIONS = transitionBuilder.build(); } - public static final List STATES_SHOWING_ONGOING_CALL = Arrays.asList( - State.PROCEED, - State.SESSION_INITIALIZED, - State.SESSION_INITIALIZED_PRE_APPROVED, - State.SESSION_ACCEPTED - ); - private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this); private final ArrayDeque pendingIceCandidates = new ArrayDeque<>(); private State state = State.NULL; @@ -196,7 +195,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web contentMap.requireDTLSFingerprint(); } catch (final IllegalArgumentException | IllegalStateException | NullPointerException e) { respondOk(jinglePacket); - sendSessionTerminate(Reason.FAILED_APPLICATION); + sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); return; } @@ -238,7 +237,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web respondOk(jinglePacket); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents in session-accept", e); webRTCWrapper.close(); - sendSessionTerminate(Reason.FAILED_APPLICATION); + sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); return; } Log.d(Config.LOGTAG, "processing session-accept with " + contentMap.contents.size() + " contents"); From 5eea961155a85d9eab64f8d0bcfccc9c506a4b86 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 10 Apr 2020 21:18:43 +0200 Subject: [PATCH 076/182] improved strategy for ignoring self addressed jingle messages --- build.gradle | 4 +-- .../xmpp/jingle/JingleConnectionManager.java | 30 ++++++++++--------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/build.gradle b/build.gradle index b8bb32878..84ec32270 100644 --- a/build.gradle +++ b/build.gradle @@ -92,8 +92,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 28 - versionCode 367 - versionName "2.8.0-alpha" + versionCode 368 + versionName "2.8.0-alpha.2" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 99f20f7f9..3a7c179e4 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -127,15 +127,17 @@ public class JingleConnectionManager extends AbstractConnectionManager { } return; } - final boolean carbonCopy = from.asBareJid().equals(account.getJid().asBareJid()); - final Jid with; - if (account.getJid().asBareJid().equals(from.asBareJid())) { - with = to; + final boolean addressedToSelf = from.asBareJid().equals(account.getJid().asBareJid()); + final AbstractJingleConnection.Id id; + if (addressedToSelf) { + if (to.isFullJid()) { + id = AbstractJingleConnection.Id.of(account, to, sessionId); + } else { + return; + } } else { - with = from; + id = AbstractJingleConnection.Id.of(account, from, sessionId); } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received jingle message from " + from + " with=" + with + " " + message); - final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with, sessionId); final AbstractJingleConnection existingJingleConnection = connections.get(id); if (existingJingleConnection != null) { if (existingJingleConnection instanceof JingleRtpConnection) { @@ -145,9 +147,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { } return; } - if (carbonCopy) { + + if (addressedToSelf) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignore jingle message from self"); - return; } if ("propose".equals(message.getName())) { @@ -158,7 +160,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { final MessagePacket reject = mXmppConnectionService.getMessageGenerator().sessionReject(from, sessionId); mXmppConnectionService.sendMessagePacket(account, reject); } else { - final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, with); + final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, from); this.connections.put(id, rtpConnection); rtpConnection.deliveryMessage(from, message); } @@ -167,7 +169,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } else if ("proceed".equals(message.getName())) { - final RtpSessionProposal proposal = new RtpSessionProposal(account, with.asBareJid(), sessionId); + final RtpSessionProposal proposal = new RtpSessionProposal(account, from.asBareJid(), sessionId); synchronized (rtpSessionProposals) { if (rtpSessionProposals.remove(proposal) != null) { final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, account.getJid()); @@ -175,16 +177,16 @@ public class JingleConnectionManager extends AbstractConnectionManager { rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED); rtpConnection.deliveryMessage(from, message); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + with + " to deliver proceed"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver proceed"); } } } else if ("reject".equals(message.getName())) { - final RtpSessionProposal proposal = new RtpSessionProposal(account, with.asBareJid(), sessionId); + final RtpSessionProposal proposal = new RtpSessionProposal(account, from.asBareJid(), sessionId); synchronized (rtpSessionProposals) { if (rtpSessionProposals.remove(proposal) != null) { mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, proposal.with, proposal.sessionId, RtpEndUserState.DECLINED_OR_BUSY); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + with + " to deliver reject"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver reject"); } } } else { From c6db651322cf069c7ea845112c140592203bb72f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 10 Apr 2020 21:33:08 +0200 Subject: [PATCH 077/182] allow all jingle states to transition into terminated --- .../xmpp/jingle/JingleRtpConnection.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index d03505fd8..e184fb8c8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -60,21 +60,25 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web )); transitionBuilder.put(State.SESSION_INITIALIZED, ImmutableList.of( State.SESSION_ACCEPTED, - State.TERMINATED_CANCEL_OR_TIMEOUT, + State.TERMINATED_SUCCESS, State.TERMINATED_DECLINED_OR_BUSY, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_CONNECTIVITY_ERROR //at this state used for IQ errors and IQ timeouts + State.TERMINATED_CONNECTIVITY_ERROR, //at this state used for IQ errors and IQ timeouts + State.TERMINATED_CANCEL_OR_TIMEOUT, + State.TERMINATED_APPLICATION_FAILURE )); transitionBuilder.put(State.SESSION_INITIALIZED_PRE_APPROVED, ImmutableList.of( State.SESSION_ACCEPTED, - State.TERMINATED_CANCEL_OR_TIMEOUT, + State.TERMINATED_SUCCESS, State.TERMINATED_DECLINED_OR_BUSY, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_CONNECTIVITY_ERROR //at this state used for IQ errors and IQ timeouts + State.TERMINATED_CONNECTIVITY_ERROR, //at this state used for IQ errors and IQ timeouts + State.TERMINATED_CANCEL_OR_TIMEOUT, + State.TERMINATED_APPLICATION_FAILURE )); transitionBuilder.put(State.SESSION_ACCEPTED, ImmutableList.of( State.TERMINATED_SUCCESS, + State.TERMINATED_DECLINED_OR_BUSY, State.TERMINATED_CONNECTIVITY_ERROR, + State.TERMINATED_CANCEL_OR_TIMEOUT, State.TERMINATED_APPLICATION_FAILURE )); VALID_TRANSITIONS = transitionBuilder.build(); From c9f7e174f79de9c7f47bc09a9666d9ef1f28cd10 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 11 Apr 2020 19:05:07 +0200 Subject: [PATCH 078/182] use foreground service for ongoing call notification --- .../services/NotificationService.java | 12 +++---- .../services/XmppConnectionService.java | 36 ++++++++++++++++--- .../xmpp/jingle/JingleRtpConnection.java | 4 +-- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index bd8e0dabb..75097a4ae 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -76,7 +76,7 @@ public class NotificationService { private static final int NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 2; private static final int ERROR_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 6; private static final int INCOMING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 8; - private static final int ONGOING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 10; + public static final int ONGOING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 10; private final XmppConnectionService mXmppConnectionService; private final LinkedHashMap> notifications = new LinkedHashMap<>(); private final HashMap mBacklogMessageCounter = new HashMap<>(); @@ -362,10 +362,10 @@ public class NotificationService { .build()); final Notification notification = builder.build(); notification.flags = notification.flags | Notification.FLAG_INSISTENT; - notify(INCOMING_CALL_NOTIFICATION_ID, builder.build()); + notify(INCOMING_CALL_NOTIFICATION_ID, notification); } - public void showOngoingCallNotification(final AbstractJingleConnection.Id id) { + public Notification getOngoingCallNotification(final AbstractJingleConnection.Id id) { final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls"); builder.setSmallIcon(R.drawable.ic_call_white_24dp); builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call)); @@ -380,9 +380,7 @@ public class NotificationService { mXmppConnectionService.getString(R.string.hang_up), createCallAction(id.sessionId, XmppConnectionService.ACTION_END_CALL, 104)) .build()); - final Notification notification = builder.build(); - notification.flags = notification.flags | Notification.FLAG_INSISTENT; - notify(ONGOING_CALL_NOTIFICATION_ID, builder.build()); + return builder.build(); } private PendingIntent createPendingRtpSession(final AbstractJingleConnection.Id id, final String action, final int requestCode) { @@ -1129,7 +1127,7 @@ public class NotificationService { } } - private void cancel(int id) { + public void cancel(int id) { final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService); try { notificationManager.cancel(id); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index ed2e5ba5a..2ad2c49f3 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -71,6 +71,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -142,6 +143,7 @@ import eu.siacs.conversations.xmpp.Patches; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.forms.Data; +import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; @@ -209,6 +211,7 @@ public class XmppConnectionService extends Service { private AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false); private AtomicBoolean mForceForegroundService = new AtomicBoolean(false); private AtomicBoolean mForceDuringOnCreate = new AtomicBoolean(false); + private AtomicReference ongoingCall = new AtomicReference<>(); private OnMessagePacketReceived mMessageParser = new MessageParser(this); private OnPresencePacketReceived mPresenceParser = new PresenceParser(this); private IqParser mIqParser = new IqParser(this); @@ -1227,11 +1230,31 @@ public class XmppConnectionService extends Service { toggleForegroundService(false); } + public void setOngoingCall(AbstractJingleConnection.Id id) { + ongoingCall.set(id); + toggleForegroundService(false); + } + + public void removeOngoingCall(AbstractJingleConnection.Id id) { + if (ongoingCall.compareAndSet(id, null)) { + toggleForegroundService(false); + } + } + private void toggleForegroundService(boolean force) { final boolean status; - if (force || mForceDuringOnCreate.get() || mForceForegroundService.get() || (Compatibility.keepForegroundService(this) && hasEnabledAccounts())) { - final Notification notification = this.mNotificationService.createForegroundNotification(); - startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, notification); + final AbstractJingleConnection.Id ongoing = ongoingCall.get(); + if (force || mForceDuringOnCreate.get() || mForceForegroundService.get() || ongoing != null || (Compatibility.keepForegroundService(this) && hasEnabledAccounts())) { + final Notification notification; + if (ongoing != null) { + notification = this.mNotificationService.getOngoingCallNotification(ongoing); + startForeground(NotificationService.ONGOING_CALL_NOTIFICATION_ID, notification); + mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID); + } else { + notification = this.mNotificationService.createForegroundNotification(); + startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, notification); + } + if (!mForceForegroundService.get()) { mNotificationService.notify(NotificationService.FOREGROUND_NOTIFICATION_ID, notification); } @@ -1241,7 +1264,10 @@ public class XmppConnectionService extends Service { status = false; } if (!mForceForegroundService.get()) { - mNotificationService.dismissForcedForegroundNotification(); //if the channel was changed the previous call might fail + mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID); + } + if (ongoing == null) { + mNotificationService.cancel(NotificationService.ONGOING_CALL_NOTIFICATION_ID); } Log.d(Config.LOGTAG, "ForegroundService: " + (status ? "on" : "off")); } @@ -1253,7 +1279,7 @@ public class XmppConnectionService extends Service { @Override public void onTaskRemoved(final Intent rootIntent) { super.onTaskRemoved(rootIntent); - if ((Compatibility.keepForegroundService(this) && hasEnabledAccounts()) || mForceForegroundService.get()) { + if ((Compatibility.keepForegroundService(this) && hasEnabledAccounts()) || mForceForegroundService.get() || ongoingCall.get() != null) { Log.d(Config.LOGTAG, "ignoring onTaskRemoved because foreground service is activated"); } else { this.logoutAndSave(false); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index e184fb8c8..83416f7cb 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -772,9 +772,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void updateOngoingCallNotification() { if (STATES_SHOWING_ONGOING_CALL.contains(this.state)) { - xmppConnectionService.getNotificationService().showOngoingCallNotification(id); + xmppConnectionService.setOngoingCall(id); } else { - xmppConnectionService.getNotificationService().cancelOngoingCallNotification(); + xmppConnectionService.removeOngoingCall(id); } } From 609120c0d88c2848960435d1841085815e4bba2c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 11 Apr 2020 19:47:30 +0200 Subject: [PATCH 079/182] only ever create one wake lock in rtpsessionactivity --- .../conversations/services/NotificationService.java | 4 ---- .../conversations/services/XmppConnectionService.java | 2 +- .../eu/siacs/conversations/ui/RtpSessionActivity.java | 11 ++++++----- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 75097a4ae..c2665ea8b 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -1105,10 +1105,6 @@ public class NotificationService { notify(FOREGROUND_NOTIFICATION_ID, notification); } - void dismissForcedForegroundNotification() { - cancel(FOREGROUND_NOTIFICATION_ID); - } - private void notify(String tag, int id, Notification notification) { final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService); try { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 2ad2c49f3..24c80713c 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1273,7 +1273,7 @@ public class XmppConnectionService extends Service { } public boolean foregroundNotificationNeedsUpdatingWhenErrorStateChanges() { - return !mForceForegroundService.get() && Compatibility.keepForegroundService(this) && hasEnabledAccounts(); + return !mForceForegroundService.get() && ongoingCall.get() == null && Compatibility.keepForegroundService(this) && hasEnabledAccounts(); } @Override diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index c98d55cb6..73c066eec 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -115,7 +115,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe return; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - this.mProximityWakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, PROXIMITY_WAKE_LOCK_TAG); + if (this.mProximityWakeLock == null) { + this.mProximityWakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, PROXIMITY_WAKE_LOCK_TAG); + } if (!this.mProximityWakeLock.isHeld()) { Log.d(Config.LOGTAG, "acquiring wake lock"); this.mProximityWakeLock.acquire(); @@ -139,6 +141,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override public void onNewIntent(final Intent intent) { super.onNewIntent(intent); + //TODO. deal with 'pending intent' in case background service isn’t here yet. final Account account = extractAccount(intent); final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH)); final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID); @@ -211,10 +214,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override public void onStop() { - if (!isChangingConfigurations()) { - releaseWakeLock(); - //TODO maybe we want to finish if call had ended - } + releaseWakeLock(); + //TODO maybe we want to finish if call had ended super.onStop(); } From deaa76b5ca46d07a967ebfc9945f4659bbc8509f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 11 Apr 2020 20:51:37 +0200 Subject: [PATCH 080/182] when using onNewIntent make sure to store intent otherwise onBackground might just overwrite it again --- .../eu/siacs/conversations/ui/RtpSessionActivity.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 73c066eec..fb4de0e95 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -141,7 +141,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override public void onNewIntent(final Intent intent) { super.onNewIntent(intent); - //TODO. deal with 'pending intent' in case background service isn’t here yet. + setIntent(intent); + if (xmppConnectionService == null) { + Log.d(Config.LOGTAG,"RtpSessionActivity: background service wasn't bound in onNewIntent()"); + return; + } final Account account = extractAccount(intent); final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH)); final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID); @@ -239,6 +243,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe finish(); return; } + if (currentState == RtpEndUserState.INCOMING_CALL) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } if (JingleRtpConnection.STATES_SHOWING_ONGOING_CALL.contains(requireRtpConnection().getState())) { putScreenInCallMode(); } From 82f9a77777fa1897e0bafb2a1265835720a27050 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 12 Apr 2020 08:32:34 +0200 Subject: [PATCH 081/182] be more conservative when parsing rtp content --- .../xmpp/jingle/SessionDescription.java | 20 ++++++++++++++++++- .../xmpp/jingle/stanzas/RtpDescription.java | 15 ++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index f6f12513d..62424e81f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -3,6 +3,7 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Log; import android.util.Pair; +import com.google.common.base.CharMatcher; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.ArrayListMultimap; @@ -131,6 +132,8 @@ public class SessionDescription { final ImmutableList.Builder mediaListBuilder = new ImmutableList.Builder<>(); final Group group = contentMap.group; if (group != null) { + final String semantics = group.getSemantics(); + checkNoWhitespace(semantics, "group semantics value must not contain any whitespace"); attributeMap.put("group", group.getSemantics() + " " + Joiner.on(' ').join(group.getIdentificationTags())); } @@ -150,9 +153,11 @@ public class SessionDescription { if (!Strings.isNullOrEmpty(ufrag)) { mediaAttributes.put("ice-ufrag", ufrag); } + checkNoWhitespace(ufrag, "ufrag value must not contain any whitespaces"); if (!Strings.isNullOrEmpty(pwd)) { mediaAttributes.put("ice-pwd", pwd); } + checkNoWhitespace(pwd, "pwd value must not contain any whitespaces"); mediaAttributes.put("ice-options", HARDCODED_ICE_OPTIONS); final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); if (fingerprint != null) { @@ -180,6 +185,7 @@ public class SessionDescription { if (Strings.isNullOrEmpty(type)) { throw new IllegalArgumentException("a feedback for payload-type " + id + " negotiation is missing type"); } + checkNoWhitespace(type, "feedback negotiation type must not contain whitespace"); mediaAttributes.put("rtcp-fb", id + " " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); } for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : payloadType.feedbackNegotiationTrrInts()) { @@ -193,6 +199,7 @@ public class SessionDescription { if (Strings.isNullOrEmpty(type)) { throw new IllegalArgumentException("a feedback negotiation is missing type"); } + checkNoWhitespace(type, "feedback negotiation type must not contain whitespace"); mediaAttributes.put("rtcp-fb", "* " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); } for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : description.feedbackNegotiationTrrInts()) { @@ -204,9 +211,11 @@ public class SessionDescription { if (Strings.isNullOrEmpty(id)) { throw new IllegalArgumentException("A header extension is missing id"); } + checkNoWhitespace(id, "header extension id must not contain whitespace"); if (Strings.isNullOrEmpty(uri)) { throw new IllegalArgumentException("A header extension is missing uri"); } + checkNoWhitespace(uri, "feedback negotiation uri must not contain whitespace"); mediaAttributes.put("extmap", id + " " + uri); } for (RtpDescription.SourceGroup sourceGroup : description.getSourceGroups()) { @@ -215,6 +224,7 @@ public class SessionDescription { if (Strings.isNullOrEmpty(semantics)) { throw new IllegalArgumentException("A SSRC group is missing semantics attribute"); } + checkNoWhitespace(semantics, "source group semantics must not contain whitespace"); if (groups.size() == 0) { throw new IllegalArgumentException("A SSRC group is missing SSRC ids"); } @@ -228,13 +238,14 @@ public class SessionDescription { if (Strings.isNullOrEmpty(id)) { throw new IllegalArgumentException("A source specific media attribute is missing the id"); } + checkNoWhitespace(id, "A source specific media attributes must not contain whitespaces"); if (Strings.isNullOrEmpty(parameterName)) { throw new IllegalArgumentException("A source specific media attribute is missing its name"); } if (Strings.isNullOrEmpty(parameterValue)) { throw new IllegalArgumentException("A source specific media attribute is missing its value"); } - mediaAttributes.put("ssrc", id + " " + parameter.getParameterName() + ":" + parameter.getParameterValue()); + mediaAttributes.put("ssrc", id + " " + parameterName + ":" + parameterValue); } } @@ -263,6 +274,13 @@ public class SessionDescription { return sessionDescriptionBuilder.createSessionDescription(); } + public static String checkNoWhitespace(final String input, final String message) { + if (CharMatcher.whitespace().matchesAnyOf(input)) { + throw new IllegalArgumentException(message); + } + return input; + } + public static int ignorantIntParser(final String input) { try { return Integer.parseInt(input); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index 00b2ddc60..5f1a76af9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -232,7 +232,10 @@ public class RtpDescription extends GenericDescription { public String toSdpAttribute() { final int channels = getChannels(); - return getId()+" "+getPayloadTypeName()+"/"+getClockRate()+(channels == 1 ? "" : "/"+channels); + final String name = getPayloadTypeName(); + Preconditions.checkArgument(name != null, "Payload-type name must not be empty"); + SessionDescription.checkNoWhitespace(name, "payload-type name must not contain whitespaces"); + return getId()+" "+name+"/"+getClockRate()+(channels == 1 ? "" : "/"+channels); } public int getIntId() { @@ -367,7 +370,15 @@ public class RtpDescription extends GenericDescription { stringBuilder.append(id).append(' '); for(int i = 0; i < parameters.size(); ++i) { Parameter p = parameters.get(i); - stringBuilder.append(p.getParameterName()).append('=').append(p.getParameterValue()); + final String name = p.getParameterName(); + Preconditions.checkArgument(name != null, String.format("parameter for %s must have a name", id)); + SessionDescription.checkNoWhitespace(name, String.format("parameter names for %s must not contain whitespaces", id)); + + final String value = p.getParameterValue(); + Preconditions.checkArgument(value != null, String.format("parameter for %s must have a value", id)); + SessionDescription.checkNoWhitespace(value, String.format("parameter values for %s must not contain whitespaces", id)); + + stringBuilder.append(name).append('=').append(value); if (i != parameters.size() - 1) { stringBuilder.append(';'); } From 1dc88f38ca7a7a29c74fa09b8bacee3c297152a2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 12 Apr 2020 09:59:32 +0200 Subject: [PATCH 082/182] avoid terminating twice --- .../jingle/JingleFileTransferConnection.java | 2 +- .../xmpp/jingle/JingleRtpConnection.java | 24 +++++++++++--- .../xmpp/jingle/stanzas/JinglePacket.java | 31 ++++++++++++++----- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index dab5b8381..b375398d2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -245,7 +245,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple if (action == JinglePacket.Action.SESSION_INITIATE) { init(packet); } else if (action == JinglePacket.Action.SESSION_TERMINATE) { - final Reason reason = packet.getReason(); + final Reason reason = packet.getReason().reason; switch (reason) { case CANCEL: this.cancelled = true; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 83416f7cb..e21e69a3d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -36,6 +36,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED ); + + private static final List TERMINATED = Arrays.asList( + State.TERMINATED_DECLINED_OR_BUSY, + State.TERMINATED_CONNECTIVITY_ERROR, + State.TERMINATED_CANCEL_OR_TIMEOUT, + State.TERMINATED_APPLICATION_FAILURE + ); + private static final Map> VALID_TRANSITIONS; static { @@ -137,11 +145,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void receiveSessionTerminate(final JinglePacket jinglePacket) { respondOk(jinglePacket); - final Reason reason = jinglePacket.getReason(); + final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason(); final State previous = this.state; - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session terminate reason=" + reason + " while in state " + previous); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session terminate reason=" + wrapper.reason + "(" + Strings.nullToEmpty(wrapper.text) + ") while in state " + previous); + if (TERMINATED.contains(previous)) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring session terminate because already in " + previous); + return; + } webRTCWrapper.close(); - transitionOrThrow(reasonToState(reason)); + transitionOrThrow(reasonToState(wrapper.reason)); if (previous == State.PROPOSED || previous == State.SESSION_INITIALIZED) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); } @@ -761,7 +773,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); updateEndUserState(); - if (newState == PeerConnection.PeerConnectionState.FAILED) { //TODO guard this in isState(initiated,initiated_approved,accepted) otherwise it might fire too late + if (newState == PeerConnection.PeerConnectionState.FAILED) { + if (TERMINATED.contains(this.state)) { + Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": not sending session-terminate after connectivity error because session is already in state "+this.state); + return; + } sendSessionTerminate(Reason.CONNECTIVITY_ERROR); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java index ef769be8e..99fa3dc60 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -69,17 +69,21 @@ public class JinglePacket extends IqPacket { addJingleChild(content); } - public Reason getReason() { - final Element reason = getJingleChild("reason"); - if (reason == null) { - return Reason.UNKNOWN; + public ReasonWrapper getReason() { + final Element reasonElement = getJingleChild("reason"); + if (reasonElement == null) { + return new ReasonWrapper(Reason.UNKNOWN,null); } - for(Element child : reason.getChildren()) { - if (!"text".equals(child.getName())) { - return Reason.of(child.getName()); + String text = null; + Reason reason = Reason.UNKNOWN; + for(Element child : reasonElement.getChildren()) { + if ("text".equals(child.getName())) { + text = child.getContent(); + } else { + reason = Reason.of(child.getName()); } } - return Reason.UNKNOWN; + return new ReasonWrapper(reason, text); } public void setReason(final Reason reason, final String text) { @@ -149,4 +153,15 @@ public class JinglePacket extends IqPacket { return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, super.toString()); } } + + + public static class ReasonWrapper { + public final Reason reason; + public final String text; + + public ReasonWrapper(Reason reason, String text) { + this.reason = reason; + this.text = text; + } + } } From 3439f40411c0bca1652933d2d182e4e41617f77c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 12 Apr 2020 17:12:59 +0200 Subject: [PATCH 083/182] show call log messages in conversation stream --- .../siacs/conversations/entities/Message.java | 26 + .../entities/RtpSessionStatus.java | 59 + .../ui/adapter/MessageAdapter.java | 1726 +++++++++-------- .../siacs/conversations/utils/UIHelper.java | 2 + .../xmpp/jingle/JingleRtpConnection.java | 62 +- .../drawable-hdpi/ic_call_made_black_18dp.png | Bin 0 -> 159 bytes .../drawable-hdpi/ic_call_made_white_18dp.png | Bin 0 -> 174 bytes .../ic_call_missed_black_18dp.png | Bin 0 -> 179 bytes .../ic_call_missed_outgoing_black_18dp.png | Bin 0 -> 180 bytes .../ic_call_missed_outgoing_white_18dp.png | Bin 0 -> 180 bytes .../ic_call_missed_white_18dp.png | Bin 0 -> 191 bytes .../ic_call_received_black_18dp.png | Bin 0 -> 159 bytes .../ic_call_received_white_18dp.png | Bin 0 -> 169 bytes .../drawable-mdpi/ic_call_made_black_18dp.png | Bin 0 -> 132 bytes .../drawable-mdpi/ic_call_made_white_18dp.png | Bin 0 -> 135 bytes .../ic_call_missed_black_18dp.png | Bin 0 -> 141 bytes .../ic_call_missed_outgoing_black_18dp.png | Bin 0 -> 134 bytes .../ic_call_missed_outgoing_white_18dp.png | Bin 0 -> 136 bytes .../ic_call_missed_white_18dp.png | Bin 0 -> 147 bytes .../ic_call_received_black_18dp.png | Bin 0 -> 133 bytes .../ic_call_received_white_18dp.png | Bin 0 -> 140 bytes .../ic_call_made_black_18dp.png | Bin 0 -> 174 bytes .../ic_call_made_white_18dp.png | Bin 0 -> 189 bytes .../ic_call_missed_black_18dp.png | Bin 0 -> 201 bytes .../ic_call_missed_outgoing_black_18dp.png | Bin 0 -> 188 bytes .../ic_call_missed_outgoing_white_18dp.png | Bin 0 -> 193 bytes .../ic_call_missed_white_18dp.png | Bin 0 -> 215 bytes .../ic_call_received_black_18dp.png | Bin 0 -> 175 bytes .../ic_call_received_white_18dp.png | Bin 0 -> 189 bytes .../ic_call_made_black_18dp.png | Bin 0 -> 202 bytes .../ic_call_made_white_18dp.png | Bin 0 -> 225 bytes .../ic_call_missed_black_18dp.png | Bin 0 -> 247 bytes .../ic_call_missed_outgoing_black_18dp.png | Bin 0 -> 235 bytes .../ic_call_missed_outgoing_white_18dp.png | Bin 0 -> 235 bytes .../ic_call_missed_white_18dp.png | Bin 0 -> 263 bytes .../ic_call_received_black_18dp.png | Bin 0 -> 202 bytes .../ic_call_received_white_18dp.png | Bin 0 -> 228 bytes .../ic_call_made_black_18dp.png | Bin 0 -> 212 bytes .../ic_call_made_white_18dp.png | Bin 0 -> 247 bytes .../ic_call_missed_black_18dp.png | Bin 0 -> 267 bytes .../ic_call_missed_outgoing_black_18dp.png | Bin 0 -> 257 bytes .../ic_call_missed_outgoing_white_18dp.png | Bin 0 -> 248 bytes .../ic_call_missed_white_18dp.png | Bin 0 -> 291 bytes .../ic_call_received_black_18dp.png | Bin 0 -> 214 bytes .../ic_call_received_white_18dp.png | Bin 0 -> 257 bytes src/main/res/layout/message_date_bubble.xml | 15 +- src/main/res/layout/message_rtp_session.xml | 38 + src/main/res/values/strings.xml | 4 + 48 files changed, 1077 insertions(+), 855 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/entities/RtpSessionStatus.java create mode 100644 src/main/res/drawable-hdpi/ic_call_made_black_18dp.png create mode 100644 src/main/res/drawable-hdpi/ic_call_made_white_18dp.png create mode 100644 src/main/res/drawable-hdpi/ic_call_missed_black_18dp.png create mode 100644 src/main/res/drawable-hdpi/ic_call_missed_outgoing_black_18dp.png create mode 100644 src/main/res/drawable-hdpi/ic_call_missed_outgoing_white_18dp.png create mode 100644 src/main/res/drawable-hdpi/ic_call_missed_white_18dp.png create mode 100644 src/main/res/drawable-hdpi/ic_call_received_black_18dp.png create mode 100644 src/main/res/drawable-hdpi/ic_call_received_white_18dp.png create mode 100644 src/main/res/drawable-mdpi/ic_call_made_black_18dp.png create mode 100644 src/main/res/drawable-mdpi/ic_call_made_white_18dp.png create mode 100644 src/main/res/drawable-mdpi/ic_call_missed_black_18dp.png create mode 100644 src/main/res/drawable-mdpi/ic_call_missed_outgoing_black_18dp.png create mode 100644 src/main/res/drawable-mdpi/ic_call_missed_outgoing_white_18dp.png create mode 100644 src/main/res/drawable-mdpi/ic_call_missed_white_18dp.png create mode 100644 src/main/res/drawable-mdpi/ic_call_received_black_18dp.png create mode 100644 src/main/res/drawable-mdpi/ic_call_received_white_18dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_call_made_black_18dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_call_made_white_18dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_call_missed_black_18dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_call_missed_outgoing_black_18dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_call_missed_outgoing_white_18dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_call_missed_white_18dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_call_received_black_18dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_call_received_white_18dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_call_made_black_18dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_call_made_white_18dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_call_missed_black_18dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_call_missed_outgoing_black_18dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_call_missed_outgoing_white_18dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_call_missed_white_18dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_call_received_black_18dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_call_received_white_18dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_call_made_black_18dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_call_made_white_18dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_call_missed_black_18dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_call_missed_outgoing_black_18dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_call_missed_outgoing_white_18dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_call_missed_white_18dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_call_received_black_18dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_call_received_white_18dp.png create mode 100644 src/main/res/layout/message_rtp_session.xml diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index e44d2a2c0..f266c18e3 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -57,6 +57,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable public static final int TYPE_STATUS = 3; public static final int TYPE_PRIVATE = 4; public static final int TYPE_PRIVATE_FILE = 5; + public static final int TYPE_RTP_SESSION = 6; public static final String CONVERSATION = "conversationUuid"; public static final String COUNTERPART = "counterpart"; @@ -151,6 +152,31 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable null); } + public Message(Conversation conversation, int status, int type, final String remoteMsgId) { + this(conversation, java.util.UUID.randomUUID().toString(), + conversation.getUuid(), + conversation.getJid() == null ? null : conversation.getJid().asBareJid(), + null, + null, + System.currentTimeMillis(), + Message.ENCRYPTION_NONE, + status, + type, + false, + remoteMsgId, + null, + null, + null, + true, + null, + false, + null, + null, + false, + false, + null); + } + protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart, final Jid trueCounterpart, final String body, final long timeSent, final int encryption, final int status, final int type, final boolean carbon, diff --git a/src/main/java/eu/siacs/conversations/entities/RtpSessionStatus.java b/src/main/java/eu/siacs/conversations/entities/RtpSessionStatus.java new file mode 100644 index 000000000..8e360cb27 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/RtpSessionStatus.java @@ -0,0 +1,59 @@ +package eu.siacs.conversations.entities; + +import android.support.annotation.DrawableRes; + +import com.google.common.base.Strings; + +import eu.siacs.conversations.R; + +public class RtpSessionStatus { + + public final boolean successful; + public final long duration; + + + public RtpSessionStatus(boolean successful, long duration) { + this.successful = successful; + this.duration = duration; + } + + @Override + public String toString() { + return successful + ":" + duration; + } + + public static RtpSessionStatus of(final String body) { + final String[] parts = Strings.nullToEmpty(body).split(":", 2); + long duration = 0; + if (parts.length == 2) { + try { + duration = Long.parseLong(parts[1]); + } catch (NumberFormatException e) { + //do nothing + } + } + boolean made; + try { + made = Boolean.parseBoolean(parts[0]); + } catch (Exception e) { + made = false; + } + return new RtpSessionStatus(made, duration); + } + + public static @DrawableRes int getDrawable(final boolean received, final boolean successful, final boolean darkTheme) { + if (received) { + if (successful) { + return darkTheme ? R.drawable.ic_call_received_white_18dp : R.drawable.ic_call_received_black_18dp; + } else { + return darkTheme ? R.drawable.ic_call_missed_white_18dp : R.drawable.ic_call_missed_black_18dp; + } + } else { + if (successful) { + return darkTheme ? R.drawable.ic_call_made_white_18dp : R.drawable.ic_call_made_black_18dp; + } else { + return darkTheme ? R.drawable.ic_call_missed_outgoing_white_18dp : R.drawable.ic_call_missed_outgoing_black_18dp; + } + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 3ee927e00..a96d67b34 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -49,6 +49,7 @@ import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message.FileParams; +import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.http.P1S3UrlStreamHandler; import eu.siacs.conversations.persistance.FileBackend; @@ -72,926 +73,959 @@ import eu.siacs.conversations.utils.Emoticons; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.StylingHelper; +import eu.siacs.conversations.utils.TimeframeUtils; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.mam.MamReference; import rocks.xmpp.addr.Jid; public class MessageAdapter extends ArrayAdapter implements CopyTextView.CopyHandler { - public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR"; - private static final int SENT = 0; - private static final int RECEIVED = 1; - private static final int STATUS = 2; - private static final int DATE_SEPARATOR = 3; - private final XmppActivity activity; - private final ListSelectionManager listSelectionManager = new ListSelectionManager(); - private final AudioPlayer audioPlayer; - private List highlightedTerm = null; - private DisplayMetrics metrics; - private OnContactPictureClicked mOnContactPictureClickedListener; - private OnContactPictureLongClicked mOnContactPictureLongClickedListener; - private boolean mUseGreenBackground = false; - private OnQuoteListener onQuoteListener; - public MessageAdapter(XmppActivity activity, List messages) { - super(activity, 0, messages); - this.audioPlayer = new AudioPlayer(this); - this.activity = activity; - metrics = getContext().getResources().getDisplayMetrics(); - updatePreferences(); - } + public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR"; + private static final int SENT = 0; + private static final int RECEIVED = 1; + private static final int STATUS = 2; + private static final int DATE_SEPARATOR = 3; + private static final int RTP_SESSION = 4; + private final XmppActivity activity; + private final ListSelectionManager listSelectionManager = new ListSelectionManager(); + private final AudioPlayer audioPlayer; + private List highlightedTerm = null; + private DisplayMetrics metrics; + private OnContactPictureClicked mOnContactPictureClickedListener; + private OnContactPictureLongClicked mOnContactPictureLongClickedListener; + private boolean mUseGreenBackground = false; + private OnQuoteListener onQuoteListener; + + public MessageAdapter(XmppActivity activity, List messages) { + super(activity, 0, messages); + this.audioPlayer = new AudioPlayer(this); + this.activity = activity; + metrics = getContext().getResources().getDisplayMetrics(); + updatePreferences(); + } + private static void resetClickListener(View... views) { + for (View view : views) { + view.setOnClickListener(null); + } + } - private static void resetClickListener(View... views) { - for (View view : views) { - view.setOnClickListener(null); - } - } + public void flagScreenOn() { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } - public void flagScreenOn() { - activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } + public void flagScreenOff() { + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } - public void flagScreenOff() { - activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } + public void setOnContactPictureClicked(OnContactPictureClicked listener) { + this.mOnContactPictureClickedListener = listener; + } - public void setOnContactPictureClicked(OnContactPictureClicked listener) { - this.mOnContactPictureClickedListener = listener; - } + public Activity getActivity() { + return activity; + } - public Activity getActivity() { - return activity; - } + public void setOnContactPictureLongClicked( + OnContactPictureLongClicked listener) { + this.mOnContactPictureLongClickedListener = listener; + } - public void setOnContactPictureLongClicked( - OnContactPictureLongClicked listener) { - this.mOnContactPictureLongClickedListener = listener; - } + public void setOnQuoteListener(OnQuoteListener listener) { + this.onQuoteListener = listener; + } - public void setOnQuoteListener(OnQuoteListener listener) { - this.onQuoteListener = listener; - } + @Override + public int getViewTypeCount() { + return 5; + } - @Override - public int getViewTypeCount() { - return 4; - } + private int getItemViewType(Message message) { + if (message.getType() == Message.TYPE_STATUS) { + if (DATE_SEPARATOR_BODY.equals(message.getBody())) { + return DATE_SEPARATOR; + } else { + return STATUS; + } + } else if (message.getType() == Message.TYPE_RTP_SESSION) { + return RTP_SESSION; + } else if (message.getStatus() <= Message.STATUS_RECEIVED) { + return RECEIVED; + } else { + return SENT; + } + } - private int getItemViewType(Message message) { - if (message.getType() == Message.TYPE_STATUS) { - if (DATE_SEPARATOR_BODY.equals(message.getBody())) { - return DATE_SEPARATOR; - } else { - return STATUS; - } - } else if (message.getStatus() <= Message.STATUS_RECEIVED) { - return RECEIVED; - } + @Override + public int getItemViewType(int position) { + return this.getItemViewType(getItem(position)); + } - return SENT; - } + private int getMessageTextColor(boolean onDark, boolean primary) { + if (onDark) { + return ContextCompat.getColor(activity, primary ? R.color.white : R.color.white70); + } else { + return ContextCompat.getColor(activity, primary ? R.color.black87 : R.color.black54); + } + } - @Override - public int getItemViewType(int position) { - return this.getItemViewType(getItem(position)); - } + private void displayStatus(ViewHolder viewHolder, Message message, int type, boolean darkBackground) { + String filesize = null; + String info = null; + boolean error = false; + if (viewHolder.indicatorReceived != null) { + viewHolder.indicatorReceived.setVisibility(View.GONE); + } - private int getMessageTextColor(boolean onDark, boolean primary) { - if (onDark) { - return ContextCompat.getColor(activity, primary ? R.color.white : R.color.white70); - } else { - return ContextCompat.getColor(activity, primary ? R.color.black87 : R.color.black54); - } - } + if (viewHolder.edit_indicator != null) { + if (message.edited()) { + viewHolder.edit_indicator.setVisibility(View.VISIBLE); + viewHolder.edit_indicator.setImageResource(darkBackground ? R.drawable.ic_mode_edit_white_18dp : R.drawable.ic_mode_edit_black_18dp); + viewHolder.edit_indicator.setAlpha(darkBackground ? 0.7f : 0.57f); + } else { + viewHolder.edit_indicator.setVisibility(View.GONE); + } + } + final Transferable transferable = message.getTransferable(); + boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI + && message.getMergedStatus() <= Message.STATUS_RECEIVED; + if (message.isFileOrImage() || transferable != null || MessageUtils.unInitiatedButKnownSize(message)) { + FileParams params = message.getFileParams(); + filesize = params.size > 0 ? UIHelper.filesizeToString(params.size) : null; + if (transferable != null && (transferable.getStatus() == Transferable.STATUS_FAILED || transferable.getStatus() == Transferable.STATUS_CANCELLED)) { + error = true; + } + } + switch (message.getMergedStatus()) { + case Message.STATUS_WAITING: + info = getContext().getString(R.string.waiting); + break; + case Message.STATUS_UNSEND: + if (transferable != null) { + info = getContext().getString(R.string.sending_file, transferable.getProgress()); + } else { + info = getContext().getString(R.string.sending); + } + break; + case Message.STATUS_OFFERED: + info = getContext().getString(R.string.offering); + break; + case Message.STATUS_SEND_RECEIVED: + case Message.STATUS_SEND_DISPLAYED: + viewHolder.indicatorReceived.setImageResource(darkBackground ? R.drawable.ic_done_white_18dp : R.drawable.ic_done_black_18dp); + viewHolder.indicatorReceived.setAlpha(darkBackground ? 0.7f : 0.57f); + viewHolder.indicatorReceived.setVisibility(View.VISIBLE); + break; + case Message.STATUS_SEND_FAILED: + final String errorMessage = message.getErrorMessage(); + if (Message.ERROR_MESSAGE_CANCELLED.equals(errorMessage)) { + info = getContext().getString(R.string.cancelled); + } else if (errorMessage != null) { + final String[] errorParts = errorMessage.split("\\u001f", 2); + if (errorParts.length == 2) { + switch (errorParts[0]) { + case "file-too-large": + info = getContext().getString(R.string.file_too_large); + break; + default: + info = getContext().getString(R.string.send_failed); + break; + } + } else { + info = getContext().getString(R.string.send_failed); + } + } else { + info = getContext().getString(R.string.send_failed); + } + error = true; + break; + default: + if (multiReceived) { + info = UIHelper.getMessageDisplayName(message); + } + break; + } + if (error && type == SENT) { + if (darkBackground) { + viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Warning_OnDark); + } else { + viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Warning); + } + } else { + if (darkBackground) { + viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_OnDark); + } else { + viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption); + } + viewHolder.time.setTextColor(this.getMessageTextColor(darkBackground, false)); + } + if (message.getEncryption() == Message.ENCRYPTION_NONE) { + viewHolder.indicator.setVisibility(View.GONE); + } else { + boolean verified = false; + if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { + final FingerprintStatus status = message.getConversation() + .getAccount().getAxolotlService().getFingerprintTrust( + message.getFingerprint()); + if (status != null && status.isVerified()) { + verified = true; + } + } + if (verified) { + viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_verified_user_white_18dp : R.drawable.ic_verified_user_black_18dp); + } else { + viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_lock_white_18dp : R.drawable.ic_lock_black_18dp); + } + if (darkBackground) { + viewHolder.indicator.setAlpha(0.7f); + } else { + viewHolder.indicator.setAlpha(0.57f); + } + viewHolder.indicator.setVisibility(View.VISIBLE); + } - private void displayStatus(ViewHolder viewHolder, Message message, int type, boolean darkBackground) { - String filesize = null; - String info = null; - boolean error = false; - if (viewHolder.indicatorReceived != null) { - viewHolder.indicatorReceived.setVisibility(View.GONE); - } + final String formattedTime = UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent()); + final String bodyLanguage = message.getBodyLanguage(); + final String bodyLanguageInfo = bodyLanguage == null ? "" : String.format(" \u00B7 %s", bodyLanguage.toUpperCase(Locale.US)); + if (message.getStatus() <= Message.STATUS_RECEIVED) { + if ((filesize != null) && (info != null)) { + viewHolder.time.setText(formattedTime + " \u00B7 " + filesize + " \u00B7 " + info + bodyLanguageInfo); + } else if ((filesize == null) && (info != null)) { + viewHolder.time.setText(formattedTime + " \u00B7 " + info + bodyLanguageInfo); + } else if ((filesize != null) && (info == null)) { + viewHolder.time.setText(formattedTime + " \u00B7 " + filesize + bodyLanguageInfo); + } else { + viewHolder.time.setText(formattedTime + bodyLanguageInfo); + } + } else { + if ((filesize != null) && (info != null)) { + viewHolder.time.setText(filesize + " \u00B7 " + info + bodyLanguageInfo); + } else if ((filesize == null) && (info != null)) { + if (error) { + viewHolder.time.setText(info + " \u00B7 " + formattedTime + bodyLanguageInfo); + } else { + viewHolder.time.setText(info); + } + } else if ((filesize != null) && (info == null)) { + viewHolder.time.setText(filesize + " \u00B7 " + formattedTime + bodyLanguageInfo); + } else { + viewHolder.time.setText(formattedTime + bodyLanguageInfo); + } + } + } - if (viewHolder.edit_indicator != null) { - if (message.edited()) { - viewHolder.edit_indicator.setVisibility(View.VISIBLE); - viewHolder.edit_indicator.setImageResource(darkBackground ? R.drawable.ic_mode_edit_white_18dp : R.drawable.ic_mode_edit_black_18dp); - viewHolder.edit_indicator.setAlpha(darkBackground ? 0.7f : 0.57f); - } else { - viewHolder.edit_indicator.setVisibility(View.GONE); - } - } - final Transferable transferable = message.getTransferable(); - boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI - && message.getMergedStatus() <= Message.STATUS_RECEIVED; - if (message.isFileOrImage() || transferable != null || MessageUtils.unInitiatedButKnownSize(message)) { - FileParams params = message.getFileParams(); - filesize = params.size > 0 ? UIHelper.filesizeToString(params.size) : null; - if (transferable != null && (transferable.getStatus() == Transferable.STATUS_FAILED || transferable.getStatus() == Transferable.STATUS_CANCELLED)) { - error = true; - } - } - switch (message.getMergedStatus()) { - case Message.STATUS_WAITING: - info = getContext().getString(R.string.waiting); - break; - case Message.STATUS_UNSEND: - if (transferable != null) { - info = getContext().getString(R.string.sending_file, transferable.getProgress()); - } else { - info = getContext().getString(R.string.sending); - } - break; - case Message.STATUS_OFFERED: - info = getContext().getString(R.string.offering); - break; - case Message.STATUS_SEND_RECEIVED: - case Message.STATUS_SEND_DISPLAYED: - viewHolder.indicatorReceived.setImageResource(darkBackground ? R.drawable.ic_done_white_18dp : R.drawable.ic_done_black_18dp); - viewHolder.indicatorReceived.setAlpha(darkBackground ? 0.7f : 0.57f); - viewHolder.indicatorReceived.setVisibility(View.VISIBLE); - break; - case Message.STATUS_SEND_FAILED: - final String errorMessage = message.getErrorMessage(); - if (Message.ERROR_MESSAGE_CANCELLED.equals(errorMessage)) { - info = getContext().getString(R.string.cancelled); - } else if (errorMessage != null) { - final String[] errorParts = errorMessage.split("\\u001f", 2); - if (errorParts.length == 2) { - switch (errorParts[0]) { - case "file-too-large": - info = getContext().getString(R.string.file_too_large); - break; - default: - info = getContext().getString(R.string.send_failed); - break; - } - } else { - info = getContext().getString(R.string.send_failed); - } - } else { - info = getContext().getString(R.string.send_failed); - } - error = true; - break; - default: - if (multiReceived) { - info = UIHelper.getMessageDisplayName(message); - } - break; - } - if (error && type == SENT) { - if (darkBackground) { - viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Warning_OnDark); - } else { - viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Warning); - } - } else { - if (darkBackground) { - viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_OnDark); - } else { - viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption); - } - viewHolder.time.setTextColor(this.getMessageTextColor(darkBackground, false)); - } - if (message.getEncryption() == Message.ENCRYPTION_NONE) { - viewHolder.indicator.setVisibility(View.GONE); - } else { - boolean verified = false; - if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { - final FingerprintStatus status = message.getConversation() - .getAccount().getAxolotlService().getFingerprintTrust( - message.getFingerprint()); - if (status != null && status.isVerified()) { - verified = true; - } - } - if (verified) { - viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_verified_user_white_18dp : R.drawable.ic_verified_user_black_18dp); - } else { - viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_lock_white_18dp : R.drawable.ic_lock_black_18dp); - } - if (darkBackground) { - viewHolder.indicator.setAlpha(0.7f); - } else { - viewHolder.indicator.setAlpha(0.57f); - } - viewHolder.indicator.setVisibility(View.VISIBLE); - } + private void displayInfoMessage(ViewHolder viewHolder, CharSequence text, boolean darkBackground) { + viewHolder.download_button.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + viewHolder.messageBody.setText(text); + if (darkBackground) { + viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Secondary_OnDark); + } else { + viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Secondary); + } + viewHolder.messageBody.setTextIsSelectable(false); + } - final String formattedTime = UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent()); - final String bodyLanguage = message.getBodyLanguage(); - final String bodyLanguageInfo = bodyLanguage == null ? "" : String.format(" \u00B7 %s", bodyLanguage.toUpperCase(Locale.US)); - if (message.getStatus() <= Message.STATUS_RECEIVED) { ; - if ((filesize != null) && (info != null)) { - viewHolder.time.setText(formattedTime + " \u00B7 " + filesize + " \u00B7 " + info + bodyLanguageInfo); - } else if ((filesize == null) && (info != null)) { - viewHolder.time.setText(formattedTime + " \u00B7 " + info + bodyLanguageInfo); - } else if ((filesize != null) && (info == null)) { - viewHolder.time.setText(formattedTime + " \u00B7 " + filesize + bodyLanguageInfo); - } else { - viewHolder.time.setText(formattedTime+bodyLanguageInfo); - } - } else { - if ((filesize != null) && (info != null)) { - viewHolder.time.setText(filesize + " \u00B7 " + info + bodyLanguageInfo); - } else if ((filesize == null) && (info != null)) { - if (error) { - viewHolder.time.setText(info + " \u00B7 " + formattedTime + bodyLanguageInfo); - } else { - viewHolder.time.setText(info); - } - } else if ((filesize != null) && (info == null)) { - viewHolder.time.setText(filesize + " \u00B7 " + formattedTime + bodyLanguageInfo); - } else { - viewHolder.time.setText(formattedTime+bodyLanguageInfo); - } - } - } + private void displayEmojiMessage(final ViewHolder viewHolder, final String body, final boolean darkBackground) { + viewHolder.download_button.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + if (darkBackground) { + viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Emoji_OnDark); + } else { + viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Emoji); + } + Spannable span = new SpannableString(body); + float size = Emoticons.isEmoji(body) ? 3.0f : 2.0f; + span.setSpan(new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + viewHolder.messageBody.setText(EmojiWrapper.transform(span)); + } - private void displayInfoMessage(ViewHolder viewHolder, CharSequence text, boolean darkBackground) { - viewHolder.download_button.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.image.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.VISIBLE); - viewHolder.messageBody.setText(text); - if (darkBackground) { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Secondary_OnDark); - } else { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Secondary); - } - viewHolder.messageBody.setTextIsSelectable(false); - } + private void applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground) { + if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) { + body.insert(start++, "\n"); + body.setSpan(new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + end++; + } + if (end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) { + body.insert(end, "\n"); + body.setSpan(new DividerSpan(false), end, end + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + int color = darkBackground ? this.getMessageTextColor(darkBackground, false) + : ContextCompat.getColor(activity, R.color.green700_desaturated); + DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); + body.setSpan(new QuoteSpan(color, metrics), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } - private void displayEmojiMessage(final ViewHolder viewHolder, final String body, final boolean darkBackground) { - viewHolder.download_button.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.image.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.VISIBLE); - if (darkBackground) { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Emoji_OnDark); - } else { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Emoji); - } - Spannable span = new SpannableString(body); - float size = Emoticons.isEmoji(body) ? 3.0f : 2.0f; - span.setSpan(new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - viewHolder.messageBody.setText(EmojiWrapper.transform(span)); - } + /** + * Applies QuoteSpan to group of lines which starts with > or » characters. + * Appends likebreaks and applies DividerSpan to them to show a padding between quote and text. + */ + private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) { + boolean startsWithQuote = false; + char previous = '\n'; + int lineStart = -1; + int lineTextStart = -1; + int quoteStart = -1; + for (int i = 0; i <= body.length(); i++) { + char current = body.length() > i ? body.charAt(i) : '\n'; + if (lineStart == -1) { + if (previous == '\n') { + if ((current == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(body, i)) + || current == '\u00bb' && !UIHelper.isPositionFollowedByQuote(body, i)) { + // Line start with quote + lineStart = i; + if (quoteStart == -1) quoteStart = i; + if (i == 0) startsWithQuote = true; + } else if (quoteStart >= 0) { + // Line start without quote, apply spans there + applyQuoteSpan(body, quoteStart, i - 1, darkBackground); + quoteStart = -1; + } + } + } else { + // Remove extra spaces between > and first character in the line + // > character will be removed too + if (current != ' ' && lineTextStart == -1) { + lineTextStart = i; + } + if (current == '\n') { + body.delete(lineStart, lineTextStart); + i -= lineTextStart - lineStart; + if (i == lineStart) { + // Avoid empty lines because span over empty line can be hidden + body.insert(i++, " "); + } + lineStart = -1; + lineTextStart = -1; + } + } + previous = current; + } + if (quoteStart >= 0) { + // Apply spans to finishing open quote + applyQuoteSpan(body, quoteStart, body.length(), darkBackground); + } + return startsWithQuote; + } - private void applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground) { - if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) { - body.insert(start++, "\n"); - body.setSpan(new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - end++; - } - if (end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) { - body.insert(end, "\n"); - body.setSpan(new DividerSpan(false), end, end + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - int color = darkBackground ? this.getMessageTextColor(darkBackground, false) - : ContextCompat.getColor(activity, R.color.green700_desaturated); - DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); - body.setSpan(new QuoteSpan(color, metrics), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } + private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground, int type) { + viewHolder.download_button.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); - /** - * Applies QuoteSpan to group of lines which starts with > or » characters. - * Appends likebreaks and applies DividerSpan to them to show a padding between quote and text. - */ - private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) { - boolean startsWithQuote = false; - char previous = '\n'; - int lineStart = -1; - int lineTextStart = -1; - int quoteStart = -1; - for (int i = 0; i <= body.length(); i++) { - char current = body.length() > i ? body.charAt(i) : '\n'; - if (lineStart == -1) { - if (previous == '\n') { - if ((current == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(body, i)) - || current == '\u00bb' && !UIHelper.isPositionFollowedByQuote(body, i)) { - // Line start with quote - lineStart = i; - if (quoteStart == -1) quoteStart = i; - if (i == 0) startsWithQuote = true; - } else if (quoteStart >= 0) { - // Line start without quote, apply spans there - applyQuoteSpan(body, quoteStart, i - 1, darkBackground); - quoteStart = -1; - } - } - } else { - // Remove extra spaces between > and first character in the line - // > character will be removed too - if (current != ' ' && lineTextStart == -1) { - lineTextStart = i; - } - if (current == '\n') { - body.delete(lineStart, lineTextStart); - i -= lineTextStart - lineStart; - if (i == lineStart) { - // Avoid empty lines because span over empty line can be hidden - body.insert(i++, " "); - } - lineStart = -1; - lineTextStart = -1; - } - } - previous = current; - } - if (quoteStart >= 0) { - // Apply spans to finishing open quote - applyQuoteSpan(body, quoteStart, body.length(), darkBackground); - } - return startsWithQuote; - } + if (darkBackground) { + viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_OnDark); + } else { + viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1); + } + viewHolder.messageBody.setHighlightColor(ContextCompat.getColor(activity, darkBackground + ? (type == SENT || !mUseGreenBackground ? R.color.black26 : R.color.grey800) : R.color.grey500)); + viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); - private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground, int type) { - viewHolder.download_button.setVisibility(View.GONE); - viewHolder.image.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.VISIBLE); + if (message.getBody() != null) { + final String nick = UIHelper.getMessageDisplayName(message); + SpannableStringBuilder body = message.getMergedBody(); + boolean hasMeCommand = message.hasMeCommand(); + if (hasMeCommand) { + body = body.replace(0, Message.ME_COMMAND.length(), nick + " "); + } + if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) { + body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS); + body.append("\u2026"); + } + Message.MergeSeparator[] mergeSeparators = body.getSpans(0, body.length(), Message.MergeSeparator.class); + for (Message.MergeSeparator mergeSeparator : mergeSeparators) { + int start = body.getSpanStart(mergeSeparator); + int end = body.getSpanEnd(mergeSeparator); + body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + boolean startsWithQuote = handleTextQuotes(body, darkBackground); + if (!message.isPrivateMessage()) { + if (hasMeCommand) { + body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } else { + String privateMarker; + if (message.getStatus() <= Message.STATUS_RECEIVED) { + privateMarker = activity.getString(R.string.private_message); + } else { + Jid cp = message.getCounterpart(); + privateMarker = activity.getString(R.string.private_message_to, Strings.nullToEmpty(cp == null ? null : cp.getResource())); + } + body.insert(0, privateMarker); + int privateMarkerIndex = privateMarker.length(); + if (startsWithQuote) { + body.insert(privateMarkerIndex, "\n\n"); + body.setSpan(new DividerSpan(false), privateMarkerIndex, privateMarkerIndex + 2, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + body.insert(privateMarkerIndex, " "); + } + body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + body.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + if (hasMeCommand) { + body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), privateMarkerIndex + 1, + privateMarkerIndex + 1 + nick.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + if (message.getConversation().getMode() == Conversation.MODE_MULTI && message.getStatus() == Message.STATUS_RECEIVED) { + if (message.getConversation() instanceof Conversation) { + final Conversation conversation = (Conversation) message.getConversation(); + Pattern pattern = NotificationService.generateNickHighlightPattern(conversation.getMucOptions().getActualNick()); + Matcher matcher = pattern.matcher(body); + while (matcher.find()) { + body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } + Matcher matcher = Emoticons.getEmojiPattern(body).matcher(body); + while (matcher.find()) { + if (matcher.start() < matcher.end()) { + body.setSpan(new RelativeSizeSpan(1.2f), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } - if (darkBackground) { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_OnDark); - } else { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1); - } - viewHolder.messageBody.setHighlightColor(ContextCompat.getColor(activity, darkBackground - ? (type == SENT || !mUseGreenBackground ? R.color.black26 : R.color.grey800) : R.color.grey500)); - viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); + StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor()); + if (highlightedTerm != null) { + StylingHelper.highlight(activity, body, highlightedTerm, StylingHelper.isDarkText(viewHolder.messageBody)); + } + MyLinkify.addLinks(body, true); + viewHolder.messageBody.setAutoLinkMask(0); + viewHolder.messageBody.setText(EmojiWrapper.transform(body)); + viewHolder.messageBody.setTextIsSelectable(true); + viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance()); + listSelectionManager.onUpdate(viewHolder.messageBody, message); + } else { + viewHolder.messageBody.setText(""); + viewHolder.messageBody.setTextIsSelectable(false); + } + } - if (message.getBody() != null) { - final String nick = UIHelper.getMessageDisplayName(message); - SpannableStringBuilder body = message.getMergedBody(); - boolean hasMeCommand = message.hasMeCommand(); - if (hasMeCommand) { - body = body.replace(0, Message.ME_COMMAND.length(), nick + " "); - } - if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) { - body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS); - body.append("\u2026"); - } - Message.MergeSeparator[] mergeSeparators = body.getSpans(0, body.length(), Message.MergeSeparator.class); - for (Message.MergeSeparator mergeSeparator : mergeSeparators) { - int start = body.getSpanStart(mergeSeparator); - int end = body.getSpanEnd(mergeSeparator); - body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - boolean startsWithQuote = handleTextQuotes(body, darkBackground); - if (!message.isPrivateMessage()) { - if (hasMeCommand) { - body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } else { - String privateMarker; - if (message.getStatus() <= Message.STATUS_RECEIVED) { - privateMarker = activity.getString(R.string.private_message); - } else { - Jid cp = message.getCounterpart(); - privateMarker = activity.getString(R.string.private_message_to, Strings.nullToEmpty(cp == null ? null : cp.getResource())); - } - body.insert(0, privateMarker); - int privateMarkerIndex = privateMarker.length(); - if (startsWithQuote) { - body.insert(privateMarkerIndex, "\n\n"); - body.setSpan(new DividerSpan(false), privateMarkerIndex, privateMarkerIndex + 2, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } else { - body.insert(privateMarkerIndex, " "); - } - body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - body.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - if (hasMeCommand) { - body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), privateMarkerIndex + 1, - privateMarkerIndex + 1 + nick.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - if (message.getConversation().getMode() == Conversation.MODE_MULTI && message.getStatus() == Message.STATUS_RECEIVED) { - if (message.getConversation() instanceof Conversation) { - final Conversation conversation = (Conversation) message.getConversation(); - Pattern pattern = NotificationService.generateNickHighlightPattern(conversation.getMucOptions().getActualNick()); - Matcher matcher = pattern.matcher(body); - while (matcher.find()) { - body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - } - Matcher matcher = Emoticons.getEmojiPattern(body).matcher(body); - while (matcher.find()) { - if (matcher.start() < matcher.end()) { - body.setSpan(new RelativeSizeSpan(1.2f), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } + private void displayDownloadableMessage(ViewHolder viewHolder, final Message message, String text, final boolean darkBackground) { + toggleWhisperInfo(viewHolder, message, darkBackground); + viewHolder.image.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText(text); + viewHolder.download_button.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message)); + } - StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor()); - if (highlightedTerm != null) { - StylingHelper.highlight(activity, body, highlightedTerm, StylingHelper.isDarkText(viewHolder.messageBody)); - } - MyLinkify.addLinks(body,true); - viewHolder.messageBody.setAutoLinkMask(0); - viewHolder.messageBody.setText(EmojiWrapper.transform(body)); - viewHolder.messageBody.setTextIsSelectable(true); - viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance()); - listSelectionManager.onUpdate(viewHolder.messageBody, message); - } else { - viewHolder.messageBody.setText(""); - viewHolder.messageBody.setTextIsSelectable(false); - } - } + private void displayOpenableMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground) { + toggleWhisperInfo(viewHolder, message, darkBackground); + viewHolder.image.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity, message))); + viewHolder.download_button.setOnClickListener(v -> openDownloadable(message)); + } - private void displayDownloadableMessage(ViewHolder viewHolder, final Message message, String text, final boolean darkBackground) { - toggleWhisperInfo(viewHolder, message, darkBackground); - viewHolder.image.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setText(text); - viewHolder.download_button.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message)); - } + private void displayLocationMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground) { + toggleWhisperInfo(viewHolder, message, darkBackground); + viewHolder.image.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText(R.string.show_location); + viewHolder.download_button.setOnClickListener(v -> showLocation(message)); + } - private void displayOpenableMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground) { - toggleWhisperInfo(viewHolder, message, darkBackground); - viewHolder.image.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity, message))); - viewHolder.download_button.setOnClickListener(v -> openDownloadable(message)); - } + private void displayAudioMessage(ViewHolder viewHolder, Message message, boolean darkBackground) { + toggleWhisperInfo(viewHolder, message, darkBackground); + viewHolder.image.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.GONE); + final RelativeLayout audioPlayer = viewHolder.audioPlayer; + audioPlayer.setVisibility(View.VISIBLE); + AudioPlayer.ViewHolder.get(audioPlayer).setDarkBackground(darkBackground); + this.audioPlayer.init(audioPlayer, message); + } - private void displayLocationMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground) { - toggleWhisperInfo(viewHolder, message, darkBackground); - viewHolder.image.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setText(R.string.show_location); - viewHolder.download_button.setOnClickListener(v -> showLocation(message)); - } + private void displayMediaPreviewMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground) { + toggleWhisperInfo(viewHolder, message, darkBackground); + viewHolder.download_button.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.VISIBLE); + final FileParams params = message.getFileParams(); + final double target = metrics.density * 288; + final int scaledW; + final int scaledH; + if (Math.max(params.height, params.width) * metrics.density <= target) { + scaledW = (int) (params.width * metrics.density); + scaledH = (int) (params.height * metrics.density); + } else if (Math.max(params.height, params.width) <= target) { + scaledW = params.width; + scaledH = params.height; + } else if (params.width <= params.height) { + scaledW = (int) (params.width / ((double) params.height / target)); + scaledH = (int) target; + } else { + scaledW = (int) target; + scaledH = (int) (params.height / ((double) params.width / target)); + } + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(scaledW, scaledH); + layoutParams.setMargins(0, (int) (metrics.density * 4), 0, (int) (metrics.density * 4)); + viewHolder.image.setLayoutParams(layoutParams); + activity.loadBitmap(message, viewHolder.image); + viewHolder.image.setOnClickListener(v -> openDownloadable(message)); + } - private void displayAudioMessage(ViewHolder viewHolder, Message message, boolean darkBackground) { - toggleWhisperInfo(viewHolder, message, darkBackground); - viewHolder.image.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.GONE); - final RelativeLayout audioPlayer = viewHolder.audioPlayer; - audioPlayer.setVisibility(View.VISIBLE); - AudioPlayer.ViewHolder.get(audioPlayer).setDarkBackground(darkBackground); - this.audioPlayer.init(audioPlayer, message); - } + private void toggleWhisperInfo(ViewHolder viewHolder, final Message message, final boolean darkBackground) { + if (message.isPrivateMessage()) { + final String privateMarker; + if (message.getStatus() <= Message.STATUS_RECEIVED) { + privateMarker = activity.getString(R.string.private_message); + } else { + Jid cp = message.getCounterpart(); + privateMarker = activity.getString(R.string.private_message_to, Strings.nullToEmpty(cp == null ? null : cp.getResource())); + } + final SpannableString body = new SpannableString(privateMarker); + body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)), 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + body.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + viewHolder.messageBody.setText(body); + viewHolder.messageBody.setVisibility(View.VISIBLE); + } else { + viewHolder.messageBody.setVisibility(View.GONE); + } + } - private void displayMediaPreviewMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground) { - toggleWhisperInfo(viewHolder, message, darkBackground); - viewHolder.download_button.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.image.setVisibility(View.VISIBLE); - final FileParams params = message.getFileParams(); - final double target = metrics.density * 288; - final int scaledW; - final int scaledH; - if (Math.max(params.height, params.width) * metrics.density <= target) { - scaledW = (int) (params.width * metrics.density); - scaledH = (int) (params.height * metrics.density); - } else if (Math.max(params.height, params.width) <= target) { - scaledW = params.width; - scaledH = params.height; - } else if (params.width <= params.height) { - scaledW = (int) (params.width / ((double) params.height / target)); - scaledH = (int) target; - } else { - scaledW = (int) target; - scaledH = (int) (params.height / ((double) params.width / target)); - } - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(scaledW, scaledH); - layoutParams.setMargins(0, (int) (metrics.density * 4), 0, (int) (metrics.density * 4)); - viewHolder.image.setLayoutParams(layoutParams); - activity.loadBitmap(message, viewHolder.image); - viewHolder.image.setOnClickListener(v -> openDownloadable(message)); - } + private void loadMoreMessages(Conversation conversation) { + conversation.setLastClearHistory(0, null); + activity.xmppConnectionService.updateConversation(conversation); + conversation.setHasMessagesLeftOnServer(true); + conversation.setFirstMamReference(null); + long timestamp = conversation.getLastMessageTransmitted().getTimestamp(); + if (timestamp == 0) { + timestamp = System.currentTimeMillis(); + } + conversation.messagesLoaded.set(true); + MessageArchiveService.Query query = activity.xmppConnectionService.getMessageArchiveService().query(conversation, new MamReference(0), timestamp, false); + if (query != null) { + Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(activity, R.string.not_fetching_history_retention_period, Toast.LENGTH_SHORT).show(); + } + } - private void toggleWhisperInfo(ViewHolder viewHolder, final Message message, final boolean darkBackground) { - if (message.isPrivateMessage()) { - final String privateMarker; - if (message.getStatus() <= Message.STATUS_RECEIVED) { - privateMarker = activity.getString(R.string.private_message); - } else { - Jid cp = message.getCounterpart(); - privateMarker = activity.getString(R.string.private_message_to, Strings.nullToEmpty(cp == null ? null : cp.getResource())); - } - final SpannableString body = new SpannableString(privateMarker); - body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)), 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - body.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - viewHolder.messageBody.setText(body); - viewHolder.messageBody.setVisibility(View.VISIBLE); - } else { - viewHolder.messageBody.setVisibility(View.GONE); - } - } - - private void loadMoreMessages(Conversation conversation) { - conversation.setLastClearHistory(0, null); - activity.xmppConnectionService.updateConversation(conversation); - conversation.setHasMessagesLeftOnServer(true); - conversation.setFirstMamReference(null); - long timestamp = conversation.getLastMessageTransmitted().getTimestamp(); - if (timestamp == 0) { - timestamp = System.currentTimeMillis(); - } - conversation.messagesLoaded.set(true); - MessageArchiveService.Query query = activity.xmppConnectionService.getMessageArchiveService().query(conversation, new MamReference(0), timestamp, false); - if (query != null) { - Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG).show(); - } else { - Toast.makeText(activity, R.string.not_fetching_history_retention_period, Toast.LENGTH_SHORT).show(); - } - } - - @Override - public View getView(int position, View view, ViewGroup parent) { - final Message message = getItem(position); - final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL; - final boolean isInValidSession = message.isValidInSession() && (!omemoEncryption || message.isTrusted()); - final Conversational conversation = message.getConversation(); - final Account account = conversation.getAccount(); - final int type = getItemViewType(position); - ViewHolder viewHolder; - if (view == null) { - viewHolder = new ViewHolder(); - switch (type) { - case DATE_SEPARATOR: - view = activity.getLayoutInflater().inflate(R.layout.message_date_bubble, parent, false); + @Override + public View getView(int position, View view, ViewGroup parent) { + final Message message = getItem(position); + final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL; + final boolean isInValidSession = message.isValidInSession() && (!omemoEncryption || message.isTrusted()); + final Conversational conversation = message.getConversation(); + final Account account = conversation.getAccount(); + final int type = getItemViewType(position); + ViewHolder viewHolder; + if (view == null) { + viewHolder = new ViewHolder(); + switch (type) { + case DATE_SEPARATOR: + view = activity.getLayoutInflater().inflate(R.layout.message_date_bubble, parent, false); + viewHolder.status_message = view.findViewById(R.id.message_body); + viewHolder.message_box = view.findViewById(R.id.message_box); + viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); + break; + case RTP_SESSION: + view = activity.getLayoutInflater().inflate(R.layout.message_rtp_session, parent, false); viewHolder.status_message = view.findViewById(R.id.message_body); viewHolder.message_box = view.findViewById(R.id.message_box); - break; - case SENT: - view = activity.getLayoutInflater().inflate(R.layout.message_sent, parent, false); - viewHolder.message_box = view.findViewById(R.id.message_box); - viewHolder.contact_picture = view.findViewById(R.id.message_photo); - viewHolder.download_button = view.findViewById(R.id.download_button); - viewHolder.indicator = view.findViewById(R.id.security_indicator); - viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); - viewHolder.image = view.findViewById(R.id.message_image); - viewHolder.messageBody = view.findViewById(R.id.message_body); - viewHolder.time = view.findViewById(R.id.message_time); viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); - viewHolder.audioPlayer = view.findViewById(R.id.audio_player); break; - case RECEIVED: - view = activity.getLayoutInflater().inflate(R.layout.message_received, parent, false); - viewHolder.message_box = view.findViewById(R.id.message_box); - viewHolder.contact_picture = view.findViewById(R.id.message_photo); - viewHolder.download_button = view.findViewById(R.id.download_button); - viewHolder.indicator = view.findViewById(R.id.security_indicator); - viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); - viewHolder.image = view.findViewById(R.id.message_image); - viewHolder.messageBody = view.findViewById(R.id.message_body); - viewHolder.time = view.findViewById(R.id.message_time); - viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); - viewHolder.encryption = view.findViewById(R.id.message_encryption); - viewHolder.audioPlayer = view.findViewById(R.id.audio_player); - break; - case STATUS: - view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false); - viewHolder.contact_picture = view.findViewById(R.id.message_photo); - viewHolder.status_message = view.findViewById(R.id.status_message); - viewHolder.load_more_messages = view.findViewById(R.id.load_more_messages); - break; - default: - throw new AssertionError("Unknown view type"); - } - if (viewHolder.messageBody != null) { - listSelectionManager.onCreate(viewHolder.messageBody, - new MessageBodyActionModeCallback(viewHolder.messageBody)); - viewHolder.messageBody.setCopyHandler(this); - } - view.setTag(viewHolder); - } else { - viewHolder = (ViewHolder) view.getTag(); - if (viewHolder == null) { - return view; - } - } + case SENT: + view = activity.getLayoutInflater().inflate(R.layout.message_sent, parent, false); + viewHolder.message_box = view.findViewById(R.id.message_box); + viewHolder.contact_picture = view.findViewById(R.id.message_photo); + viewHolder.download_button = view.findViewById(R.id.download_button); + viewHolder.indicator = view.findViewById(R.id.security_indicator); + viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); + viewHolder.image = view.findViewById(R.id.message_image); + viewHolder.messageBody = view.findViewById(R.id.message_body); + viewHolder.time = view.findViewById(R.id.message_time); + viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); + viewHolder.audioPlayer = view.findViewById(R.id.audio_player); + break; + case RECEIVED: + view = activity.getLayoutInflater().inflate(R.layout.message_received, parent, false); + viewHolder.message_box = view.findViewById(R.id.message_box); + viewHolder.contact_picture = view.findViewById(R.id.message_photo); + viewHolder.download_button = view.findViewById(R.id.download_button); + viewHolder.indicator = view.findViewById(R.id.security_indicator); + viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); + viewHolder.image = view.findViewById(R.id.message_image); + viewHolder.messageBody = view.findViewById(R.id.message_body); + viewHolder.time = view.findViewById(R.id.message_time); + viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); + viewHolder.encryption = view.findViewById(R.id.message_encryption); + viewHolder.audioPlayer = view.findViewById(R.id.audio_player); + break; + case STATUS: + view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false); + viewHolder.contact_picture = view.findViewById(R.id.message_photo); + viewHolder.status_message = view.findViewById(R.id.status_message); + viewHolder.load_more_messages = view.findViewById(R.id.load_more_messages); + break; + default: + throw new AssertionError("Unknown view type"); + } + if (viewHolder.messageBody != null) { + listSelectionManager.onCreate(viewHolder.messageBody, + new MessageBodyActionModeCallback(viewHolder.messageBody)); + viewHolder.messageBody.setCopyHandler(this); + } + view.setTag(viewHolder); + } else { + viewHolder = (ViewHolder) view.getTag(); + if (viewHolder == null) { + return view; + } + } - boolean darkBackground = type == RECEIVED && (!isInValidSession || mUseGreenBackground) || activity.isDarkTheme(); + boolean darkBackground = type == RECEIVED && (!isInValidSession || mUseGreenBackground) || activity.isDarkTheme(); - if (type == DATE_SEPARATOR) { - if (UIHelper.today(message.getTimeSent())) { - viewHolder.status_message.setText(R.string.today); - } else if (UIHelper.yesterday(message.getTimeSent())) { - viewHolder.status_message.setText(R.string.yesterday); - } else { - viewHolder.status_message.setText(DateUtils.formatDateTime(activity, message.getTimeSent(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR)); - } - viewHolder.message_box.setBackgroundResource(activity.isDarkTheme() ? R.drawable.date_bubble_grey : R.drawable.date_bubble_white); - return view; - } else if (type == STATUS) { - if ("LOAD_MORE".equals(message.getBody())) { - viewHolder.status_message.setVisibility(View.GONE); - viewHolder.contact_picture.setVisibility(View.GONE); - viewHolder.load_more_messages.setVisibility(View.VISIBLE); - viewHolder.load_more_messages.setOnClickListener(v -> loadMoreMessages((Conversation) message.getConversation())); - } else { - viewHolder.status_message.setVisibility(View.VISIBLE); - viewHolder.load_more_messages.setVisibility(View.GONE); - viewHolder.status_message.setText(message.getBody()); - boolean showAvatar; - if (conversation.getMode() == Conversation.MODE_SINGLE) { - showAvatar = true; - AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar_on_status_message); - } else if (message.getCounterpart() != null || message.getTrueCounterpart() != null || (message.getCounterparts() != null && message.getCounterparts().size() > 0)) { - showAvatar = true; - AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar_on_status_message); - } else { - showAvatar = false; - } - if (showAvatar) { - viewHolder.contact_picture.setAlpha(0.5f); - viewHolder.contact_picture.setVisibility(View.VISIBLE); - } else { - viewHolder.contact_picture.setVisibility(View.GONE); - } - } - return view; - } else { - AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar); - } + if (type == DATE_SEPARATOR) { + if (UIHelper.today(message.getTimeSent())) { + viewHolder.status_message.setText(R.string.today); + } else if (UIHelper.yesterday(message.getTimeSent())) { + viewHolder.status_message.setText(R.string.yesterday); + } else { + viewHolder.status_message.setText(DateUtils.formatDateTime(activity, message.getTimeSent(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR)); + } + viewHolder.message_box.setBackgroundResource(activity.isDarkTheme() ? R.drawable.date_bubble_grey : R.drawable.date_bubble_white); + return view; + } else if (type == RTP_SESSION) { + final boolean isDarkTheme = activity.isDarkTheme(); + final boolean received = message.getStatus() <= Message.STATUS_RECEIVED; + final RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody()); + final long duration = rtpSessionStatus.duration; + if (received) { + if (duration > 0) { + viewHolder.status_message.setText(activity.getString(R.string.incoming_call_duration, TimeframeUtils.resolve(activity,duration))); + } else { + viewHolder.status_message.setText(R.string.incoming_call); + } + } else { + if (duration > 0) { + viewHolder.status_message.setText(activity.getString(R.string.outgoing_call_duration, TimeframeUtils.resolve(activity,duration))); + } else { + viewHolder.status_message.setText(R.string.outgoing_call); + } + } + viewHolder.indicatorReceived.setImageResource(RtpSessionStatus.getDrawable(received,rtpSessionStatus.successful,isDarkTheme)); + viewHolder.indicatorReceived.setAlpha(isDarkTheme ? 0.7f : 0.57f); + viewHolder.message_box.setBackgroundResource(isDarkTheme ? R.drawable.date_bubble_grey : R.drawable.date_bubble_white); + return view; + } else if (type == STATUS) { + if ("LOAD_MORE".equals(message.getBody())) { + viewHolder.status_message.setVisibility(View.GONE); + viewHolder.contact_picture.setVisibility(View.GONE); + viewHolder.load_more_messages.setVisibility(View.VISIBLE); + viewHolder.load_more_messages.setOnClickListener(v -> loadMoreMessages((Conversation) message.getConversation())); + } else { + viewHolder.status_message.setVisibility(View.VISIBLE); + viewHolder.load_more_messages.setVisibility(View.GONE); + viewHolder.status_message.setText(message.getBody()); + boolean showAvatar; + if (conversation.getMode() == Conversation.MODE_SINGLE) { + showAvatar = true; + AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar_on_status_message); + } else if (message.getCounterpart() != null || message.getTrueCounterpart() != null || (message.getCounterparts() != null && message.getCounterparts().size() > 0)) { + showAvatar = true; + AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar_on_status_message); + } else { + showAvatar = false; + } + if (showAvatar) { + viewHolder.contact_picture.setAlpha(0.5f); + viewHolder.contact_picture.setVisibility(View.VISIBLE); + } else { + viewHolder.contact_picture.setVisibility(View.GONE); + } + } + return view; + } else { + AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar); + } - resetClickListener(viewHolder.message_box, viewHolder.messageBody); + resetClickListener(viewHolder.message_box, viewHolder.messageBody); - viewHolder.contact_picture.setOnClickListener(v -> { - if (MessageAdapter.this.mOnContactPictureClickedListener != null) { - MessageAdapter.this.mOnContactPictureClickedListener - .onContactPictureClicked(message); - } + viewHolder.contact_picture.setOnClickListener(v -> { + if (MessageAdapter.this.mOnContactPictureClickedListener != null) { + MessageAdapter.this.mOnContactPictureClickedListener + .onContactPictureClicked(message); + } - }); - viewHolder.contact_picture.setOnLongClickListener(v -> { - if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) { - MessageAdapter.this.mOnContactPictureLongClickedListener - .onContactPictureLongClicked(v, message); - return true; - } else { - return false; - } - }); + }); + viewHolder.contact_picture.setOnLongClickListener(v -> { + if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) { + MessageAdapter.this.mOnContactPictureLongClickedListener + .onContactPictureLongClicked(v, message); + return true; + } else { + return false; + } + }); - final Transferable transferable = message.getTransferable(); - final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message); - if (unInitiatedButKnownSize || message.isDeleted() || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING)) { - if (unInitiatedButKnownSize || transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER) { - displayDownloadableMessage(viewHolder, message, activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)), darkBackground); - } else if (transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) { - displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)), darkBackground); - } else { - displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first, darkBackground); - } - } else if (message.isFileOrImage() && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) { - if (message.getFileParams().width > 0 && message.getFileParams().height > 0) { - displayMediaPreviewMessage(viewHolder, message, darkBackground); - } else if (message.getFileParams().runtime > 0) { - displayAudioMessage(viewHolder, message, darkBackground); - } else { - displayOpenableMessage(viewHolder, message, darkBackground); - } - } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { - if (account.isPgpDecryptionServiceConnected()) { - if (conversation instanceof Conversation && !account.hasPendingPgpIntent((Conversation) conversation)) { - displayInfoMessage(viewHolder, activity.getString(R.string.message_decrypting), darkBackground); - } else { - displayInfoMessage(viewHolder, activity.getString(R.string.pgp_message), darkBackground); - } - } else { - displayInfoMessage(viewHolder, activity.getString(R.string.install_openkeychain), darkBackground); - viewHolder.message_box.setOnClickListener(this::promptOpenKeychainInstall); - viewHolder.messageBody.setOnClickListener(this::promptOpenKeychainInstall); - } - } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { - displayInfoMessage(viewHolder, activity.getString(R.string.decryption_failed), darkBackground); - } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) { - displayInfoMessage(viewHolder, activity.getString(R.string.not_encrypted_for_this_device), darkBackground); - } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) { - displayInfoMessage(viewHolder, activity.getString(R.string.omemo_decryption_failed), darkBackground); - } else { - if (message.isGeoUri()) { - displayLocationMessage(viewHolder, message, darkBackground); - } else if (message.bodyIsOnlyEmojis() && message.getType() != Message.TYPE_PRIVATE) { - displayEmojiMessage(viewHolder, message.getBody().trim(), darkBackground); - } else if (message.treatAsDownloadable()) { - try { - URL url = new URL(message.getBody()); - if (P1S3UrlStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(url.getProtocol())) { - displayDownloadableMessage(viewHolder, - message, - activity.getString(R.string.check_x_filesize, - UIHelper.getFileDescriptionString(activity, message)), - darkBackground); - } else { - displayDownloadableMessage(viewHolder, - message, - activity.getString(R.string.check_x_filesize_on_host, - UIHelper.getFileDescriptionString(activity, message), - url.getHost()), - darkBackground); - } - } catch (Exception e) { - displayDownloadableMessage(viewHolder, - message, - activity.getString(R.string.check_x_filesize, - UIHelper.getFileDescriptionString(activity, message)), - darkBackground); - } - } else { - displayTextMessage(viewHolder, message, darkBackground, type); - } - } + final Transferable transferable = message.getTransferable(); + final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message); + if (unInitiatedButKnownSize || message.isDeleted() || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING)) { + if (unInitiatedButKnownSize || transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER) { + displayDownloadableMessage(viewHolder, message, activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)), darkBackground); + } else if (transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) { + displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)), darkBackground); + } else { + displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first, darkBackground); + } + } else if (message.isFileOrImage() && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) { + if (message.getFileParams().width > 0 && message.getFileParams().height > 0) { + displayMediaPreviewMessage(viewHolder, message, darkBackground); + } else if (message.getFileParams().runtime > 0) { + displayAudioMessage(viewHolder, message, darkBackground); + } else { + displayOpenableMessage(viewHolder, message, darkBackground); + } + } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { + if (account.isPgpDecryptionServiceConnected()) { + if (conversation instanceof Conversation && !account.hasPendingPgpIntent((Conversation) conversation)) { + displayInfoMessage(viewHolder, activity.getString(R.string.message_decrypting), darkBackground); + } else { + displayInfoMessage(viewHolder, activity.getString(R.string.pgp_message), darkBackground); + } + } else { + displayInfoMessage(viewHolder, activity.getString(R.string.install_openkeychain), darkBackground); + viewHolder.message_box.setOnClickListener(this::promptOpenKeychainInstall); + viewHolder.messageBody.setOnClickListener(this::promptOpenKeychainInstall); + } + } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { + displayInfoMessage(viewHolder, activity.getString(R.string.decryption_failed), darkBackground); + } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) { + displayInfoMessage(viewHolder, activity.getString(R.string.not_encrypted_for_this_device), darkBackground); + } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) { + displayInfoMessage(viewHolder, activity.getString(R.string.omemo_decryption_failed), darkBackground); + } else { + if (message.isGeoUri()) { + displayLocationMessage(viewHolder, message, darkBackground); + } else if (message.bodyIsOnlyEmojis() && message.getType() != Message.TYPE_PRIVATE) { + displayEmojiMessage(viewHolder, message.getBody().trim(), darkBackground); + } else if (message.treatAsDownloadable()) { + try { + URL url = new URL(message.getBody()); + if (P1S3UrlStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(url.getProtocol())) { + displayDownloadableMessage(viewHolder, + message, + activity.getString(R.string.check_x_filesize, + UIHelper.getFileDescriptionString(activity, message)), + darkBackground); + } else { + displayDownloadableMessage(viewHolder, + message, + activity.getString(R.string.check_x_filesize_on_host, + UIHelper.getFileDescriptionString(activity, message), + url.getHost()), + darkBackground); + } + } catch (Exception e) { + displayDownloadableMessage(viewHolder, + message, + activity.getString(R.string.check_x_filesize, + UIHelper.getFileDescriptionString(activity, message)), + darkBackground); + } + } else { + displayTextMessage(viewHolder, message, darkBackground, type); + } + } - if (type == RECEIVED) { - if (isInValidSession) { - int bubble; - if (!mUseGreenBackground) { - bubble = activity.getThemeResource(R.attr.message_bubble_received_monochrome, R.drawable.message_bubble_received_white); - } else { - bubble = activity.getThemeResource(R.attr.message_bubble_received_green, R.drawable.message_bubble_received); - } - viewHolder.message_box.setBackgroundResource(bubble); - viewHolder.encryption.setVisibility(View.GONE); - } else { - viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received_warning); - viewHolder.encryption.setVisibility(View.VISIBLE); - if (omemoEncryption && !message.isTrusted()) { - viewHolder.encryption.setText(R.string.not_trusted); - } else { - viewHolder.encryption.setText(CryptoHelper.encryptionTypeToText(message.getEncryption())); - } - } - } + if (type == RECEIVED) { + if (isInValidSession) { + int bubble; + if (!mUseGreenBackground) { + bubble = activity.getThemeResource(R.attr.message_bubble_received_monochrome, R.drawable.message_bubble_received_white); + } else { + bubble = activity.getThemeResource(R.attr.message_bubble_received_green, R.drawable.message_bubble_received); + } + viewHolder.message_box.setBackgroundResource(bubble); + viewHolder.encryption.setVisibility(View.GONE); + } else { + viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received_warning); + viewHolder.encryption.setVisibility(View.VISIBLE); + if (omemoEncryption && !message.isTrusted()) { + viewHolder.encryption.setText(R.string.not_trusted); + } else { + viewHolder.encryption.setText(CryptoHelper.encryptionTypeToText(message.getEncryption())); + } + } + } - displayStatus(viewHolder, message, type, darkBackground); + displayStatus(viewHolder, message, type, darkBackground); - return view; - } + return view; + } - private void promptOpenKeychainInstall(View view) { - activity.showInstallPgpDialog(); - } + private void promptOpenKeychainInstall(View view) { + activity.showInstallPgpDialog(); + } - @Override - public void notifyDataSetChanged() { - listSelectionManager.onBeforeNotifyDataSetChanged(); - super.notifyDataSetChanged(); - listSelectionManager.onAfterNotifyDataSetChanged(); - } + @Override + public void notifyDataSetChanged() { + listSelectionManager.onBeforeNotifyDataSetChanged(); + super.notifyDataSetChanged(); + listSelectionManager.onAfterNotifyDataSetChanged(); + } - private String transformText(CharSequence text, int start, int end, boolean forCopy) { - SpannableStringBuilder builder = new SpannableStringBuilder(text); - Object copySpan = new Object(); - builder.setSpan(copySpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - DividerSpan[] dividerSpans = builder.getSpans(0, builder.length(), DividerSpan.class); - for (DividerSpan dividerSpan : dividerSpans) { - builder.replace(builder.getSpanStart(dividerSpan), builder.getSpanEnd(dividerSpan), - dividerSpan.isLarge() ? "\n\n" : "\n"); - } - start = builder.getSpanStart(copySpan); - end = builder.getSpanEnd(copySpan); - if (start == -1 || end == -1) return ""; - builder = new SpannableStringBuilder(builder, start, end); - if (forCopy) { - QuoteSpan[] quoteSpans = builder.getSpans(0, builder.length(), QuoteSpan.class); - for (QuoteSpan quoteSpan : quoteSpans) { - builder.insert(builder.getSpanStart(quoteSpan), "> "); - } - } - return builder.toString(); - } + private String transformText(CharSequence text, int start, int end, boolean forCopy) { + SpannableStringBuilder builder = new SpannableStringBuilder(text); + Object copySpan = new Object(); + builder.setSpan(copySpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + DividerSpan[] dividerSpans = builder.getSpans(0, builder.length(), DividerSpan.class); + for (DividerSpan dividerSpan : dividerSpans) { + builder.replace(builder.getSpanStart(dividerSpan), builder.getSpanEnd(dividerSpan), + dividerSpan.isLarge() ? "\n\n" : "\n"); + } + start = builder.getSpanStart(copySpan); + end = builder.getSpanEnd(copySpan); + if (start == -1 || end == -1) return ""; + builder = new SpannableStringBuilder(builder, start, end); + if (forCopy) { + QuoteSpan[] quoteSpans = builder.getSpans(0, builder.length(), QuoteSpan.class); + for (QuoteSpan quoteSpan : quoteSpans) { + builder.insert(builder.getSpanStart(quoteSpan), "> "); + } + } + return builder.toString(); + } - @Override - public String transformTextForCopy(CharSequence text, int start, int end) { - if (text instanceof Spanned) { - return transformText(text, start, end, true); - } else { - return text.toString().substring(start, end); - } - } + @Override + public String transformTextForCopy(CharSequence text, int start, int end) { + if (text instanceof Spanned) { + return transformText(text, start, end, true); + } else { + return text.toString().substring(start, end); + } + } - public FileBackend getFileBackend() { - return activity.xmppConnectionService.getFileBackend(); - } + public FileBackend getFileBackend() { + return activity.xmppConnectionService.getFileBackend(); + } - public void stopAudioPlayer() { - audioPlayer.stop(); - } + public void stopAudioPlayer() { + audioPlayer.stop(); + } - public void unregisterListenerInAudioPlayer() { - audioPlayer.unregisterListener(); - } + public void unregisterListenerInAudioPlayer() { + audioPlayer.unregisterListener(); + } - public void startStopPending() { - audioPlayer.startStopPending(); - } + public void startStopPending() { + audioPlayer.startStopPending(); + } - public void openDownloadable(Message message) { - if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - ConversationFragment.registerPendingMessage(activity, message); - ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, ConversationsActivity.REQUEST_OPEN_MESSAGE); - return; - } - final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); - ViewUtil.view(activity, file); - } + public void openDownloadable(Message message) { + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ConversationFragment.registerPendingMessage(activity, message); + ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, ConversationsActivity.REQUEST_OPEN_MESSAGE); + return; + } + final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); + ViewUtil.view(activity, file); + } - private void showLocation(Message message) { - for (Intent intent : GeoHelper.createGeoIntentsFromMessage(activity, message)) { - if (intent.resolveActivity(getContext().getPackageManager()) != null) { - getContext().startActivity(intent); - return; - } - } - Toast.makeText(activity, R.string.no_application_found_to_display_location, Toast.LENGTH_SHORT).show(); - } + private void showLocation(Message message) { + for (Intent intent : GeoHelper.createGeoIntentsFromMessage(activity, message)) { + if (intent.resolveActivity(getContext().getPackageManager()) != null) { + getContext().startActivity(intent); + return; + } + } + Toast.makeText(activity, R.string.no_application_found_to_display_location, Toast.LENGTH_SHORT).show(); + } - public void updatePreferences() { - SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity); - this.mUseGreenBackground = p.getBoolean("use_green_background", activity.getResources().getBoolean(R.bool.use_green_background)); - } + public void updatePreferences() { + SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity); + this.mUseGreenBackground = p.getBoolean("use_green_background", activity.getResources().getBoolean(R.bool.use_green_background)); + } - public void setHighlightedTerm(List terms) { - this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms); - } + public void setHighlightedTerm(List terms) { + this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms); + } - public interface OnQuoteListener { - void onQuote(String text); - } + public interface OnQuoteListener { + void onQuote(String text); + } - public interface OnContactPictureClicked { - void onContactPictureClicked(Message message); - } + public interface OnContactPictureClicked { + void onContactPictureClicked(Message message); + } - public interface OnContactPictureLongClicked { - void onContactPictureLongClicked(View v, Message message); - } + public interface OnContactPictureLongClicked { + void onContactPictureLongClicked(View v, Message message); + } - private static class ViewHolder { + private static class ViewHolder { - public Button load_more_messages; - public ImageView edit_indicator; - public RelativeLayout audioPlayer; - protected LinearLayout message_box; - protected Button download_button; - protected ImageView image; - protected ImageView indicator; - protected ImageView indicatorReceived; - protected TextView time; - protected CopyTextView messageBody; - protected ImageView contact_picture; - protected TextView status_message; - protected TextView encryption; - } + public Button load_more_messages; + public ImageView edit_indicator; + public RelativeLayout audioPlayer; + protected LinearLayout message_box; + protected Button download_button; + protected ImageView image; + protected ImageView indicator; + protected ImageView indicatorReceived; + protected TextView time; + protected CopyTextView messageBody; + protected ImageView contact_picture; + protected TextView status_message; + protected TextView encryption; + } - private class MessageBodyActionModeCallback implements ActionMode.Callback { + private class MessageBodyActionModeCallback implements ActionMode.Callback { - private final TextView textView; + private final TextView textView; - public MessageBodyActionModeCallback(TextView textView) { - this.textView = textView; - } + public MessageBodyActionModeCallback(TextView textView) { + this.textView = textView; + } - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - if (onQuoteListener != null) { - int quoteResId = activity.getThemeResource(R.attr.icon_quote, R.drawable.ic_action_reply); - // 3rd item is placed after "copy" item - menu.add(0, android.R.id.button1, 3, R.string.quote).setIcon(quoteResId) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); - } - return false; - } + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + if (onQuoteListener != null) { + int quoteResId = activity.getThemeResource(R.attr.icon_quote, R.drawable.ic_action_reply); + // 3rd item is placed after "copy" item + menu.add(0, android.R.id.button1, 3, R.string.quote).setIcon(quoteResId) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + } + return false; + } - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - if (item.getItemId() == android.R.id.button1) { - int start = textView.getSelectionStart(); - int end = textView.getSelectionEnd(); - if (end > start) { - String text = transformText(textView.getText(), start, end, false); - if (onQuoteListener != null) { - onQuoteListener.onQuote(text); - } - mode.finish(); - } - return true; - } - return false; - } + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + if (item.getItemId() == android.R.id.button1) { + int start = textView.getSelectionStart(); + int end = textView.getSelectionEnd(); + if (end > start) { + String text = transformText(textView.getText(), start, end, false); + if (onQuoteListener != null) { + onQuoteListener.onQuote(text); + } + mode.finish(); + } + return true; + } + return false; + } - @Override - public void onDestroyActionMode(ActionMode mode) { - } - } + @Override + public void onDestroyActionMode(ActionMode mode) { + } + } } diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 47fec58c6..908e572cd 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -299,6 +299,8 @@ public class UIHelper { return new Pair<>(context.getString(R.string.omemo_decryption_failed), true); } else if (message.isFileOrImage()) { return new Pair<>(getFileDescriptionString(context, message), true); + } else if (message.getType() == Message.TYPE_RTP_SESSION) { + return new Pair<>(context.getString(message.getStatus() == Message.STATUS_RECEIVED ? R.string.incoming_call : R.string.outgoing_call), true); } else { final String body = MessageUtils.filterLtrRtl(message.getBody()); if (body.startsWith(Message.ME_COMMAND)) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index e21e69a3d..a375ac657 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.xmpp.jingle; +import android.os.SystemClock; import android.util.Log; import com.google.common.base.Strings; @@ -18,6 +19,10 @@ import java.util.List; import java.util.Map; import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; @@ -94,13 +99,27 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this); private final ArrayDeque pendingIceCandidates = new ArrayDeque<>(); + private final Message message; private State state = State.NULL; private RtpContentMap initiatorRtpContentMap; private RtpContentMap responderRtpContentMap; + private long rtpConnectionStarted = 0; //time of 'connected' JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { super(jingleConnectionManager, id, initiator); + final Conversation conversation = jingleConnectionManager.getXmppConnectionService().findOrCreateConversation( + id.account, + id.with.asBareJid(), + false, + false + ); + this.message = new Message( + conversation, + isInitiator() ? Message.STATUS_SEND : Message.STATUS_RECEIVED, + Message.TYPE_RTP_SESSION, + id.sessionId + ); } private static State reasonToState(Reason reason) { @@ -153,7 +172,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return; } webRTCWrapper.close(); - transitionOrThrow(reasonToState(wrapper.reason)); + final State target = reasonToState(wrapper.reason); + transitionOrThrow(target); + writeLogMessage(target); if (previous == State.PROPOSED || previous == State.SESSION_INITIALIZED) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); } @@ -455,7 +476,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (transition(State.RETRACTED)) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": session with " + id.with + " has been retracted"); - //TODO create missed call notification/message + writeLogMessageMissed(); jingleConnectionManager.finishConnection(this); } else { Log.d(Config.LOGTAG, "ignoring retract because already in " + this.state); @@ -509,6 +530,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void sendSessionTerminate(final Reason reason, final String text) { final State target = reasonToState(reason); transitionOrThrow(target); + writeLogMessage(target); final JinglePacket jinglePacket = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); jinglePacket.setReason(reason, text); send(jinglePacket); @@ -773,9 +795,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); updateEndUserState(); + if (newState == PeerConnection.PeerConnectionState.CONNECTED && this.rtpConnectionStarted == 0) { + this.rtpConnectionStarted = SystemClock.elapsedRealtime(); + } if (newState == PeerConnection.PeerConnectionState.FAILED) { if (TERMINATED.contains(this.state)) { - Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": not sending session-terminate after connectivity error because session is already in state "+this.state); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); return; } sendSessionTerminate(Reason.CONNECTIVITY_ERROR); @@ -850,6 +875,37 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } + private void writeLogMessage(final State state) { + final long started = this.rtpConnectionStarted; + long duration = started <= 0 ? 0 : SystemClock.elapsedRealtime() - started; + if (state == State.TERMINATED_SUCCESS || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) { + writeLogMessageSuccess(duration); + } else { + writeLogMessageMissed(); + } + } + + private void writeLogMessageSuccess(final long duration) { + this.message.setBody(new RtpSessionStatus(true, duration).toString()); + this.writeMessage(); + } + + private void writeLogMessageMissed() { + this.message.setBody(new RtpSessionStatus(false,0).toString()); + this.writeMessage(); + } + + private void writeMessage() { + final Conversational conversational = message.getConversation(); + if (conversational instanceof Conversation) { + ((Conversation) conversational).add(this.message); + xmppConnectionService.databaseBackend.createMessage(message); + xmppConnectionService.updateConversationUi(); + } else { + throw new IllegalStateException("Somehow the conversation in a message was a stub"); + } + } + public State getState() { return this.state; } diff --git a/src/main/res/drawable-hdpi/ic_call_made_black_18dp.png b/src/main/res/drawable-hdpi/ic_call_made_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..1ef5b84e9112caae702fd957a79fba148ff2eea2 GIT binary patch literal 159 zcmeAS@N?(olHy`uVBq!ia0vp^(jd&i0wmS%+S~(DX`U{QAr*{oFD(>oFc4w6n9HPf zV1nc>Pe#pYLK+XE0|FRt2-mBr3*NkZUN&yAm-n^{niZ2xt=_i034FG#FjB`+efwD! z(L-rX0?(9F?Si0$rNh^WU)V!Le)`IE{YeXRs}Zya{}5NccQVjM22WQ%mvv4FO#s^% BHnso& literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_call_made_white_18dp.png b/src/main/res/drawable-hdpi/ic_call_made_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..1b126d2dc570a79242b6df3a89efd8640a1e5d5b GIT binary patch literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^(jd&i0wmS%+S~(DrJgR1Ar*{oFKy&yG8AFCc%LQ2 zp=s&sW2`%FXg}mC;1Uek;NsDf(%C6!{qgj+f5G18BT78?u3j@MGPfqV`kA}`$qUnV zr@L&oQDM%SIf-#SH zRfBV6H}lf&sO1|=rK{UFgme7qaDQ+>%k|#6PoNBmsO dym{L%hWCFW0>1A)vkvGA22WQ%mvv4FO#mi4N7w)W literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_call_missed_outgoing_black_18dp.png b/src/main/res/drawable-hdpi/ic_call_missed_outgoing_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..7cba4f0ac614a35761cd24e376fcbf3b218588aa GIT binary patch literal 180 zcmeAS@N?(olHy`uVBq!ia0vp^(jd&i0wmS%+S~(D)t)YnAr*{o&#ja^B*`{;V2t< z*7ite`NW8#V+Ysp{C%1;t-bE^YVm}I(( zK|HlAX2RLycaloY%fu7^ubdFFr1RkI3oHMfw-o&Jy>)_;aahvX0GDT?&5@Z4`D`C4 zs7+i^u=(Iu&(^FWzfCuPmh_q(3poA!nc^+U`@9w1e9IEJU%6UcPI%&2v*l2W*W56R qstI@54o~nn6rketZuXDrwX7x~_fB#@wfF;c5re0zpUXO@geCxa!cS8G literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_call_received_black_18dp.png b/src/main/res/drawable-hdpi/ic_call_received_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..af45ceb84180e770986ff0437eeea12c257e59ed GIT binary patch literal 159 zcmeAS@N?(olHy`uVBq!ia0vp^(jd&i0wmS%+S~(DX`U{QAr*{oPjBRHF%V(7n8~E2 za8T&(4hHepLK+X23&@JjC}?ddxz}^R_#?A~s+xT6oHu9evSijgSp0=`3g@cs1s6EP zJ0m^LI3}?PZJscr`G}rkyQ$*zzZ@lRQ_nw^kJivnceB0w(lPwQSF_y_KpPo6UHx3v IIVCg!03!7`CIA2c literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_call_received_white_18dp.png b/src/main/res/drawable-hdpi/ic_call_received_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..8080c7879fecde2727d0df1096f3930d3ad1b3b8 GIT binary patch literal 169 zcmeAS@N?(olHy`uVBq!ia0vp^(jd&i0wmS%+S~(D1)eUBAr*{oPaER$Dm?W;Wf{-c{|hy=2zcN9eqX`!Q-Ki<1&`P0EO4|ueInAO z(RY@v#s=0c3W;3QRw*Ebq RCJeNj!PC{xWt~$(69B>&KVbj> literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_call_made_black_18dp.png b/src/main/res/drawable-mdpi/ic_call_made_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..e7293a0a908533354492db32075a495fce03c286 GIT binary patch literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+0wn(&ce?|myggkULn;`PB|H=kJZNBI-LQg@ z>%V&=-w$6ap3oNdFU+Dc44Q}5FOkmK%3P<)Bj8#w^?=C3z@~tfXwE~+nJlIXd=g{~ fUsg|e?eV;MYM{an^LB{Ts5^4KS0 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_call_made_white_18dp.png b/src/main/res/drawable-mdpi/ic_call_made_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3c6a23529c5c3178c8c8a59cde53fd8fbd911e43 GIT binary patch literal 135 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+0wn(&ce?|m{5)M8Lp07OCrDU)h<~u3nM>rr zvJ(&OwF(a8UrLrwe71HY+yMjvcP!%`TXxjua*@R2l8gl3z)u6d)6h{Fr|o#0+*7Vxhgk5 pZxi{o;osZ~CS9U8#SiT-Vc4=z-NJ8Y+%cf}44$rjF6*2UngC%{Go}Cl literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_call_missed_outgoing_black_18dp.png b/src/main/res/drawable-mdpi/ic_call_missed_outgoing_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ef4ad9622324dde04abeff9ba9ebbdc0f5d525b1 GIT binary patch literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+0wn(&ce?|md_7$pLn;`PC2l!1EtKg@KTyEH zr+&b-(d(b_mt>7!ydt&@Ibrq@lDkgi&rtG7<+#VVLn(waMXXJ!Ma9X`NpP7_%T_jr g4&DY|DLn=TmeuOHUh`@~fCe*oy85}Sb4q9e0CYws#sB~S literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_call_missed_outgoing_white_18dp.png b/src/main/res/drawable-mdpi/ic_call_missed_outgoing_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..8fe42b28cb80dce13ff80b2712a2fa481c6ce8fb GIT binary patch literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+0wn(&ce?|m{5@S9Ln;{Wo@Ez3V8Fp}@x+Rr z9o;-yoUyUj&DS%t>fZbypE;jT?bzF^p2u@$F}gTvPxu~Jn4T6En7Gd)F!7$t)Vl?) jM*Ejuj&~4yt6#`e-j`@xnen0*Xf%VTtDnm{r-UW|*$FPg literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_call_missed_white_18dp.png b/src/main/res/drawable-mdpi/ic_call_missed_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6d77962a322eb08c404b91ac1022bc2def1eff GIT binary patch literal 147 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+0wn(&ce?|mqC8z3Ln;`ro;t|epdjFKG4rrq z#<_?it0II-3xCR)2^h&5{5Lsy`P$>fXSrs@%5Xf*bT}T!dQP|dBm_VUF~kvH%H+QQ)J>gTe~DWM4fe5W&x literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_call_received_black_18dp.png b/src/main/res/drawable-mdpi/ic_call_received_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..fe45b55175eb4c6ab1e2bab7687b0465a432c3de GIT binary patch literal 133 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+0wn(&ce?|md^}woLn;`PC2SVVU)Zr>(tn=2 zDH?(a1q?|{%PbgLWj|;+YB*^eT*1jYo_rZG6)Dy+`Xb+e7ep0L*g6_UDoPtYI79Bt7 nQZRXYk%_pot5yA^eUA3hQ`Py5oR)3{8qeVA>gTe~DWM4f^MNbz literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_call_made_black_18dp.png b/src/main/res/drawable-xhdpi/ic_call_made_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..01cceead35b95f48595e406dde7072d435cfc84d GIT binary patch literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8si%u$NCo5D%Lh4`3tc`0~kDA{an^LB{Ts5%ZWOy literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_call_made_white_18dp.png b/src/main/res/drawable-xhdpi/ic_call_made_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ea6a8ab5f2382f8641bd014b7b0e12b0f4e936e8 GIT binary patch literal 189 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8i>HfYNCo5DD+hTU10`4=&Oeo> zFY+n4wPQuVlm|CXwalKrG2!F6H~XKiEY07pr)-%n%S1wR++{XtK*z8y*-&D^qb|x fLV@Uo^)2)A+vZOyKVz)}bPt24tDnm{r-UW|%D6=- literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_call_missed_black_18dp.png b/src/main/res/drawable-xhdpi/ic_call_missed_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..930fa4373f3af1007419f0cc4cfc42fab24989e7 GIT binary patch literal 201 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8Lp1Wy;okP61Pmv{3XHV`7@mHQewvkr4$gUuLrF0P? yuNiOFuT_|0T|k0wldT1B8K8v!{z=NCo5D%LfG=3`ATnPCv9j zvi8rGiBgxDHk75`JHd5lKNI@`{x9FJYj;YES}b1R+07NqwbsHhe6!5#hFu)0qHEK+ z@^n`#J<+_;t+?!roT!5T!D~DdmM3^Kq-aKPPgt73>okw4P0^k4@Px$;6WxAX=V9&X m*G!YrNRwKzB>n66?TnG~bAIQ2PpSqwhr!d;&t;ucLK6V@E<%R@ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_call_missed_outgoing_white_18dp.png b/src/main/res/drawable-xhdpi/ic_call_missed_outgoing_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..b4fc9bc8b77a700ade84b8a974eaad93c42eb0f7 GIT binary patch literal 193 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8ho_5UNCo5D%clho8SuCU21&G} zy>QNJmWpkbkE?!nhf{l>zIODblmEZ`{rP-;%%(!s>mqXDS^H9-oD0#KvuL&C%*0oc z8M9r)y7Jd9<$fYkX81&;q@-=mqO9Z-nk$ybByp8V7Il@&BxR+_7y+>mmt5VtKkFq~ odycPo+!J(Ma`_CPx#s7XKlt=Y7TlN~33L;Kr>mdKI;Vst0LbA|k0wldT1B8LpJWm(LkP61PmmIkc8HltzwBZjc z64##;w>ZrIhH;5YjL(Bq8G$<<|86c3u;bYBr+l-d)~y23)E(T@{>?t#R_JTMKW9fB z|HR7Dq@OIZ5nnG`vE2~h@)DO>yudl2?c{|n-uHPN=QCC)icFPYp4xXyaMs6wWhTiB z=Iv4jk}i?U+OvNB^@wZ>kkdSG(J>{F$K{kEo097>=Q`n87v3*aZ(3t}<@>*u9H2`X NJYD@<);T3K0RSDTP~-pr literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_call_received_black_18dp.png b/src/main/res/drawable-xhdpi/ic_call_received_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4008ba956eb4e269434852097e63bc7713eb82c2 GIT binary patch literal 175 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8nWu|mNCo5D^9MN_3`CkAzDUUz zE1d8`MCnn>BMz-9&I$olR{q?7**{-6n`y5$T|k0wldT1B8K8i>HfYNCo5DGY2`H90iU&DAk@F zb;QYSwSb_uh_^x9(N_5)p)ac6^Z$59`>cB-`JVsJWa$r2HaN{=wBXh-5HFMC>Suie)>v%*KbMn|s@vJM`)}UW;@btB;wOc3d+%wUBcBNjv*zc!l-}!yA=f74` z3rJ97eHJ|Z&Vih)>6~f7jGtFF{0uozqm^JMYA~N`hCl0>u9|Y; r=JFR4+WjwnJo{7R&P(pAQQ7*LXXC%rx<5|@I*!59)z4*}Q$iB}mbFj~ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_call_made_white_18dp.png b/src/main/res/drawable-xxhdpi/ic_call_made_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..41a58b948ebca9850665dd0d733a9656010717b1 GIT binary patch literal 225 zcmeAS@N?(olHy`uVBq!ia0vp^W+2SL0wmRZ7KH(+6`n4RArXh)UJc}8GURD_$hddW zyw&XjB70mM^0)GD)>rB-+wEw~s9Mkefpu2Uf>o~?OhHZYS& zKA$agZDB~n^h*i)S6a;_zq!4gC!JlC_0h)1uXBT)>cMLojNANLla;k3q=OgU5EJdl z4NjcV&UL6JgyY##CIkDeKhDmc<8|Y}qOMDU4CgaN4lplQ;bN$GGR4JqN3c+6=+*aT Z%-_Q$!yQ6j%mlib!PC{xWt~$(69DIyRzUy& literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_call_missed_black_18dp.png b/src/main/res/drawable-xxhdpi/ic_call_missed_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2326b5fba588accecb7535c4f22caa4b1e2b4e4e GIT binary patch literal 247 zcmeAS@N?(olHy`uVBq!ia0vp^W+2SL0wmRZ7KH(+eV#6kAr*{oFCFAOk=os> zR}apJinl9S*KD};-O6=r$FA%O?CsZ;`q6A~R6_oTNU|BbNHVv)(bJ^nV`o2#_BAT4 zS&`VMDSlb}`acK7V=}>JK&-dz^j$NVU^})s{y%TnJr7nZJK*b6xcAxls+SW!YwACl v@n}Z!D!bz%$74#lcI*-bP0l+XkKWgBC| literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_call_missed_outgoing_black_18dp.png b/src/main/res/drawable-xxhdpi/ic_call_missed_outgoing_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..f7f8e0746bb7db0f081d0fed08b4de54e45700d1 GIT binary patch literal 235 zcmV0qc8H(sY?Eu2W_islg+JD)(Ckd2NN-3?^R-IZZI^#I1+D;GM zo48&t9A~1&P0UTlwttGb>6p5acroW=KF5EMn+Cv4j!!bioKG>v9G@5f^ElXj1I*)K z=NT}IgI$1G9P9wh;$RgpmxC3+Yz`>EY_38P=5rN_FrTYZQc?dOMVi0?W#n=`<#RqY liSwxmoKH>Qh)OA?l-{ekVYL5oO*;Sp002ovPDHLkV1mfTV0{1p literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_call_missed_outgoing_white_18dp.png b/src/main/res/drawable-xxhdpi/ic_call_missed_outgoing_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a34511a03c4283ab0a8df85f70db9470a6177db5 GIT binary patch literal 235 zcmeAS@N?(olHy`uVBq!ia0vp^W+2SL0wmRZ7KH(+O`a}}Ar*{oFKw4RWWeB(=;SHm zDm`IZDo=RMgUY@qQU2d8L%v_p{JSl2YlP;~DU-h4GJC7N>ygFv^%gIdrgJa%yE9e8 z^GxrtE%F+M=5n{|I1HaB+a0f;&|Y<{rqAE#*c@lWV>0t!NE%z*`0H@yLBjHN9g;F{ zlFmF#U{B^_U)^IVv&JB;ptQ?y^6J2znK8XdXP!K;kGjIUSc0X9`RVgwyQjsEHK$8^ jOWw&i^`OHG=!AQD=R9Aq`rdg7bUlNotDnm{r-UW|(Dz9t4uK-BflIopV#0*hUtPKHuidvt z&wQ^e*}Y47gVR(wtM_e;p;fDH^D=u$UClWz#{0){MpDkxMt{a<#SV(Y9QnCA@};=D zNb=Vi$z6ueW+a_?I3uaYa+!p&#S$Q1Ch=K3BB7C0?1p@z{<7kpudS!PC{x JWt~$(69B(fX%qke literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_call_received_black_18dp.png b/src/main/res/drawable-xxhdpi/ic_call_received_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..75b98ec1cb1816263ca18bcb15cfdbfa96f36b6e GIT binary patch literal 202 zcmeAS@N?(olHy`uVBq!ia0vp^W+2SL0wmRZ7KH(+iJmTwAr*{o&mQD-aui^>$mSdP zc$NYS>tu-!=}!||ZLc2fXRmgAxr$-lr8>=}i`QALUGz?>>eZeusRw_q{MppE>!pC! z75$*4^QQMYtu@TIcRk}LxH=imc>L=3E1b3O+hFwn4PJJsL#DVhK4 q?bo+&+>`Cu7*t=Qxpd0E)#5!K@n14654!>#$KdJe=d#Wzp$P!RpHamC literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_call_received_white_18dp.png b/src/main/res/drawable-xxhdpi/ic_call_received_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..13d588caf711c39f6dd8cce11b980c84fbbcc2b0 GIT binary patch literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^W+2SL0wmRZ7KH(+)t)YnAr*{ouN~w(lpwUw<3f&^iHpKv+13#&P~h4b^5H@x4q`16!oqT{rG+~m^j0G%eD;R`B`YN@Kt9{Mn(mK}l-YbAfc*+4LuY@L{8H<^G0+=p` zG;n$)%sOT0e{{z3q%*;W&sIuA+fGg904m`!aP)lRnQLY{ZN1d(%J9O=Nvn=YWv|)z bxSIJP<670GojoByXES)Z`njxgN@xNATCG+A literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_call_made_black_18dp.png b/src/main/res/drawable-xxxhdpi/ic_call_made_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..c7d00c654c52ed0fd311bba2d2be07db95f096cf GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawW_!9ghEy=VJ$IVZ$x-CMh257P z{V~zvXgYiLRJcRfDF=Z`CI!#zR{_DVKXnTn8n>0s+s>TBT^`Q_REjw! zSxR>atNYEdc%fN*A$8gr$Lo zw!JF?1gb2W%(t$7qR&0(>+P0pdHvB5ZQ z$>nzn4}30FrVA82)8kcmptI_|V@=LeAA!kpWEccb9$*xh_(z6KH~)N|mU+7)tBP>D zBh#er!wL$LmOQ^)cCF5lVG}%gBvD|pj|`h3m>~mXWQNYP;7JlH%u!HdJkFXlsoz;a zq0&NT!Y@mgPsJTqjxq*`vwOtKD3lg9TsguR^itj%?8cXz{f;GG68j4-0e#2d>FVdQ I&MBb@0J_Lxl>h($ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_call_missed_outgoing_black_18dp.png b/src/main/res/drawable-xxxhdpi/ic_call_missed_outgoing_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..5117934594e1692df7bdb1ae20a252c9b3fe861e GIT binary patch literal 257 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawPI$UFhEy=VJ-1ts$xwjh;#U!w zjq}P6KIG)zyn5Lz`^%rcMDES}AC$dz>$Rxt>6U4}xA?*j>^wKmSglf^pv>=+!ZU-H zsS6(0&Gpo0UTZn?OV$LXgD#9rlOAvgOkikh9iqt$)MT>{6`JpP?_bonIDS0v;s zeZte`rGom!Da~_SGT1FSE%rAwY$yedyyPX7d=sBpdPIeTos*sahTFt4s<6F9TL$PA22WQ%mvv4FO#sTXU!ed1 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_call_missed_white_18dp.png b/src/main/res/drawable-xxxhdpi/ic_call_missed_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2374dc5a1158455c8a7ccfc02d821e2144e9952b GIT binary patch literal 291 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawUU|AWhEy=VJ-5-T#ZaL2;{GoF z<#sD0*1S6%*_)u1C}!y+IN|qQy~GDGnoFkyd1)@KkIxi;xANZWpBMALuh4uR7aBji zL~e`aSLZ5Awk(U;U#d8+6qMz&JMumcJMH+_kk^7CNvME*9@MnelF{r5}E*E!EJp2 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_call_received_black_18dp.png b/src/main/res/drawable-xxxhdpi/ic_call_received_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..81dc0c36797531a9ed390414b571d9c24223da61 GIT binary patch literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw=6bp~hEy=VJ!i!ELGXlU()!gZT{=KOwBdB&*lTX%_3;k_lB8=7Nd=G>Nf zbfi=Gx%tt98ylD>G&qxq8ja1)-LP2vK;X+?CRVO5{i}drUTS_prMs3{DbS$|p00i_ I>zopr0QLu2B>(^b literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_call_received_white_18dp.png b/src/main/res/drawable-xxxhdpi/ic_call_received_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..58421114fd7acefd4713ed01e079a58747d96a7e GIT binary patch literal 257 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawPI$UFhGg7(d)=3>$v~hjF|5Mk zPn*U9r~Uo04yq z@5#!^dNVD~uiI(S-COYBKqK>W|M~h}2Q0a`enh%EC}tn$=ZZQN7jWa;r$#38UF;%N z@7^>w^&hki__6X1J7=2GwHvkD9-$96#NDAf@tr zjU%UpsK@!04pn~aOR8?DJT`H-lzyL|k%^@P2s|z_H_WiQDQejD0_Z0OPgg&ebxsLQ E015VIaR2}S literal 0 HcmV?d00001 diff --git a/src/main/res/layout/message_date_bubble.xml b/src/main/res/layout/message_date_bubble.xml index 37d43bd3b..5e5cd0c4d 100644 --- a/src/main/res/layout/message_date_bubble.xml +++ b/src/main/res/layout/message_date_bubble.xml @@ -1,24 +1,27 @@ + android:paddingBottom="5dp"> + android:layout_centerHorizontal="true" + android:background="@drawable/date_bubble_white"> + + tools:text="Yesterday" /> \ No newline at end of file diff --git a/src/main/res/layout/message_rtp_session.xml b/src/main/res/layout/message_rtp_session.xml new file mode 100644 index 000000000..ad7f06d6a --- /dev/null +++ b/src/main/res/layout/message_rtp_session.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index ef2267a4a..f353145b0 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -903,6 +903,10 @@ Hang up Ongoing call Disable Tor to make calls + Incoming call + Incoming call · %s + Outgoing call + Outgoing call · %s View %1$d Participant View %1$d Participants From 4be2309202aaa17317dc47cf55ba0b64964e4f2d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 12 Apr 2020 18:07:31 +0200 Subject: [PATCH 084/182] more conditions under which to print call log --- .../conversations/parser/MessageParser.java | 2 +- .../siacs/conversations/utils/UIHelper.java | 9 +++- .../xmpp/jingle/JingleConnectionManager.java | 44 ++++++++++++++--- .../xmpp/jingle/JingleRtpConnection.java | 48 ++++++++++++++----- src/main/res/values/strings.xml | 1 + 5 files changed, 84 insertions(+), 20 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index f7c40269e..a3ebb9730 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -832,7 +832,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece if (!account.getJid().asBareJid().equals(from.asBareJid())) { processMessageReceipts(account, packet, query); } - mXmppConnectionService.getJingleConnectionManager().deliverMessage(account, packet.getTo(), packet.getFrom(), child); + mXmppConnectionService.getJingleConnectionManager().deliverMessage(account, packet.getTo(), packet.getFrom(), child, serverMsgId, timestamp); break; } } diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 908e572cd..30a62bedf 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -31,6 +31,7 @@ import eu.siacs.conversations.entities.ListItem; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.Presence; +import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.services.ExportBackupService; import rocks.xmpp.addr.Jid; @@ -300,7 +301,13 @@ public class UIHelper { } else if (message.isFileOrImage()) { return new Pair<>(getFileDescriptionString(context, message), true); } else if (message.getType() == Message.TYPE_RTP_SESSION) { - return new Pair<>(context.getString(message.getStatus() == Message.STATUS_RECEIVED ? R.string.incoming_call : R.string.outgoing_call), true); + RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody()); + final boolean received = message.getStatus() == Message.STATUS_RECEIVED; + if (!rtpSessionStatus.successful && received) { + return new Pair<>(context.getString(R.string.missed_call),true); + } else { + return new Pair<>(context.getString(received ? R.string.incoming_call : R.string.outgoing_call), true); + } } else { final String body = MessageUtils.filterLtrRtl(message.getBody()); if (body.startsWith(Message.ME_COMMAND)) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 3a7c179e4..7df61767b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -14,6 +14,8 @@ import java.util.concurrent.ConcurrentHashMap; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.services.AbstractConnectionManager; @@ -108,7 +110,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { account.getXmppConnection().sendIqPacket(response, null); } - public void deliverMessage(final Account account, final Jid to, final Jid from, final Element message) { + public void deliverMessage(final Account account, final Jid to, final Jid from, final Element message, String serverMsgId, long timestamp) { Preconditions.checkArgument(Namespace.JINGLE_MESSAGE.equals(message.getNamespace())); final String sessionId = message.getAttribute("id"); if (sessionId == null) { @@ -120,7 +122,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection; final AbstractJingleConnection.Id id = connection.getId(); if (id.account == account && id.sessionId.equals(sessionId)) { - rtpConnection.deliveryMessage(from, message); + rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); return; } } @@ -141,7 +143,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { final AbstractJingleConnection existingJingleConnection = connections.get(id); if (existingJingleConnection != null) { if (existingJingleConnection instanceof JingleRtpConnection) { - ((JingleRtpConnection) existingJingleConnection).deliveryMessage(from, message); + ((JingleRtpConnection) existingJingleConnection).deliveryMessage(from, message, serverMsgId, timestamp); } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + existingJingleConnection.getClass().getName() + " does not support jingle messages"); } @@ -162,7 +164,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { } else { final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, from); this.connections.put(id, rtpConnection); - rtpConnection.deliveryMessage(from, message); + rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); } } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to react to proposed " + namespace + " session"); @@ -175,7 +177,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, account.getJid()); this.connections.put(id, rtpConnection); rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED); - rtpConnection.deliveryMessage(from, message); + rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver proceed"); } @@ -184,6 +186,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { final RtpSessionProposal proposal = new RtpSessionProposal(account, from.asBareJid(), sessionId); synchronized (rtpSessionProposals) { if (rtpSessionProposals.remove(proposal) != null) { + writeLogMissedOutgoing(account, proposal.with, proposal.sessionId, serverMsgId, timestamp); mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, proposal.with, proposal.sessionId, RtpEndUserState.DECLINED_OR_BUSY); } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver reject"); @@ -195,6 +198,35 @@ public class JingleConnectionManager extends AbstractConnectionManager { } + private void writeLogMissedOutgoing(final Account account, Jid with, final String sessionId, String serverMsgId, long timestamp) { + final Conversation conversation = mXmppConnectionService.findOrCreateConversation( + account, + with.asBareJid(), + false, + false + ); + final Message message = new Message( + conversation, + Message.STATUS_SEND, + Message.TYPE_RTP_SESSION, + sessionId + ); + message.setServerMsgId(serverMsgId); + message.setTime(timestamp); + writeMessage(message); + } + + private void writeMessage(final Message message) { + final Conversational conversational = message.getConversation(); + if (conversational instanceof Conversation) { + ((Conversation) conversational).add(message); + mXmppConnectionService.databaseBackend.createMessage(message); + mXmppConnectionService.updateConversationUi(); + } else { + throw new IllegalStateException("Somehow the conversation in a message was a stub"); + } + } + public void startJingleFileTransfer(final Message message) { Preconditions.checkArgument(message.isFileOrImage(), "Message is not of type file or image"); final Transferable old = message.getTransferable(); @@ -271,7 +303,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (matchingProposal != null) { this.rtpSessionProposals.remove(matchingProposal); final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(matchingProposal); - Log.d(Config.LOGTAG, messagePacket.toString()); + writeLogMissedOutgoing(account, matchingProposal.with, matchingProposal.sessionId, null, System.currentTimeMillis()); mXmppConnectionService.sendMessagePacket(account, messagePacket); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index a375ac657..de1edf97c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -371,23 +371,23 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web send(sessionAccept); } - void deliveryMessage(final Jid from, final Element message) { + void deliveryMessage(final Jid from, final Element message, final String serverMessageId, final long timestamp) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": delivered message to JingleRtpConnection " + message); switch (message.getName()) { case "propose": - receivePropose(from, message); + receivePropose(from, serverMessageId, timestamp); break; case "proceed": - receiveProceed(from, message); + receiveProceed(from, serverMessageId, timestamp); break; case "retract": - receiveRetract(from, message); + receiveRetract(from, serverMessageId, timestamp); break; case "reject": - receiveReject(from, message); + receiveReject(from, serverMessageId, timestamp); break; case "accept": - receiveAccept(from, message); + receiveAccept(from, serverMessageId, timestamp); break; default: break; @@ -403,10 +403,16 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - private void receiveAccept(Jid from, Element message) { + private void receiveAccept(final Jid from, final String serverMsgId, final long timestamp) { final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); if (originatedFromMyself) { if (transition(State.ACCEPTED)) { + if (serverMsgId != null) { + this.message.setServerMsgId(serverMsgId); + } + this.message.setTime(timestamp); + this.message.setCarbon(true); //indicate that call was accepted on other device + this.writeLogMessageSuccess(0); this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); this.jingleConnectionManager.finishConnection(this); } else { @@ -417,13 +423,19 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - private void receiveReject(Jid from, Element message) { + private void receiveReject(Jid from, String serverMsgId, long timestamp) { final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); //reject from another one of my clients if (originatedFromMyself) { if (transition(State.REJECTED)) { this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); this.jingleConnectionManager.finishConnection(this); + if (serverMsgId != null) { + this.message.setServerMsgId(serverMsgId); + } + this.message.setTime(timestamp); + this.message.setCarbon(true); //indicate that call was rejected on other device + writeLogMessageMissed(); } else { Log.d(Config.LOGTAG, "not able to transition into REJECTED because already in " + this.state); } @@ -432,12 +444,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - private void receivePropose(final Jid from, final Element propose) { + private void receivePropose(final Jid from, final String serverMsgId, final long timestamp) { final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); - //TODO we can use initiator logic here if (originatedFromMyself) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": saw proposal from mysql. ignoring"); } else if (transition(State.PROPOSED)) { + if (serverMsgId != null) { + this.message.setServerMsgId(serverMsgId); + } + this.message.setTime(timestamp); startRinging(); } else { Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring session proposal because already in " + state); @@ -449,10 +464,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web xmppConnectionService.getNotificationService().showIncomingCallNotification(id); } - private void receiveProceed(final Jid from, final Element proceed) { + private void receiveProceed(final Jid from, final String serverMsgId, final long timestamp) { if (from.equals(id.with)) { if (isInitiator()) { if (transition(State.PROCEED)) { + if (serverMsgId != null) { + this.message.setServerMsgId(serverMsgId); + } + this.message.setTime(timestamp); this.sendSessionInitiate(State.SESSION_INITIALIZED_PRE_APPROVED); } else { Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because already in %s", id.account.getJid().asBareJid(), this.state)); @@ -471,11 +490,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - private void receiveRetract(final Jid from, final Element retract) { + private void receiveRetract(final Jid from, final String serverMsgId, final long timestamp) { if (from.equals(id.with)) { if (transition(State.RETRACTED)) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": session with " + id.with + " has been retracted"); + if (serverMsgId != null) { + this.message.setServerMsgId(serverMsgId); + } + this.message.setTime(timestamp); writeLogMessageMissed(); jingleConnectionManager.finishConnection(this); } else { @@ -729,6 +752,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void rejectCallFromProposed() { transitionOrThrow(State.REJECTED); + writeLogMessageMissed(); xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); this.sendJingleMessage("reject"); jingleConnectionManager.finishConnection(this); diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index f353145b0..6aacd3673 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -907,6 +907,7 @@ Incoming call · %s Outgoing call Outgoing call · %s + Missed call View %1$d Participant View %1$d Participants From 9a41d11aed6ade49d75d34f8bbe4f284f1353075 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 12 Apr 2020 19:18:40 +0200 Subject: [PATCH 085/182] do not show context menu for call logs --- .../java/eu/siacs/conversations/parser/MessageParser.java | 7 +++++++ .../eu/siacs/conversations/ui/ConversationFragment.java | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index a3ebb9730..c37155772 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -829,10 +829,17 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece if (!isTypeGroupChat) { for (Element child : packet.getChildren()) { if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace()) && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) { + //TODO in this case we probably only want to send receipts for live messages + //as soon as it comes from MAM it is probably too late anyway if (!account.getJid().asBareJid().equals(from.asBareJid())) { processMessageReceipts(account, packet, query); } + //TODO only live propose messages should get processed that way; however we may want to deliver 'accept' and 'reject' to stop ringing mXmppConnectionService.getJingleConnectionManager().deliverMessage(account, packet.getTo(), packet.getFrom(), child, serverMsgId, timestamp); + + + //TODO for queries we might want to process 'propose' and 'proceed' + //TODO propose will trigger a 'missed call' entry; 'proceed' might update that to a non missed call break; } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index aec25048e..cee0b54a6 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1044,7 +1044,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke while (relevantForCorrection.mergeable(relevantForCorrection.next())) { relevantForCorrection = relevantForCorrection.next(); } - if (m.getType() != Message.TYPE_STATUS) { + if (m.getType() != Message.TYPE_STATUS && m.getType() != Message.TYPE_RTP_SESSION) { if (m.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || m.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) { return; From 5b98107e9a0e738e7647c6a947539558eb6d67b5 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 13 Apr 2020 09:00:25 +0200 Subject: [PATCH 086/182] put jingle messages in MAM and parse call log during catchup --- .../conversations/entities/Conversation.java | 14 +- .../generator/MessageGenerator.java | 447 +++++++++--------- .../conversations/parser/MessageParser.java | 65 ++- .../xmpp/jingle/JingleRtpConnection.java | 4 +- 4 files changed, 295 insertions(+), 235 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index ec90e294f..53d2d74b3 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -733,6 +733,18 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } } + public Message findRtpSession(final String sessionId, final int s) { + synchronized (this.messages) { + for (int i = this.messages.size() - 1; i >= 0; --i) { + final Message message = this.messages.get(i); + if ((message.getStatus() == s) && (message.getType() == Message.TYPE_RTP_SESSION) && sessionId.equals(message.getRemoteMsgId())) { + return message; + } + } + } + return null; + } + public boolean possibleDuplicate(final String serverMsgId, final String remoteMsgId) { if (serverMsgId == null || remoteMsgId == null) { return false; @@ -1007,7 +1019,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return UIHelper.getColorForName(getName().toString()); } - public interface OnMessageFound { + public interface OnMessageFound { void onMessageFound(final Message message); } diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 3eb3c1aa6..41de4ec30 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -24,245 +24,248 @@ import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; public class MessageGenerator extends AbstractGenerator { - private static final String OMEMO_FALLBACK_MESSAGE = "I sent you an OMEMO encrypted message but your client doesn’t seem to support that. Find more information on https://conversations.im/omemo"; - private static final String PGP_FALLBACK_MESSAGE = "I sent you a PGP encrypted message but your client doesn’t seem to support that."; + private static final String OMEMO_FALLBACK_MESSAGE = "I sent you an OMEMO encrypted message but your client doesn’t seem to support that. Find more information on https://conversations.im/omemo"; + private static final String PGP_FALLBACK_MESSAGE = "I sent you a PGP encrypted message but your client doesn’t seem to support that."; - public MessageGenerator(XmppConnectionService service) { - super(service); - } + public MessageGenerator(XmppConnectionService service) { + super(service); + } - private MessagePacket preparePacket(Message message) { - Conversation conversation = (Conversation) message.getConversation(); - Account account = conversation.getAccount(); - MessagePacket packet = new MessagePacket(); - final boolean isWithSelf = conversation.getContact().isSelf(); - if (conversation.getMode() == Conversation.MODE_SINGLE) { - packet.setTo(message.getCounterpart()); - packet.setType(MessagePacket.TYPE_CHAT); - if (!isWithSelf) { - packet.addChild("request", "urn:xmpp:receipts"); - } - } else if (message.isPrivateMessage()) { - packet.setTo(message.getCounterpart()); - packet.setType(MessagePacket.TYPE_CHAT); - packet.addChild("x", "http://jabber.org/protocol/muc#user"); - packet.addChild("request", "urn:xmpp:receipts"); - } else { - packet.setTo(message.getCounterpart().asBareJid()); - packet.setType(MessagePacket.TYPE_GROUPCHAT); - } - if (conversation.isSingleOrPrivateAndNonAnonymous() && !message.isPrivateMessage()) { - packet.addChild("markable", "urn:xmpp:chat-markers:0"); - } - packet.setFrom(account.getJid()); - packet.setId(message.getUuid()); - packet.addChild("origin-id", Namespace.STANZA_IDS).setAttribute("id", message.getUuid()); - if (message.edited()) { - packet.addChild("replace", "urn:xmpp:message-correct:0").setAttribute("id", message.getEditedIdWireFormat()); - } - return packet; - } + private MessagePacket preparePacket(Message message) { + Conversation conversation = (Conversation) message.getConversation(); + Account account = conversation.getAccount(); + MessagePacket packet = new MessagePacket(); + final boolean isWithSelf = conversation.getContact().isSelf(); + if (conversation.getMode() == Conversation.MODE_SINGLE) { + packet.setTo(message.getCounterpart()); + packet.setType(MessagePacket.TYPE_CHAT); + if (!isWithSelf) { + packet.addChild("request", "urn:xmpp:receipts"); + } + } else if (message.isPrivateMessage()) { + packet.setTo(message.getCounterpart()); + packet.setType(MessagePacket.TYPE_CHAT); + packet.addChild("x", "http://jabber.org/protocol/muc#user"); + packet.addChild("request", "urn:xmpp:receipts"); + } else { + packet.setTo(message.getCounterpart().asBareJid()); + packet.setType(MessagePacket.TYPE_GROUPCHAT); + } + if (conversation.isSingleOrPrivateAndNonAnonymous() && !message.isPrivateMessage()) { + packet.addChild("markable", "urn:xmpp:chat-markers:0"); + } + packet.setFrom(account.getJid()); + packet.setId(message.getUuid()); + packet.addChild("origin-id", Namespace.STANZA_IDS).setAttribute("id", message.getUuid()); + if (message.edited()) { + packet.addChild("replace", "urn:xmpp:message-correct:0").setAttribute("id", message.getEditedIdWireFormat()); + } + return packet; + } - public void addDelay(MessagePacket packet, long timestamp) { - final SimpleDateFormat mDateFormat = new SimpleDateFormat( - "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); - mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - Element delay = packet.addChild("delay", "urn:xmpp:delay"); - Date date = new Date(timestamp); - delay.setAttribute("stamp", mDateFormat.format(date)); - } + public void addDelay(MessagePacket packet, long timestamp) { + final SimpleDateFormat mDateFormat = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + Element delay = packet.addChild("delay", "urn:xmpp:delay"); + Date date = new Date(timestamp); + delay.setAttribute("stamp", mDateFormat.format(date)); + } - public MessagePacket generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) { - MessagePacket packet = preparePacket(message); - if (axolotlMessage == null) { - return null; - } - packet.setAxolotlMessage(axolotlMessage.toElement()); - packet.setBody(OMEMO_FALLBACK_MESSAGE); - packet.addChild("store", "urn:xmpp:hints"); - packet.addChild("encryption", "urn:xmpp:eme:0") - .setAttribute("name", "OMEMO") - .setAttribute("namespace", AxolotlService.PEP_PREFIX); - return packet; - } + public MessagePacket generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) { + MessagePacket packet = preparePacket(message); + if (axolotlMessage == null) { + return null; + } + packet.setAxolotlMessage(axolotlMessage.toElement()); + packet.setBody(OMEMO_FALLBACK_MESSAGE); + packet.addChild("store", "urn:xmpp:hints"); + packet.addChild("encryption", "urn:xmpp:eme:0") + .setAttribute("name", "OMEMO") + .setAttribute("namespace", AxolotlService.PEP_PREFIX); + return packet; + } - public MessagePacket generateKeyTransportMessage(Jid to, XmppAxolotlMessage axolotlMessage) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); - packet.setTo(to); - packet.setAxolotlMessage(axolotlMessage.toElement()); - packet.addChild("store", "urn:xmpp:hints"); - return packet; - } + public MessagePacket generateKeyTransportMessage(Jid to, XmppAxolotlMessage axolotlMessage) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_CHAT); + packet.setTo(to); + packet.setAxolotlMessage(axolotlMessage.toElement()); + packet.addChild("store", "urn:xmpp:hints"); + return packet; + } - public MessagePacket generateChat(Message message) { - MessagePacket packet = preparePacket(message); - String content; - if (message.hasFileOnRemoteHost()) { - Message.FileParams fileParams = message.getFileParams(); - final URL url = fileParams.url; - if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) { - Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER); - final String file = url.getFile(); - x.setAttribute("name", file.charAt(0) == '/' ? file.substring(1) : file); - x.setAttribute("fileid", url.getHost()); - return packet; - } else { - content = url.toString(); - packet.addChild("x", Namespace.OOB).addChild("url").setContent(content); - } - } else { - content = message.getBody(); - } - packet.setBody(content); - return packet; - } + public MessagePacket generateChat(Message message) { + MessagePacket packet = preparePacket(message); + String content; + if (message.hasFileOnRemoteHost()) { + Message.FileParams fileParams = message.getFileParams(); + final URL url = fileParams.url; + if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) { + Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER); + final String file = url.getFile(); + x.setAttribute("name", file.charAt(0) == '/' ? file.substring(1) : file); + x.setAttribute("fileid", url.getHost()); + return packet; + } else { + content = url.toString(); + packet.addChild("x", Namespace.OOB).addChild("url").setContent(content); + } + } else { + content = message.getBody(); + } + packet.setBody(content); + return packet; + } - public MessagePacket generatePgpChat(Message message) { - MessagePacket packet = preparePacket(message); - if (message.hasFileOnRemoteHost()) { - Message.FileParams fileParams = message.getFileParams(); - final URL url = fileParams.url; - if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) { - Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER); - final String file = url.getFile(); - x.setAttribute("name", file.charAt(0) == '/' ? file.substring(1) : file); - x.setAttribute("fileid", url.getHost()); - } else { - packet.setBody(url.toString()); - packet.addChild("x", Namespace.OOB).addChild("url").setContent(url.toString()); - } - } else { - if (Config.supportUnencrypted()) { - packet.setBody(PGP_FALLBACK_MESSAGE); - } - if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - packet.addChild("x", "jabber:x:encrypted").setContent(message.getEncryptedBody()); - } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { - packet.addChild("x", "jabber:x:encrypted").setContent(message.getBody()); - } - packet.addChild("encryption", "urn:xmpp:eme:0") - .setAttribute("namespace", "jabber:x:encrypted"); - } - return packet; - } + public MessagePacket generatePgpChat(Message message) { + MessagePacket packet = preparePacket(message); + if (message.hasFileOnRemoteHost()) { + Message.FileParams fileParams = message.getFileParams(); + final URL url = fileParams.url; + if (P1S3UrlStreamHandler.PROTOCOL_NAME.equals(url.getProtocol())) { + Element x = packet.addChild("x", Namespace.P1_S3_FILE_TRANSFER); + final String file = url.getFile(); + x.setAttribute("name", file.charAt(0) == '/' ? file.substring(1) : file); + x.setAttribute("fileid", url.getHost()); + } else { + packet.setBody(url.toString()); + packet.addChild("x", Namespace.OOB).addChild("url").setContent(url.toString()); + } + } else { + if (Config.supportUnencrypted()) { + packet.setBody(PGP_FALLBACK_MESSAGE); + } + if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + packet.addChild("x", "jabber:x:encrypted").setContent(message.getEncryptedBody()); + } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { + packet.addChild("x", "jabber:x:encrypted").setContent(message.getBody()); + } + packet.addChild("encryption", "urn:xmpp:eme:0") + .setAttribute("namespace", "jabber:x:encrypted"); + } + return packet; + } - public MessagePacket generateChatState(Conversation conversation) { - final Account account = conversation.getAccount(); - MessagePacket packet = new MessagePacket(); - packet.setType(conversation.getMode() == Conversation.MODE_MULTI ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT); - packet.setTo(conversation.getJid().asBareJid()); - packet.setFrom(account.getJid()); - packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); - packet.addChild("no-store", "urn:xmpp:hints"); - packet.addChild("no-storage", "urn:xmpp:hints"); //wrong! don't copy this. Its *store* - return packet; - } + public MessagePacket generateChatState(Conversation conversation) { + final Account account = conversation.getAccount(); + MessagePacket packet = new MessagePacket(); + packet.setType(conversation.getMode() == Conversation.MODE_MULTI ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT); + packet.setTo(conversation.getJid().asBareJid()); + packet.setFrom(account.getJid()); + packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); + packet.addChild("no-store", "urn:xmpp:hints"); + packet.addChild("no-storage", "urn:xmpp:hints"); //wrong! don't copy this. Its *store* + return packet; + } - public MessagePacket confirm(final Account account, final Jid to, final String id, final Jid counterpart, final boolean groupChat) { - MessagePacket packet = new MessagePacket(); - packet.setType(groupChat ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT); - packet.setTo(groupChat ? to.asBareJid() : to); - packet.setFrom(account.getJid()); - Element displayed = packet.addChild("displayed", "urn:xmpp:chat-markers:0"); - displayed.setAttribute("id", id); - if (groupChat && counterpart != null) { - displayed.setAttribute("sender", counterpart.toString()); - } - packet.addChild("store", "urn:xmpp:hints"); - return packet; - } + public MessagePacket confirm(final Account account, final Jid to, final String id, final Jid counterpart, final boolean groupChat) { + MessagePacket packet = new MessagePacket(); + packet.setType(groupChat ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT); + packet.setTo(groupChat ? to.asBareJid() : to); + packet.setFrom(account.getJid()); + Element displayed = packet.addChild("displayed", "urn:xmpp:chat-markers:0"); + displayed.setAttribute("id", id); + if (groupChat && counterpart != null) { + displayed.setAttribute("sender", counterpart.toString()); + } + packet.addChild("store", "urn:xmpp:hints"); + return packet; + } - public MessagePacket conferenceSubject(Conversation conversation, String subject) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_GROUPCHAT); - packet.setTo(conversation.getJid().asBareJid()); - packet.addChild("subject").setContent(subject); - packet.setFrom(conversation.getAccount().getJid().asBareJid()); - return packet; - } + public MessagePacket conferenceSubject(Conversation conversation, String subject) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_GROUPCHAT); + packet.setTo(conversation.getJid().asBareJid()); + packet.addChild("subject").setContent(subject); + packet.setFrom(conversation.getAccount().getJid().asBareJid()); + return packet; + } - public MessagePacket directInvite(final Conversation conversation, final Jid contact) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_NORMAL); - packet.setTo(contact); - packet.setFrom(conversation.getAccount().getJid()); - Element x = packet.addChild("x", "jabber:x:conference"); - x.setAttribute("jid", conversation.getJid().asBareJid().toString()); - String password = conversation.getMucOptions().getPassword(); - if (password != null) { - x.setAttribute("password", password); - } - if (contact.isFullJid()) { - packet.addChild("no-store", "urn:xmpp:hints"); - packet.addChild("no-copy", "urn:xmpp:hints"); - } - return packet; - } + public MessagePacket directInvite(final Conversation conversation, final Jid contact) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_NORMAL); + packet.setTo(contact); + packet.setFrom(conversation.getAccount().getJid()); + Element x = packet.addChild("x", "jabber:x:conference"); + x.setAttribute("jid", conversation.getJid().asBareJid().toString()); + String password = conversation.getMucOptions().getPassword(); + if (password != null) { + x.setAttribute("password", password); + } + if (contact.isFullJid()) { + packet.addChild("no-store", "urn:xmpp:hints"); + packet.addChild("no-copy", "urn:xmpp:hints"); + } + return packet; + } - public MessagePacket invite(Conversation conversation, Jid contact) { - MessagePacket packet = new MessagePacket(); - packet.setTo(conversation.getJid().asBareJid()); - packet.setFrom(conversation.getAccount().getJid()); - Element x = new Element("x"); - x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user"); - Element invite = new Element("invite"); - invite.setAttribute("to", contact.asBareJid().toString()); - x.addChild(invite); - packet.addChild(x); - return packet; - } + public MessagePacket invite(Conversation conversation, Jid contact) { + MessagePacket packet = new MessagePacket(); + packet.setTo(conversation.getJid().asBareJid()); + packet.setFrom(conversation.getAccount().getJid()); + Element x = new Element("x"); + x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user"); + Element invite = new Element("invite"); + invite.setAttribute("to", contact.asBareJid().toString()); + x.addChild(invite); + packet.addChild(x); + return packet; + } - public MessagePacket received(Account account, MessagePacket originalMessage, ArrayList namespaces, int type) { - MessagePacket receivedPacket = new MessagePacket(); - receivedPacket.setType(type); - receivedPacket.setTo(originalMessage.getFrom()); - receivedPacket.setFrom(account.getJid()); - for (String namespace : namespaces) { - receivedPacket.addChild("received", namespace).setAttribute("id", originalMessage.getId()); - } - receivedPacket.addChild("store", "urn:xmpp:hints"); - return receivedPacket; - } + public MessagePacket received(Account account, MessagePacket originalMessage, ArrayList namespaces, int type) { + MessagePacket receivedPacket = new MessagePacket(); + receivedPacket.setType(type); + receivedPacket.setTo(originalMessage.getFrom()); + receivedPacket.setFrom(account.getJid()); + for (String namespace : namespaces) { + receivedPacket.addChild("received", namespace).setAttribute("id", originalMessage.getId()); + } + receivedPacket.addChild("store", "urn:xmpp:hints"); + return receivedPacket; + } - public MessagePacket received(Account account, Jid to, String id) { - MessagePacket packet = new MessagePacket(); - packet.setFrom(account.getJid()); - packet.setTo(to); - packet.addChild("received", "urn:xmpp:receipts").setAttribute("id", id); - packet.addChild("store", "urn:xmpp:hints"); - return packet; - } + public MessagePacket received(Account account, Jid to, String id) { + MessagePacket packet = new MessagePacket(); + packet.setFrom(account.getJid()); + packet.setTo(to); + packet.addChild("received", "urn:xmpp:receipts").setAttribute("id", id); + packet.addChild("store", "urn:xmpp:hints"); + return packet; + } - public MessagePacket sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) { - final MessagePacket packet = new MessagePacket(); + public MessagePacket sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) { + final MessagePacket packet = new MessagePacket(); packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those - packet.setTo(proposal.with); - packet.setId(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX +proposal.sessionId); - final Element propose = packet.addChild("propose", Namespace.JINGLE_MESSAGE); - propose.setAttribute("id", proposal.sessionId); - propose.addChild("description", Namespace.JINGLE_APPS_RTP); - packet.addChild("request", "urn:xmpp:receipts"); - return packet; - } + packet.setTo(proposal.with); + packet.setId(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX + proposal.sessionId); + final Element propose = packet.addChild("propose", Namespace.JINGLE_MESSAGE); + propose.setAttribute("id", proposal.sessionId); + propose.addChild("description", Namespace.JINGLE_APPS_RTP); + packet.addChild("request", "urn:xmpp:receipts"); + packet.addChild("store", "urn:xmpp:hints"); + return packet; + } - public MessagePacket sessionRetract(final JingleConnectionManager.RtpSessionProposal proposal) { - final MessagePacket packet = new MessagePacket(); + public MessagePacket sessionRetract(final JingleConnectionManager.RtpSessionProposal proposal) { + final MessagePacket packet = new MessagePacket(); packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those - packet.setTo(proposal.with); - final Element propose = packet.addChild("retract", Namespace.JINGLE_MESSAGE); - propose.setAttribute("id", proposal.sessionId); - propose.addChild("description", Namespace.JINGLE_APPS_RTP); - return packet; - } + packet.setTo(proposal.with); + final Element propose = packet.addChild("retract", Namespace.JINGLE_MESSAGE); + propose.setAttribute("id", proposal.sessionId); + propose.addChild("description", Namespace.JINGLE_APPS_RTP); + packet.addChild("store", "urn:xmpp:hints"); + return packet; + } - public MessagePacket sessionReject(final Jid with, final String sessionId) { - final MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those - packet.setTo(with); - final Element propose = packet.addChild("reject", Namespace.JINGLE_MESSAGE); - propose.setAttribute("id", sessionId); - propose.addChild("description", Namespace.JINGLE_APPS_RTP); - return packet; - } + public MessagePacket sessionReject(final Jid with, final String sessionId) { + final MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those + packet.setTo(with); + final Element propose = packet.addChild("reject", Namespace.JINGLE_MESSAGE); + propose.setAttribute("id", sessionId); + propose.addChild("description", Namespace.JINGLE_APPS_RTP); + packet.addChild("store", "urn:xmpp:hints"); + return packet; + } } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index c37155772..908166de4 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -31,6 +31,7 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.ReadByMarker; import eu.siacs.conversations.entities.ReceiptRequest; +import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.http.P1S3UrlStreamHandler; import eu.siacs.conversations.services.MessageArchiveService; @@ -73,6 +74,11 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece return safeToExtract ? extractStanzaId(packet, by) : null; } + private static String extractStanzaId(Account account, Element packet) { + final boolean safeToExtract = account.getXmppConnection().getFeatures().stanzaIds(); + return safeToExtract ? extractStanzaId(packet, account.getJid().asBareJid()) : null; + } + private static String extractStanzaId(Element packet, Jid by) { for (Element child : packet.getChildren()) { if (child.getName().equals("stanza-id") @@ -829,17 +835,56 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece if (!isTypeGroupChat) { for (Element child : packet.getChildren()) { if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace()) && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) { - //TODO in this case we probably only want to send receipts for live messages - //as soon as it comes from MAM it is probably too late anyway - if (!account.getJid().asBareJid().equals(from.asBareJid())) { - processMessageReceipts(account, packet, query); + final String action = child.getName(); + if (query == null) { + if (!account.getJid().asBareJid().equals(from.asBareJid())) { + processMessageReceipts(account, packet, query); + } + if (serverMsgId == null) { + serverMsgId = extractStanzaId(account, packet); + } + mXmppConnectionService.getJingleConnectionManager().deliverMessage(account, packet.getTo(), packet.getFrom(), child, serverMsgId, timestamp); + } 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(); + if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { + final Conversation c = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), false, false); + final Message message = new Message( + c, + status, + Message.TYPE_RTP_SESSION, + sessionId + ); + message.setServerMsgId(serverMsgId); + message.setTime(timestamp); + message.setBody(new RtpSessionStatus(false, 0).toString()); + 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); + final int s = packet.fromAccount(account) ? Message.STATUS_RECEIVED : Message.STATUS_SEND; + final Message message = c.findRtpSession(sessionId, s); + if (message != null) { + message.setBody(new RtpSessionStatus(true, 0).toString()); + if (serverMsgId != null) { + message.setServerMsgId(serverMsgId); + } + message.setTime(timestamp); + mXmppConnectionService.updateMessage(message, true); + } else { + Log.d(Config.LOGTAG, "unable to find original rtp session message for received propose"); + } + + } } - //TODO only live propose messages should get processed that way; however we may want to deliver 'accept' and 'reject' to stop ringing - mXmppConnectionService.getJingleConnectionManager().deliverMessage(account, packet.getTo(), packet.getFrom(), child, serverMsgId, timestamp); - - - //TODO for queries we might want to process 'propose' and 'proceed' - //TODO propose will trigger a 'missed call' entry; 'proceed' might update that to a non missed call break; } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index de1edf97c..3cfb2b15c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -494,7 +494,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (from.equals(id.with)) { if (transition(State.RETRACTED)) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": session with " + id.with + " has been retracted"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": session with " + id.with + " has been retracted (serverMsgId="+serverMsgId+")"); if (serverMsgId != null) { this.message.setServerMsgId(serverMsgId); } @@ -776,7 +776,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web messagePacket.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those messagePacket.setTo(to); messagePacket.addChild(action, Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId); - Log.d(Config.LOGTAG, messagePacket.toString()); + messagePacket.addChild("store", "urn:xmpp:hints"); xmppConnectionService.sendMessagePacket(id.account, messagePacket); } From b924a63d01ad2a1dea955c8434053ec41065b339 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 13 Apr 2020 12:02:34 +0200 Subject: [PATCH 087/182] copy audio manager from AppRTCDemo --- src/main/AndroidManifest.xml | 1 + .../services/AppRTCAudioManager.java | 578 ++++++++++++++++++ .../services/AppRTCBluetoothManager.java | 549 +++++++++++++++++ .../services/AppRTCProximitySensor.java | 170 ++++++ .../services/XmppConnectionService.java | 10 +- .../conversations/ui/RtpSessionActivity.java | 22 +- .../conversations/utils/AppRTCUtils.java | 55 ++ .../xmpp/jingle/JingleRtpConnection.java | 7 + .../xmpp/jingle/WebRTCWrapper.java | 27 +- .../res/drawable-hdpi/ic_mic_black_24dp.png | Bin 0 -> 344 bytes .../drawable-hdpi/ic_mic_off_black_24dp.png | Bin 0 -> 402 bytes .../ic_volume_off_black_24dp.png | Bin 0 -> 407 bytes .../drawable-hdpi/ic_volume_up_black_24dp.png | Bin 0 -> 364 bytes .../res/drawable-mdpi/ic_mic_black_24dp.png | Bin 0 -> 232 bytes .../drawable-mdpi/ic_mic_off_black_24dp.png | Bin 0 -> 271 bytes .../ic_volume_off_black_24dp.png | Bin 0 -> 279 bytes .../drawable-mdpi/ic_volume_up_black_24dp.png | Bin 0 -> 235 bytes .../res/drawable-xhdpi/ic_mic_black_24dp.png | Bin 0 -> 418 bytes .../drawable-xhdpi/ic_mic_off_black_24dp.png | Bin 0 -> 454 bytes .../ic_volume_off_black_24dp.png | Bin 0 -> 493 bytes .../ic_volume_up_black_24dp.png | Bin 0 -> 434 bytes .../res/drawable-xxhdpi/ic_mic_black_24dp.png | Bin 0 -> 581 bytes .../drawable-xxhdpi/ic_mic_off_black_24dp.png | Bin 0 -> 671 bytes .../ic_volume_off_black_24dp.png | Bin 0 -> 704 bytes .../ic_volume_up_black_24dp.png | Bin 0 -> 626 bytes .../drawable-xxxhdpi/ic_mic_black_24dp.png | Bin 0 -> 773 bytes .../ic_mic_off_black_24dp.png | Bin 0 -> 832 bytes .../ic_volume_off_black_24dp.png | Bin 0 -> 924 bytes .../ic_volume_up_black_24dp.png | Bin 0 -> 828 bytes src/main/res/layout/activity_rtp_session.xml | 34 +- src/main/res/values/attrs.xml | 2 + src/main/res/values/themes.xml | 7 + 32 files changed, 1453 insertions(+), 9 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java create mode 100644 src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java create mode 100644 src/main/java/eu/siacs/conversations/services/AppRTCProximitySensor.java create mode 100644 src/main/java/eu/siacs/conversations/utils/AppRTCUtils.java create mode 100644 src/main/res/drawable-hdpi/ic_mic_black_24dp.png create mode 100644 src/main/res/drawable-hdpi/ic_mic_off_black_24dp.png create mode 100644 src/main/res/drawable-hdpi/ic_volume_off_black_24dp.png create mode 100644 src/main/res/drawable-hdpi/ic_volume_up_black_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_mic_black_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_mic_off_black_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_volume_off_black_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_volume_up_black_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_mic_black_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_mic_off_black_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_volume_off_black_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_volume_up_black_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_mic_black_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_mic_off_black_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_volume_off_black_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_volume_up_black_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_mic_black_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_mic_off_black_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_volume_off_black_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_volume_up_black_24dp.png diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 9ab4073b2..3cbf0e51c 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -31,6 +31,7 @@ + diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java new file mode 100644 index 000000000..db1b8e1e1 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java @@ -0,0 +1,578 @@ +/* + * Copyright 2014 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ +package eu.siacs.conversations.services; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.media.AudioDeviceInfo; +import android.media.AudioManager; +import android.os.Build; +import android.support.annotation.Nullable; +import android.util.Log; + +import org.webrtc.ThreadUtils; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.utils.AppRTCUtils; + +/** + * AppRTCAudioManager manages all audio related parts of the AppRTC demo. + */ +public class AppRTCAudioManager { + private final Context apprtcContext; + // Contains speakerphone setting: auto, true or false + @Nullable + private final SpeakerPhonePreference speakerPhonePreference; + // Handles all tasks related to Bluetooth headset devices. + private final AppRTCBluetoothManager bluetoothManager; + @Nullable + private AudioManager audioManager; + @Nullable + private AudioManagerEvents audioManagerEvents; + private AudioManagerState amState; + private int savedAudioMode = AudioManager.MODE_INVALID; + private boolean savedIsSpeakerPhoneOn; + private boolean savedIsMicrophoneMute; + private boolean hasWiredHeadset; + // Default audio device; speaker phone for video calls or earpiece for audio + // only calls. + private AudioDevice defaultAudioDevice; + // Contains the currently selected audio device. + // This device is changed automatically using a certain scheme where e.g. + // a wired headset "wins" over speaker phone. It is also possible for a + // user to explicitly select a device (and overrid any predefined scheme). + // See |userSelectedAudioDevice| for details. + private AudioDevice selectedAudioDevice; + // Contains the user-selected audio device which overrides the predefined + // selection scheme. + // TODO(henrika): always set to AudioDevice.NONE today. Add support for + // explicit selection based on choice by userSelectedAudioDevice. + private AudioDevice userSelectedAudioDevice; + // Proximity sensor object. It measures the proximity of an object in cm + // relative to the view screen of a device and can therefore be used to + // assist device switching (close to ear <=> use headset earpiece if + // available, far from ear <=> use speaker phone). + @Nullable + private AppRTCProximitySensor proximitySensor; + // Contains a list of available audio devices. A Set collection is used to + // avoid duplicate elements. + private Set audioDevices = new HashSet<>(); + // Broadcast receiver for wired headset intent broadcasts. + private BroadcastReceiver wiredHeadsetReceiver; + // Callback method for changes in audio focus. + @Nullable + private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; + + private AppRTCAudioManager(Context context, final SpeakerPhonePreference speakerPhonePreference) { + Log.d(Config.LOGTAG, "ctor"); + ThreadUtils.checkIsOnMainThread(); + apprtcContext = context; + audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE)); + bluetoothManager = AppRTCBluetoothManager.create(context, this); + wiredHeadsetReceiver = new WiredHeadsetReceiver(); + amState = AudioManagerState.UNINITIALIZED; + Log.d(Config.LOGTAG, "speaker phone preference: " + speakerPhonePreference); + this.speakerPhonePreference = speakerPhonePreference; + if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE) { + defaultAudioDevice = AudioDevice.EARPIECE; + } else { + defaultAudioDevice = AudioDevice.SPEAKER_PHONE; + } + // Create and initialize the proximity sensor. + // Tablet devices (e.g. Nexus 7) does not support proximity sensors. + // Note that, the sensor will not be active until start() has been called. + proximitySensor = AppRTCProximitySensor.create(context, + // This method will be called each time a state change is detected. + // Example: user holds his hand over the device (closer than ~5 cm), + // or removes his hand from the device. + this::onProximitySensorChangedState); + Log.d(Config.LOGTAG, "defaultAudioDevice: " + defaultAudioDevice); + AppRTCUtils.logDeviceInfo(Config.LOGTAG); + } + + /** + * Construction. + */ + public static AppRTCAudioManager create(Context context, SpeakerPhonePreference speakerPhonePreference) { + return new AppRTCAudioManager(context, speakerPhonePreference); + } + + /** + * This method is called when the proximity sensor reports a state change, + * e.g. from "NEAR to FAR" or from "FAR to NEAR". + */ + private void onProximitySensorChangedState() { + if (speakerPhonePreference != SpeakerPhonePreference.AUTO) { + return; + } + // The proximity sensor should only be activated when there are exactly two + // available audio devices. + if (audioDevices.size() == 2 && audioDevices.contains(AppRTCAudioManager.AudioDevice.EARPIECE) + && audioDevices.contains(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE)) { + if (proximitySensor.sensorReportsNearState()) { + // Sensor reports that a "handset is being held up to a person's ear", + // or "something is covering the light sensor". + setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.EARPIECE); + } else { + // Sensor reports that a "handset is removed from a person's ear", or + // "the light sensor is no longer covered". + setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE); + } + } + } + + @SuppressWarnings("deprecation") + // TODO(henrika): audioManager.requestAudioFocus() is deprecated. + public void start(AudioManagerEvents audioManagerEvents) { + Log.d(Config.LOGTAG, "start"); + ThreadUtils.checkIsOnMainThread(); + if (amState == AudioManagerState.RUNNING) { + Log.e(Config.LOGTAG, "AudioManager is already active"); + return; + } + // TODO(henrika): perhaps call new method called preInitAudio() here if UNINITIALIZED. + Log.d(Config.LOGTAG, "AudioManager starts..."); + this.audioManagerEvents = audioManagerEvents; + amState = AudioManagerState.RUNNING; + // Store current audio state so we can restore it when stop() is called. + savedAudioMode = audioManager.getMode(); + savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn(); + savedIsMicrophoneMute = audioManager.isMicrophoneMute(); + hasWiredHeadset = hasWiredHeadset(); + // Create an AudioManager.OnAudioFocusChangeListener instance. + audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { + // Called on the listener to notify if the audio focus for this listener has been changed. + // The |focusChange| value indicates whether the focus was gained, whether the focus was lost, + // and whether that loss is transient, or whether the new focus holder will hold it for an + // unknown amount of time. + // TODO(henrika): possibly extend support of handling audio-focus changes. Only contains + // logging for now. + @Override + public void onAudioFocusChange(int focusChange) { + final String typeOfChange; + switch (focusChange) { + case AudioManager.AUDIOFOCUS_GAIN: + typeOfChange = "AUDIOFOCUS_GAIN"; + break; + case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT: + typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT"; + break; + case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE: + typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE"; + break; + case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: + typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK"; + break; + case AudioManager.AUDIOFOCUS_LOSS: + typeOfChange = "AUDIOFOCUS_LOSS"; + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT"; + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: + typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK"; + break; + default: + typeOfChange = "AUDIOFOCUS_INVALID"; + break; + } + Log.d(Config.LOGTAG, "onAudioFocusChange: " + typeOfChange); + } + }; + // Request audio playout focus (without ducking) and install listener for changes in focus. + int result = audioManager.requestAudioFocus(audioFocusChangeListener, + AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + Log.d(Config.LOGTAG, "Audio focus request granted for VOICE_CALL streams"); + } else { + Log.e(Config.LOGTAG, "Audio focus request failed"); + } + // Start by setting MODE_IN_COMMUNICATION as default audio mode. It is + // required to be in this mode when playout and/or recording starts for + // best possible VoIP performance. + audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); + // Always disable microphone mute during a WebRTC call. + setMicrophoneMute(false); + // Set initial device states. + userSelectedAudioDevice = AudioDevice.NONE; + selectedAudioDevice = AudioDevice.NONE; + audioDevices.clear(); + // Initialize and start Bluetooth if a BT device is available or initiate + // detection of new (enabled) BT devices. + bluetoothManager.start(); + // Do initial selection of audio device. This setting can later be changed + // either by adding/removing a BT or wired headset or by covering/uncovering + // the proximity sensor. + updateAudioDeviceState(); + // Register receiver for broadcast intents related to adding/removing a + // wired headset. + registerReceiver(wiredHeadsetReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); + Log.d(Config.LOGTAG, "AudioManager started"); + } + + @SuppressWarnings("deprecation") + // TODO(henrika): audioManager.abandonAudioFocus() is deprecated. + public void stop() { + Log.d(Config.LOGTAG, "stop"); + ThreadUtils.checkIsOnMainThread(); + if (amState != AudioManagerState.RUNNING) { + Log.e(Config.LOGTAG, "Trying to stop AudioManager in incorrect state: " + amState); + return; + } + amState = AudioManagerState.UNINITIALIZED; + unregisterReceiver(wiredHeadsetReceiver); + bluetoothManager.stop(); + // Restore previously stored audio states. + setSpeakerphoneOn(savedIsSpeakerPhoneOn); + setMicrophoneMute(savedIsMicrophoneMute); + audioManager.setMode(savedAudioMode); + // Abandon audio focus. Gives the previous focus owner, if any, focus. + audioManager.abandonAudioFocus(audioFocusChangeListener); + audioFocusChangeListener = null; + Log.d(Config.LOGTAG, "Abandoned audio focus for VOICE_CALL streams"); + if (proximitySensor != null) { + proximitySensor.stop(); + proximitySensor = null; + } + audioManagerEvents = null; + Log.d(Config.LOGTAG, "AudioManager stopped"); + } + + /** + * Changes selection of the currently active audio device. + */ + private void setAudioDeviceInternal(AudioDevice device) { + Log.d(Config.LOGTAG, "setAudioDeviceInternal(device=" + device + ")"); + AppRTCUtils.assertIsTrue(audioDevices.contains(device)); + switch (device) { + case SPEAKER_PHONE: + setSpeakerphoneOn(true); + break; + case EARPIECE: + setSpeakerphoneOn(false); + break; + case WIRED_HEADSET: + setSpeakerphoneOn(false); + break; + case BLUETOOTH: + setSpeakerphoneOn(false); + break; + default: + Log.e(Config.LOGTAG, "Invalid audio device selection"); + break; + } + selectedAudioDevice = device; + } + + /** + * Changes default audio device. + * TODO(henrika): add usage of this method in the AppRTCMobile client. + */ + public void setDefaultAudioDevice(AudioDevice defaultDevice) { + ThreadUtils.checkIsOnMainThread(); + switch (defaultDevice) { + case SPEAKER_PHONE: + defaultAudioDevice = defaultDevice; + break; + case EARPIECE: + if (hasEarpiece()) { + defaultAudioDevice = defaultDevice; + } else { + defaultAudioDevice = AudioDevice.SPEAKER_PHONE; + } + break; + default: + Log.e(Config.LOGTAG, "Invalid default audio device selection"); + break; + } + Log.d(Config.LOGTAG, "setDefaultAudioDevice(device=" + defaultAudioDevice + ")"); + updateAudioDeviceState(); + } + + /** + * Changes selection of the currently active audio device. + */ + public void selectAudioDevice(AudioDevice device) { + ThreadUtils.checkIsOnMainThread(); + if (!audioDevices.contains(device)) { + Log.e(Config.LOGTAG, "Can not select " + device + " from available " + audioDevices); + } + userSelectedAudioDevice = device; + updateAudioDeviceState(); + } + + /** + * Returns current set of available/selectable audio devices. + */ + public Set getAudioDevices() { + ThreadUtils.checkIsOnMainThread(); + return Collections.unmodifiableSet(new HashSet<>(audioDevices)); + } + + /** + * Returns the currently selected audio device. + */ + public AudioDevice getSelectedAudioDevice() { + ThreadUtils.checkIsOnMainThread(); + return selectedAudioDevice; + } + + /** + * Helper method for receiver registration. + */ + private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { + apprtcContext.registerReceiver(receiver, filter); + } + + /** + * Helper method for unregistration of an existing receiver. + */ + private void unregisterReceiver(BroadcastReceiver receiver) { + apprtcContext.unregisterReceiver(receiver); + } + + /** + * Sets the speaker phone mode. + */ + private void setSpeakerphoneOn(boolean on) { + boolean wasOn = audioManager.isSpeakerphoneOn(); + if (wasOn == on) { + return; + } + audioManager.setSpeakerphoneOn(on); + } + + /** + * Sets the microphone mute state. + */ + private void setMicrophoneMute(boolean on) { + boolean wasMuted = audioManager.isMicrophoneMute(); + if (wasMuted == on) { + return; + } + audioManager.setMicrophoneMute(on); + } + + /** + * Gets the current earpiece state. + */ + private boolean hasEarpiece() { + return apprtcContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY); + } + + /** + * Checks whether a wired headset is connected or not. + * This is not a valid indication that audio playback is actually over + * the wired headset as audio routing depends on other conditions. We + * only use it as an early indicator (during initialization) of an attached + * wired headset. + */ + @Deprecated + private boolean hasWiredHeadset() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return audioManager.isWiredHeadsetOn(); + } else { + final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL); + for (AudioDeviceInfo device : devices) { + final int type = device.getType(); + if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) { + Log.d(Config.LOGTAG, "hasWiredHeadset: found wired headset"); + return true; + } else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) { + Log.d(Config.LOGTAG, "hasWiredHeadset: found USB audio device"); + return true; + } + } + return false; + } + } + + /** + * Updates list of possible audio devices and make new device selection. + * TODO(henrika): add unit test to verify all state transitions. + */ + public void updateAudioDeviceState() { + ThreadUtils.checkIsOnMainThread(); + Log.d(Config.LOGTAG, "--- updateAudioDeviceState: " + + "wired headset=" + hasWiredHeadset + ", " + + "BT state=" + bluetoothManager.getState()); + Log.d(Config.LOGTAG, "Device status: " + + "available=" + audioDevices + ", " + + "selected=" + selectedAudioDevice + ", " + + "user selected=" + userSelectedAudioDevice); + // Check if any Bluetooth headset is connected. The internal BT state will + // change accordingly. + // TODO(henrika): perhaps wrap required state into BT manager. + if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE + || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE + || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_DISCONNECTING) { + bluetoothManager.updateDevice(); + } + // Update the set of available audio devices. + Set newAudioDevices = new HashSet<>(); + if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED + || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING + || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) { + newAudioDevices.add(AudioDevice.BLUETOOTH); + } + if (hasWiredHeadset) { + // If a wired headset is connected, then it is the only possible option. + newAudioDevices.add(AudioDevice.WIRED_HEADSET); + } else { + // No wired headset, hence the audio-device list can contain speaker + // phone (on a tablet), or speaker phone and earpiece (on mobile phone). + newAudioDevices.add(AudioDevice.SPEAKER_PHONE); + if (hasEarpiece()) { + newAudioDevices.add(AudioDevice.EARPIECE); + } + } + // Store state which is set to true if the device list has changed. + boolean audioDeviceSetUpdated = !audioDevices.equals(newAudioDevices); + // Update the existing audio device set. + audioDevices = newAudioDevices; + // Correct user selected audio devices if needed. + if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE + && userSelectedAudioDevice == AudioDevice.BLUETOOTH) { + // If BT is not available, it can't be the user selection. + userSelectedAudioDevice = AudioDevice.NONE; + } + if (hasWiredHeadset && userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE) { + // If user selected speaker phone, but then plugged wired headset then make + // wired headset as user selected device. + userSelectedAudioDevice = AudioDevice.WIRED_HEADSET; + } + if (!hasWiredHeadset && userSelectedAudioDevice == AudioDevice.WIRED_HEADSET) { + // If user selected wired headset, but then unplugged wired headset then make + // speaker phone as user selected device. + userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE; + } + // Need to start Bluetooth if it is available and user either selected it explicitly or + // user did not select any output device. + boolean needBluetoothAudioStart = + bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE + && (userSelectedAudioDevice == AudioDevice.NONE + || userSelectedAudioDevice == AudioDevice.BLUETOOTH); + // Need to stop Bluetooth audio if user selected different device and + // Bluetooth SCO connection is established or in the process. + boolean needBluetoothAudioStop = + (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED + || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING) + && (userSelectedAudioDevice != AudioDevice.NONE + && userSelectedAudioDevice != AudioDevice.BLUETOOTH); + if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE + || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING + || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) { + Log.d(Config.LOGTAG, "Need BT audio: start=" + needBluetoothAudioStart + ", " + + "stop=" + needBluetoothAudioStop + ", " + + "BT state=" + bluetoothManager.getState()); + } + // Start or stop Bluetooth SCO connection given states set earlier. + if (needBluetoothAudioStop) { + bluetoothManager.stopScoAudio(); + bluetoothManager.updateDevice(); + } + if (needBluetoothAudioStart && !needBluetoothAudioStop) { + // Attempt to start Bluetooth SCO audio (takes a few second to start). + if (!bluetoothManager.startScoAudio()) { + // Remove BLUETOOTH from list of available devices since SCO failed. + audioDevices.remove(AudioDevice.BLUETOOTH); + audioDeviceSetUpdated = true; + } + } + // Update selected audio device. + final AudioDevice newAudioDevice; + if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) { + // If a Bluetooth is connected, then it should be used as output audio + // device. Note that it is not sufficient that a headset is available; + // an active SCO channel must also be up and running. + newAudioDevice = AudioDevice.BLUETOOTH; + } else if (hasWiredHeadset) { + // If a wired headset is connected, but Bluetooth is not, then wired headset is used as + // audio device. + newAudioDevice = AudioDevice.WIRED_HEADSET; + } else { + // No wired headset and no Bluetooth, hence the audio-device list can contain speaker + // phone (on a tablet), or speaker phone and earpiece (on mobile phone). + // |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or AudioDevice.EARPIECE + // depending on the user's selection. + newAudioDevice = defaultAudioDevice; + } + // Switch to new device but only if there has been any changes. + if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) { + // Do the required device switch. + setAudioDeviceInternal(newAudioDevice); + Log.d(Config.LOGTAG, "New device status: " + + "available=" + audioDevices + ", " + + "selected=" + newAudioDevice); + if (audioManagerEvents != null) { + // Notify a listening client that audio device has been changed. + audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices); + } + } + Log.d(Config.LOGTAG, "--- updateAudioDeviceState done"); + } + + /** + * AudioDevice is the names of possible audio devices that we currently + * support. + */ + public enum AudioDevice {SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE} + + /** + * AudioManager state. + */ + public enum AudioManagerState { + UNINITIALIZED, + PREINITIALIZED, + RUNNING, + } + + public enum SpeakerPhonePreference { + AUTO, EARPIECE, SPEAKER + } + + /** + * Selected audio device change event. + */ + public interface AudioManagerEvents { + // Callback fired once audio device is changed or list of available audio devices changed. + void onAudioDeviceChanged( + AudioDevice selectedAudioDevice, Set availableAudioDevices); + } + + /* Receiver which handles changes in wired headset availability. */ + private class WiredHeadsetReceiver extends BroadcastReceiver { + private static final int STATE_UNPLUGGED = 0; + private static final int STATE_PLUGGED = 1; + private static final int HAS_NO_MIC = 0; + private static final int HAS_MIC = 1; + + @Override + public void onReceive(Context context, Intent intent) { + int state = intent.getIntExtra("state", STATE_UNPLUGGED); + int microphone = intent.getIntExtra("microphone", HAS_NO_MIC); + String name = intent.getStringExtra("name"); + Log.d(Config.LOGTAG, "WiredHeadsetReceiver.onReceive" + AppRTCUtils.getThreadInfo() + ": " + + "a=" + intent.getAction() + ", s=" + + (state == STATE_UNPLUGGED ? "unplugged" : "plugged") + ", m=" + + (microphone == HAS_MIC ? "mic" : "no mic") + ", n=" + name + ", sb=" + + isInitialStickyBroadcast()); + hasWiredHeadset = (state == STATE_PLUGGED); + updateAudioDeviceState(); + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java b/src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java new file mode 100644 index 000000000..e5ea9be02 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java @@ -0,0 +1,549 @@ +/* + * Copyright 2016 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ +package eu.siacs.conversations.services; + +import android.annotation.SuppressLint; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHeadset; +import android.bluetooth.BluetoothProfile; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.media.AudioManager; +import android.os.Handler; +import android.os.Looper; +import android.os.Process; +import android.support.annotation.Nullable; +import android.util.Log; + +import java.util.List; +import java.util.Set; + +import org.webrtc.ThreadUtils; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.utils.AppRTCUtils; + +/** + * AppRTCProximitySensor manages functions related to Bluetoth devices in the + * AppRTC demo. + */ +public class AppRTCBluetoothManager { + // Timeout interval for starting or stopping audio to a Bluetooth SCO device. + private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000; + // Maximum number of SCO connection attempts. + private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2; + private final Context apprtcContext; + private final AppRTCAudioManager apprtcAudioManager; + @Nullable + private final AudioManager audioManager; + private final Handler handler; + private final BluetoothProfile.ServiceListener bluetoothServiceListener; + private final BroadcastReceiver bluetoothHeadsetReceiver; + int scoConnectionAttempts; + private State bluetoothState; + @Nullable + private BluetoothAdapter bluetoothAdapter; + @Nullable + private BluetoothHeadset bluetoothHeadset; + @Nullable + private BluetoothDevice bluetoothDevice; + // Runs when the Bluetooth timeout expires. We use that timeout after calling + // startScoAudio() or stopScoAudio() because we're not guaranteed to get a + // callback after those calls. + private final Runnable bluetoothTimeoutRunnable = new Runnable() { + @Override + public void run() { + bluetoothTimeout(); + } + }; + protected AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManager) { + Log.d(Config.LOGTAG, "ctor"); + ThreadUtils.checkIsOnMainThread(); + apprtcContext = context; + apprtcAudioManager = audioManager; + this.audioManager = getAudioManager(context); + bluetoothState = State.UNINITIALIZED; + bluetoothServiceListener = new BluetoothServiceListener(); + bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver(); + handler = new Handler(Looper.getMainLooper()); + } + + /** + * Construction. + */ + static AppRTCBluetoothManager create(Context context, AppRTCAudioManager audioManager) { + Log.d(Config.LOGTAG, "create" + AppRTCUtils.getThreadInfo()); + return new AppRTCBluetoothManager(context, audioManager); + } + + /** + * Returns the internal state. + */ + public State getState() { + ThreadUtils.checkIsOnMainThread(); + return bluetoothState; + } + + /** + * Activates components required to detect Bluetooth devices and to enable + * BT SCO (audio is routed via BT SCO) for the headset profile. The end + * state will be HEADSET_UNAVAILABLE but a state machine has started which + * will start a state change sequence where the final outcome depends on + * if/when the BT headset is enabled. + * Example of state change sequence when start() is called while BT device + * is connected and enabled: + * UNINITIALIZED --> HEADSET_UNAVAILABLE --> HEADSET_AVAILABLE --> + * SCO_CONNECTING --> SCO_CONNECTED <==> audio is now routed via BT SCO. + * Note that the AppRTCAudioManager is also involved in driving this state + * change. + */ + public void start() { + ThreadUtils.checkIsOnMainThread(); + Log.d(Config.LOGTAG, "start"); + if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) { + Log.w(Config.LOGTAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission"); + return; + } + if (bluetoothState != State.UNINITIALIZED) { + Log.w(Config.LOGTAG, "Invalid BT state"); + return; + } + bluetoothHeadset = null; + bluetoothDevice = null; + scoConnectionAttempts = 0; + // Get a handle to the default local Bluetooth adapter. + bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + if (bluetoothAdapter == null) { + Log.w(Config.LOGTAG, "Device does not support Bluetooth"); + return; + } + // Ensure that the device supports use of BT SCO audio for off call use cases. + if (!audioManager.isBluetoothScoAvailableOffCall()) { + Log.e(Config.LOGTAG, "Bluetooth SCO audio is not available off call"); + return; + } + logBluetoothAdapterInfo(bluetoothAdapter); + // Establish a connection to the HEADSET profile (includes both Bluetooth Headset and + // Hands-Free) proxy object and install a listener. + if (!getBluetoothProfileProxy( + apprtcContext, bluetoothServiceListener, BluetoothProfile.HEADSET)) { + Log.e(Config.LOGTAG, "BluetoothAdapter.getProfileProxy(HEADSET) failed"); + return; + } + // Register receivers for BluetoothHeadset change notifications. + IntentFilter bluetoothHeadsetFilter = new IntentFilter(); + // Register receiver for change in connection state of the Headset profile. + bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); + // Register receiver for change in audio connection state of the Headset profile. + bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED); + registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter); + Log.d(Config.LOGTAG, "HEADSET profile state: " + + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET))); + Log.d(Config.LOGTAG, "Bluetooth proxy for headset profile has started"); + bluetoothState = State.HEADSET_UNAVAILABLE; + Log.d(Config.LOGTAG, "start done: BT state=" + bluetoothState); + } + + /** + * Stops and closes all components related to Bluetooth audio. + */ + public void stop() { + ThreadUtils.checkIsOnMainThread(); + Log.d(Config.LOGTAG, "stop: BT state=" + bluetoothState); + if (bluetoothAdapter == null) { + return; + } + // Stop BT SCO connection with remote device if needed. + stopScoAudio(); + // Close down remaining BT resources. + if (bluetoothState == State.UNINITIALIZED) { + return; + } + unregisterReceiver(bluetoothHeadsetReceiver); + cancelTimer(); + if (bluetoothHeadset != null) { + bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset); + bluetoothHeadset = null; + } + bluetoothAdapter = null; + bluetoothDevice = null; + bluetoothState = State.UNINITIALIZED; + Log.d(Config.LOGTAG, "stop done: BT state=" + bluetoothState); + } + + /** + * Starts Bluetooth SCO connection with remote device. + * Note that the phone application always has the priority on the usage of the SCO connection + * for telephony. If this method is called while the phone is in call it will be ignored. + * Similarly, if a call is received or sent while an application is using the SCO connection, + * the connection will be lost for the application and NOT returned automatically when the call + * ends. Also note that: up to and including API version JELLY_BEAN_MR1, this method initiates a + * virtual voice call to the Bluetooth headset. After API version JELLY_BEAN_MR2 only a raw SCO + * audio connection is established. + * TODO(henrika): should we add support for virtual voice call to BT headset also for JBMR2 and + * higher. It might be required to initiates a virtual voice call since many devices do not + * accept SCO audio without a "call". + */ + public boolean startScoAudio() { + ThreadUtils.checkIsOnMainThread(); + Log.d(Config.LOGTAG, "startSco: BT state=" + bluetoothState + ", " + + "attempts: " + scoConnectionAttempts + ", " + + "SCO is on: " + isScoOn()); + if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) { + Log.e(Config.LOGTAG, "BT SCO connection fails - no more attempts"); + return false; + } + if (bluetoothState != State.HEADSET_AVAILABLE) { + Log.e(Config.LOGTAG, "BT SCO connection fails - no headset available"); + return false; + } + // Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED. + Log.d(Config.LOGTAG, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_CHANGED..."); + // The SCO connection establishment can take several seconds, hence we cannot rely on the + // connection to be available when the method returns but instead register to receive the + // intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be SCO_AUDIO_STATE_CONNECTED. + bluetoothState = State.SCO_CONNECTING; + audioManager.startBluetoothSco(); + audioManager.setBluetoothScoOn(true); + scoConnectionAttempts++; + startTimer(); + Log.d(Config.LOGTAG, "startScoAudio done: BT state=" + bluetoothState + ", " + + "SCO is on: " + isScoOn()); + return true; + } + + /** + * Stops Bluetooth SCO connection with remote device. + */ + public void stopScoAudio() { + ThreadUtils.checkIsOnMainThread(); + Log.d(Config.LOGTAG, "stopScoAudio: BT state=" + bluetoothState + ", " + + "SCO is on: " + isScoOn()); + if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) { + return; + } + cancelTimer(); + audioManager.stopBluetoothSco(); + audioManager.setBluetoothScoOn(false); + bluetoothState = State.SCO_DISCONNECTING; + Log.d(Config.LOGTAG, "stopScoAudio done: BT state=" + bluetoothState + ", " + + "SCO is on: " + isScoOn()); + } + + /** + * Use the BluetoothHeadset proxy object (controls the Bluetooth Headset + * Service via IPC) to update the list of connected devices for the HEADSET + * profile. The internal state will change to HEADSET_UNAVAILABLE or to + * HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the connected + * device if available. + */ + public void updateDevice() { + if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) { + return; + } + Log.d(Config.LOGTAG, "updateDevice"); + // Get connected devices for the headset profile. Returns the set of + // devices which are in state STATE_CONNECTED. The BluetoothDevice class + // is just a thin wrapper for a Bluetooth hardware address. + List devices = bluetoothHeadset.getConnectedDevices(); + if (devices.isEmpty()) { + bluetoothDevice = null; + bluetoothState = State.HEADSET_UNAVAILABLE; + Log.d(Config.LOGTAG, "No connected bluetooth headset"); + } else { + // Always use first device in list. Android only supports one device. + bluetoothDevice = devices.get(0); + bluetoothState = State.HEADSET_AVAILABLE; + Log.d(Config.LOGTAG, "Connected bluetooth headset: " + + "name=" + bluetoothDevice.getName() + ", " + + "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) + + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice)); + } + Log.d(Config.LOGTAG, "updateDevice done: BT state=" + bluetoothState); + } + + /** + * Stubs for test mocks. + */ + @Nullable + protected AudioManager getAudioManager(Context context) { + return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + } + + protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { + apprtcContext.registerReceiver(receiver, filter); + } + + protected void unregisterReceiver(BroadcastReceiver receiver) { + apprtcContext.unregisterReceiver(receiver); + } + + protected boolean getBluetoothProfileProxy( + Context context, BluetoothProfile.ServiceListener listener, int profile) { + return bluetoothAdapter.getProfileProxy(context, listener, profile); + } + + protected boolean hasPermission(Context context, String permission) { + return apprtcContext.checkPermission(permission, Process.myPid(), Process.myUid()) + == PackageManager.PERMISSION_GRANTED; + } + + /** + * Logs the state of the local Bluetooth adapter. + */ + @SuppressLint("HardwareIds") + protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) { + Log.d(Config.LOGTAG, "BluetoothAdapter: " + + "enabled=" + localAdapter.isEnabled() + ", " + + "state=" + stateToString(localAdapter.getState()) + ", " + + "name=" + localAdapter.getName() + ", " + + "address=" + localAdapter.getAddress()); + // Log the set of BluetoothDevice objects that are bonded (paired) to the local adapter. + Set pairedDevices = localAdapter.getBondedDevices(); + if (!pairedDevices.isEmpty()) { + Log.d(Config.LOGTAG, "paired devices:"); + for (BluetoothDevice device : pairedDevices) { + Log.d(Config.LOGTAG, " name=" + device.getName() + ", address=" + device.getAddress()); + } + } + } + + /** + * Ensures that the audio manager updates its list of available audio devices. + */ + private void updateAudioDeviceState() { + ThreadUtils.checkIsOnMainThread(); + Log.d(Config.LOGTAG, "updateAudioDeviceState"); + apprtcAudioManager.updateAudioDeviceState(); + } + + /** + * Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds. + */ + private void startTimer() { + ThreadUtils.checkIsOnMainThread(); + Log.d(Config.LOGTAG, "startTimer"); + handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS); + } + + /** + * Cancels any outstanding timer tasks. + */ + private void cancelTimer() { + ThreadUtils.checkIsOnMainThread(); + Log.d(Config.LOGTAG, "cancelTimer"); + handler.removeCallbacks(bluetoothTimeoutRunnable); + } + + /** + * Called when start of the BT SCO channel takes too long time. Usually + * happens when the BT device has been turned on during an ongoing call. + */ + private void bluetoothTimeout() { + ThreadUtils.checkIsOnMainThread(); + if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) { + return; + } + Log.d(Config.LOGTAG, "bluetoothTimeout: BT state=" + bluetoothState + ", " + + "attempts: " + scoConnectionAttempts + ", " + + "SCO is on: " + isScoOn()); + if (bluetoothState != State.SCO_CONNECTING) { + return; + } + // Bluetooth SCO should be connecting; check the latest result. + boolean scoConnected = false; + List devices = bluetoothHeadset.getConnectedDevices(); + if (devices.size() > 0) { + bluetoothDevice = devices.get(0); + if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) { + Log.d(Config.LOGTAG, "SCO connected with " + bluetoothDevice.getName()); + scoConnected = true; + } else { + Log.d(Config.LOGTAG, "SCO is not connected with " + bluetoothDevice.getName()); + } + } + if (scoConnected) { + // We thought BT had timed out, but it's actually on; updating state. + bluetoothState = State.SCO_CONNECTED; + scoConnectionAttempts = 0; + } else { + // Give up and "cancel" our request by calling stopBluetoothSco(). + Log.w(Config.LOGTAG, "BT failed to connect after timeout"); + stopScoAudio(); + } + updateAudioDeviceState(); + Log.d(Config.LOGTAG, "bluetoothTimeout done: BT state=" + bluetoothState); + } + + /** + * Checks whether audio uses Bluetooth SCO. + */ + private boolean isScoOn() { + return audioManager.isBluetoothScoOn(); + } + + /** + * Converts BluetoothAdapter states into local string representations. + */ + private String stateToString(int state) { + switch (state) { + case BluetoothAdapter.STATE_DISCONNECTED: + return "DISCONNECTED"; + case BluetoothAdapter.STATE_CONNECTED: + return "CONNECTED"; + case BluetoothAdapter.STATE_CONNECTING: + return "CONNECTING"; + case BluetoothAdapter.STATE_DISCONNECTING: + return "DISCONNECTING"; + case BluetoothAdapter.STATE_OFF: + return "OFF"; + case BluetoothAdapter.STATE_ON: + return "ON"; + case BluetoothAdapter.STATE_TURNING_OFF: + // Indicates the local Bluetooth adapter is turning off. Local clients should immediately + // attempt graceful disconnection of any remote links. + return "TURNING_OFF"; + case BluetoothAdapter.STATE_TURNING_ON: + // Indicates the local Bluetooth adapter is turning on. However local clients should wait + // for STATE_ON before attempting to use the adapter. + return "TURNING_ON"; + default: + return "INVALID"; + } + } + + // Bluetooth connection state. + public enum State { + // Bluetooth is not available; no adapter or Bluetooth is off. + UNINITIALIZED, + // Bluetooth error happened when trying to start Bluetooth. + ERROR, + // Bluetooth proxy object for the Headset profile exists, but no connected headset devices, + // SCO is not started or disconnected. + HEADSET_UNAVAILABLE, + // Bluetooth proxy object for the Headset profile connected, connected Bluetooth headset + // present, but SCO is not started or disconnected. + HEADSET_AVAILABLE, + // Bluetooth audio SCO connection with remote device is closing. + SCO_DISCONNECTING, + // Bluetooth audio SCO connection with remote device is initiated. + SCO_CONNECTING, + // Bluetooth audio SCO connection with remote device is established. + SCO_CONNECTED + } + + /** + * Implementation of an interface that notifies BluetoothProfile IPC clients when they have been + * connected to or disconnected from the service. + */ + private class BluetoothServiceListener implements BluetoothProfile.ServiceListener { + @Override + // Called to notify the client when the proxy object has been connected to the service. + // Once we have the profile proxy object, we can use it to monitor the state of the + // connection and perform other operations that are relevant to the headset profile. + public void onServiceConnected(int profile, BluetoothProfile proxy) { + if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { + return; + } + Log.d(Config.LOGTAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState); + // Android only supports one connected Bluetooth Headset at a time. + bluetoothHeadset = (BluetoothHeadset) proxy; + updateAudioDeviceState(); + Log.d(Config.LOGTAG, "onServiceConnected done: BT state=" + bluetoothState); + } + + @Override + /** Notifies the client when the proxy object has been disconnected from the service. */ + public void onServiceDisconnected(int profile) { + if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { + return; + } + Log.d(Config.LOGTAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState); + stopScoAudio(); + bluetoothHeadset = null; + bluetoothDevice = null; + bluetoothState = State.HEADSET_UNAVAILABLE; + updateAudioDeviceState(); + Log.d(Config.LOGTAG, "onServiceDisconnected done: BT state=" + bluetoothState); + } + } + + // Intent broadcast receiver which handles changes in Bluetooth device availability. + // Detects headset changes and Bluetooth SCO state changes. + private class BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (bluetoothState == State.UNINITIALIZED) { + return; + } + final String action = intent.getAction(); + // Change in connection state of the Headset profile. Note that the + // change does not tell us anything about whether we're streaming + // audio to BT over SCO. Typically received when user turns on a BT + // headset while audio is active using another audio device. + if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { + final int state = + intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED); + Log.d(Config.LOGTAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " + + "a=ACTION_CONNECTION_STATE_CHANGED, " + + "s=" + stateToString(state) + ", " + + "sb=" + isInitialStickyBroadcast() + ", " + + "BT state: " + bluetoothState); + if (state == BluetoothHeadset.STATE_CONNECTED) { + scoConnectionAttempts = 0; + updateAudioDeviceState(); + } else if (state == BluetoothHeadset.STATE_CONNECTING) { + // No action needed. + } else if (state == BluetoothHeadset.STATE_DISCONNECTING) { + // No action needed. + } else if (state == BluetoothHeadset.STATE_DISCONNECTED) { + // Bluetooth is probably powered off during the call. + stopScoAudio(); + updateAudioDeviceState(); + } + // Change in the audio (SCO) connection state of the Headset profile. + // Typically received after call to startScoAudio() has finalized. + } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { + final int state = intent.getIntExtra( + BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED); + Log.d(Config.LOGTAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " + + "a=ACTION_AUDIO_STATE_CHANGED, " + + "s=" + stateToString(state) + ", " + + "sb=" + isInitialStickyBroadcast() + ", " + + "BT state: " + bluetoothState); + if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { + cancelTimer(); + if (bluetoothState == State.SCO_CONNECTING) { + Log.d(Config.LOGTAG, "+++ Bluetooth audio SCO is now connected"); + bluetoothState = State.SCO_CONNECTED; + scoConnectionAttempts = 0; + updateAudioDeviceState(); + } else { + Log.w(Config.LOGTAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED"); + } + } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) { + Log.d(Config.LOGTAG, "+++ Bluetooth audio SCO is now connecting..."); + } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { + Log.d(Config.LOGTAG, "+++ Bluetooth audio SCO is now disconnected"); + if (isInitialStickyBroadcast()) { + Log.d(Config.LOGTAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast."); + return; + } + updateAudioDeviceState(); + } + } + Log.d(Config.LOGTAG, "onReceive done: BT state=" + bluetoothState); + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCProximitySensor.java b/src/main/java/eu/siacs/conversations/services/AppRTCProximitySensor.java new file mode 100644 index 000000000..8bdc65f2e --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/AppRTCProximitySensor.java @@ -0,0 +1,170 @@ +/* + * Copyright 2014 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ +package eu.siacs.conversations.services; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Build; +import android.support.annotation.Nullable; +import android.util.Log; + +import org.webrtc.ThreadUtils; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.utils.AppRTCUtils; + +/** + * AppRTCProximitySensor manages functions related to the proximity sensor in + * the AppRTC demo. + * On most device, the proximity sensor is implemented as a boolean-sensor. + * It returns just two values "NEAR" or "FAR". Thresholding is done on the LUX + * value i.e. the LUX value of the light sensor is compared with a threshold. + * A LUX-value more than the threshold means the proximity sensor returns "FAR". + * Anything less than the threshold value and the sensor returns "NEAR". + */ +public class AppRTCProximitySensor implements SensorEventListener { + // This class should be created, started and stopped on one thread + // (e.g. the main thread). We use |nonThreadSafe| to ensure that this is + // the case. Only active when |DEBUG| is set to true. + private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker(); + private final Runnable onSensorStateListener; + private final SensorManager sensorManager; + @Nullable + private Sensor proximitySensor; + private boolean lastStateReportIsNear; + + private AppRTCProximitySensor(Context context, Runnable sensorStateListener) { + Log.d(Config.LOGTAG, "AppRTCProximitySensor" + AppRTCUtils.getThreadInfo()); + onSensorStateListener = sensorStateListener; + sensorManager = ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE)); + } + + /** + * Construction + */ + static AppRTCProximitySensor create(Context context, Runnable sensorStateListener) { + return new AppRTCProximitySensor(context, sensorStateListener); + } + + /** + * Activate the proximity sensor. Also do initialization if called for the + * first time. + */ + public boolean start() { + threadChecker.checkIsOnValidThread(); + Log.d(Config.LOGTAG, "start" + AppRTCUtils.getThreadInfo()); + if (!initDefaultSensor()) { + // Proximity sensor is not supported on this device. + return false; + } + sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL); + return true; + } + + /** + * Deactivate the proximity sensor. + */ + public void stop() { + threadChecker.checkIsOnValidThread(); + Log.d(Config.LOGTAG, "stop" + AppRTCUtils.getThreadInfo()); + if (proximitySensor == null) { + return; + } + sensorManager.unregisterListener(this, proximitySensor); + } + + /** + * Getter for last reported state. Set to true if "near" is reported. + */ + public boolean sensorReportsNearState() { + threadChecker.checkIsOnValidThread(); + return lastStateReportIsNear; + } + + @Override + public final void onAccuracyChanged(Sensor sensor, int accuracy) { + threadChecker.checkIsOnValidThread(); + AppRTCUtils.assertIsTrue(sensor.getType() == Sensor.TYPE_PROXIMITY); + if (accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) { + Log.e(Config.LOGTAG, "The values returned by this sensor cannot be trusted"); + } + } + + @Override + public final void onSensorChanged(SensorEvent event) { + threadChecker.checkIsOnValidThread(); + AppRTCUtils.assertIsTrue(event.sensor.getType() == Sensor.TYPE_PROXIMITY); + // As a best practice; do as little as possible within this method and + // avoid blocking. + float distanceInCentimeters = event.values[0]; + if (distanceInCentimeters < proximitySensor.getMaximumRange()) { + Log.d(Config.LOGTAG, "Proximity sensor => NEAR state"); + lastStateReportIsNear = true; + } else { + Log.d(Config.LOGTAG, "Proximity sensor => FAR state"); + lastStateReportIsNear = false; + } + // Report about new state to listening client. Client can then call + // sensorReportsNearState() to query the current state (NEAR or FAR). + if (onSensorStateListener != null) { + onSensorStateListener.run(); + } + Log.d(Config.LOGTAG, "onSensorChanged" + AppRTCUtils.getThreadInfo() + ": " + + "accuracy=" + event.accuracy + ", timestamp=" + event.timestamp + ", distance=" + + event.values[0]); + } + + /** + * Get default proximity sensor if it exists. Tablet devices (e.g. Nexus 7) + * does not support this type of sensor and false will be returned in such + * cases. + */ + private boolean initDefaultSensor() { + if (proximitySensor != null) { + return true; + } + proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); + if (proximitySensor == null) { + return false; + } + logProximitySensorInfo(); + return true; + } + + /** + * Helper method for logging information about the proximity sensor. + */ + private void logProximitySensorInfo() { + if (proximitySensor == null) { + return; + } + StringBuilder info = new StringBuilder("Proximity sensor: "); + info.append("name=").append(proximitySensor.getName()); + info.append(", vendor: ").append(proximitySensor.getVendor()); + info.append(", power: ").append(proximitySensor.getPower()); + info.append(", resolution: ").append(proximitySensor.getResolution()); + info.append(", max range: ").append(proximitySensor.getMaximumRange()); + info.append(", min delay: ").append(proximitySensor.getMinDelay()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { + // Added in API level 20. + info.append(", type: ").append(proximitySensor.getStringType()); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // Added in API level 21. + info.append(", max delay: ").append(proximitySensor.getMaxDelay()); + info.append(", reporting mode: ").append(proximitySensor.getReportingMode()); + info.append(", isWakeUpSensor: ").append(proximitySensor.isWakeUpSensor()); + } + Log.d(Config.LOGTAG, info.toString()); + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 24c80713c..543eb9fd5 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -655,7 +655,7 @@ public class XmppConnectionService extends Service { Log.d(Config.LOGTAG, "received intent to end call with session id " + sessionId); mJingleConnectionManager.endRtpSession(sessionId); } - break; + break; case ACTION_DISMISS_ERROR_NOTIFICATIONS: dismissErrorNotifications(); break; @@ -4017,6 +4017,12 @@ public class XmppConnectionService extends Service { } } + public void notifyJingleRtpConnectionUpdate(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { + for (OnJingleRtpConnectionUpdate listener : threadSafeList(this.onJingleRtpConnectionUpdate)) { + listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices); + } + } + public void updateAccountUi() { for (OnAccountUpdate listener : threadSafeList(this.mOnAccountUpdates)) { listener.onAccountUpdate(); @@ -4696,6 +4702,8 @@ public class XmppConnectionService extends Service { public interface OnJingleRtpConnectionUpdate { void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state); + + void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices); } public interface OnAccountUpdate { diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index fb4de0e95..bdef662bd 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -20,14 +20,17 @@ import com.google.common.collect.ImmutableList; import java.lang.ref.WeakReference; import java.util.Arrays; +import java.util.Set; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityRtpSessionBinding; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.PermissionUtils; +import eu.siacs.conversations.utils.ThemeHelper; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; @@ -57,6 +60,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private ActivityRtpSessionBinding binding; private PowerManager.WakeLock mProximityWakeLock; + private static AppRTCAudioManager audioManager; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -143,7 +148,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe super.onNewIntent(intent); setIntent(intent); if (xmppConnectionService == null) { - Log.d(Config.LOGTAG,"RtpSessionActivity: background service wasn't bound in onNewIntent()"); + Log.d(Config.LOGTAG, "RtpSessionActivity: background service wasn't bound in onNewIntent()"); return; } final Account account = extractAccount(intent); @@ -339,6 +344,16 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.binding.endCall.setVisibility(View.VISIBLE); this.binding.acceptCall.setVisibility(View.INVISIBLE); } + + if (state == RtpEndUserState.CONNECTED) { + this.binding.inCallActionLeft.setImageResource(R.drawable.ic_volume_off_black_24dp); + this.binding.inCallActionLeft.setVisibility(View.VISIBLE); + this.binding.inCallActionRight.setImageResource(R.drawable.ic_mic_black_24dp); + this.binding.inCallActionRight.setVisibility(View.VISIBLE); + } else { + this.binding.inCallActionLeft.setVisibility(View.GONE); + this.binding.inCallActionRight.setVisibility(View.GONE); + } } private void retry(View view) { @@ -401,6 +416,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } + @Override + public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { + Log.d(Config.LOGTAG, "onAudioDeviceChanged in activity: selected:" + selectedAudioDevice + ", available:" + availableAudioDevices); + } + private void updateRtpSessionProposalState(final Account account, final Jid with, final RtpEndUserState state) { final Intent currentIntent = getIntent(); final String withExtra = currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH); diff --git a/src/main/java/eu/siacs/conversations/utils/AppRTCUtils.java b/src/main/java/eu/siacs/conversations/utils/AppRTCUtils.java new file mode 100644 index 000000000..1b6e43f75 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/AppRTCUtils.java @@ -0,0 +1,55 @@ + + +/* + * Copyright 2014 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ +package eu.siacs.conversations.utils; + +import android.os.Build; +import android.util.Log; + +/** + * AppRTCUtils provides helper functions for managing thread safety. + */ +public final class AppRTCUtils { + private AppRTCUtils() { + } + + /** + * Helper method which throws an exception when an assertion has failed. + */ + public static void assertIsTrue(boolean condition) { + if (!condition) { + throw new AssertionError("Expected condition to be true"); + } + } + + /** + * Helper method for building a string of thread information. + */ + public static String getThreadInfo() { + return "@[name=" + Thread.currentThread().getName() + ", id=" + Thread.currentThread().getId() + + "]"; + } + + /** + * Information about the current build, taken from system properties. + */ + public static void logDeviceInfo(String tag) { + Log.d(tag, "Android SDK: " + Build.VERSION.SDK_INT + ", " + + "Release: " + Build.VERSION.RELEASE + ", " + + "Brand: " + Build.BRAND + ", " + + "Device: " + Build.DEVICE + ", " + + "Id: " + Build.ID + ", " + + "Hardware: " + Build.HARDWARE + ", " + + "Manufacturer: " + Build.MANUFACTURER + ", " + + "Model: " + Build.MODEL + ", " + + "Product: " + Build.PRODUCT); + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 3cfb2b15c..d3dba70a9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -17,12 +17,14 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.RtpSessionStatus; +import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; @@ -831,6 +833,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } + @Override + public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { + xmppConnectionService.notifyJingleRtpConnectionUpdate(selectedAudioDevice, availableAudioDevices); + } + private void updateEndUserState() { xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, getEndUserState()); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 8eb46fe1e..0b776e431 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -1,6 +1,8 @@ package eu.siacs.conversations.xmpp.jingle; import android.content.Context; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import com.google.common.collect.ImmutableList; @@ -29,11 +31,13 @@ import org.webrtc.VideoSource; import org.webrtc.VideoTrack; import java.util.List; +import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; import eu.siacs.conversations.Config; +import eu.siacs.conversations.services.AppRTCAudioManager; public class WebRTCWrapper { @@ -119,8 +123,16 @@ public class WebRTCWrapper { } }; + private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = new AppRTCAudioManager.AudioManagerEvents() { + @Override + public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { + eventCallback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices); + } + }; @Nullable private PeerConnection peerConnection = null; + private AppRTCAudioManager appRTCAudioManager = null; + private final Handler mainHandler = new Handler(Looper.getMainLooper()); public WebRTCWrapper(final EventCallback eventCallback) { this.eventCallback = eventCallback; @@ -130,6 +142,10 @@ public class WebRTCWrapper { PeerConnectionFactory.initialize( PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions() ); + mainHandler.post(() -> { + appRTCAudioManager = AppRTCAudioManager.create(context, AppRTCAudioManager.SpeakerPhonePreference.EARPIECE); + appRTCAudioManager.start(audioManagerEvents); + }); } public void initializePeerConnection(final List iceServers) throws InitializationException { @@ -202,16 +218,15 @@ public class WebRTCWrapper { peerConnection.setAudioRecording(true); this.peerConnection = peerConnection; } - - public void closeOrThrow() { - requirePeerConnection().close(); - } - public void close() { final PeerConnection peerConnection = this.peerConnection; if (peerConnection != null) { peerConnection.close(); } + final AppRTCAudioManager audioManager = this.appRTCAudioManager; + if (audioManager != null) { + mainHandler.post(audioManager::stop); + } } @@ -355,5 +370,7 @@ public class WebRTCWrapper { void onIceCandidate(IceCandidate iceCandidate); void onConnectionChange(PeerConnection.PeerConnectionState newState); + + void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices); } } diff --git a/src/main/res/drawable-hdpi/ic_mic_black_24dp.png b/src/main/res/drawable-hdpi/ic_mic_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..74218e1a654239fed6b687d62cd6af594c5cf71d GIT binary patch literal 344 zcmV-e0jK_nP)El6=!u!2@xw3|4KiT_1kLfjMw!ATHt%!^dxF41^} z8gpD6E*DJ9ji6}0-+sp9|Fz|{PbzPmHHXVa@}rSGEv*`SBTTIoomj#i2^ zy8pX|y8rG_=dT;O_Xmcy6~z+hN~61ASy618=#GeWa6=kh1*b*aHqiwU7r{v%rPc>0 zz!ed@COWPXF93V>8*vHnQVcDd;;|UptR36X&?}pOb0Qv@YF#DXIt7~6yceIXG2MZP zLuWK?>5I7O7d;g5%`@jMfn#<(Hx}{03=Vt|5nsL2Hxdz*TV}N3i6g0X_6vd8(xqSzZwB?CPi)9u70000`j^ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_mic_off_black_24dp.png b/src/main/res/drawable-hdpi/ic_mic_off_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..1755dbf3fab08d39326fa7a5963a9752a2a70aa2 GIT binary patch literal 402 zcmV;D0d4+?P)VX4gfi6} zj9m3m2%SyJYy1#GIPldM2SSJ%>Wr6uiQayC;Xw_>NjJO~!UyY4fbBYpR=)eYnW}|k wsvhEwPUuW3p+r5gptxnN*0LOz%bEYd9};=|!lODW)&Kwi07*qoM6N<$f_Q1S=>Px# literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_volume_off_black_24dp.png b/src/main/res/drawable-hdpi/ic_volume_off_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..f1326ba7c66527698d531e576b8c52cc391189ba GIT binary patch literal 407 zcmV;I0cie-P);#*5)f=HEFu<48e1*I2N0h? zL`cw#Fmp*}vunniI%leI;UhbDNtmW8B4%8<|1#(FDe-qb+7yWh6w}9KQb)LqJx}Bb zR2Hg?Uju{dN2qW1_0%zD04>tsL$D@+DhX%z_|8YoHKCT{6K2l>=)0y0oDkMcHnL$G=Z%A$5aN0CX*0q7{QDbP~GHh9BKPPx`-(iM6_$|VFn zE+>6JA9`fd`(SA8EEWNd@XsA`1=9f1^7b^bxnRzA1Z zVG5)i_pndFov5ybeUJ}~f+V!mTMaoPmrQ`Gs`J7?+@USeR%a(}j0|(9{U?%`!8XQ? zzrW>}QM<&W$8UxY|3%J874k_nkKfTdWn%cquTvyalLKby3-k@)_AuR&OJC9e0000< KMNUMnLSTYh!I?_{ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_mic_black_24dp.png b/src/main/res/drawable-mdpi/ic_mic_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..19a16138fe6e7d8b474b947b28c73307a8fce199 GIT binary patch literal 232 zcmVP)H;?C}1_!@?CvWq1VFURB+Cu!3L>r6hhJe(6cYIJ2%uL;t)9p{%BV39DvuU|=GIiGgFQMv{d9^xKgHN#?CUlDSKNwI4Rh VaA?C^Ro?&r002ovPDHLkV1oGvbIbq$ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_volume_off_black_24dp.png b/src/main/res/drawable-mdpi/ic_volume_off_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a629cfff539acfd740f2f453b31ab595f3aca279 GIT binary patch literal 279 zcmV+y0qFjTP)VA~UEv`kM<9e}OA1x`895eU-<$BM{Ws~WOz9-RtP5diu}lVu zGm3I0|&xc5oDx=D`6cg!s}R&$h2#Fm=V6}KqA%> z_^t!JL||2TsRLC;c7;>L4kbWFnmRTaPmBy31VVVUtS8*6&xs9C d>1vzQegTRRkWq31_4xn*002ovPDHLkV1fXya%KPk literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_volume_up_black_24dp.png b/src/main/res/drawable-mdpi/ic_volume_up_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4ed27786a14065d9c6593b522ed2b0f99e580138 GIT binary patch literal 235 zcmVSZ-ns}-Ox1blNm2bV#qBV-0>fz>GiJ>};*0p%m)yCHw led{*W=WWm_b7I@y=L_YThWS09+57+i002ovPDHLkV1jr0XO#c| literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_mic_black_24dp.png b/src/main/res/drawable-xhdpi/ic_mic_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..cac51c37a38c0ae7800b2f93c2d567508b417c52 GIT binary patch literal 418 zcmV;T0bTxyP)_%aKH zx>y7uq*Z&n1RjKgB=rdx$o1RL4QKfqBNGiPI(mBAZfU6586QN|drdWJsGhGTrqpId z3f)wjjudLCO-~AS)h6XeJ+MFa1HVQ+@N?`3evEoxZ|nzlM?KK{j|cWdTm#UPLOTGK zhyAi`5qAN!q|h?}_e5+N*i#XY0o;&6ivZR{tQpuH5naatQ{ENM%^|?7T@kAWc1}dx z1~BKXsA@Y6U{yq%HH38$-^>A+vTRdV*M=pB0nGa@;)$VLurK1JX@4_gQ^daWhV(#0 ztUHOaS$rWEjxowx#;HV{U#GiFsH?2lZ3({{v(@3NrCIgd!8cj_626bPT(G584*Z=?k M07*qoM6N<$f)`D-EC2ui literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_mic_off_black_24dp.png b/src/main/res/drawable-xhdpi/ic_mic_off_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..fa741be1c0e71d611d0b63c8f7d3c210be2eb581 GIT binary patch literal 454 zcmV;%0XhDOP)!_7sd*(?%MX;je7rHyoQ zC?WpA*X8gV?4jfr&l!Y#zWe(mkZ{cVk6N^D%f!UMeG3YA-bbOC_c~^3MNc>L>6`P- zh8AWy|5?~MK&*?**0ioCM26VP0)4IP+Q|}=EHI`N(HGe%MJ(kBY7=S`Y7=S`Y7=S` zY7=S`N)f;Fgkt>?Sz_jX2;sJreUTwfNVzM7pQdgkgvV0eh#b-ML_pWJ(M+~?Zv_iWEOxu4~nCVC2kW?K-ZMczY{xH;tl}?QhuYvTMiRhzks+aQ|&ZU)*11ULmr~T zI~35)v&e`ybgB@2nH?0uwvQ(naUbG|RRICEJv_~bD}Exc{d~$UZHwrVTmtDe3EGA1 zP7T3BXn`LL&>qOiSCkY=*2)m!FQKzDY~0K33PP(o-9 zg8xlycbTt*dI{|;k7ZUXA@P!+i%ormwDs|OoMTlNVSx_}%pXXKGX-97gWX6%Ea*~8 jV@_a8Na@nlRC9j-C(7lD#HSVVB&sm*<6cg}E*jxzHS6Z3~`GLBaah6ZaSQDx5+*wvz>8E?V`vdW~j z;$1up7q=8O-tZOu zw5{7F87MGbP+{wAR&Z8 zrdbq3QQ?elV2TvO=8w&3_i&DvnZt1JoLlgGe!F||@MZAA;UA}z$}q(e>ollSVUbBv zm7|2gU6Cv)Im<5$(QT0@1-WRBm6Rp03?)p5trFN2#d7Sz-r&l(!!SzUwDy$@-S|EfBIt?mul#5S7L|JDyV=oq$& zH|P_#6Y{)DEFa~di`W*uK?Q7=O8W!Y+*v%x5~97cAOn7+tSPjKbiByDmUS3%x=Cbrww2YVP-meokk8^&-Q zp($@~5!-$?RCIN6P=Z0J1;I^3 z97|h7&_NMn7XK7l6;WH7qNk|?cD=RDI<>;eAjRtkz zafkhQZ7Y)~_1(&3T5i}1jNgq(g!6hk=nS^~(GZ6{bq%H%Aw2|;b z`se^;Aoa&2En>z0BH@p;<*y{tM#4AglNscfDs&*1=7G)r9CfEjBkG5a*LE8xS1{Ip7Ajx*dNtx*qyVYLo%i z5o|rDct{iFhBTu5FDW?Ile(q;$BQnR#sM4)^CUa^M@3H-y{G4002ovPDHLk FV1mZ`Cw>3` literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_volume_off_black_24dp.png b/src/main/res/drawable-xxhdpi/ic_volume_off_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2d57c8674a901fd8e6717b910423fa4ac9a4f1dd GIT binary patch literal 704 zcmV;x0zdtUP)dRN-E8J&OtIg8IWx-#vU|IPJiGGp^77^|OqrS1 z*?i6jhU$z-hL2of3x#HKj04I1o0lA5F3t^OFd0dMXh!BOdvL6WYDUs2Obnzm%F5=F zdKz&WfDGU03q=~j#6Z6=33HZ$8)?#lbQ+TYYGXeS6G+!7`XnjG5N|tUG13k2M_S5< zx*#ZWgdY*o8K0!JLQ1QX@d&98rz%QfGD2E2O-gSG_ajf-YQLoS4M4CULb?k+N$r10 z^;^s#K=VnJx}P7H zh_(|Wvkl9P?Ie@r3df1|NKRjM2U(J)S8K6*X+X|?rsxi`BFX9#3TSQUAAN&6$d2?* zR?C1kn*B*L&ZKrB-2t>AId`q;AR+apkgCi_>()FZPkEWDolnB21APuB3+~+1|dz<(hkJJ#UM2_#8f0$B?hS$5mj@HdgurmiK495EfdRQVaQB6afivTMZb z3uf%$FOh$$yk9s=%riCPmc7st1BBGnj4xEIg@hMmXsRr#(SCa&;UFP(z0y2{?h|rR zjotPTYO)&5klsQ(goNE_dez9Rk5CQGIyLguN9a77F8w*nCiD}4^vMR>{;3;2%YIec zW&glK=pG@L^!Dl@G(^ZlHFm;YXqpVo+F*0tW-YYH4nj_<8K3QsA7jMmL+qsW5%wxE z2Z9-cmWTW%r-|w0jmp2ZyaqjDJFScd`QvsM#eB8~ZL9_>e6P|Qm<`gSzRiYe)K8o8 zZO3fnNsyeU$FpJ8Pa{em@@wgJFwR1xbHT6axt~`^R5yLv!rjzzWP}@*ooY=pN9Gq3 zWqMkeXShs{FH>wP&-2s=hH*s;z2*p=;EyoJb#_u3wvb395{duCZ*dvEM$7kCH~;_u M07*qoM6N<$f=QhsL;wH) literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_mic_black_24dp.png b/src/main/res/drawable-xxxhdpi/ic_mic_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..cf70b63bebaf755b4c6697fb7959f07a7484c8e0 GIT binary patch literal 773 zcmV+g1N!`lP)7{~GVYQO#gO-0?6g48-hbO;X}5)z3dq92lhL6L(!!}hr){xg<4?* zc1cOQrc`$6Vo2Fx(Mr)mx6;*h>ge}7vpVDK?01Cc^&IzMzRzco-I-@a2qAX>I@+jBjGB*9HoR( zHK488@c<0YrQ*cb}unWq!r$pUJq_$^a$ku6YV@5#7$9x7TgT0Y}kA zNqM-L&O>)X>9|kEomURHhuss`0ot)^S31s-aXXX)#)zORVb0a~GvuSoA+Gd93BVWS zh(p-byV}XsVOOj4j1q|3${{as(!}x_OWg}N=~cRJ5{L?InEg08#cbN~e{l>aRa)P! z?EH{7NDfYl>0-%YnfQo8oaFLe>3m6Gs$KuzLn$rv5oh6l#p&Y;#jby#JxG8CwDpw# z!>UOQ~;E`WXL*cC{M>ZWHfppDi`4#}V7-)eyO4 zL5xY;m(v}#;>9-Z+P*QqQ$m(hF^{FyC+cwPIHL}mq61`6twRKhx74$l)oh`GVaG*R z4Ov!8BF(MZ15Q$au87k-s% z6rqW^bq_zwB_bGC#AB8{51*owLPDdGE)ScbBps9!I{7s7Va*4cVJ?$Lh!xUE8$-6y zhiIdb0yYdiKn6(w2_OL^fCP{L5%!Xga|nR(nD$XsI1Bs0T3aKFFx49^EHb73?!H8nLg4WS9I zTd-u!nnmYK*{zs%`%p*ptmBM370?kYVuLMvO3iykY_j7<+>2(kPwi+A}F1pbb`_eN+&3tpbWyp z8xoXGP&z^B1f>&{PEa~QiG%|Zfzk=;M!4Q5p>`!eCZXPd48q7;)lU0SwQE8?VZUk@ zL|VtdyDHKys`k6+i%{*NNV{f`Rla^j^`KA1wg@f%inQMhyz@eualIWE+ain$X}&P< zP6}xr*V|6*Z4nL%Y2G&QjtXhsRPBAS4Z>SOnr95WJwlpaRom(BEfZS)5YpUjz*T=| z9#ZX)?iL9LgfzbxO4CA`PgLWW*b?DEt0L_UL+Ku|ZuhFjVauYr{Qp#S?J=Ylgfw3p zQ;l6t>uiJYnV~f)*6o;$nlNR-Wo;cxF7!p%FE#Af*_!)pRtsZJWfML&)b{nfuKu)F zPWgl}LvC8E+v30pL+)ljh;_SavMj<~u84JOn|4b{ga_OZJ^R_CB@rI05`=F&>84E{ zz(ZE+LRb~mxnj-}cIfR^2hICS2vHBheJ+a)x8y4yIc45yU%DjHYDC!SyvSc231eRN zPuT>qw{oN+VPNMG*1<`Giscw9M6sPyv)ir~t|#bUjCLfJEqemNEgf%>Li`O-)TrO-)TrP5%M7+Xys|z?EVE0000< KMNUMnLSTX;#)AU@ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_volume_off_black_24dp.png b/src/main/res/drawable-xxxhdpi/ic_volume_off_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..6d9b355842a2debb4e9f7da09d1a8852d4cfa70c GIT binary patch literal 924 zcmV;N17rM&P)*v`}dgEy{e^6cn`R#a@UYq6l(Eds(#% zj8G&9T1g@;5|f~1X)hvyNoJ$tX%mspIWzMxcf8LXIN!TH_u=^n^ZRiwu{y-EoZEVPht$Y;!QCCl|)D+&M3tMUbp(v{Wvtt7brJ-a+hZwBk9Ak6y! z@f0~k#QI^t1k3zglW@v}J>fN+1c>^0iG9L^Gaj=f$n_nyx{jAQ3KGtlv?C02 ztONku%4(2s!iXK=VL@or3@3rfzcbu=!lIHec7f2iyZJdV`5U^jA(R^sG~LGcfywWJ z9pRz>2)b_Jhro~dzB~}L4Y23U-KJZ|Xih)&$u$ zqttH2oBBN15@ZfXiE9Wqz@K8~uN3VG65!b2X2QMb%^Zg;35z5OfMbW72-U-ZnD`Y< z<+P8UAm(n+O{f>V{}1{z0O$x}Zi!HRRK&dL3;;@koI6ISPsORV3;;@koV!B-zj1Nu z4+T5A+$_C(AQV@pldr!=H+W?>@%;NXBn6hjD zP!m4EZ$y6dG5{zE;pGO!gr5umYQh12H^@uA0YFWt;MXPIt{4E+gfsjK^6Rw$Kuy@i zuUEWzYyeObZpUv(y!VR%Ku=KoGrDC0NI`f?yt8TtSZIn+mS5@Y07+AX9dZ5%I{-}( zu6n2MG6Xb1nDZ_)9fp7=2m49bOnK&|2>UwADc zB7%&h0!xvgXc1wlIE6);Gq2sCO>pn|;t;}|3->wOb1t6;<~uWd?;+qnfj}S-2n0ML zj_1tqFP)U)E#`MEaG$*-{S^399QTDtQ{? z%K~z}qEcIYSs=Qm*{LnQE6_|I^N|V}T6D!XNECB{smP~czoz(xC<^G}o2c-URz2|z z(ZD9|iVE}8X^C%Gj1GPXQ(vgk5#O*l+xRHVy&$O}z9AdRc`Zy{*AU;39VNUG9_HHI z$2Vk48RO3ED2ZIhH)PLt=A7wMxsLA&#E7QdnVzB`xA9$pQ5uM*-u6w2J{My5yPd)>iSAdtAL>emqIdD*H*m(Pmp+Obsg3#V5r39 zhShUTvp^><2dthx%>uV@sk3@s>J^Z~RaiYAGz(1QvfAqTtXUv~OVa8|6G;#VAE9C2 zAL5(1)cf=`+JVbKpMJ>7ap|#oy7dY?Mqr?mWd%4>~ z9x~<3TJxVs=z6_ALOsz8IP+(Gy5KnKOiz&S&lOXRaASPJpG&?9CWHl?xG&eN8+j+J zeN_8$(Otz`VY1toRf8HPg}G-W{8;ulNm`gnQR&0F&1N28gqovP|H4xtSD201*=p3j za#qS^QjuG8t-6=~_R&uo6H)W*_Id4cl3Je^U!POv@9OdYTq5D`^7k;+{;uCo(nbs~ za0T9ToO}TP1Ty?bBXK+=#cd8zgtx?4A_9RxAP@)y0>1!Yns3wvLsGc_0000 + android:layout_height="match_parent" + android:background="?color_background_secondary"> + + + + + + @@ -45,6 +46,7 @@ + diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index 054194fbe..e07c3bf67 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -15,6 +15,7 @@ @color/green600 @color/red800 @color/black87 + @color/black54 @drawable/search_background_light @drawable/no_results_background_light @@ -136,6 +137,7 @@ @color/green500 @color/red500 @color/white + @color/white70 @color/white @@ -147,6 +149,7 @@ 14sp 16sp 20sp + 45sp 16sp 5sp 18sp @@ -237,6 +240,7 @@ 16sp 18sp 22sp + 47sp 18sp 6sp 20sp @@ -248,6 +252,7 @@ 16sp 18sp 22sp + 47sp 18sp 6sp 20sp @@ -259,6 +264,7 @@ 18sp 20sp 24sp + 48sp 20sp 7sp 22sp @@ -270,6 +276,7 @@ 18sp 20sp 24sp + 48sp 20sp 7sp 22sp From 981aeaf264c90b4a4d645040c816e2b175e76c72 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 13 Apr 2020 12:53:23 +0200 Subject: [PATCH 088/182] make mute and speaker button work --- .../conversations/ui/RtpSessionActivity.java | 94 +++++++++++++++++- .../xmpp/jingle/JingleRtpConnection.java | 12 +++ .../xmpp/jingle/WebRTCWrapper.java | 26 ++++- .../ic_bluetooth_audio_black_24dp.png | Bin 0 -> 420 bytes .../drawable-hdpi/ic_headset_black_24dp.png | Bin 0 -> 349 bytes .../ic_bluetooth_audio_black_24dp.png | Bin 0 -> 283 bytes .../drawable-mdpi/ic_headset_black_24dp.png | Bin 0 -> 230 bytes .../ic_bluetooth_audio_black_24dp.png | Bin 0 -> 479 bytes .../drawable-xhdpi/ic_headset_black_24dp.png | Bin 0 -> 412 bytes .../ic_bluetooth_audio_black_24dp.png | Bin 0 -> 724 bytes .../drawable-xxhdpi/ic_headset_black_24dp.png | Bin 0 -> 586 bytes .../ic_bluetooth_audio_black_24dp.png | Bin 0 -> 867 bytes .../ic_headset_black_24dp.png | Bin 0 -> 786 bytes 13 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 src/main/res/drawable-hdpi/ic_bluetooth_audio_black_24dp.png create mode 100644 src/main/res/drawable-hdpi/ic_headset_black_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_bluetooth_audio_black_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_headset_black_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_bluetooth_audio_black_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_headset_black_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_bluetooth_audio_black_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_headset_black_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_bluetooth_audio_black_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_headset_black_24dp.png diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index bdef662bd..7e1b3592e 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -16,6 +16,7 @@ import android.view.View; import android.view.WindowManager; import android.widget.Toast; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import java.lang.ref.WeakReference; @@ -344,18 +345,91 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.binding.endCall.setVisibility(View.VISIBLE); this.binding.acceptCall.setVisibility(View.INVISIBLE); } + updateInCallButtonConfiguration(state); + } + private void updateInCallButtonConfiguration() { + updateInCallButtonConfiguration(requireRtpConnection().getEndUserState()); + } + + @SuppressLint("RestrictedApi") + private void updateInCallButtonConfiguration(final RtpEndUserState state) { if (state == RtpEndUserState.CONNECTED) { - this.binding.inCallActionLeft.setImageResource(R.drawable.ic_volume_off_black_24dp); - this.binding.inCallActionLeft.setVisibility(View.VISIBLE); - this.binding.inCallActionRight.setImageResource(R.drawable.ic_mic_black_24dp); - this.binding.inCallActionRight.setVisibility(View.VISIBLE); + final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager(); + updateInCallButtonConfiguration( + audioManager.getSelectedAudioDevice(), + audioManager.getAudioDevices().size(), + requireRtpConnection().isMicrophoneEnabled() + ); } else { this.binding.inCallActionLeft.setVisibility(View.GONE); this.binding.inCallActionRight.setVisibility(View.GONE); } } + @SuppressLint("RestrictedApi") + private void updateInCallButtonConfiguration(final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices, final boolean microphoneEnabled) { + switch (selectedAudioDevice) { + case EARPIECE: + this.binding.inCallActionLeft.setImageResource(R.drawable.ic_volume_off_black_24dp); + if (numberOfChoices >= 2) { + this.binding.inCallActionLeft.setOnClickListener(this::switchToSpeaker); + } else { + this.binding.inCallActionLeft.setOnClickListener(null); + this.binding.inCallActionLeft.setClickable(false); + } + break; + case WIRED_HEADSET: + this.binding.inCallActionLeft.setImageResource(R.drawable.ic_headset_black_24dp); + this.binding.inCallActionLeft.setOnClickListener(null); + this.binding.inCallActionLeft.setClickable(false); + break; + case SPEAKER_PHONE: + this.binding.inCallActionLeft.setImageResource(R.drawable.ic_volume_up_black_24dp); + if (numberOfChoices >= 2) { + this.binding.inCallActionLeft.setOnClickListener(this::switchToEarpiece); + } else { + this.binding.inCallActionLeft.setOnClickListener(null); + this.binding.inCallActionLeft.setClickable(false); + } + break; + case BLUETOOTH: + this.binding.inCallActionLeft.setImageResource(R.drawable.ic_bluetooth_audio_black_24dp); + this.binding.inCallActionLeft.setOnClickListener(null); + this.binding.inCallActionLeft.setClickable(false); + break; + } + this.binding.inCallActionLeft.setVisibility(View.VISIBLE); + if (microphoneEnabled) { + this.binding.inCallActionRight.setImageResource(R.drawable.ic_mic_black_24dp); + this.binding.inCallActionRight.setOnClickListener(this::disableMicrophone); + } else { + this.binding.inCallActionRight.setImageResource(R.drawable.ic_mic_off_black_24dp); + this.binding.inCallActionRight.setOnClickListener(this::enableMicrophone); + } + this.binding.inCallActionRight.setVisibility(View.VISIBLE); + } + + private void disableMicrophone(View view) { + JingleRtpConnection rtpConnection = requireRtpConnection(); + rtpConnection.setMicrophoneEnabled(false); + updateInCallButtonConfiguration(); + } + + private void enableMicrophone(View view) { + JingleRtpConnection rtpConnection = requireRtpConnection(); + rtpConnection.setMicrophoneEnabled(true); + updateInCallButtonConfiguration(); + } + + private void switchToEarpiece(View view) { + requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE); + } + + private void switchToSpeaker(View view) { + requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE); + } + private void retry(View view) { Log.d(Config.LOGTAG, "attempting retry"); final Intent intent = getIntent(); @@ -419,6 +493,18 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { Log.d(Config.LOGTAG, "onAudioDeviceChanged in activity: selected:" + selectedAudioDevice + ", available:" + availableAudioDevices); + try { + if (requireRtpConnection().getEndUserState() == RtpEndUserState.CONNECTED) { + final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager(); + updateInCallButtonConfiguration( + audioManager.getSelectedAudioDevice(), + audioManager.getAudioDevices().size(), + requireRtpConnection().isMicrophoneEnabled() + ); + } + } catch (IllegalStateException e) { + Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed"); + } } private void updateRtpSessionProposalState(final Account account, final Jid with, final RtpEndUserState state) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index d3dba70a9..b8a67cb0e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -833,6 +833,18 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } + public AppRTCAudioManager getAudioManager() { + return webRTCWrapper.getAudioManager(); + } + + public void setMicrophoneEnabled(final boolean enabled) { + webRTCWrapper.setMicrophoneEnabled(enabled); + } + + public boolean isMicrophoneEnabled() { + return webRTCWrapper.isMicrophoneEnabled(); + } + @Override public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { xmppConnectionService.notifyJingleRtpConnectionUpdate(selectedAudioDevice, availableAudioDevices); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 0b776e431..e21a65f19 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -131,6 +131,7 @@ public class WebRTCWrapper { }; @Nullable private PeerConnection peerConnection = null; + private AudioTrack localAudioTrack = null; private AppRTCAudioManager appRTCAudioManager = null; private final Handler mainHandler = new Handler(Looper.getMainLooper()); @@ -201,10 +202,9 @@ public class WebRTCWrapper { final AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints()); - final AudioTrack audioTrack = peerConnectionFactory.createAudioTrack("my-audio-track", audioSource); - Log.d(Config.LOGTAG, "audioTrack enabled:" + audioTrack.enabled() + " state=" + audioTrack.state()); + this.localAudioTrack = peerConnectionFactory.createAudioTrack("my-audio-track", audioSource); final MediaStream stream = peerConnectionFactory.createLocalMediaStream("my-media-stream"); - stream.addTrack(audioTrack); + stream.addTrack(this.localAudioTrack); //stream.addTrack(videoTrack); this.localVideoTrack = videoTrack; @@ -229,6 +229,22 @@ public class WebRTCWrapper { } } + public void setMicrophoneEnabled(final boolean enabled) { + final AudioTrack audioTrack = this.localAudioTrack; + if (audioTrack == null) { + throw new IllegalStateException("Local audio track does not exist (yet)"); + } + audioTrack.setEnabled(enabled); + } + + public boolean isMicrophoneEnabled() { + final AudioTrack audioTrack = this.localAudioTrack; + if (audioTrack == null) { + throw new IllegalStateException("Local audio track does not exist (yet)"); + } + return audioTrack.enabled(); + } + public ListenableFuture createOffer() { return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { @@ -330,6 +346,10 @@ public class WebRTCWrapper { return peerConnection; } + public AppRTCAudioManager getAudioManager() { + return appRTCAudioManager; + } + private static abstract class SetSdpObserver implements SdpObserver { @Override diff --git a/src/main/res/drawable-hdpi/ic_bluetooth_audio_black_24dp.png b/src/main/res/drawable-hdpi/ic_bluetooth_audio_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..14a5a5584a2a102a8862f1166bdfe6582989212f GIT binary patch literal 420 zcmV;V0bBlwP)I|gBTHP1$*&QS$G4HU@ur|DPY}(`MxSAm4S5Ph8(+d#5D{WbrGk(V z4u{MjF_{h1>3?CfGsFHC&Yqp{H&ez?4g>|D6jCY-Gz66+>ItQ!PUsLTWucF>RE`b? zV=0BMxJgGvRP}}`5$;>da@&JiB|n&|5jhZy^Q)-& z<8v5HB~;}1lDdOy7~uuOBlM#Zz42Aftsj+}L81?w@}u-{m_=Gj9mxD9{bH-4N9-_$ zJpk5ZRoG$H3h(L(nC O00009xqKhbqYg4a7Y6r1bpj4a$C&?w)P)eYfU2qLf;vGmc6!8M;2i}O(OeVF$ z%R2;44n55v1o}H$-ltDFAN-FUI`{+x_#CrM1?zN4$e0&a#)wnaa7UX@=9cwA3rBrs z`lA^WVj?19UisCpIXxWQX?)5MkD?|weCza<1^OBs)1ZvUvnH;=uC1GTU;~>?9(98U z)|=8J9&3%g>P2#Iwj1o~*)&+BNh;YBN+mKJ;v$mlQ~4{-R$3!7y=nYD92OkVF+aq;!5-NDJrheHBufm*CJ;oC8=@+t!@p>$f}a hK5f7SZNQ}^?i-8UfZfY2g-`$h002ovPDHLkV1nmwdAk4r literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_headset_black_24dp.png b/src/main/res/drawable-mdpi/ic_headset_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..d872b05d5c54f5bab40e21c182bd41f2124e4dac GIT binary patch literal 230 zcmVt!WO>|nVbGP2b+NB*j8_^s1Z)hggMwA+J(Q8m@%)~RL zu~-_|<;;h;R_Mh<&7F8uScp!|g?LriidN03_*RIhBXVf)x5|Hn!PnkkrSPyfm@2gH geO7~&sS`)z7w37E&qCDKX8-^I07*qoM6N<$f~VSMG5`Po literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_bluetooth_audio_black_24dp.png b/src/main/res/drawable-xhdpi/ic_bluetooth_audio_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..c1d6b931dc0c7f2a629e528f44e6def0eddd546d GIT binary patch literal 479 zcmV<50U-W~P)k

O;i9j5G`(4h@l3OM2iK-U?da(uVgN`LkKrn;P^|M zjTu&mkpjk)QvU!6_rmrq*a-n*^^!}!|H&0IAwadh%X%J$+kO5kBZLqjR^Q|n_*@Iu z8CVGYE9h0~zcH2Ri@%>k4MNpB-phRIpZJU+<29mG^^WhddF|&Fn2AKaqmbRq&wuIw zec9h;sR5bo^$B7rn>T*`VXsdU7c!suCqCNi)5Jukul{~$trvzOO0>Y|dbmEe*7pVI z#t6xyaBExZ5deA#;)CCRve#DtPn1!j6Shx`^$LKOGMDOw^NXv-dIfY?$^1;cGN0P& z6%enNG&vxdchPzQRJ}P3j*=f&aX%zzcFk>UT2x`~!8& V1Rl8!)*k=>002ovPDHLkV1m0l*M|TA literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_headset_black_24dp.png b/src/main/res/drawable-xhdpi/ic_headset_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..f2664dcde2705ebf36252d28c5a116286b9fca14 GIT binary patch literal 412 zcmV;N0b~A&P)8G96VOKO`}$sT|iMQYZrlc5P1WUNP$7ul0~cV4uVz{K@nmDz8WG9OUE?D!|N9(K&C9!(MXRhL7nrzG+goVWRiuiEeYJgpR!2n&m{?WY zcWCugB!h`5wf&CPKt)_kB-QpOTAmtHvTqB_3p8V%w*|Jq7W{p|M=S+>v|6zgG|<|O zrJ#aVlE;w?+PL_b;bEkLHq!)1vO|-8O=2U-Y;$%Z=o6 zBe~p2E;o|PjpVkTbb$#fHYhzwa~5$lOp;4a3OP=ZEYQc4B;m+t3dsT;PEC5IK3s&61BNrMV{Cdbbmuj5!8QkzNuD(I@Nj>qw{ zmtWE|2JuM#&mpHRoG=s8^&U1#YU^L9o<_*AbJgoS5RIgVdVYQi9^+>p%hG!pl9Dt> zcGn5~e30%3ke1X^oWa$zH-euh(%)cHDgHrfYL5J6dWxS>S@9OqkwVUt)ZXFerp$kZ zRHOzr$FYE)i!%Qk(vUQ#N*}-e2S2C0%Fe9;4e<&abXw+@lW=4Q*`#myxg_&HQf}&2 zK$Rnmy@PsDgKj*Lz~dX3{aerBcnc0!Jft|qDH zaM0^L8H>|UCoCk{xgmYu8HmJD7qsvS*JBVX9qD==kE5@!A!*Ve?Pp1PuEgVL&|0La zHA!`;FHnufQ6PO^vPrx7BGpGRISM2z`f5dAZQF_*hb3yUI5y}gpVXSWs0e917RLq+ z+nj*(lS6Si3S@H((izs37Y4@$4Kp8bBIYcMHYWs8OU99$Th~iZ&8d?h=QsRXxSrRG z^E%rUo0Adf9^07AION75GmZj{l0h=#kQ)b!lYh#TDf1UPWQQxijuDvv0000veM1>mHdB7GiIw6lxcre07Lit-38RazRxWl`IQ!PfwNF~hKr;d#| z-X}~_WMW>{FIE`B)-X+eV_sYa4WsNMlen7VlhJaMl&Z#?^@=iSjPV97@{@5=*)isv zA+59Q8UL43STHWDOcrCt#WiqL;v4c&o+PV_$jAFL5l)uTA}jcCA1!hh2MYN`-)9IP zhWQ{`4^dc~MT^w&*n&_zwp|Z_C3shpdd)D#)zUZY9RzW1%-%Buro~* zG%ARljY85`*}Qvm6(mS9HbMMmcm3|L_hT}!XSnmwJLrJ;9}(;8IU;sVBJ96I|*EZl@*;utcqNK(vGb-r#7VC%A|SioWY8 z!Np6^^|J|jz>7!;QePbF)B*zfVka!&aD@qa0I?HltSbpxK>36o+XOwJT*5V`z%)S* zD3Nd-$5#$oC5-CuJAuY=W_= z>a=t9htb&W{hZTuje22%cTUmQcqmpq2J3{jeg;RgK)(;4f5f`aV3?pEivBdS`0Qs( ztUVq-p~*;rzGICn@5Sl~h?Jn{U)ml&fzN%heg?Xq&<@Za{jI)Z5}z~T3JVY~p;e$i z^UIvO`1~!du>y7p<@FuU@i`^NSHUtNT>r8+Zw;SA-u;6Z2`$Dt(sz8pr&o+`fN4TW zea9v~Jz{)oCk5Q;DBuq<{@q-^WI`LCJ#svPeo(@y@<;qBf__lKva&CG7ePNLVP3hB zJ&B;N-3>&-f6Al8?Fjlo3CHlcC)Q8K)z=a}s*lqnk@b~?tICGv8@sIaTiM-jz+?43 zWlQw~#|VE~Ni3F@oG zhP{4q0F2TKOc?Z@Q;(SHs{!E&3u4})wZ0wzV?hZw#jHQg^-KJV=`8u&lAC>7eLbL$ zm$~{=_+)`i4AJ^p$|v`mmSR0s7g1 zeqQD3Hvl3egnsZ8Utc7Ie{vR6UnF$#qrXPlpItWC;i&p42V5Wl64xhneNxvab$t~5 t1OqOT7>VnXx<0Av6GOkEqN1Xr;s*_K4zEGH2DShI002ovPDHLkV1gV_nlAtV literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_headset_black_24dp.png b/src/main/res/drawable-xxxhdpi/ic_headset_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..974457ee1dc25f4c8e68fb4908cd1d5eb4639e0c GIT binary patch literal 786 zcmV+t1MU2YP)*g2?U|Ie)Zs)xOsK$73 zW@#q~9V{8TXNVIN>Smzv`cLUlcrtJ8 z{E_4vHH`-`a-vNSwj33cH~0_NI+1Vmo!C-GQB2>DYeS;N0ye$!fBVmf7Sq^tLrlMk zYiZG90GkHI^vAfC5iPo~>42F28rN1tixf7=OLQ66Y6;rV%StArWbI{ z5oMd4uK{e+02)98XaHAbr@sc!02)98XaEhM0W^RH&;S}h184vZU?IR}qypq|Z7EU# zvbgpnQUTJqb}3Q;MsY1gF){%PG~>bj$OM?hlV;W<5nzo*JUSkM03~|y;T)xk1}HI% zPsjLCu>foI5`ZS|QLrAMz$}dfCdmcTWXXpfAWxPwqco$GQc5YMlu~8=1ygWd*VB(W Qz5oCK07*qoM6N<$g5iW Date: Mon, 13 Apr 2020 14:24:32 +0200 Subject: [PATCH 089/182] do not use proximity wake lock on speaker phone --- .../conversations/ui/RtpSessionActivity.java | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 7e1b3592e..1d9c19a4a 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -10,13 +10,11 @@ import android.os.Bundle; import android.os.PowerManager; import android.support.annotation.NonNull; import android.support.annotation.StringRes; -import android.support.v4.content.ContextCompat; import android.util.Log; import android.view.View; import android.view.WindowManager; import android.widget.Toast; -import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import java.lang.ref.WeakReference; @@ -31,7 +29,6 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.PermissionUtils; -import eu.siacs.conversations.utils.ThemeHelper; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; @@ -61,8 +58,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private ActivityRtpSessionBinding binding; private PowerManager.WakeLock mProximityWakeLock; - private static AppRTCAudioManager audioManager; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -115,6 +110,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @SuppressLint("WakelockTimeout") private void putScreenInCallMode() { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + final JingleRtpConnection rtpConnection = rtpConnectionReference != null ? rtpConnectionReference.get() : null; + if (rtpConnection == null || rtpConnection.getAudioManager().getSelectedAudioDevice() == AppRTCAudioManager.AudioDevice.EARPIECE) { + acquireProximityWakeLock(); + } + } + + private void acquireProximityWakeLock() { final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); if (powerManager == null) { Log.e(Config.LOGTAG, "power manager not available"); @@ -125,13 +127,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.mProximityWakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, PROXIMITY_WAKE_LOCK_TAG); } if (!this.mProximityWakeLock.isHeld()) { - Log.d(Config.LOGTAG, "acquiring wake lock"); + Log.d(Config.LOGTAG, "acquiring proximity wake lock"); this.mProximityWakeLock.acquire(); } } } - private void releaseWakeLock() { + private void releaseProximityWakeLock() { if (this.mProximityWakeLock != null && mProximityWakeLock.isHeld()) { Log.d(Config.LOGTAG, "releasing wake lock"); this.mProximityWakeLock.release(); @@ -157,7 +159,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID); if (sessionId != null) { Log.d(Config.LOGTAG, "reinitializing from onNewIntent()"); - initializeActivityWithRunningRapSession(account, with, sessionId); + initializeActivityWithRunningRtpSession(account, with, sessionId); if (ACTION_ACCEPT_CALL.equals(intent.getAction())) { Log.d(Config.LOGTAG, "accepting call from onNewIntent()"); requestPermissionsAndAcceptCall(); @@ -175,7 +177,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH)); final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID); if (sessionId != null) { - initializeActivityWithRunningRapSession(account, with, sessionId); + initializeActivityWithRunningRtpSession(account, with, sessionId); if (ACTION_ACCEPT_CALL.equals(intent.getAction())) { Log.d(Config.LOGTAG, "intent action was accept"); requestPermissionsAndAcceptCall(); @@ -224,7 +226,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override public void onStop() { - releaseWakeLock(); + releaseProximityWakeLock(); //TODO maybe we want to finish if call had ended super.onStop(); } @@ -236,7 +238,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } - private void initializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) { + private void initializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) { final WeakReference reference = xmppConnectionService.getJingleConnectionManager() .findJingleRtpConnection(account, with, sessionId); if (reference == null || reference.get() == null) { @@ -262,7 +264,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void reInitializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) { runOnUiThread(() -> { - initializeActivityWithRunningRapSession(account, with, sessionId); + initializeActivityWithRunningRtpSession(account, with, sessionId); }); final Intent intent = new Intent(Intent.ACTION_VIEW); intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString()); @@ -424,10 +426,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void switchToEarpiece(View view) { requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE); + acquireProximityWakeLock(); } private void switchToSpeaker(View view) { requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE); + releaseProximityWakeLock(); } private void retry(View view) { @@ -460,7 +464,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) { if (Arrays.asList(RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.DECLINED_OR_BUSY).contains(state)) { - releaseWakeLock(); + releaseProximityWakeLock(); } Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")"); if (with.isBareJid()) { From ef22071bd16d6a9fd6d48f5d6d2df21f317d43b9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 13 Apr 2020 14:55:07 +0200 Subject: [PATCH 090/182] turn proximity wake lock and/off depending on speaker configuration --- .../siacs/conversations/ui/RtpSessionActivity.java | 12 +++++++++++- .../conversations/xmpp/jingle/WebRTCWrapper.java | 4 +++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 1d9c19a4a..2d31274cb 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -135,12 +135,20 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void releaseProximityWakeLock() { if (this.mProximityWakeLock != null && mProximityWakeLock.isHeld()) { - Log.d(Config.LOGTAG, "releasing wake lock"); + Log.d(Config.LOGTAG, "releasing proximity wake lock"); this.mProximityWakeLock.release(); this.mProximityWakeLock = null; } } + private void putProximityWakeLockInProperState() { + if (requireRtpConnection().getAudioManager().getSelectedAudioDevice() == AppRTCAudioManager.AudioDevice.EARPIECE) { + acquireProximityWakeLock(); + } else { + releaseProximityWakeLock(); + } + } + @Override protected void refreshUiReal() { @@ -200,6 +208,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void proposeJingleRtpSession(final Account account, final Jid with) { xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with); + //TODO maybe we don’t want to acquire a wake lock just yet and wait for audio manager to discover what speaker we are using putScreenInCallMode(); } @@ -506,6 +515,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe requireRtpConnection().isMicrophoneEnabled() ); } + putProximityWakeLockInProperState(); } catch (IllegalStateException e) { Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed"); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index e21a65f19..d723e96e5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -145,7 +145,8 @@ public class WebRTCWrapper { ); mainHandler.post(() -> { appRTCAudioManager = AppRTCAudioManager.create(context, AppRTCAudioManager.SpeakerPhonePreference.EARPIECE); - appRTCAudioManager.start(audioManagerEvents); + appRTCAudioManager.start(audioManagerEvents); + eventCallback.onAudioDeviceChanged(appRTCAudioManager.getSelectedAudioDevice(), appRTCAudioManager.getAudioDevices()); }); } @@ -218,6 +219,7 @@ public class WebRTCWrapper { peerConnection.setAudioRecording(true); this.peerConnection = peerConnection; } + public void close() { final PeerConnection peerConnection = this.peerConnection; if (peerConnection != null) { From 493ca68464f7028487355ad505ca624edecd1efd Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 13 Apr 2020 15:16:26 +0200 Subject: [PATCH 091/182] add in description --- .../eu/siacs/conversations/ui/RtpSessionActivity.java | 4 +++- .../conversations/xmpp/jingle/SessionDescription.java | 5 +---- .../xmpp/jingle/stanzas/RtpDescription.java | 9 ++++++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 2d31274cb..3fdc765cf 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -102,6 +102,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void requestPermissionsAndAcceptCall() { if (PermissionUtils.hasPermission(this, ImmutableList.of(Manifest.permission.RECORD_AUDIO), REQUEST_ACCEPT_CALL)) { + //TODO like wise the propose; we might just wait here for the audio manager to come up putScreenInCallMode(); requireRtpConnection().acceptCall(); } @@ -111,7 +112,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void putScreenInCallMode() { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); final JingleRtpConnection rtpConnection = rtpConnectionReference != null ? rtpConnectionReference.get() : null; - if (rtpConnection == null || rtpConnection.getAudioManager().getSelectedAudioDevice() == AppRTCAudioManager.AudioDevice.EARPIECE) { + final AppRTCAudioManager audioManager = rtpConnection == null ? null : rtpConnection.getAudioManager(); + if (audioManager == null || audioManager.getSelectedAudioDevice() == AppRTCAudioManager.AudioDevice.EARPIECE) { acquireProximityWakeLock(); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index 62424e81f..dc4007d69 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -139,10 +139,7 @@ public class SessionDescription { attributeMap.put("msid-semantic", " WMS my-media-stream"); - for (Map.Entry entry : contentMap.contents.entrySet()) { - - //TODO sprinkle in a few noWhiteSpaces checks into various parameters and types - + for (final Map.Entry entry : contentMap.contents.entrySet()) { final String name = entry.getKey(); RtpContentMap.DescriptionTransport descriptionTransport = entry.getValue(); RtpDescription description = descriptionTransport.description; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index 5f1a76af9..3351e9301 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -235,7 +235,7 @@ public class RtpDescription extends GenericDescription { final String name = getPayloadTypeName(); Preconditions.checkArgument(name != null, "Payload-type name must not be empty"); SessionDescription.checkNoWhitespace(name, "payload-type name must not contain whitespaces"); - return getId()+" "+name+"/"+getClockRate()+(channels == 1 ? "" : "/"+channels); + return getId() + " " + name + "/" + getClockRate() + (channels == 1 ? "" : "/" + channels); } public int getIntId() { @@ -368,7 +368,7 @@ public class RtpDescription extends GenericDescription { public static String toSdpString(final String id, List parameters) { final StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(id).append(' '); - for(int i = 0; i < parameters.size(); ++i) { + for (int i = 0; i < parameters.size(); ++i) { Parameter p = parameters.get(i); final String name = p.getParameterName(); Preconditions.checkArgument(name != null, String.format("parameter for %s must have a name", id)); @@ -488,7 +488,7 @@ public class RtpDescription extends GenericDescription { public List getSsrcs() { ImmutableList.Builder builder = new ImmutableList.Builder<>(); - for(Element child : this.children) { + for (Element child : this.children) { if ("source".equals(child.getName())) { final String ssrc = child.getAttribute("ssrc"); if (ssrc != null) { @@ -580,6 +580,9 @@ public class RtpDescription extends GenericDescription { for (Map.Entry> source : sourceParameterMap.asMap().entrySet()) { rtpDescription.addChild(new Source(source.getKey(), source.getValue())); } + if (media.attributes.containsKey("rtcp-mux")) { + rtpDescription.addChild("rtcp-mux"); + } return rtpDescription; } From e16e0d895e0e277b431b3a2f814e5122b4874442 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 13 Apr 2020 18:30:12 +0200 Subject: [PATCH 092/182] cancle ongoing jingle sessions on xmpp rebind --- .../services/XmppConnectionService.java | 4 +-- .../xmpp/jingle/AbstractJingleConnection.java | 2 ++ .../xmpp/jingle/JingleConnectionManager.java | 8 ++--- .../jingle/JingleFileTransferConnection.java | 7 ++++ .../xmpp/jingle/JingleRtpConnection.java | 34 ++++++++++++++----- .../xmpp/jingle/WebRTCWrapper.java | 4 ++- 6 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 543eb9fd5..d7adc05e9 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -145,9 +145,7 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; -import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; -import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.mam.MamReference; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.pep.PublishOptions; @@ -314,7 +312,7 @@ public class XmppConnectionService extends Service { synchronized (account.inProgressConferencePings) { account.inProgressConferencePings.clear(); } - mJingleConnectionManager.cancelInTransmission(); + mJingleConnectionManager.notifyRebound(); mQuickConversationsService.considerSyncBackground(false); fetchRosterFromServer(account); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java index bea185902..b6e160898 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -36,6 +36,8 @@ public abstract class AbstractJingleConnection { return id; } + abstract void notifyRebound(); + public static class Id { public final Account account; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 7df61767b..e6f17ac7f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -376,11 +376,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { account.getXmppConnection().sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null); } - public void cancelInTransmission() { - for (AbstractJingleConnection connection : this.connections.values()) { - /*if (connection.getJingleStatus() == JingleFileTransferConnection.JINGLE_STATUS_TRANSMITTING) { - connection.abort("connectivity-error"); - }*/ + public void notifyRebound() { + for (final AbstractJingleConnection connection : this.connections.values()) { + connection.notifyRebound(); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index b375398d2..f0941d27c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -293,6 +293,13 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple } } + @Override + void notifyRebound() { + if (getJingleStatus() == JINGLE_STATUS_TRANSMITTING) { + abort(Reason.CONNECTIVITY_ERROR); + } + } + private void respondToIq(final IqPacket packet, final boolean result) { final IqPacket response; if (result) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index b8a67cb0e..4bb502633 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -65,7 +65,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web State.PROCEED, State.REJECTED, State.RETRACTED, - State.TERMINATED_APPLICATION_FAILURE + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_CONNECTIVITY_ERROR //only used when the xmpp connection rebinds )); transitionBuilder.put(State.PROCEED, ImmutableList.of( State.SESSION_INITIALIZED_PRE_APPROVED, @@ -164,6 +165,24 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } + @Override + void notifyRebound() { + if (TERMINATED.contains(this.state)) { + return; + } + webRTCWrapper.close(); + if (!isInitiator() && isInState(State.PROPOSED,State.SESSION_INITIALIZED)) { + xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); + } + if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { + //we might have already changed resources (full jid) at this point; so this might not even reach the other party + sendSessionTerminate(Reason.CONNECTIVITY_ERROR); + } else { + transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR); + jingleConnectionManager.finishConnection(this); + } + } + private void receiveSessionTerminate(final JinglePacket jinglePacket) { respondOk(jinglePacket); final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason(); @@ -496,7 +515,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (from.equals(id.with)) { if (transition(State.RETRACTED)) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": session with " + id.with + " has been retracted (serverMsgId="+serverMsgId+")"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": session with " + id.with + " has been retracted (serverMsgId=" + serverMsgId + ")"); if (serverMsgId != null) { this.message.setServerMsgId(serverMsgId); } @@ -559,7 +578,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final JinglePacket jinglePacket = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); jinglePacket.setReason(reason, text); send(jinglePacket); - Log.d(Config.LOGTAG, jinglePacket.toString()); jingleConnectionManager.finishConnection(this); } @@ -837,14 +855,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return webRTCWrapper.getAudioManager(); } - public void setMicrophoneEnabled(final boolean enabled) { - webRTCWrapper.setMicrophoneEnabled(enabled); - } - public boolean isMicrophoneEnabled() { return webRTCWrapper.isMicrophoneEnabled(); } + public void setMicrophoneEnabled(final boolean enabled) { + webRTCWrapper.setMicrophoneEnabled(enabled); + } + @Override public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { xmppConnectionService.notifyJingleRtpConnectionUpdate(selectedAudioDevice, availableAudioDevices); @@ -934,7 +952,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void writeLogMessageMissed() { - this.message.setBody(new RtpSessionStatus(false,0).toString()); + this.message.setBody(new RtpSessionStatus(false, 0).toString()); this.writeMessage(); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index d723e96e5..c3d71d4a6 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -50,7 +50,9 @@ public class WebRTCWrapper { @Override public void onSignalingChange(PeerConnection.SignalingState signalingState) { Log.d(Config.LOGTAG, "onSignalingChange(" + signalingState + ")"); - + //this is called after removeTrack or addTrack + //and should then trigger a content-add or content-remove or something + //https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/removeTrack } @Override From 172d2c693f1f83d203baf1b45e8bdc50f0212aaf Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 14 Apr 2020 09:06:07 +0200 Subject: [PATCH 093/182] depulicate 'propose's when doing mam catchup --- build.gradle | 4 +- .../conversations/parser/MessageParser.java | 6 ++ .../xmpp/jingle/JingleRtpConnection.java | 100 +++++++++--------- 3 files changed, 59 insertions(+), 51 deletions(-) diff --git a/build.gradle b/build.gradle index 84ec32270..606021a97 100644 --- a/build.gradle +++ b/build.gradle @@ -92,8 +92,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 28 - versionCode 368 - versionName "2.8.0-alpha.2" + versionCode 369 + versionName "2.8.0-alpha.3" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 908166de4..9cb6e7bbd 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -854,6 +854,12 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece 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, diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 4bb502633..883995416 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -349,31 +349,33 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web sendSessionAccept(offer); } - private void sendSessionAccept(SessionDescription offer) { - discoverIceServers(iceServers -> { - try { - setupWebRTC(iceServers); - } catch (WebRTCWrapper.InitializationException e) { - sendSessionTerminate(Reason.FAILED_APPLICATION); - return; - } - final org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription( - org.webrtc.SessionDescription.Type.OFFER, - offer.toString() - ); - try { - this.webRTCWrapper.setRemoteDescription(sdp).get(); - addIceCandidatesFromBlackLog(); - org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createAnswer().get(); - final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); - final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); - sendSessionAccept(respondingRtpContentMap); - this.webRTCWrapper.setLocalDescription(webRTCSessionDescription); - } catch (Exception e) { - Log.d(Config.LOGTAG, "unable to send session accept", e); + private void sendSessionAccept(final SessionDescription offer) { + discoverIceServers(iceServers -> sendSessionAccept(offer,iceServers)); + } - } - }); + private void sendSessionAccept(final SessionDescription offer, final List iceServers) { + try { + setupWebRTC(iceServers); + } catch (WebRTCWrapper.InitializationException e) { + sendSessionTerminate(Reason.FAILED_APPLICATION); + return; + } + final org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription( + org.webrtc.SessionDescription.Type.OFFER, + offer.toString() + ); + try { + this.webRTCWrapper.setRemoteDescription(sdp).get(); + addIceCandidatesFromBlackLog(); + org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createAnswer().get(); + final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); + final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); + sendSessionAccept(respondingRtpContentMap); + this.webRTCWrapper.setLocalDescription(webRTCSessionDescription); + } catch (Exception e) { + Log.d(Config.LOGTAG, "unable to send session accept", e); + + } } private void addIceCandidatesFromBlackLog() { @@ -532,38 +534,39 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void sendSessionInitiate(final State targetState) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate"); - discoverIceServers(iceServers -> { - try { - setupWebRTC(iceServers); - } catch (WebRTCWrapper.InitializationException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to initialize webrtc"); + discoverIceServers(iceServers -> sendSessionInitiate(targetState, iceServers)); + } + + private void sendSessionInitiate(final State targetState, final List iceServers) { + try { + setupWebRTC(iceServers); + } catch (WebRTCWrapper.InitializationException e) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to initialize webrtc"); + transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE); + return; + } + try { + org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get(); + final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); + Log.d(Config.LOGTAG, "description: " + webRTCSessionDescription.description); + final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); + sendSessionInitiate(rtpContentMap, targetState); + this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); + } catch (final Exception e) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", e); + webRTCWrapper.close(); + if (isInState(targetState)) { + sendSessionTerminate(Reason.FAILED_APPLICATION); + } else { transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE); - return; } - try { - org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get(); - final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); - Log.d(Config.LOGTAG, "description: " + webRTCSessionDescription.description); - final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); - sendSessionInitiate(rtpContentMap, targetState); - this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); - } catch (final Exception e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", e); - webRTCWrapper.close(); - if (isInState(targetState)) { - sendSessionTerminate(Reason.FAILED_APPLICATION); - } else { - transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE); - } - } - }); + } } private void sendSessionInitiate(RtpContentMap rtpContentMap, final State targetState) { this.initiatorRtpContentMap = rtpContentMap; this.transitionOrThrow(targetState); final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); - Log.d(Config.LOGTAG, sessionInitiate.toString()); send(sessionInitiate); } @@ -591,7 +594,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return; } final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); - Log.d(Config.LOGTAG, jinglePacket.toString()); send(jinglePacket); } From 65b43661dda505a3704b3546b31796f64bf2547a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 14 Apr 2020 09:53:01 +0200 Subject: [PATCH 094/182] RtpConnection: synchronize all externally call methods to guard state transitions --- .../xmpp/jingle/JingleRtpConnection.java | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 883995416..3f2e97d00 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -45,6 +45,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web ); private static final List TERMINATED = Arrays.asList( + State.TERMINATED_SUCCESS, State.TERMINATED_DECLINED_OR_BUSY, State.TERMINATED_CONNECTIVITY_ERROR, State.TERMINATED_CANCEL_OR_TIMEOUT, @@ -143,7 +144,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } @Override - void deliverPacket(final JinglePacket jinglePacket) { + synchronized void deliverPacket(final JinglePacket jinglePacket) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": packet delivered to JingleRtpConnection"); switch (jinglePacket.getAction()) { case SESSION_INITIATE: @@ -166,7 +167,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } @Override - void notifyRebound() { + synchronized void notifyRebound() { if (TERMINATED.contains(this.state)) { return; } @@ -353,7 +354,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web discoverIceServers(iceServers -> sendSessionAccept(offer,iceServers)); } - private void sendSessionAccept(final SessionDescription offer, final List iceServers) { + private synchronized void sendSessionAccept(final SessionDescription offer, final List iceServers) { + if (TERMINATED.contains(this.state)) { + Log.w(Config.LOGTAG,id.account.getJid().asBareJid()+": ICE servers got discovered when session was already terminated. nothing to do."); + return; + } try { setupWebRTC(iceServers); } catch (WebRTCWrapper.InitializationException e) { @@ -394,7 +399,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web send(sessionAccept); } - void deliveryMessage(final Jid from, final Element message, final String serverMessageId, final long timestamp) { + synchronized void deliveryMessage(final Jid from, final Element message, final String serverMessageId, final long timestamp) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": delivered message to JingleRtpConnection " + message); switch (message.getName()) { case "propose": @@ -537,7 +542,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web discoverIceServers(iceServers -> sendSessionInitiate(targetState, iceServers)); } - private void sendSessionInitiate(final State targetState, final List iceServers) { + private synchronized void sendSessionInitiate(final State targetState, final List iceServers) { + if (TERMINATED.contains(this.state)) { + Log.w(Config.LOGTAG,id.account.getJid().asBareJid()+": ICE servers got discovered when session was already terminated. nothing to do."); + return; + } try { setupWebRTC(iceServers); } catch (WebRTCWrapper.InitializationException e) { @@ -701,7 +710,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } - public void acceptCall() { + public synchronized void acceptCall() { switch (this.state) { case PROPOSED: acceptCallFromProposed(); @@ -714,7 +723,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - public void rejectCall() { + public synchronized void rejectCall() { switch (this.state) { case PROPOSED: rejectCallFromProposed(); @@ -727,7 +736,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - public void endCall() { + public synchronized void endCall() { if (isInState(State.PROPOSED) && !isInitiator()) { rejectCallFromProposed(); return; From dd42a6b850336f8103a66ead7c691c8444cd650e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 14 Apr 2020 11:56:02 +0200 Subject: [PATCH 095/182] =?UTF-8?q?don=E2=80=99t=20transition=20when=20cal?= =?UTF-8?q?ling=20endCall=20and=20session=20was=20already=20terminated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../siacs/conversations/xmpp/jingle/JingleRtpConnection.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 3f2e97d00..06e15ac0e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -737,6 +737,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } public synchronized void endCall() { + if (TERMINATED.contains(this.state)) { + Log.w(Config.LOGTAG,id.account.getJid().asBareJid()+": received endCall() when session has already been terminated. nothing to do"); + return; + } if (isInState(State.PROPOSED) && !isInitiator()) { rejectCallFromProposed(); return; From bfb9a6267a88547aac5db4f5f9ae688f3e1e09eb Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 14 Apr 2020 18:18:16 +0200 Subject: [PATCH 096/182] complete list of reasons --- .../xmpp/jingle/RtpContentMap.java | 2 + .../xmpp/jingle/stanzas/Reason.java | 43 +++++++++++++------ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index fcac3d729..96339061a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -120,11 +120,13 @@ public class RtpContentMap { rtpDescription = (RtpDescription) description; } else { Log.d(Config.LOGTAG, "description was " + description); + //todo throw unsupported application throw new IllegalArgumentException("Content does not contain RtpDescription"); } if (transportInfo instanceof IceUdpTransportInfo) { iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo; } else { + //TODO throw UNSUPPORTED_TRANSPORT exception throw new IllegalArgumentException("Content does not contain ICE-UDP transport"); } return new DescriptionTransport(rtpDescription, iceUdpTransportInfo); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java index 635f26d54..8fee7a552 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java @@ -5,19 +5,36 @@ import android.support.annotation.NonNull; import com.google.common.base.CaseFormat; public enum Reason { - SUCCESS, DECLINE, BUSY, CANCEL, CONNECTIVITY_ERROR, FAILED_TRANSPORT, FAILED_APPLICATION, TIMEOUT, UNKNOWN; + ALTERNATIVE_SESSION, + BUSY, + CANCEL, + CONNECTIVITY_ERROR, + DECLINE, + EXPIRED, + FAILED_APPLICATION, + FAILED_TRANSPORT, + GENERAL_ERROR, + GONE, + INCOMPATIBLE_PARAMETERS, + MEDIA_ERROR, + SECURITY_ERROR, + SUCCESS, + TIMEOUT, + UNSUPPORTED_APPLICATIONS, + UNSUPPORTED_TRANSPORTS, + UNKNOWN; - public static Reason of(final String value) { - try { - return Reason.valueOf(CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value)); - } catch (Exception e) { - return UNKNOWN; - } - } + public static Reason of(final String value) { + try { + return Reason.valueOf(CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, value)); + } catch (Exception e) { + return UNKNOWN; + } + } - @Override - @NonNull - public String toString() { - return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, super.toString()); - } + @Override + @NonNull + public String toString() { + return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, super.toString()); + } } \ No newline at end of file From 339bdaea06dfdc941db8664796a9a1f92e6f989a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 14 Apr 2020 19:06:39 +0200 Subject: [PATCH 097/182] rudimentary video caputuring --- .../conversations/ui/RtpSessionActivity.java | 32 +++- .../xmpp/jingle/JingleRtpConnection.java | 16 ++ .../xmpp/jingle/WebRTCWrapper.java | 176 ++++++++++-------- src/main/res/layout/activity_rtp_session.xml | 15 ++ 4 files changed, 159 insertions(+), 80 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 3fdc765cf..6d4e7316c 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -15,8 +15,12 @@ import android.view.View; import android.view.WindowManager; import android.widget.Toast; +import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; +import org.webrtc.SurfaceViewRenderer; +import org.webrtc.VideoTrack; + import java.lang.ref.WeakReference; import java.util.Arrays; import java.util.Set; @@ -37,8 +41,6 @@ import rocks.xmpp.addr.Jid; import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied; import static java.util.Arrays.asList; -//TODO if last state was BUSY (or RETRY); we want to reset action to view or something so we don’t automatically call again on recreate - public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate { private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session"; @@ -53,6 +55,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe public static final String ACTION_MAKE_VOICE_CALL = "action_make_voice_call"; public static final String ACTION_MAKE_VIDEO_CALL = "action_make_video_call"; + private WeakReference rtpConnectionReference; private ActivityRtpSessionBinding binding; @@ -284,6 +287,30 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe setIntent(intent); } + private void updateVideoViews() { + final Optional localVideoTrack = requireRtpConnection().geLocalVideoTrack(); + if (localVideoTrack.isPresent()) { + try { + binding.localVideo.init(requireRtpConnection().getEglBaseContext(), null); + } catch (IllegalStateException e) { + Log.d(Config.LOGTAG,"ignoring already init for now",e); + } + binding.localVideo.setEnableHardwareScaler(true); + binding.localVideo.setMirror(true); + localVideoTrack.get().addSink(binding.localVideo); + } + final Optional remoteVideoTrack = requireRtpConnection().getRemoteVideoTrack(); + if (remoteVideoTrack.isPresent()) { + try { + binding.remoteVideo.init(requireRtpConnection().getEglBaseContext(), null); + } catch (IllegalStateException e) { + Log.d(Config.LOGTAG,"ignoring already init for now",e); + } + binding.remoteVideo.setEnableHardwareScaler(true); + remoteVideoTrack.get().addSink(binding.remoteVideo); + } + } + private void updateStateDisplay(final RtpEndUserState state) { switch (state) { case INCOMING_CALL: @@ -498,6 +525,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe runOnUiThread(() -> { updateStateDisplay(state); updateButtonConfiguration(state); + updateVideoViews(); }); } else { Log.d(Config.LOGTAG, "received update for other rtp session"); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 06e15ac0e..b7284cc4a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -3,13 +3,16 @@ package eu.siacs.conversations.xmpp.jingle; import android.os.SystemClock; import android.util.Log; +import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.primitives.Ints; +import org.webrtc.EglBase; import org.webrtc.IceCandidate; import org.webrtc.PeerConnection; +import org.webrtc.VideoTrack; import java.util.ArrayDeque; import java.util.Arrays; @@ -986,6 +989,19 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return this.state; } + public Optional geLocalVideoTrack() { + return webRTCWrapper.getLocalVideoTrack(); + } + + public Optional getRemoteVideoTrack() { + return webRTCWrapper.getRemoteVideoTrack(); + } + + + public EglBase.Context getEglBaseContext() { + return webRTCWrapper.getEglBaseContext(); + } + private interface OnIceServersDiscovered { void onIceServersDiscovered(List iceServers); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index c3d71d4a6..a6fe340ee 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -1,11 +1,13 @@ package eu.siacs.conversations.xmpp.jingle; import android.content.Context; +import android.os.Build; import android.os.Handler; import android.os.Looper; import android.util.Log; -import com.google.common.collect.ImmutableList; +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -13,11 +15,15 @@ import com.google.common.util.concurrent.SettableFuture; import org.webrtc.AudioSource; import org.webrtc.AudioTrack; -import org.webrtc.Camera1Capturer; import org.webrtc.Camera1Enumerator; +import org.webrtc.Camera2Enumerator; +import org.webrtc.CameraEnumerator; import org.webrtc.CameraVideoCapturer; import org.webrtc.CandidatePairChangeEvent; import org.webrtc.DataChannel; +import org.webrtc.DefaultVideoDecoderFactory; +import org.webrtc.DefaultVideoEncoderFactory; +import org.webrtc.EglBase; import org.webrtc.IceCandidate; import org.webrtc.MediaConstraints; import org.webrtc.MediaStream; @@ -26,7 +32,7 @@ import org.webrtc.PeerConnectionFactory; import org.webrtc.RtpReceiver; import org.webrtc.SdpObserver; import org.webrtc.SessionDescription; -import org.webrtc.VideoCapturer; +import org.webrtc.SurfaceTextureHelper; import org.webrtc.VideoSource; import org.webrtc.VideoTrack; @@ -41,11 +47,16 @@ import eu.siacs.conversations.services.AppRTCAudioManager; public class WebRTCWrapper { + private final EventCallback eventCallback; + private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = new AppRTCAudioManager.AudioManagerEvents() { + @Override + public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { + eventCallback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices); + } + }; + private final Handler mainHandler = new Handler(Looper.getMainLooper()); private VideoTrack localVideoTrack = null; private VideoTrack remoteVideoTrack = null; - - private final EventCallback eventCallback; - private final PeerConnection.Observer peerConnectionObserver = new PeerConnection.Observer() { @Override public void onSignalingChange(PeerConnection.SignalingState signalingState) { @@ -94,9 +105,6 @@ public class WebRTCWrapper { @Override public void onAddStream(MediaStream mediaStream) { Log.d(Config.LOGTAG, "onAddStream"); - for (AudioTrack audioTrack : mediaStream.audioTracks) { - Log.d(Config.LOGTAG, "remote? - audioTrack enabled:" + audioTrack.enabled() + " state=" + audioTrack.state()); - } final List videoTracks = mediaStream.videoTracks; if (videoTracks.size() > 0) { Log.d(Config.LOGTAG, "more than zero remote video tracks found. using first"); @@ -125,17 +133,12 @@ public class WebRTCWrapper { } }; - private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = new AppRTCAudioManager.AudioManagerEvents() { - @Override - public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { - eventCallback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices); - } - }; @Nullable private PeerConnection peerConnection = null; private AudioTrack localAudioTrack = null; private AppRTCAudioManager appRTCAudioManager = null; - private final Handler mainHandler = new Handler(Looper.getMainLooper()); + private Context context = null; + private EglBase eglBase = null; public WebRTCWrapper(final EventCallback eventCallback) { this.eventCallback = eventCallback; @@ -145,6 +148,8 @@ public class WebRTCWrapper { PeerConnectionFactory.initialize( PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions() ); + this.eglBase = EglBase.create(); + this.context = context; mainHandler.post(() -> { appRTCAudioManager = AppRTCAudioManager.create(context, AppRTCAudioManager.SpeakerPhonePreference.EARPIECE); appRTCAudioManager.start(audioManagerEvents); @@ -153,64 +158,35 @@ public class WebRTCWrapper { } public void initializePeerConnection(final List iceServers) throws InitializationException { - PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory(); + Preconditions.checkState(this.eglBase != null); + PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder() + .setVideoDecoderFactory(new DefaultVideoDecoderFactory(eglBase.getEglBaseContext())) + .setVideoEncoderFactory(new DefaultVideoEncoderFactory(eglBase.getEglBaseContext(), true, true)) + .createPeerConnectionFactory(); - CameraVideoCapturer capturer = null; - Camera1Enumerator camera1Enumerator = new Camera1Enumerator(); - for (String deviceName : camera1Enumerator.getDeviceNames()) { - Log.d(Config.LOGTAG, "camera device name: " + deviceName); - if (camera1Enumerator.isFrontFacing(deviceName)) { - capturer = camera1Enumerator.createCapturer(deviceName, new CameraVideoCapturer.CameraEventsHandler() { - @Override - public void onCameraError(String s) { - } + final MediaStream stream = peerConnectionFactory.createLocalMediaStream("my-media-stream"); - @Override - public void onCameraDisconnected() { + final Optional optionalCapturer = getVideoCapturer(); - } + if (optionalCapturer.isPresent()) { + final CameraVideoCapturer capturer = optionalCapturer.get(); + final VideoSource videoSource = peerConnectionFactory.createVideoSource(false); + SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("webrtc", eglBase.getEglBaseContext()); + capturer.initialize(surfaceTextureHelper, requireContext(), videoSource.getCapturerObserver()); + capturer.startCapture(320, 240, 30); - @Override - public void onCameraFreezed(String s) { + this.localVideoTrack = peerConnectionFactory.createVideoTrack("my-video-track", videoSource); - } - - @Override - public void onCameraOpening(String s) { - Log.d(Config.LOGTAG, "onCameraOpening"); - } - - @Override - public void onFirstFrameAvailable() { - Log.d(Config.LOGTAG, "onFirstFrameAvailable"); - } - - @Override - public void onCameraClosed() { - - } - }); - } + stream.addTrack(this.localVideoTrack); } - /*if (capturer != null) { - capturer.initialize(); - Log.d(Config.LOGTAG,"start capturing"); - capturer.startCapture(800,600,30); - }*/ - - final VideoSource videoSource = peerConnectionFactory.createVideoSource(false); - final VideoTrack videoTrack = peerConnectionFactory.createVideoTrack("my-video-track", videoSource); + //set up audio track final AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints()); - this.localAudioTrack = peerConnectionFactory.createAudioTrack("my-audio-track", audioSource); - final MediaStream stream = peerConnectionFactory.createLocalMediaStream("my-media-stream"); stream.addTrack(this.localAudioTrack); - //stream.addTrack(videoTrack); - this.localVideoTrack = videoTrack; final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(iceServers, peerConnectionObserver); if (peerConnection == null) { @@ -225,7 +201,7 @@ public class WebRTCWrapper { public void close() { final PeerConnection peerConnection = this.peerConnection; if (peerConnection != null) { - peerConnection.close(); + peerConnection.dispose(); } final AppRTCAudioManager audioManager = this.appRTCAudioManager; if (audioManager != null) { @@ -233,14 +209,6 @@ public class WebRTCWrapper { } } - public void setMicrophoneEnabled(final boolean enabled) { - final AudioTrack audioTrack = this.localAudioTrack; - if (audioTrack == null) { - throw new IllegalStateException("Local audio track does not exist (yet)"); - } - audioTrack.setEnabled(enabled); - } - public boolean isMicrophoneEnabled() { final AudioTrack audioTrack = this.localAudioTrack; if (audioTrack == null) { @@ -249,6 +217,13 @@ public class WebRTCWrapper { return audioTrack.enabled(); } + public void setMicrophoneEnabled(final boolean enabled) { + final AudioTrack audioTrack = this.localAudioTrack; + if (audioTrack == null) { + throw new IllegalStateException("Local audio track does not exist (yet)"); + } + audioTrack.setEnabled(enabled); + } public ListenableFuture createOffer() { return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { @@ -261,6 +236,7 @@ public class WebRTCWrapper { @Override public void onCreateFailure(String s) { + Log.d(Config.LOGTAG, "create failure" + s); future.setException(new IllegalStateException("Unable to create offer: " + s)); } }, new MediaConstraints()); @@ -297,6 +273,7 @@ public class WebRTCWrapper { @Override public void onSetFailure(String s) { + Log.d(Config.LOGTAG, "unable to set local " + s); future.setException(new IllegalArgumentException("unable to set local session description: " + s)); } @@ -338,10 +315,45 @@ public class WebRTCWrapper { requirePeerConnection().addIceCandidate(iceCandidate); } + private CameraEnumerator getCameraEnumerator() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return new Camera2Enumerator(requireContext()); + } else { + return new Camera1Enumerator(); + } + } + + private Optional getVideoCapturer() { + final CameraEnumerator enumerator = getCameraEnumerator(); + final String[] deviceNames = enumerator.getDeviceNames(); + for (String deviceName : deviceNames) { + if (enumerator.isFrontFacing(deviceName)) { + return Optional.fromNullable(enumerator.createCapturer(deviceName, null)); + } + } + if (deviceNames.length == 0) { + return Optional.absent(); + } else { + return Optional.fromNullable(enumerator.createCapturer(deviceNames[0], null)); + } + } + public PeerConnection.PeerConnectionState getState() { return requirePeerConnection().connectionState(); } + public EglBase.Context getEglBaseContext() { + return this.eglBase.getEglBaseContext(); + } + + public Optional getLocalVideoTrack() { + return Optional.fromNullable(this.localVideoTrack); + } + + public Optional getRemoteVideoTrack() { + return Optional.fromNullable(this.remoteVideoTrack); + } + private PeerConnection requirePeerConnection() { final PeerConnection peerConnection = this.peerConnection; if (peerConnection == null) { @@ -350,10 +362,26 @@ public class WebRTCWrapper { return peerConnection; } + private Context requireContext() { + final Context context = this.context; + if (context == null) { + throw new IllegalStateException("call setup first"); + } + return context; + } + public AppRTCAudioManager getAudioManager() { return appRTCAudioManager; } + public interface EventCallback { + void onIceCandidate(IceCandidate iceCandidate); + + void onConnectionChange(PeerConnection.PeerConnectionState newState); + + void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices); + } + private static abstract class SetSdpObserver implements SdpObserver { @Override @@ -389,12 +417,4 @@ public class WebRTCWrapper { super(message); } } - - public interface EventCallback { - void onIceCandidate(IceCandidate iceCandidate); - - void onConnectionChange(PeerConnection.PeerConnectionState newState); - - void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices); - } } diff --git a/src/main/res/layout/activity_rtp_session.xml b/src/main/res/layout/activity_rtp_session.xml index 6f0521029..1c08e0deb 100644 --- a/src/main/res/layout/activity_rtp_session.xml +++ b/src/main/res/layout/activity_rtp_session.xml @@ -9,6 +9,7 @@ android:background="?color_background_secondary"> + + + + Date: Tue, 14 Apr 2020 19:35:26 +0200 Subject: [PATCH 098/182] use toolbar to display status text in RtpSessionActivity --- .../conversations/ui/RtpSessionActivity.java | 21 ++++++------ src/main/res/layout/activity_rtp_session.xml | 33 ++++++++++--------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 6d4e7316c..479fa9769 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -70,6 +70,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); Log.d(Config.LOGTAG, "RtpSessionActivity.onCreate()"); this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session); + setSupportActionBar(binding.toolbar); } @Override @@ -314,34 +315,34 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void updateStateDisplay(final RtpEndUserState state) { switch (state) { case INCOMING_CALL: - binding.status.setText(R.string.rtp_state_incoming_call); + setTitle(R.string.rtp_state_incoming_call); break; case CONNECTING: - binding.status.setText(R.string.rtp_state_connecting); + setTitle(R.string.rtp_state_connecting); break; case CONNECTED: - binding.status.setText(R.string.rtp_state_connected); + setTitle(R.string.rtp_state_connected); break; case ACCEPTING_CALL: - binding.status.setText(R.string.rtp_state_accepting_call); + setTitle(R.string.rtp_state_accepting_call); break; case ENDING_CALL: - binding.status.setText(R.string.rtp_state_ending_call); + setTitle(R.string.rtp_state_ending_call); break; case FINDING_DEVICE: - binding.status.setText(R.string.rtp_state_finding_device); + setTitle(R.string.rtp_state_finding_device); break; case RINGING: - binding.status.setText(R.string.rtp_state_ringing); + setTitle(R.string.rtp_state_ringing); break; case DECLINED_OR_BUSY: - binding.status.setText(R.string.rtp_state_declined_or_busy); + setTitle(R.string.rtp_state_declined_or_busy); break; case CONNECTIVITY_ERROR: - binding.status.setText(R.string.rtp_state_connectivity_error); + setTitle(R.string.rtp_state_connectivity_error); break; case APPLICATION_ERROR: - binding.status.setText(R.string.rtp_state_application_failure); + setTitle(R.string.rtp_state_application_failure); break; case ENDED: throw new IllegalStateException("Activity should have called finishAndReleaseWakeLock();"); diff --git a/src/main/res/layout/activity_rtp_session.xml b/src/main/res/layout/activity_rtp_session.xml index 1c08e0deb..5f7fdec69 100644 --- a/src/main/res/layout/activity_rtp_session.xml +++ b/src/main/res/layout/activity_rtp_session.xml @@ -11,45 +11,48 @@ + android:layout_height="wrap_content"> - + - + + android:layout_alignParentEnd="true" + android:layout_alignParentRight="true" + android:visibility="gone" /> + android:layout_alignParentStart="true" + android:layout_alignParentLeft="true" + android:visibility="gone" /> Date: Tue, 14 Apr 2020 21:06:26 +0200 Subject: [PATCH 099/182] release resource. stop caputuring when webrtc ends --- .../conversations/ui/RtpSessionActivity.java | 3 ++ .../xmpp/jingle/WebRTCWrapper.java | 19 ++++++++++--- src/main/res/layout/activity_rtp_session.xml | 28 +++++++++++-------- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 479fa9769..a696d1c39 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -18,6 +18,7 @@ import android.widget.Toast; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; +import org.webrtc.RendererCommon; import org.webrtc.SurfaceViewRenderer; import org.webrtc.VideoTrack; @@ -241,6 +242,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override public void onStop() { + binding.remoteVideo.release(); + binding.localVideo.release(); releaseProximityWakeLock(); //TODO maybe we want to finish if call had ended super.onStop(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index a6fe340ee..4b207dcc5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -139,6 +139,7 @@ public class WebRTCWrapper { private AppRTCAudioManager appRTCAudioManager = null; private Context context = null; private EglBase eglBase = null; + private Optional optionalCapturer; public WebRTCWrapper(final EventCallback eventCallback) { this.eventCallback = eventCallback; @@ -167,10 +168,10 @@ public class WebRTCWrapper { final MediaStream stream = peerConnectionFactory.createLocalMediaStream("my-media-stream"); - final Optional optionalCapturer = getVideoCapturer(); + this.optionalCapturer = getVideoCapturer(); - if (optionalCapturer.isPresent()) { - final CameraVideoCapturer capturer = optionalCapturer.get(); + if (this.optionalCapturer.isPresent()) { + final CameraVideoCapturer capturer = this.optionalCapturer.get(); final VideoSource videoSource = peerConnectionFactory.createVideoSource(false); SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("webrtc", eglBase.getEglBaseContext()); capturer.initialize(surfaceTextureHelper, requireContext(), videoSource.getCapturerObserver()); @@ -207,6 +208,16 @@ public class WebRTCWrapper { if (audioManager != null) { mainHandler.post(audioManager::stop); } + this.localVideoTrack = null; + this.remoteVideoTrack = null; + if (this.optionalCapturer.isPresent()) { + try { + this.optionalCapturer.get().stopCapture(); + } catch (InterruptedException e) { + Log.e(Config.LOGTAG,"unable to stop capturing"); + } + } + eglBase.release(); } public boolean isMicrophoneEnabled() { @@ -326,7 +337,7 @@ public class WebRTCWrapper { private Optional getVideoCapturer() { final CameraEnumerator enumerator = getCameraEnumerator(); final String[] deviceNames = enumerator.getDeviceNames(); - for (String deviceName : deviceNames) { + for (final String deviceName : deviceNames) { if (enumerator.isFrontFacing(deviceName)) { return Optional.fromNullable(enumerator.createCapturer(deviceName, null)); } diff --git a/src/main/res/layout/activity_rtp_session.xml b/src/main/res/layout/activity_rtp_session.xml index 5f7fdec69..3b1390db6 100644 --- a/src/main/res/layout/activity_rtp_session.xml +++ b/src/main/res/layout/activity_rtp_session.xml @@ -35,24 +35,28 @@ - - - + android:layout_alignParentBottom="true" + android:visibility="visible" /> + + + Date: Wed, 15 Apr 2020 10:49:38 +0200 Subject: [PATCH 100/182] parse media from session proposal --- .../generator/MessageGenerator.java | 6 +- .../conversations/ui/RtpSessionActivity.java | 10 +-- .../xmpp/jingle/JingleConnectionManager.java | 68 +++++++++++++++---- .../xmpp/jingle/JingleRtpConnection.java | 23 ++++++- .../conversations/xmpp/jingle/Media.java | 20 ++++++ .../xmpp/jingle/RtpContentMap.java | 10 +++ .../xmpp/jingle/stanzas/Propose.java | 41 +++++++++++ .../xmpp/jingle/stanzas/RtpDescription.java | 18 +---- 8 files changed, 157 insertions(+), 39 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 41de4ec30..a0cb0ca1f 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -20,6 +20,7 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.chatstate.ChatState; 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.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; @@ -241,7 +242,10 @@ public class MessageGenerator extends AbstractGenerator { packet.setId(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX + proposal.sessionId); final Element propose = packet.addChild("propose", Namespace.JINGLE_MESSAGE); propose.setAttribute("id", proposal.sessionId); - propose.addChild("description", Namespace.JINGLE_APPS_RTP); + for (final Media media : proposal.media) { + propose.addChild("description", Namespace.JINGLE_APPS_RTP).setAttribute("media", media.toString()); + } + packet.addChild("request", "urn:xmpp:receipts"); packet.addChild("store", "urn:xmpp:hints"); return packet; diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index a696d1c39..72db2d88e 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -17,6 +17,7 @@ import android.widget.Toast; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import org.webrtc.RendererCommon; import org.webrtc.SurfaceViewRenderer; @@ -36,6 +37,7 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.PermissionUtils; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; +import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; import rocks.xmpp.addr.Jid; @@ -199,7 +201,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe resetIntent(intent.getExtras()); } } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(intent.getAction())) { - proposeJingleRtpSession(account, with); + proposeJingleRtpSession(account, with, ImmutableSet.of(Media.AUDIO)); binding.with.setText(account.getRoster().getContact(with).getDisplayName()); } else if (Intent.ACTION_VIEW.equals(intent.getAction())) { final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE); @@ -213,8 +215,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } - private void proposeJingleRtpSession(final Account account, final Jid with) { - xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with); + private void proposeJingleRtpSession(final Account account, final Jid with, final Set media) { + xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with, media); //TODO maybe we don’t want to acquire a wake lock just yet and wait for audio manager to discover what speaker we are using putScreenInCallMode(); } @@ -482,7 +484,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final Account account = extractAccount(intent); final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH)); this.rtpConnectionReference = null; - proposeJingleRtpSession(account, with); + proposeJingleRtpSession(account, with, ImmutableSet.of(Media.AUDIO)); } private void exit(View view) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index e6f17ac7f..e56105c2e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -3,13 +3,21 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Base64; import android.util.Log; +import com.google.common.base.Function; import com.google.common.base.Objects; import com.google.common.base.Preconditions; +import com.google.common.collect.Collections2; + +import org.checkerframework.checker.nullness.compatqual.NullableDecl; import java.lang.ref.WeakReference; import java.security.SecureRandom; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import eu.siacs.conversations.Config; @@ -25,8 +33,11 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; +import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.jingle.stanzas.Propose; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; +import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; @@ -129,9 +140,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { } return; } - final boolean addressedToSelf = from.asBareJid().equals(account.getJid().asBareJid()); + final boolean fromSelf = from.asBareJid().equals(account.getJid().asBareJid()); final AbstractJingleConnection.Id id; - if (addressedToSelf) { + if (fromSelf) { if (to.isFullJid()) { id = AbstractJingleConnection.Id.of(account, to, sessionId); } else { @@ -150,15 +161,26 @@ public class JingleConnectionManager extends AbstractConnectionManager { return; } - if (addressedToSelf) { + if (fromSelf) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignore jingle message from self"); + return; } if ("propose".equals(message.getName())) { - final Element description = message.findChild("description"); - final String namespace = description == null ? null : description.getNamespace(); - if (Namespace.JINGLE_APPS_RTP.equals(namespace) && !usesTor(account)) { - if (isBusy()) { + final Propose propose = Propose.upgrade(message); + final List descriptions = propose.getDescriptions(); + final Collection rtpDescriptions = Collections2.transform( + Collections2.filter(descriptions, d -> d instanceof RtpDescription), + input -> (RtpDescription) input + ); + if (rtpDescriptions.size() > 0 && rtpDescriptions.size() == descriptions.size() && !usesTor(account)) { + final Collection media = Collections2.transform(rtpDescriptions, RtpDescription::getMedia); + if (media.contains(Media.UNKNOWN)) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": encountered unknown media in session proposal. "+propose); + return; + } + if (isBusy()) { //TODO only if no other devices are active + //TODO create final MessagePacket reject = mXmppConnectionService.getMessageGenerator().sessionReject(from, sessionId); mXmppConnectionService.sendMessagePacket(account, reject); } else { @@ -167,14 +189,15 @@ public class JingleConnectionManager extends AbstractConnectionManager { rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); } } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to react to proposed " + namespace + " session"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to react to proposed session with " + rtpDescriptions.size() + " rtp descriptions of " + descriptions.size() + " total descriptions"); } } else if ("proceed".equals(message.getName())) { - - final RtpSessionProposal proposal = new RtpSessionProposal(account, from.asBareJid(), sessionId); synchronized (rtpSessionProposals) { - if (rtpSessionProposals.remove(proposal) != null) { + final RtpSessionProposal proposal = getRtpSessionProposal(account,from.asBareJid(),sessionId); + if (proposal != null) { + rtpSessionProposals.remove(proposal); final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, account.getJid()); + rtpConnection.setProposedMedia(proposal.media); this.connections.put(id, rtpConnection); rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); @@ -198,6 +221,15 @@ public class JingleConnectionManager extends AbstractConnectionManager { } + private RtpSessionProposal getRtpSessionProposal(final Account account, Jid from, String sessionId) { + for(RtpSessionProposal rtpSessionProposal : rtpSessionProposals.keySet()) { + if (rtpSessionProposal.sessionId.equals(sessionId) && rtpSessionProposal.with.equals(from) && rtpSessionProposal.account.getJid().equals(account.getJid())) { + return rtpSessionProposal; + } + } + return null; + } + private void writeLogMissedOutgoing(final Account account, Jid with, final String sessionId, String serverMsgId, long timestamp) { final Conversation conversation = mXmppConnectionService.findOrCreateConversation( account, @@ -310,7 +342,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } - public void proposeJingleRtpSession(final Account account, final Jid with) { + public void proposeJingleRtpSession(final Account account, final Jid with, final Set media) { synchronized (this.rtpSessionProposals) { for (Map.Entry entry : this.rtpSessionProposals.entrySet()) { RtpSessionProposal proposal = entry.getKey(); @@ -327,7 +359,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } } - final RtpSessionProposal proposal = RtpSessionProposal.of(account, with.asBareJid()); + final RtpSessionProposal proposal = RtpSessionProposal.of(account, with.asBareJid(), media); this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING); mXmppConnectionService.notifyJingleRtpConnectionUpdate( account, @@ -456,15 +488,21 @@ public class JingleConnectionManager extends AbstractConnectionManager { public final Jid with; public final String sessionId; private final Account account; + public final Set media; private RtpSessionProposal(Account account, Jid with, String sessionId) { + this(account,with,sessionId, Collections.emptySet()); + } + + private RtpSessionProposal(Account account, Jid with, String sessionId, Set media) { this.account = account; this.with = with; this.sessionId = sessionId; + this.media = media; } - public static RtpSessionProposal of(Account account, Jid with) { - return new RtpSessionProposal(account, with, nextRandomId()); + public static RtpSessionProposal of(Account account, Jid with, Set media) { + return new RtpSessionProposal(account, with, nextRandomId(), media); } @Override diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index b7284cc4a..7daabe163 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -4,9 +4,12 @@ import android.os.SystemClock; import android.util.Log; import com.google.common.base.Optional; +import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; import com.google.common.primitives.Ints; import org.webrtc.EglBase; @@ -30,10 +33,13 @@ import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.jingle.stanzas.Propose; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; +import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; @@ -108,6 +114,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private final ArrayDeque pendingIceCandidates = new ArrayDeque<>(); private final Message message; private State state = State.NULL; + private Set proposedMedia; private RtpContentMap initiatorRtpContentMap; private RtpContentMap responderRtpContentMap; private long rtpConnectionStarted = 0; //time of 'connected' @@ -406,7 +413,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": delivered message to JingleRtpConnection " + message); switch (message.getName()) { case "propose": - receivePropose(from, serverMessageId, timestamp); + receivePropose(from, Propose.upgrade(message), serverMessageId, timestamp); break; case "proceed": receiveProceed(from, serverMessageId, timestamp); @@ -475,11 +482,19 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - private void receivePropose(final Jid from, final String serverMsgId, final long timestamp) { + private void receivePropose(final Jid from, final Propose propose, final String serverMsgId, final long timestamp) { final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); if (originatedFromMyself) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": saw proposal from mysql. ignoring"); } else if (transition(State.PROPOSED)) { + final Collection descriptions = Collections2.transform( + Collections2.filter(propose.getDescriptions(), d -> d instanceof RtpDescription), + input -> (RtpDescription) input + ); + final Collection media = Collections2.transform(descriptions, RtpDescription::getMedia); + Preconditions.checkState(!media.contains(Media.UNKNOWN),"RTP descriptions contain unknown media"); + Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": received session proposal from "+from+" for "+media); + this.proposedMedia = Sets.newHashSet(media); if (serverMsgId != null) { this.message.setServerMsgId(serverMsgId); } @@ -1002,6 +1017,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return webRTCWrapper.getEglBaseContext(); } + public void setProposedMedia(final Set media) { + + } + private interface OnIceServersDiscovered { void onIceServersDiscovered(List iceServers); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java new file mode 100644 index 000000000..da25516ca --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java @@ -0,0 +1,20 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.util.Locale; + +public enum Media { + VIDEO, AUDIO, UNKNOWN; + + @Override + public String toString() { + return super.toString().toLowerCase(Locale.ROOT); + } + + public static Media of(String value) { + try { + return value == null ? UNKNOWN : Media.valueOf(value.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + return UNKNOWN; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index 96339061a..db9902874 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -5,13 +5,16 @@ import android.util.Log; import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; +import com.google.common.collect.Sets; import org.checkerframework.checker.nullness.compatqual.NullableDecl; import java.util.Map; +import java.util.Set; import eu.siacs.conversations.Config; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; @@ -48,6 +51,13 @@ public class RtpContentMap { return new RtpContentMap(group, contentMapBuilder.build()); } + public Set getMedia() { + return Sets.newHashSet(Collections2.transform(contents.values(), input -> { + final RtpDescription rtpDescription = input == null ? null : input.description; + return rtpDescription == null ? Media.UNKNOWN : input.description.getMedia(); + })); + } + public void requireContentDescriptions() { if (this.contents.size() == 0) { throw new IllegalStateException("No contents available"); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java new file mode 100644 index 000000000..da3a93da3 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java @@ -0,0 +1,41 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; + +public class Propose extends Element { + private Propose() { + super("propose", Namespace.JINGLE_MESSAGE); + } + + public List getDescriptions() { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for (final Element child : this.children) { + if ("description".equals(child.getName())) { + final String namespace = child.getNamespace(); + if (FileTransferDescription.NAMESPACES.contains(namespace)) { + builder.add(FileTransferDescription.upgrade(child)); + } else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { + builder.add(RtpDescription.upgrade(child)); + } else { + builder.add(GenericDescription.upgrade(child)); + } + } + } + return builder.build(); + } + + public static Propose upgrade(final Element element) { + Preconditions.checkArgument("propose".equals(element.getName())); + Preconditions.checkArgument(Namespace.JINGLE_MESSAGE.equals(element.getNamespace())); + final Propose propose = new Propose(); + propose.setAttributes(element.getAttributes()); + propose.setChildren(element.getChildren()); + return propose; + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index 3351e9301..70f3f0f6a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -14,6 +14,7 @@ import java.util.Map; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.SessionDescription; public class RtpDescription extends GenericDescription { @@ -509,23 +510,6 @@ public class RtpDescription extends GenericDescription { } } - public enum Media { - VIDEO, AUDIO, UNKNOWN; - - @Override - public String toString() { - return super.toString().toLowerCase(Locale.ROOT); - } - - public static Media of(String value) { - try { - return value == null ? UNKNOWN : Media.valueOf(value.toUpperCase(Locale.ROOT)); - } catch (IllegalArgumentException e) { - return UNKNOWN; - } - } - } - public static RtpDescription of(final SessionDescription.Media media) { final RtpDescription rtpDescription = new RtpDescription(media.media); final Map> parameterMap = new HashMap<>(); From d057ae3439ca83d4c1a00dc0f8d8941e3fd83723 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 15 Apr 2020 12:07:19 +0200 Subject: [PATCH 101/182] transmit media from proposal to actual session --- .../xmpp/jingle/JingleConnectionManager.java | 17 +++--- .../xmpp/jingle/JingleRtpConnection.java | 57 ++++++++++++------- .../xmpp/jingle/WebRTCWrapper.java | 30 ++++++---- 3 files changed, 66 insertions(+), 38 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index e56105c2e..a50dcd70f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -7,6 +7,7 @@ import com.google.common.base.Function; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableSet; import org.checkerframework.checker.nullness.compatqual.NullableDecl; @@ -163,6 +164,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (fromSelf) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignore jingle message from self"); + //TODO proceed from self should maybe dedup/change the busy that we set earlier return; } @@ -176,16 +178,17 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (rtpDescriptions.size() > 0 && rtpDescriptions.size() == descriptions.size() && !usesTor(account)) { final Collection media = Collections2.transform(rtpDescriptions, RtpDescription::getMedia); if (media.contains(Media.UNKNOWN)) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": encountered unknown media in session proposal. "+propose); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": encountered unknown media in session proposal. " + propose); return; } if (isBusy()) { //TODO only if no other devices are active - //TODO create + //TODO create busy final MessagePacket reject = mXmppConnectionService.getMessageGenerator().sessionReject(from, sessionId); mXmppConnectionService.sendMessagePacket(account, reject); } else { final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, from); this.connections.put(id, rtpConnection); + rtpConnection.setProposedMedia(ImmutableSet.copyOf(media)); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); } } else { @@ -193,7 +196,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } else if ("proceed".equals(message.getName())) { synchronized (rtpSessionProposals) { - final RtpSessionProposal proposal = getRtpSessionProposal(account,from.asBareJid(),sessionId); + final RtpSessionProposal proposal = getRtpSessionProposal(account, from.asBareJid(), sessionId); if (proposal != null) { rtpSessionProposals.remove(proposal); final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, account.getJid()); @@ -222,7 +225,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { } private RtpSessionProposal getRtpSessionProposal(final Account account, Jid from, String sessionId) { - for(RtpSessionProposal rtpSessionProposal : rtpSessionProposals.keySet()) { + for (RtpSessionProposal rtpSessionProposal : rtpSessionProposals.keySet()) { if (rtpSessionProposal.sessionId.equals(sessionId) && rtpSessionProposal.with.equals(from) && rtpSessionProposal.account.getJid().equals(account.getJid())) { return rtpSessionProposal; } @@ -424,9 +427,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { } public void updateProposedSessionDiscovered(Account account, Jid from, String sessionId, final DeviceDiscoveryState target) { - final RtpSessionProposal sessionProposal = new RtpSessionProposal(account, from.asBareJid(), sessionId); synchronized (this.rtpSessionProposals) { - final DeviceDiscoveryState currentState = rtpSessionProposals.get(sessionProposal); + final RtpSessionProposal sessionProposal = getRtpSessionProposal(account, from.asBareJid(), sessionId); + final DeviceDiscoveryState currentState = sessionProposal == null ? null : rtpSessionProposals.get(sessionProposal); if (currentState == null) { Log.d(Config.LOGTAG, "unable to find session proposal for session id " + sessionId); return; @@ -491,7 +494,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { public final Set media; private RtpSessionProposal(Account account, Jid with, String sessionId) { - this(account,with,sessionId, Collections.emptySet()); + this(account, with, sessionId, Collections.emptySet()); } private RtpSessionProposal(Account account, Jid with, String sessionId, Set media) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 7daabe163..e4ef46b60 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -33,7 +33,6 @@ import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; @@ -147,6 +146,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web case TIMEOUT: return State.TERMINATED_CANCEL_OR_TIMEOUT; case FAILED_APPLICATION: + case SECURITY_ERROR: + case UNSUPPORTED_TRANSPORTS: + case UNSUPPORTED_APPLICATIONS: return State.TERMINATED_APPLICATION_FAILURE; default: return State.TERMINATED_CONNECTIVITY_ERROR; @@ -182,7 +184,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return; } webRTCWrapper.close(); - if (!isInitiator() && isInState(State.PROPOSED,State.SESSION_INITIALIZED)) { + if (!isInitiator() && isInState(State.PROPOSED, State.SESSION_INITIALIZED)) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); } if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { @@ -271,6 +273,18 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web Log.d(Config.LOGTAG, "processing session-init with " + contentMap.contents.size() + " contents"); final State target; if (this.state == State.PROCEED) { + Preconditions.checkState( + proposedMedia != null && proposedMedia.size() > 0, + "proposed media must be set when processing pre-approved session-initiate" + ); + if (!this.proposedMedia.equals(contentMap.getMedia())) { + sendSessionTerminate(Reason.SECURITY_ERROR,String.format( + "Your session proposal (Jingle Message Initiation) included media %s but your session-initiate was %s", + this.proposedMedia, + contentMap.getMedia() + )); + return; + } target = State.SESSION_INITIALIZED_PRE_APPROVED; } else { target = State.SESSION_INITIALIZED; @@ -357,20 +371,20 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); return; } - sendSessionAccept(offer); + sendSessionAccept(rtpContentMap.getMedia(), offer); } - private void sendSessionAccept(final SessionDescription offer) { - discoverIceServers(iceServers -> sendSessionAccept(offer,iceServers)); + private void sendSessionAccept(final Set media, final SessionDescription offer) { + discoverIceServers(iceServers -> sendSessionAccept(media, offer, iceServers)); } - private synchronized void sendSessionAccept(final SessionDescription offer, final List iceServers) { + private synchronized void sendSessionAccept(final Set media, final SessionDescription offer, final List iceServers) { if (TERMINATED.contains(this.state)) { - Log.w(Config.LOGTAG,id.account.getJid().asBareJid()+": ICE servers got discovered when session was already terminated. nothing to do."); + Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": ICE servers got discovered when session was already terminated. nothing to do."); return; } try { - setupWebRTC(iceServers); + setupWebRTC(media, iceServers); } catch (WebRTCWrapper.InitializationException e) { sendSessionTerminate(Reason.FAILED_APPLICATION); return; @@ -492,8 +506,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web input -> (RtpDescription) input ); final Collection media = Collections2.transform(descriptions, RtpDescription::getMedia); - Preconditions.checkState(!media.contains(Media.UNKNOWN),"RTP descriptions contain unknown media"); - Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": received session proposal from "+from+" for "+media); + Preconditions.checkState(!media.contains(Media.UNKNOWN), "RTP descriptions contain unknown media"); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session proposal from " + from + " for " + media); this.proposedMedia = Sets.newHashSet(media); if (serverMsgId != null) { this.message.setServerMsgId(serverMsgId); @@ -511,6 +525,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void receiveProceed(final Jid from, final String serverMsgId, final long timestamp) { + final Set media = Preconditions.checkNotNull(this.proposedMedia, "Proposed media has to be set before handling proceed"); + Preconditions.checkState(media.size() > 0, "Proposed media should not be empty"); if (from.equals(id.with)) { if (isInitiator()) { if (transition(State.PROCEED)) { @@ -518,7 +534,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.message.setServerMsgId(serverMsgId); } this.message.setTime(timestamp); - this.sendSessionInitiate(State.SESSION_INITIALIZED_PRE_APPROVED); + this.sendSessionInitiate(media, State.SESSION_INITIALIZED_PRE_APPROVED); } else { Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because already in %s", id.account.getJid().asBareJid(), this.state)); } @@ -555,18 +571,18 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - private void sendSessionInitiate(final State targetState) { + private void sendSessionInitiate(final Set media, final State targetState) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": prepare session-initiate"); - discoverIceServers(iceServers -> sendSessionInitiate(targetState, iceServers)); + discoverIceServers(iceServers -> sendSessionInitiate(media, targetState, iceServers)); } - private synchronized void sendSessionInitiate(final State targetState, final List iceServers) { + private synchronized void sendSessionInitiate(final Set media, final State targetState, final List iceServers) { if (TERMINATED.contains(this.state)) { - Log.w(Config.LOGTAG,id.account.getJid().asBareJid()+": ICE servers got discovered when session was already terminated. nothing to do."); + Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": ICE servers got discovered when session was already terminated. nothing to do."); return; } try { - setupWebRTC(iceServers); + setupWebRTC(media, iceServers); } catch (WebRTCWrapper.InitializationException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to initialize webrtc"); transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE); @@ -607,6 +623,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web writeLogMessage(target); final JinglePacket jinglePacket = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); jinglePacket.setReason(reason, text); + Log.d(Config.LOGTAG,jinglePacket.toString()); send(jinglePacket); jingleConnectionManager.finishConnection(this); } @@ -756,7 +773,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web public synchronized void endCall() { if (TERMINATED.contains(this.state)) { - Log.w(Config.LOGTAG,id.account.getJid().asBareJid()+": received endCall() when session has already been terminated. nothing to do"); + Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": received endCall() when session has already been terminated. nothing to do"); return; } if (isInState(State.PROPOSED) && !isInitiator()) { @@ -791,9 +808,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web throw new IllegalStateException("called 'endCall' while in state " + this.state + ". isInitiator=" + isInitiator()); } - private void setupWebRTC(final List iceServers) throws WebRTCWrapper.InitializationException { + private void setupWebRTC(final Set media, final List iceServers) throws WebRTCWrapper.InitializationException { this.webRTCWrapper.setup(this.xmppConnectionService); - this.webRTCWrapper.initializePeerConnection(iceServers); + this.webRTCWrapper.initializePeerConnection(media, iceServers); } private void acceptCallFromProposed() { @@ -1018,7 +1035,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } public void setProposedMedia(final Set media) { - + this.proposedMedia = media; } private interface OnIceServersDiscovered { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 4b207dcc5..8562f2caa 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -158,8 +158,10 @@ public class WebRTCWrapper { }); } - public void initializePeerConnection(final List iceServers) throws InitializationException { + public void initializePeerConnection(final Set media, final List iceServers) throws InitializationException { Preconditions.checkState(this.eglBase != null); + Preconditions.checkNotNull(media); + Preconditions.checkArgument(media.size() > 0, "media can not be empty when initializing peer connection"); PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder() .setVideoDecoderFactory(new DefaultVideoDecoderFactory(eglBase.getEglBaseContext())) .setVideoEncoderFactory(new DefaultVideoEncoderFactory(eglBase.getEglBaseContext(), true, true)) @@ -168,7 +170,7 @@ public class WebRTCWrapper { final MediaStream stream = peerConnectionFactory.createLocalMediaStream("my-media-stream"); - this.optionalCapturer = getVideoCapturer(); + this.optionalCapturer = media.contains(Media.VIDEO) ? getVideoCapturer() : Optional.absent(); if (this.optionalCapturer.isPresent()) { final CameraVideoCapturer capturer = this.optionalCapturer.get(); @@ -183,10 +185,12 @@ public class WebRTCWrapper { } - //set up audio track - final AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints()); - this.localAudioTrack = peerConnectionFactory.createAudioTrack("my-audio-track", audioSource); - stream.addTrack(this.localAudioTrack); + if (media.contains(Media.AUDIO)) { + //set up audio track + final AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints()); + this.localAudioTrack = peerConnectionFactory.createAudioTrack("my-audio-track", audioSource); + stream.addTrack(this.localAudioTrack); + } final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(iceServers, peerConnectionObserver); @@ -201,23 +205,27 @@ public class WebRTCWrapper { public void close() { final PeerConnection peerConnection = this.peerConnection; + final Optional optionalCapturer = this.optionalCapturer; + final AppRTCAudioManager audioManager = this.appRTCAudioManager; + final EglBase eglBase = this.eglBase; if (peerConnection != null) { peerConnection.dispose(); } - final AppRTCAudioManager audioManager = this.appRTCAudioManager; if (audioManager != null) { mainHandler.post(audioManager::stop); } this.localVideoTrack = null; this.remoteVideoTrack = null; - if (this.optionalCapturer.isPresent()) { + if (optionalCapturer != null && optionalCapturer.isPresent()) { try { - this.optionalCapturer.get().stopCapture(); + optionalCapturer.get().stopCapture(); } catch (InterruptedException e) { - Log.e(Config.LOGTAG,"unable to stop capturing"); + Log.e(Config.LOGTAG, "unable to stop capturing"); } } - eglBase.release(); + if (eglBase != null) { + eglBase.release(); + } } public boolean isMicrophoneEnabled() { From 17d9b02f413c057c231db058b42e34cc3b4cf0de Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 15 Apr 2020 13:21:21 +0200 Subject: [PATCH 102/182] properly paint local video over remote --- src/main/AndroidManifest.xml | 1 + .../conversations/ui/RtpSessionActivity.java | 34 +++++++++++-------- src/main/res/layout/activity_rtp_session.xml | 22 ++++++------ 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 3cbf0e51c..8b0cbd6f1 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -62,6 +62,7 @@ android:networkSecurityConfig="@xml/network_security_configuration" android:theme="@style/ConversationsTheme" tools:replace="android:label" + android:hardwareAccelerated="true" tools:targetApi="o"> localVideoTrack = requireRtpConnection().geLocalVideoTrack(); if (localVideoTrack.isPresent()) { - try { - binding.localVideo.init(requireRtpConnection().getEglBaseContext(), null); - } catch (IllegalStateException e) { - Log.d(Config.LOGTAG,"ignoring already init for now",e); - } - binding.localVideo.setEnableHardwareScaler(true); + ensureSurfaceViewRendererIsSetup(binding.localVideo); + //paint local view over remote view + binding.localVideo.setZOrderMediaOverlay(true); binding.localVideo.setMirror(true); localVideoTrack.get().addSink(binding.localVideo); + } else { + binding.localVideo.setVisibility(View.GONE); } final Optional remoteVideoTrack = requireRtpConnection().getRemoteVideoTrack(); if (remoteVideoTrack.isPresent()) { - try { - binding.remoteVideo.init(requireRtpConnection().getEglBaseContext(), null); - } catch (IllegalStateException e) { - Log.d(Config.LOGTAG,"ignoring already init for now",e); - } - binding.remoteVideo.setEnableHardwareScaler(true); + ensureSurfaceViewRendererIsSetup(binding.remoteVideo); remoteVideoTrack.get().addSink(binding.remoteVideo); + } else { + binding.remoteVideo.setVisibility(View.GONE); } } + private void ensureSurfaceViewRendererIsSetup(final SurfaceViewRenderer surfaceViewRenderer) { + surfaceViewRenderer.setVisibility(View.VISIBLE); + try { + surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null); + } catch (IllegalStateException e) { + Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized"); + } + surfaceViewRenderer.setEnableHardwareScaler(true); + } + private void updateStateDisplay(final RtpEndUserState state) { switch (state) { case INCOMING_CALL: @@ -484,7 +490,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final Account account = extractAccount(intent); final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH)); this.rtpConnectionReference = null; - proposeJingleRtpSession(account, with, ImmutableSet.of(Media.AUDIO)); + proposeJingleRtpSession(account, with, ImmutableSet.of(Media.AUDIO, Media.VIDEO)); } private void exit(View view) { diff --git a/src/main/res/layout/activity_rtp_session.xml b/src/main/res/layout/activity_rtp_session.xml index 3b1390db6..a9b279334 100644 --- a/src/main/res/layout/activity_rtp_session.xml +++ b/src/main/res/layout/activity_rtp_session.xml @@ -35,16 +35,6 @@ - - + android:visibility="gone" /> + + Date: Wed, 15 Apr 2020 16:26:53 +0200 Subject: [PATCH 103/182] make seperate menu items for audio and video calls --- .../ui/ConversationFragment.java | 31 ++++++++++++++---- .../conversations/ui/RtpSessionActivity.java | 13 ++++++-- .../res/drawable-hdpi/ic_call_black_24dp.png | Bin 0 -> 326 bytes .../drawable-hdpi/ic_videocam_black_24dp.png | Bin 0 -> 169 bytes .../drawable-hdpi/ic_videocam_white_24dp.png | Bin 0 -> 173 bytes .../res/drawable-mdpi/ic_call_black_24dp.png | Bin 0 -> 244 bytes .../drawable-mdpi/ic_videocam_black_24dp.png | Bin 0 -> 127 bytes .../drawable-mdpi/ic_videocam_white_24dp.png | Bin 0 -> 131 bytes .../res/drawable-xhdpi/ic_call_black_24dp.png | Bin 0 -> 408 bytes .../drawable-xhdpi/ic_videocam_black_24dp.png | Bin 0 -> 171 bytes .../drawable-xhdpi/ic_videocam_white_24dp.png | Bin 0 -> 178 bytes .../drawable-xxhdpi/ic_call_black_24dp.png | Bin 0 -> 574 bytes .../ic_videocam_black_24dp.png | Bin 0 -> 224 bytes .../ic_videocam_white_24dp.png | Bin 0 -> 234 bytes .../drawable-xxxhdpi/ic_call_black_24dp.png | Bin 0 -> 758 bytes .../ic_videocam_black_24dp.png | Bin 0 -> 270 bytes .../ic_videocam_white_24dp.png | Bin 0 -> 290 bytes .../res/drawable/ic_call_black54_24dp.xml | 4 +++ .../res/drawable/ic_call_white70_24dp.xml | 4 +++ .../res/drawable/ic_videocam_black54_24dp.xml | 4 +++ .../res/drawable/ic_videocam_white70_24dp.xml | 4 +++ src/main/res/menu/fragment_conversation.xml | 13 +++++++- src/main/res/values/attrs.xml | 3 ++ src/main/res/values/strings.xml | 2 ++ src/main/res/values/themes.xml | 6 ++++ 25 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 src/main/res/drawable-hdpi/ic_call_black_24dp.png create mode 100644 src/main/res/drawable-hdpi/ic_videocam_black_24dp.png create mode 100644 src/main/res/drawable-hdpi/ic_videocam_white_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_call_black_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_videocam_black_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_videocam_white_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_call_black_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_videocam_black_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_videocam_white_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_call_black_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_videocam_black_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_videocam_white_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_call_black_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_videocam_black_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_videocam_white_24dp.png create mode 100644 src/main/res/drawable/ic_call_black54_24dp.xml create mode 100644 src/main/res/drawable/ic_call_white70_24dp.xml create mode 100644 src/main/res/drawable/ic_videocam_black54_24dp.xml create mode 100644 src/main/res/drawable/ic_videocam_white70_24dp.xml diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index cee0b54a6..9d704778b 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -140,6 +140,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke public static final int REQUEST_ADD_EDITOR_CONTENT = 0x0211; public static final int REQUEST_COMMIT_ATTACHMENTS = 0x0212; public static final int REQUEST_START_AUDIO_CALL = 0x213; + public static final int REQUEST_START_VIDEO_CALL = 0x214; public static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301; public static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302; public static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303; @@ -1234,8 +1235,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke BlockContactDialog.show((XmppActivity) activity, conversation); } break; - case R.id.action_call: - checkPermissionAndTriggerRtpSession(); + case R.id.action_audio_call: + checkPermissionAndTriggerAudioCall(); + break; + case R.id.action_video_call: + checkPermissionAndTriggerVideoCall(); break; default: break; @@ -1243,21 +1247,31 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke return super.onOptionsItemSelected(item); } - private void checkPermissionAndTriggerRtpSession() { + private void checkPermissionAndTriggerAudioCall() { if (activity.xmppConnectionService.useTorToConnect() || conversation.getAccount().isOnion()) { Toast.makeText(activity, R.string.disable_tor_to_make_call, Toast.LENGTH_SHORT).show(); return; } if (hasPermissions(REQUEST_START_AUDIO_CALL, Manifest.permission.RECORD_AUDIO)) { - triggerRtpSession(); + triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VOICE_CALL); + } + } + + private void checkPermissionAndTriggerVideoCall() { + if (activity.xmppConnectionService.useTorToConnect() || conversation.getAccount().isOnion()) { + Toast.makeText(activity, R.string.disable_tor_to_make_call, Toast.LENGTH_SHORT).show(); + return; + } + if (hasPermissions(REQUEST_START_VIDEO_CALL, Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)) { + triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); } } - private void triggerRtpSession() { + private void triggerRtpSession(final String action) { final Contact contact = conversation.getContact(); final Intent intent = new Intent(activity, RtpSessionActivity.class); - intent.setAction(RtpSessionActivity.ACTION_MAKE_VOICE_CALL); + intent.setAction(action); intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, contact.getAccount().getJid().toEscapedString()); intent.putExtra(RtpSessionActivity.EXTRA_WITH, contact.getJid().asBareJid().toEscapedString()); startActivity(intent); @@ -1414,7 +1428,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke commitAttachments(); break; case REQUEST_START_AUDIO_CALL: - triggerRtpSession(); + triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VOICE_CALL); + break; + case REQUEST_START_VIDEO_CALL: + triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); break; default: attachFile(requestCode); diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 2d335ec75..22692f86b 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -190,6 +190,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override void onBackendConnected() { final Intent intent = getIntent(); + final String action = intent.getAction(); final Account account = extractAccount(intent); final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH)); final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID); @@ -200,10 +201,16 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe requestPermissionsAndAcceptCall(); resetIntent(intent.getExtras()); } - } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(intent.getAction())) { - proposeJingleRtpSession(account, with, ImmutableSet.of(Media.AUDIO, Media.VIDEO)); + } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) { + final Set media; + if (ACTION_MAKE_VIDEO_CALL.equals(action)) { + media = ImmutableSet.of(Media.AUDIO, Media.VIDEO); + } else { + media = ImmutableSet.of(Media.AUDIO); + } + proposeJingleRtpSession(account, with, media); binding.with.setText(account.getRoster().getContact(with).getDisplayName()); - } else if (Intent.ACTION_VIEW.equals(intent.getAction())) { + } else if (Intent.ACTION_VIEW.equals(action)) { final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE); if (extraLastState != null) { Log.d(Config.LOGTAG, "restored last state from intent extra"); diff --git a/src/main/res/drawable-hdpi/ic_call_black_24dp.png b/src/main/res/drawable-hdpi/ic_call_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..d4077acf91bca566c143501ecd3589960f297eef GIT binary patch literal 326 zcmV-M0lEH(P)4oZOd_E%#fpSbNL+;V=L!_+Etq8MiDoruwr~#$o6U*>g@g+b1re>d16c(P z9fdfOlatNt*Esn;<^JCODPGczkkO<)Sc@E`CVpr=l=ejE7NuM9U%b!vbbEA$5*f9Ur!iSto>EnZc+= zKcwy%$7q%Bkh)|HqcT2q#ROFu`*dkAk0a4ROg%7L$#n5?A3Sl#F*$#5gVY-vGE-TL z1ZDinbHQTF$hA{%@8jh(A*gX=2nltr4IyEPQ$Ee1Xp&8OJP(Bm45nCNi;`UmGDed9 Y0y5j*N*3HAS^xk507*qoM6N<$f^r^!$^ZZW literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_videocam_black_24dp.png b/src/main/res/drawable-hdpi/ic_videocam_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..0a1e9b08ac01f46cbe0360bdb4df44b4ef2d114b GIT binary patch literal 169 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8fv1aONCo5DD~fy#20U&L)h#(B z!ykI6D3qD_Io;4La1K1;Gr2x#(fWVFJ<~GIENXQs^-=t2ez7EA?&i;1H(i`^H^A(f zqW1*D(3YM+#blQkCT=Y`Ot1cL{QCTQy+^HU%hI>?ZbI9;ch0zdSj@HgD^uN(t*u*R S*WU)(&EV|k0wldT1B8K8iKmNWh{y5d1PRu~2?kC7U;UT= zr_N^cXMNj}ZsyAiX8k|Xl(j>y|G(%AHdBGz0OmV0M;L^lw`#y_Big-bALY&7E&hT#K| z8D5!l>tAs#u6t4M+fd4nXf~EIE5V7B=Mqe$oQv{eMZ%~AkCr5CG`FAhC0z?|`qFlU zX+EImR=AL7%cn4sXJ44xRamyF)3{C!^}xW3xjUzh4cnd^3E|S3fmc!XrPW^jW#(P{ uS%*zG;@@=JcBM)N#kvD$9(<@wEUI4=uVbN7%%kf70000R0E zB@$rzfKlM-fB6Xu7?;Q_nyA4m@N`K=MMHs9q13<2G0Z3Zl&)S@Vb}iFVci(~H*k$- dvd9Bg24TD2RUWTqYyg_d;OXk;vd$@?2>^H7D%Jo1 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_call_black_24dp.png b/src/main/res/drawable-xhdpi/ic_call_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..99f28bbeca97715d85975921dd64fbde691c7ed9 GIT binary patch literal 408 zcmV;J0cZY+P)Jz(nTd8(K&U>#oAFn^y`zjyGXQF#zM7()Aet$noem0I<%p-vB_K zOIkhy0F#7VQy1oIC^F2m$2s@BQssv}HMU74RQ&>86OIco?%@#t00006 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_videocam_black_24dp.png b/src/main/res/drawable-xhdpi/ic_videocam_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..b0a3b44d154ae04a54b04ceac0d7322a08f8bf68 GIT binary patch literal 171 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DB2O2`kP61PR}XR?HV|;Rct645 zWyYzqyE;z>o>WZWp5|eY|L&jo;#5mPL4U6McTOHmZB-Ae5w@=^n%`p0d}uD?!*dM< z=?*)L74Aq1yyLZKWV!a=SF$s9e%HhsKX=M?+Rr;9?Ej|p?+f`WN1n^Oxahp&eQwIR UE#vBiFre)Wp00i_>zopr08juyvH$=8 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_videocam_white_24dp.png b/src/main/res/drawable-xhdpi/ic_videocam_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..1b2583d34e8bafff26a20f89c9d7cacf4525617e GIT binary patch literal 178 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DN>3NZkP61P*9^Is4S8Gw;|*rq z@i^Z5O~`&LSF^H2(&3uW+b>MJ-#(mEy!`0wC)efFl5M{lznoI4x{+;s;upRj$rU~^>bP0l+XkKQP@0n literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_call_black_24dp.png b/src/main/res/drawable-xxhdpi/ic_call_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..7c9d1b09c55f77d44877d4a354280ae5c2e1f645 GIT binary patch literal 574 zcmV-E0>S->P)!2PJ-H@rHV$L$%!{Psm^&nbmf4NDs!00GCXBW5P9(i&9mcltT_n9>4aSD3%Ow3uICgQ2 zjr=FWc(D&`CQobQ>*Xz@9Ht9Lp&X-lu|@Xg_43>xNU@I>XQUCw| literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_videocam_black_24dp.png b/src/main/res/drawable-xxhdpi/ic_videocam_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..0039e804eb1684ea4ebc31799709a087f572e5f5 GIT binary patch literal 224 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawmV3H5hEy=Vy|MA+VFv-1hn?^8 z4~ug1g)FcZ2tQ~w=Lplx+X;0Q`O-fM6yDE!@w@{FUidGcwkmVlvbJqjp=XW1?wu~T z^_Jsx-sfxGL;v3?&|*&U<2W%#V1lKfLa~y=Gna-loeVRD8I9DLAQAzLOe`D%^2a{x z=bL%t*T#(tHXb^qa&Ey|=ZV3$mi+%IV|?53_`T+<7p`3W@mfYIs|#!;^C!uxN8C@h R>H?k2;OXk;vd$@?2>^U;U)TTu literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_videocam_white_24dp.png b/src/main/res/drawable-xxhdpi/ic_videocam_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..44c28e2f2830f927973beaa3a143ddfe439f20ed GIT binary patch literal 234 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawHhQ`^hEy=Vyh zQBv~(_A|Uaeg>M$g-&EJ3;(E>F0WfsuqEq@i4qW8@}AzdO6rx>m9XRe^SrNqJ^A{6 z_JW14-f=$P%=XJsxKGL9nM=c&PKKGnj7I8ADSj*`=EzL26jUe%O0+bb;bi0#P;hAY zVbJ!!)MTmp67X<=dB~fI3~wU z@{lUI^G7ieZjAMal)^;3`-n6iIbafzGMLC0L!e&{0YwNj>JYFPfqps!R3Oj?hkzsk zJ$DG$k3jbv0@@I$!y(`b0<}2=yh5M@4gu2?BF+|!!zB=>gpBsb*yWqxF#_Fk2$-P^ zaZX|!t^sipP64fmvw$hxp8#}6p8thn1UeCM{xF*nX-%a0X=)HE;8TS8wFq{a-1A3d z&xZ{;=Z{i{Z~+53<|nY|`AalL5*3y*6FPq@YTO8!zYRs~f7(1inI~340#)`|0eu80 z67bSWXhfAgR=`){C=!G`2j@_w+WKFLzwAVn%hqS`nKBev#*mfJNq{0pFxDMKk$@g6 zV46A+dQvtg~hm-^*4iE*y^nL zd4DS`;0z|qU7*Z)e)!F?W6WYgZ;*oo06?uhUrGSfGmOb{&qx9_q%m>>pn^9!AHy_8 zb^w%e6*EUSZyHJK4uF+(>kfc`!whK-04S$}jOGA o57JDLVS-fne9!Yd&+|O*AE$UXZG6IlIsgCw07*qoM6N<$g3dij&;S4c literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_videocam_black_24dp.png b/src/main/res/drawable-xxxhdpi/ic_videocam_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..be3ba821a49f63bf6e78284bdc8abce83bf5faeb GIT binary patch literal 270 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcgu6nvShEy=Vy=j=mY$)LNuzco* zC)0`-nSML?sNwA||M^ZETG`m{nTua&)(unsH&MlN5)kdH{Gd4d`PsS2=RekaUbZP$ zvGI6(Cqi5A_ZGH25t@fmGv_`1sLn1jQzjs#$YJ71Mprew99aQoo*Tw&5(y2AY!VO7 z&)I*OS6piSl(<*ooO;`0bj@_iW7q#au+ihg#QZ15ud9DKf8T5RAZ6{2HxE2#J!{uD ov)J)5>qo`8)yJnpocE8RWU|_J_i3@NKo2r_y85}Sb4q9e0RN|LDgXcg literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_videocam_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_videocam_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ed20c0706292403018b019329a4608db85d99e06 GIT binary patch literal 290 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcgUV6GXhEy=Vz2%$L>L9?7s4sjZ z`dg=nZ3NGX*QczyQwj`vc|$kX%>MU4_u27-XQ1Fsx=&8bpK^6=W#{+5{x0v--*iiI zK6_5eHWRkq-woYo`i}j$!>)2&V!wgH0wW@j83QAeoL2*oWZ@_>deZ&>ak+XyvA!1j^LK3P nm_Pheb$}Qopa5qqD6eI%v1Ev>S}gk$=xGK|S3j3^P6 + \ No newline at end of file diff --git a/src/main/res/drawable/ic_call_white70_24dp.xml b/src/main/res/drawable/ic_call_white70_24dp.xml new file mode 100644 index 000000000..f1a2e46a1 --- /dev/null +++ b/src/main/res/drawable/ic_call_white70_24dp.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/src/main/res/drawable/ic_videocam_black54_24dp.xml b/src/main/res/drawable/ic_videocam_black54_24dp.xml new file mode 100644 index 000000000..5fe6bfea7 --- /dev/null +++ b/src/main/res/drawable/ic_videocam_black54_24dp.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/src/main/res/drawable/ic_videocam_white70_24dp.xml b/src/main/res/drawable/ic_videocam_white70_24dp.xml new file mode 100644 index 000000000..83e61f012 --- /dev/null +++ b/src/main/res/drawable/ic_videocam_white70_24dp.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/src/main/res/menu/fragment_conversation.xml b/src/main/res/menu/fragment_conversation.xml index c5a574279..b01e8cfa5 100644 --- a/src/main/res/menu/fragment_conversation.xml +++ b/src/main/res/menu/fragment_conversation.xml @@ -65,7 +65,18 @@ android:icon="?attr/icon_call" android:orderInCategory="35" android:title="@string/make_call" - app:showAsAction="always" /> + app:showAsAction="always"> +

+ + + + + + + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 6aacd3673..854dfa3c6 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -908,6 +908,8 @@ Outgoing call Outgoing call · %s Missed call + Audio call + Video call View %1$d Participant View %1$d Participants diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index e07c3bf67..2f8ecc434 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -54,6 +54,9 @@ @drawable/ic_attach_photo @drawable/ic_attach_record + @drawable/ic_call_black54_24dp + @drawable/ic_videocam_black54_24dp + @drawable/message_bubble_received_white @drawable/message_bubble_sent @drawable/message_bubble_received @@ -164,6 +167,9 @@ @drawable/ic_send_videocam_offline_white @drawable/ic_send_voice_offline_white + @drawable/ic_call_white70_24dp + @drawable/ic_videocam_white70_24dp + @drawable/ic_attach_camera_white @drawable/ic_attach_videocam_white @drawable/ic_attach_document_white From d4788fc1f4a9924dc98ea407ead19029c5e301ab Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 15 Apr 2020 16:54:54 +0200 Subject: [PATCH 104/182] display video call based on availability --- .../java/eu/siacs/conversations/ui/ConversationFragment.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 9d704778b..03c90320f 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -955,6 +955,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke final MenuItem menuMute = menu.findItem(R.id.action_mute); final MenuItem menuUnmute = menu.findItem(R.id.action_unmute); final MenuItem menuCall = menu.findItem(R.id.action_call); + final MenuItem menuVideoCall = menu.findItem(R.id.action_video_call); if (conversation != null) { @@ -964,7 +965,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke menuMucDetails.setTitle(conversation.getMucOptions().isPrivateAndNonAnonymous() ? R.string.action_muc_details : R.string.channel_details); menuCall.setVisible(false); } else { - menuCall.setVisible(RtpCapability.check(conversation.getContact()) != RtpCapability.Capability.NONE); + final RtpCapability.Capability rtpCapability = RtpCapability.check(conversation.getContact()); + menuCall.setVisible(rtpCapability != RtpCapability.Capability.NONE); + menuVideoCall.setVisible(rtpCapability == RtpCapability.Capability.VIDEO); menuContactDetails.setVisible(!this.conversation.withSelf()); menuMucDetails.setVisible(false); final XmppConnectionService service = activity.xmppConnectionService; From 5a20faaf0f74854a700a4bb11277a35f6d7efa4c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 15 Apr 2020 18:28:04 +0200 Subject: [PATCH 105/182] show 'incoming video cal' notification --- .../services/NotificationService.java | 13 ++++++++++--- .../conversations/ui/RtpSessionActivity.java | 10 +++++++++- .../xmpp/jingle/JingleRtpConnection.java | 19 +++++++++++++++---- src/main/res/values/strings.xml | 1 + 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index c2665ea8b..cbd11f1d7 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -41,6 +41,7 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -63,6 +64,7 @@ import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; +import eu.siacs.conversations.xmpp.jingle.Media; public class NotificationService { @@ -334,7 +336,7 @@ public class NotificationService { } } - public void showIncomingCallNotification(AbstractJingleConnection.Id id) { + public void showIncomingCallNotification(final AbstractJingleConnection.Id id, final Set media) { final Intent fullScreenIntent = new Intent(mXmppConnectionService, RtpSessionActivity.class); fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString()); fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString()); @@ -342,8 +344,13 @@ public class NotificationService { fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "incoming_calls"); - builder.setSmallIcon(R.drawable.ic_call_white_24dp); - builder.setContentTitle(mXmppConnectionService.getString(R.string.rtp_state_incoming_call)); + if (media.contains(Media.VIDEO)) { + builder.setSmallIcon(R.drawable.ic_videocam_white_24dp); + builder.setContentTitle(mXmppConnectionService.getString(R.string.rtp_state_incoming_video_call)); + } else { + builder.setSmallIcon(R.drawable.ic_call_white_24dp); + builder.setContentTitle(mXmppConnectionService.getString(R.string.rtp_state_incoming_call)); + } builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName()); builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); builder.setPriority(NotificationCompat.PRIORITY_HIGH); diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 22692f86b..33d17315d 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -333,7 +333,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void updateStateDisplay(final RtpEndUserState state) { switch (state) { case INCOMING_CALL: - setTitle(R.string.rtp_state_incoming_call); + if (getMedia().contains(Media.VIDEO)) { + setTitle(R.string.rtp_state_incoming_video_call); + } else { + setTitle(R.string.rtp_state_incoming_call); + } break; case CONNECTING: setTitle(R.string.rtp_state_connecting); @@ -369,6 +373,10 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } + private Set getMedia() { + return requireRtpConnection().getMedia(); + } + @SuppressLint("RestrictedApi") private void updateButtonConfiguration(final RtpEndUserState state) { if (state == RtpEndUserState.INCOMING_CALL) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index e4ef46b60..b762ab7ec 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -278,7 +278,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web "proposed media must be set when processing pre-approved session-initiate" ); if (!this.proposedMedia.equals(contentMap.getMedia())) { - sendSessionTerminate(Reason.SECURITY_ERROR,String.format( + sendSessionTerminate(Reason.SECURITY_ERROR, String.format( "Your session proposal (Jingle Message Initiation) included media %s but your session-initiate was %s", this.proposedMedia, contentMap.getMedia() @@ -500,7 +500,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); if (originatedFromMyself) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": saw proposal from mysql. ignoring"); - } else if (transition(State.PROPOSED)) { + } else if (isInState(State.NULL)) { final Collection descriptions = Collections2.transform( Collections2.filter(propose.getDescriptions(), d -> d instanceof RtpDescription), input -> (RtpDescription) input @@ -509,6 +509,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web Preconditions.checkState(!media.contains(Media.UNKNOWN), "RTP descriptions contain unknown media"); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session proposal from " + from + " for " + media); this.proposedMedia = Sets.newHashSet(media); + transitionOrThrow(State.PROPOSED); if (serverMsgId != null) { this.message.setServerMsgId(serverMsgId); } @@ -521,7 +522,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void startRinging() { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received call from " + id.with + ". start ringing"); - xmppConnectionService.getNotificationService().showIncomingCallNotification(id); + xmppConnectionService.getNotificationService().showIncomingCallNotification(id, getMedia()); } private void receiveProceed(final Jid from, final String serverMsgId, final long timestamp) { @@ -623,7 +624,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web writeLogMessage(target); final JinglePacket jinglePacket = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); jinglePacket.setReason(reason, text); - Log.d(Config.LOGTAG,jinglePacket.toString()); + Log.d(Config.LOGTAG, jinglePacket.toString()); send(jinglePacket); jingleConnectionManager.finishConnection(this); } @@ -744,6 +745,16 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web throw new IllegalStateException(String.format("%s has no equivalent EndUserState", this.state)); } + public Set getMedia() { + if (isInState(State.NULL)) { + throw new IllegalStateException("RTP connection has not been initialized yet"); + } + if (isInState(State.PROPOSED, State.PROCEED)) { + return Preconditions.checkNotNull(this.proposedMedia, "RTP connection has not been initialized properly"); + } + return Preconditions.checkNotNull(initiatorRtpContentMap.getMedia()); + } + public synchronized void acceptCall() { switch (this.state) { diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 854dfa3c6..bcb757cac 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -889,6 +889,7 @@ Please enable an account Make call Incoming call + Incoming video call Connecting Connected Accepting call From 445009c55827ffd0934344a51ec85c8320ee6195 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 15 Apr 2020 18:47:15 +0200 Subject: [PATCH 106/182] request camera permissions --- .../conversations/ui/RtpSessionActivity.java | 50 +++++++++++-------- .../xmpp/jingle/JingleRtpConnection.java | 8 ++- .../xmpp/jingle/WebRTCWrapper.java | 4 +- 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 33d17315d..ba22604e4 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -25,6 +25,7 @@ import org.webrtc.VideoTrack; import java.lang.ref.WeakReference; import java.util.Arrays; +import java.util.List; import java.util.Set; import eu.siacs.conversations.Config; @@ -108,7 +109,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void requestPermissionsAndAcceptCall() { - if (PermissionUtils.hasPermission(this, ImmutableList.of(Manifest.permission.RECORD_AUDIO), REQUEST_ACCEPT_CALL)) { + final List permissions; + if (getMedia().contains(Media.VIDEO)) { + permissions = ImmutableList.of(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO); + } else { + permissions = ImmutableList.of(Manifest.permission.RECORD_AUDIO); + } + if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) { //TODO like wise the propose; we might just wait here for the audio manager to come up putScreenInCallMode(); requireRtpConnection().acceptCall(); @@ -285,6 +292,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe putScreenInCallMode(); } binding.with.setText(getWith().getDisplayName()); + updateVideoViews(); updateStateDisplay(currentState); updateButtonConfiguration(currentState); } @@ -300,26 +308,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe setIntent(intent); } - private void updateVideoViews() { - final Optional localVideoTrack = requireRtpConnection().geLocalVideoTrack(); - if (localVideoTrack.isPresent()) { - ensureSurfaceViewRendererIsSetup(binding.localVideo); - //paint local view over remote view - binding.localVideo.setZOrderMediaOverlay(true); - binding.localVideo.setMirror(true); - localVideoTrack.get().addSink(binding.localVideo); - } else { - binding.localVideo.setVisibility(View.GONE); - } - final Optional remoteVideoTrack = requireRtpConnection().getRemoteVideoTrack(); - if (remoteVideoTrack.isPresent()) { - ensureSurfaceViewRendererIsSetup(binding.remoteVideo); - remoteVideoTrack.get().addSink(binding.remoteVideo); - } else { - binding.remoteVideo.setVisibility(View.GONE); - } - } - private void ensureSurfaceViewRendererIsSetup(final SurfaceViewRenderer surfaceViewRenderer) { surfaceViewRenderer.setVisibility(View.VISIBLE); try { @@ -477,6 +465,26 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.binding.inCallActionRight.setVisibility(View.VISIBLE); } + private void updateVideoViews() { + final Optional localVideoTrack = requireRtpConnection().geLocalVideoTrack(); + if (localVideoTrack.isPresent()) { + ensureSurfaceViewRendererIsSetup(binding.localVideo); + //paint local view over remote view + binding.localVideo.setZOrderMediaOverlay(true); + binding.localVideo.setMirror(true); + localVideoTrack.get().addSink(binding.localVideo); + } else { + binding.localVideo.setVisibility(View.GONE); + } + final Optional remoteVideoTrack = requireRtpConnection().getRemoteVideoTrack(); + if (remoteVideoTrack.isPresent()) { + ensureSurfaceViewRendererIsSetup(binding.remoteVideo); + remoteVideoTrack.get().addSink(binding.remoteVideo); + } else { + binding.remoteVideo.setVisibility(View.GONE); + } + } + private void disableMicrophone(View view) { JingleRtpConnection rtpConnection = requireRtpConnection(); rtpConnection.setMicrophoneEnabled(false); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index b762ab7ec..317dd7cf0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -820,7 +820,13 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void setupWebRTC(final Set media, final List iceServers) throws WebRTCWrapper.InitializationException { - this.webRTCWrapper.setup(this.xmppConnectionService); + final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference; + if (media.contains(Media.VIDEO)) { + speakerPhonePreference = AppRTCAudioManager.SpeakerPhonePreference.SPEAKER; + } else { + speakerPhonePreference = AppRTCAudioManager.SpeakerPhonePreference.EARPIECE; + } + this.webRTCWrapper.setup(this.xmppConnectionService, speakerPhonePreference); this.webRTCWrapper.initializePeerConnection(media, iceServers); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 8562f2caa..8e87291cd 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -145,14 +145,14 @@ public class WebRTCWrapper { this.eventCallback = eventCallback; } - public void setup(final Context context) { + public void setup(final Context context, final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) { PeerConnectionFactory.initialize( PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions() ); this.eglBase = EglBase.create(); this.context = context; mainHandler.post(() -> { - appRTCAudioManager = AppRTCAudioManager.create(context, AppRTCAudioManager.SpeakerPhonePreference.EARPIECE); + appRTCAudioManager = AppRTCAudioManager.create(context, speakerPhonePreference); appRTCAudioManager.start(audioManagerEvents); eventCallback.onAudioDeviceChanged(appRTCAudioManager.getSelectedAudioDevice(), appRTCAudioManager.getAudioDevices()); }); From 01a9a5299067c846d87d24cc2ca62b06c7bdb173 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 15 Apr 2020 19:16:47 +0200 Subject: [PATCH 107/182] show enable/disable video in video calls --- .../conversations/ui/RtpSessionActivity.java | 96 ++++++++++++------ .../xmpp/jingle/JingleRtpConnection.java | 8 ++ .../xmpp/jingle/WebRTCWrapper.java | 20 +++- .../ic_videocam_off_black_24dp.png | Bin 0 -> 260 bytes .../ic_videocam_off_black_24dp.png | Bin 0 -> 193 bytes .../ic_videocam_off_black_24dp.png | Bin 0 -> 274 bytes .../ic_videocam_off_black_24dp.png | Bin 0 -> 375 bytes .../ic_videocam_off_black_24dp.png | Bin 0 -> 447 bytes 8 files changed, 89 insertions(+), 35 deletions(-) create mode 100644 src/main/res/drawable-hdpi/ic_videocam_off_black_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_videocam_off_black_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_videocam_off_black_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_videocam_off_black_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_videocam_off_black_24dp.png diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index ba22604e4..4c4a35d0c 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -410,12 +410,16 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @SuppressLint("RestrictedApi") private void updateInCallButtonConfiguration(final RtpEndUserState state) { if (state == RtpEndUserState.CONNECTED) { - final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager(); - updateInCallButtonConfiguration( - audioManager.getSelectedAudioDevice(), - audioManager.getAudioDevices().size(), - requireRtpConnection().isMicrophoneEnabled() - ); + if (getMedia().contains(Media.VIDEO)) { + updateInCallButtonConfigurationVideo(requireRtpConnection().isVideoEnabled()); + } else { + final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager(); + updateInCallButtonConfigurationSpeaker( + audioManager.getSelectedAudioDevice(), + audioManager.getAudioDevices().size() + ); + } + updateInCallButtonConfigurationMicrophone(requireRtpConnection().isMicrophoneEnabled()); } else { this.binding.inCallActionLeft.setVisibility(View.GONE); this.binding.inCallActionRight.setVisibility(View.GONE); @@ -423,48 +427,75 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } @SuppressLint("RestrictedApi") - private void updateInCallButtonConfiguration(final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices, final boolean microphoneEnabled) { + private void updateInCallButtonConfigurationSpeaker(final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices) { switch (selectedAudioDevice) { case EARPIECE: - this.binding.inCallActionLeft.setImageResource(R.drawable.ic_volume_off_black_24dp); + this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_off_black_24dp); if (numberOfChoices >= 2) { - this.binding.inCallActionLeft.setOnClickListener(this::switchToSpeaker); + this.binding.inCallActionRight.setOnClickListener(this::switchToSpeaker); } else { - this.binding.inCallActionLeft.setOnClickListener(null); - this.binding.inCallActionLeft.setClickable(false); + this.binding.inCallActionRight.setOnClickListener(null); + this.binding.inCallActionRight.setClickable(false); } break; case WIRED_HEADSET: - this.binding.inCallActionLeft.setImageResource(R.drawable.ic_headset_black_24dp); - this.binding.inCallActionLeft.setOnClickListener(null); - this.binding.inCallActionLeft.setClickable(false); + this.binding.inCallActionRight.setImageResource(R.drawable.ic_headset_black_24dp); + this.binding.inCallActionRight.setOnClickListener(null); + this.binding.inCallActionRight.setClickable(false); break; case SPEAKER_PHONE: - this.binding.inCallActionLeft.setImageResource(R.drawable.ic_volume_up_black_24dp); + this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_up_black_24dp); if (numberOfChoices >= 2) { - this.binding.inCallActionLeft.setOnClickListener(this::switchToEarpiece); + this.binding.inCallActionRight.setOnClickListener(this::switchToEarpiece); } else { - this.binding.inCallActionLeft.setOnClickListener(null); - this.binding.inCallActionLeft.setClickable(false); + this.binding.inCallActionRight.setOnClickListener(null); + this.binding.inCallActionRight.setClickable(false); } break; case BLUETOOTH: - this.binding.inCallActionLeft.setImageResource(R.drawable.ic_bluetooth_audio_black_24dp); - this.binding.inCallActionLeft.setOnClickListener(null); - this.binding.inCallActionLeft.setClickable(false); + this.binding.inCallActionRight.setImageResource(R.drawable.ic_bluetooth_audio_black_24dp); + this.binding.inCallActionRight.setOnClickListener(null); + this.binding.inCallActionRight.setClickable(false); break; } - this.binding.inCallActionLeft.setVisibility(View.VISIBLE); - if (microphoneEnabled) { - this.binding.inCallActionRight.setImageResource(R.drawable.ic_mic_black_24dp); - this.binding.inCallActionRight.setOnClickListener(this::disableMicrophone); - } else { - this.binding.inCallActionRight.setImageResource(R.drawable.ic_mic_off_black_24dp); - this.binding.inCallActionRight.setOnClickListener(this::enableMicrophone); - } this.binding.inCallActionRight.setVisibility(View.VISIBLE); } + @SuppressLint("RestrictedApi") + private void updateInCallButtonConfigurationVideo(final boolean videoEnabled) { + this.binding.inCallActionRight.setVisibility(View.VISIBLE); + if (videoEnabled) { + this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_black_24dp); + this.binding.inCallActionRight.setOnClickListener(this::disableVideo); + } else { + this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_off_black_24dp); + this.binding.inCallActionRight.setOnClickListener(this::enableVideo); + } + } + + private void enableVideo(View view) { + requireRtpConnection().setVideoEnabled(true); + updateInCallButtonConfigurationVideo(true); + } + + private void disableVideo(View view) { + requireRtpConnection().setVideoEnabled(false); + updateInCallButtonConfigurationVideo(false); + + } + + @SuppressLint("RestrictedApi") + private void updateInCallButtonConfigurationMicrophone(final boolean microphoneEnabled) { + if (microphoneEnabled) { + this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_black_24dp); + this.binding.inCallActionLeft.setOnClickListener(this::disableMicrophone); + } else { + this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_off_black_24dp); + this.binding.inCallActionLeft.setOnClickListener(this::enableMicrophone); + } + this.binding.inCallActionLeft.setVisibility(View.VISIBLE); + } + private void updateVideoViews() { final Optional localVideoTrack = requireRtpConnection().geLocalVideoTrack(); if (localVideoTrack.isPresent()) { @@ -572,12 +603,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { Log.d(Config.LOGTAG, "onAudioDeviceChanged in activity: selected:" + selectedAudioDevice + ", available:" + availableAudioDevices); try { - if (requireRtpConnection().getEndUserState() == RtpEndUserState.CONNECTED) { + if (requireRtpConnection().getEndUserState() == RtpEndUserState.CONNECTED && !getMedia().contains(Media.VIDEO)) { final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager(); - updateInCallButtonConfiguration( + updateInCallButtonConfigurationSpeaker( audioManager.getSelectedAudioDevice(), - audioManager.getAudioDevices().size(), - requireRtpConnection().isMicrophoneEnabled() + audioManager.getAudioDevices().size() ); } putProximityWakeLockInProperState(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 317dd7cf0..976e3ad3e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -930,6 +930,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web webRTCWrapper.setMicrophoneEnabled(enabled); } + public boolean isVideoEnabled() { + return webRTCWrapper.isVideoEnabled(); + } + + public void setVideoEnabled(final boolean enabled) { + webRTCWrapper.setVideoEnabled(enabled); + } + @Override public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { xmppConnectionService.notifyJingleRtpConnectionUpdate(selectedAudioDevice, availableAudioDevices); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 8e87291cd..70c4f8e7b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -228,7 +228,7 @@ public class WebRTCWrapper { } } - public boolean isMicrophoneEnabled() { + boolean isMicrophoneEnabled() { final AudioTrack audioTrack = this.localAudioTrack; if (audioTrack == null) { throw new IllegalStateException("Local audio track does not exist (yet)"); @@ -236,7 +236,7 @@ public class WebRTCWrapper { return audioTrack.enabled(); } - public void setMicrophoneEnabled(final boolean enabled) { + void setMicrophoneEnabled(final boolean enabled) { final AudioTrack audioTrack = this.localAudioTrack; if (audioTrack == null) { throw new IllegalStateException("Local audio track does not exist (yet)"); @@ -244,6 +244,22 @@ public class WebRTCWrapper { audioTrack.setEnabled(enabled); } + public boolean isVideoEnabled() { + final VideoTrack videoTrack = this.localVideoTrack; + if (videoTrack == null) { + throw new IllegalStateException("Local video track does not exist"); + } + return videoTrack.enabled(); + } + + public void setVideoEnabled(final boolean enabled) { + final VideoTrack videoTrack = this.localVideoTrack; + if (videoTrack == null) { + throw new IllegalStateException("Local video track does not exist"); + } + videoTrack.setEnabled(enabled); + } + public ListenableFuture createOffer() { return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { final SettableFuture future = SettableFuture.create(); diff --git a/src/main/res/drawable-hdpi/ic_videocam_off_black_24dp.png b/src/main/res/drawable-hdpi/ic_videocam_off_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..06140f119b52d79a175c37a706eaa22ee883ffeb GIT binary patch literal 260 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8LpX-^l&kP61PS8V+b2MD-6e9RZo zv6*|qiW?l;env6~iR6n;{65L$RM4X}O|N))drUUXlQ0QhIOlf#)2IKh8-Dh1GIlz< zfGtbR^jToA+}A0Nxe*iIhq5u`Styk0e94H}_=?#v=8Cnw?T2Hlnk-f>I-|an({jb4 z9t#EMSm{+7k6Wce0*`TA3~!2V6Io`n((;pn-QM6E zMQTy$Q~nCEBzC^@XO^YP)@AcgO4?nfMGa3|;`40TZF~9Iy~7ZvZq=&Jhj(A)EnCwEh4mtNEo@{wvUJBQj9~k;D^6AdaYUg7fn7 YK4Zf4Nt~M^>Hq)$07*qoM6N<$f=T;pw*UYD literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_videocam_off_black_24dp.png b/src/main/res/drawable-xxhdpi/ic_videocam_off_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..32d61d8dff7c7cb6eb0be698441b12190a4e3579 GIT binary patch literal 375 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXoKNz-Z^`;uuoF`1YK=7jvP=k&l`t zafc68%>VQ8{aUdiCT>-Zd77?oS~k30XJFREk$-YQ|3mSL`!C!1--L#i_UVf07T$B< ziP&-^(Rzt4$2OO!q>O%(c}goiRx5F??KZi|YWAXDWAc*hJMN`T%$Yj!z3>D9d z9+MVLRB=`I>~fpLD>8ZAhVRWKlNd!NhZ&ya5Ar}!bXD3jNH#;+^X!F(6Wo3r5Vk9w zwEXxt5B}wzT9<08m+X#Tl5TQA_gMZp?Q<7g%q7;!EHsUC@e2(N{mOdXZSn8ME4M>| P0m|U%>gTe~DWM4f4=9?R literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_videocam_off_black_24dp.png b/src/main/res/drawable-xxxhdpi/ic_videocam_off_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2a86702f8283cd3b8e7dc27c615f9ff51405fd78 GIT binary patch literal 447 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z}V*L;uuoF`1btGT&6;S=E8ha zjoc?1s^Q|AO5EOYK}=1@93EBN@nk;oV*U)#X@7hfAIk@MX)fKocd`3(n|hbTzH^mz zf=!2OoD62=d(2oIaIS@qyZ(?s(;|UsiW*!GwOjU!9g;P(m^bqrhrh+bS??N5)2`3l zzxzjxqN2#Q>k2tPf(;W{@3GIaUZke-=4Xw_EbS{xSnT5YCkX$jv32l!@cAo)W#jql z%ui4Rmoo?)dYsBMe?9vP&YFF43hEzzRyE8y@ccDn5lHX^LU3Ne?1n=QoNNq!8yEzd z7#jrS3O+FwG_f`a>oCkoV0^&B&7dB^U~!P?0H+v84OFTnfkC0AdQJ^gjYFa*#0Z4+ z0~US;b(8WPhEB!VS9Aq$%}#vnACXx9b)|JpvL}aHZ5uEQ>{|YdJC#o}U^v$zc)meA z(eK%8LAN~&4cduEv~ylxc+9(h8YnQQ)G=%~yr3j{vgs=@Vi-JK{an^LB{Ts53zn*T literal 0 HcmV?d00001 From f995965deace048ec071a11708371c7ce859895b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 15 Apr 2020 20:19:09 +0200 Subject: [PATCH 108/182] parse 0339 source groups from sdp --- .../xmpp/jingle/stanzas/RtpDescription.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index 70f3f0f6a..d40363b49 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -9,7 +9,6 @@ import com.google.common.collect.ImmutableList; import java.util.Collection; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import eu.siacs.conversations.xml.Element; @@ -479,6 +478,14 @@ public class RtpDescription extends GenericDescription { public static class SourceGroup extends Element { + public SourceGroup(final String semantics, List ssrcs) { + this(); + this.setAttribute("semantics", semantics); + for (String ssrc : ssrcs) { + this.addChild("source").setAttribute("ssrc", ssrc); + } + } + private SourceGroup() { super("ssrc-group", Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES); } @@ -561,6 +568,17 @@ public class RtpDescription extends GenericDescription { rtpDescription.addChild(extension); } } + for (final String ssrcGroup : media.attributes.get("ssrc-group")) { + final String[] parts = ssrcGroup.split(" "); + if (parts.length >= 2) { + ImmutableList.Builder builder = new ImmutableList.Builder<>(); + final String semantics = parts[0]; + for (int i = 1; i < parts.length; ++i) { + builder.add(parts[i]); + } + rtpDescription.addChild(new SourceGroup(semantics, builder.build())); + } + } for (Map.Entry> source : sourceParameterMap.asMap().entrySet()) { rtpDescription.addChild(new Source(source.getKey(), source.getValue())); } From d7e93e18e5577541430b4a0019974a0195d73699 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 15 Apr 2020 21:08:49 +0200 Subject: [PATCH 109/182] add a couple of todos to RtpSessionActivity --- .../conversations/ui/RtpSessionActivity.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 4c4a35d0c..a920dfa51 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -124,6 +124,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @SuppressLint("WakelockTimeout") private void putScreenInCallMode() { + //TODO for video calls we actually do want to keep the screen on getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); final JingleRtpConnection rtpConnection = rtpConnectionReference != null ? rtpConnectionReference.get() : null; final AppRTCAudioManager audioManager = rtpConnection == null ? null : rtpConnection.getAudioManager(); @@ -209,13 +210,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe resetIntent(intent.getExtras()); } } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) { - final Set media; - if (ACTION_MAKE_VIDEO_CALL.equals(action)) { - media = ImmutableSet.of(Media.AUDIO, Media.VIDEO); - } else { - media = ImmutableSet.of(Media.AUDIO); - } - proposeJingleRtpSession(account, with, media); + proposeJingleRtpSession(account, with, actionToMedia(action)); binding.with.setText(account.getRoster().getContact(with).getDisplayName()); } else if (Intent.ACTION_VIEW.equals(action)) { final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE); @@ -229,6 +224,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } + private static Set actionToMedia(final String action) { + if (ACTION_MAKE_VIDEO_CALL.equals(action)) { + return ImmutableSet.of(Media.AUDIO, Media.VIDEO); + } else { + return ImmutableSet.of(Media.AUDIO); + } + } + private void proposeJingleRtpSession(final Account account, final Jid with, final Set media) { xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with, media); //TODO maybe we don’t want to acquire a wake lock just yet and wait for audio manager to discover what speaker we are using @@ -586,11 +589,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe finish(); return; } else if (asList(RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.CONNECTIVITY_ERROR).contains(state)) { + //todo remember if we were video resetIntent(account, with, state); } runOnUiThread(() -> { updateStateDisplay(state); updateButtonConfiguration(state); + //TODO kill video when in final or error stages updateVideoViews(); }); } else { From 36e117979a0e890c5be0f29c560252d610f11d18 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 15 Apr 2020 22:40:37 +0200 Subject: [PATCH 110/182] put 'video' in ongoing video call notification --- .../services/NotificationService.java | 11 ++-- .../services/XmppConnectionService.java | 42 +++++++++++---- .../conversations/ui/RtpSessionActivity.java | 51 +++++++++++++------ .../xmpp/jingle/JingleRtpConnection.java | 27 ++++++---- src/main/res/values/strings.xml | 1 + 5 files changed, 93 insertions(+), 39 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index cbd11f1d7..b6257cf92 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -372,10 +372,15 @@ public class NotificationService { notify(INCOMING_CALL_NOTIFICATION_ID, notification); } - public Notification getOngoingCallNotification(final AbstractJingleConnection.Id id) { + public Notification getOngoingCallNotification(final AbstractJingleConnection.Id id, final Set media) { final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls"); - builder.setSmallIcon(R.drawable.ic_call_white_24dp); - builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call)); + if (media.contains(Media.VIDEO)) { + builder.setSmallIcon(R.drawable.ic_videocam_white_24dp); + builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call)); + } else { + builder.setSmallIcon(R.drawable.ic_call_white_24dp); + builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call)); + } builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName()); builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); builder.setPriority(NotificationCompat.PRIORITY_HIGH); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index d7adc05e9..0efa27618 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -42,6 +42,7 @@ import android.util.Log; import android.util.LruCache; import android.util.Pair; +import com.google.common.base.Objects; import com.google.common.base.Strings; import org.conscrypt.Conscrypt; @@ -145,6 +146,7 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; +import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; import eu.siacs.conversations.xmpp.mam.MamReference; import eu.siacs.conversations.xmpp.pep.Avatar; @@ -209,7 +211,7 @@ public class XmppConnectionService extends Service { private AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false); private AtomicBoolean mForceForegroundService = new AtomicBoolean(false); private AtomicBoolean mForceDuringOnCreate = new AtomicBoolean(false); - private AtomicReference ongoingCall = new AtomicReference<>(); + private AtomicReference ongoingCall = new AtomicReference<>(); private OnMessagePacketReceived mMessageParser = new MessageParser(this); private OnPresencePacketReceived mPresenceParser = new PresenceParser(this); private IqParser mIqParser = new IqParser(this); @@ -1228,24 +1230,23 @@ public class XmppConnectionService extends Service { toggleForegroundService(false); } - public void setOngoingCall(AbstractJingleConnection.Id id) { - ongoingCall.set(id); + public void setOngoingCall(AbstractJingleConnection.Id id, Set media) { + ongoingCall.set(new OngoingCall(id, media)); toggleForegroundService(false); } - public void removeOngoingCall(AbstractJingleConnection.Id id) { - if (ongoingCall.compareAndSet(id, null)) { - toggleForegroundService(false); - } + public void removeOngoingCall() { + ongoingCall.set(null); + toggleForegroundService(false); } private void toggleForegroundService(boolean force) { final boolean status; - final AbstractJingleConnection.Id ongoing = ongoingCall.get(); + final OngoingCall ongoing = ongoingCall.get(); if (force || mForceDuringOnCreate.get() || mForceForegroundService.get() || ongoing != null || (Compatibility.keepForegroundService(this) && hasEnabledAccounts())) { final Notification notification; if (ongoing != null) { - notification = this.mNotificationService.getOngoingCallNotification(ongoing); + notification = this.mNotificationService.getOngoingCallNotification(ongoing.id, ongoing.media); startForeground(NotificationService.ONGOING_CALL_NOTIFICATION_ID, notification); mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID); } else { @@ -4753,4 +4754,27 @@ public class XmppConnectionService extends Service { onStartCommand(intent, 0, 0); } } + + public static class OngoingCall { + private final AbstractJingleConnection.Id id; + private final Set media; + + public OngoingCall(AbstractJingleConnection.Id id, Set media) { + this.id = id; + this.media = media; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OngoingCall that = (OngoingCall) o; + return Objects.equal(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } + } } diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index a920dfa51..be8c21862 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -47,6 +47,12 @@ import static java.util.Arrays.asList; public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate { + private static final List END_CARD = Arrays.asList( + RtpEndUserState.APPLICATION_ERROR, + RtpEndUserState.DECLINED_OR_BUSY, + RtpEndUserState.CONNECTIVITY_ERROR + ); + private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session"; private static final int REQUEST_ACCEPT_CALL = 0x1111; @@ -116,23 +122,27 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe permissions = ImmutableList.of(Manifest.permission.RECORD_AUDIO); } if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) { - //TODO like wise the propose; we might just wait here for the audio manager to come up putScreenInCallMode(); requireRtpConnection().acceptCall(); } } - @SuppressLint("WakelockTimeout") private void putScreenInCallMode() { - //TODO for video calls we actually do want to keep the screen on - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - final JingleRtpConnection rtpConnection = rtpConnectionReference != null ? rtpConnectionReference.get() : null; - final AppRTCAudioManager audioManager = rtpConnection == null ? null : rtpConnection.getAudioManager(); - if (audioManager == null || audioManager.getSelectedAudioDevice() == AppRTCAudioManager.AudioDevice.EARPIECE) { - acquireProximityWakeLock(); + putScreenInCallMode(requireRtpConnection().getMedia()); + } + + private void putScreenInCallMode(final Set media) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + if (!media.contains(Media.VIDEO)) { + final JingleRtpConnection rtpConnection = rtpConnectionReference != null ? rtpConnectionReference.get() : null; + final AppRTCAudioManager audioManager = rtpConnection == null ? null : rtpConnection.getAudioManager(); + if (audioManager == null || audioManager.getSelectedAudioDevice() == AppRTCAudioManager.AudioDevice.EARPIECE) { + acquireProximityWakeLock(); + } } } + @SuppressLint("WakelockTimeout") private void acquireProximityWakeLock() { final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); if (powerManager == null) { @@ -234,8 +244,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void proposeJingleRtpSession(final Account account, final Jid with, final Set media) { xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with, media); - //TODO maybe we don’t want to acquire a wake lock just yet and wait for audio manager to discover what speaker we are using - putScreenInCallMode(); + putScreenInCallMode(media); } @Override @@ -570,10 +579,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) { - if (Arrays.asList(RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.DECLINED_OR_BUSY).contains(state)) { - releaseProximityWakeLock(); - } Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")"); + if (END_CARD.contains(state)) { + Log.d(Config.LOGTAG,"end card reached"); + releaseProximityWakeLock(); + runOnUiThread(()-> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)); + } if (with.isBareJid()) { updateRtpSessionProposalState(account, with, state); return; @@ -588,7 +599,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe if (state == RtpEndUserState.ENDED) { finish(); return; - } else if (asList(RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.CONNECTIVITY_ERROR).contains(state)) { + } else if (END_CARD.contains(state)) { //todo remember if we were video resetIntent(account, with, state); } @@ -608,14 +619,22 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { Log.d(Config.LOGTAG, "onAudioDeviceChanged in activity: selected:" + selectedAudioDevice + ", available:" + availableAudioDevices); try { - if (requireRtpConnection().getEndUserState() == RtpEndUserState.CONNECTED && !getMedia().contains(Media.VIDEO)) { + if (getMedia().contains(Media.VIDEO)) { + Log.d(Config.LOGTAG,"nothing to do; in video mode"); + return; + } + final RtpEndUserState endUserState = requireRtpConnection().getEndUserState(); + if (endUserState == RtpEndUserState.CONNECTED) { final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager(); updateInCallButtonConfigurationSpeaker( audioManager.getSelectedAudioDevice(), audioManager.getAudioDevices().size() ); + } else if (END_CARD.contains(endUserState)) { + Log.d(Config.LOGTAG,"onAudioDeviceChanged() nothing to do because end card has been reached"); + } else { + putProximityWakeLockInProperState(); } - putProximityWakeLockInProperState(); } catch (IllegalStateException e) { Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed"); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 976e3ad3e..5e2446788 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -289,9 +289,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } else { target = State.SESSION_INITIALIZED; } - if (transition(target)) { + if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) { respondOk(jinglePacket); - this.initiatorRtpContentMap = contentMap; if (target == State.SESSION_INITIALIZED_PRE_APPROVED) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate"); sendSessionAccept(); @@ -323,6 +322,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); return; } + //TODO check that session accept content media matched ours Log.d(Config.LOGTAG, "processing session-accept with " + contentMap.contents.size() + " contents"); if (transition(State.SESSION_ACCEPTED)) { respondOk(jinglePacket); @@ -500,7 +500,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); if (originatedFromMyself) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": saw proposal from mysql. ignoring"); - } else if (isInState(State.NULL)) { + } else if (transition(State.PROPOSED, () -> { final Collection descriptions = Collections2.transform( Collections2.filter(propose.getDescriptions(), d -> d instanceof RtpDescription), input -> (RtpDescription) input @@ -509,7 +509,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web Preconditions.checkState(!media.contains(Media.UNKNOWN), "RTP descriptions contain unknown media"); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session proposal from " + from + " for " + media); this.proposedMedia = Sets.newHashSet(media); - transitionOrThrow(State.PROPOSED); + })) { if (serverMsgId != null) { this.message.setServerMsgId(serverMsgId); } @@ -720,10 +720,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return RtpEndUserState.CONNECTING; } else if (state == PeerConnection.PeerConnectionState.CLOSED) { return RtpEndUserState.ENDING_CALL; - } else if (state == PeerConnection.PeerConnectionState.FAILED) { - return RtpEndUserState.CONNECTIVITY_ERROR; } else { - return RtpEndUserState.ENDING_CALL; + return RtpEndUserState.CONNECTIVITY_ERROR; } case REJECTED: case TERMINATED_DECLINED_OR_BUSY: @@ -876,10 +874,17 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return Arrays.asList(state).contains(this.state); } - private synchronized boolean transition(final State target) { + private boolean transition(final State target) { + return transition(target, null); + } + + private synchronized boolean transition(final State target, final Runnable runnable) { final Collection validTransitions = VALID_TRANSITIONS.get(this.state); if (validTransitions != null && validTransitions.contains(target)) { this.state = target; + if (runnable != null) { + runnable.run(); + } Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into " + target); updateEndUserState(); updateOngoingCallNotification(); @@ -909,7 +914,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (newState == PeerConnection.PeerConnectionState.CONNECTED && this.rtpConnectionStarted == 0) { this.rtpConnectionStarted = SystemClock.elapsedRealtime(); } - if (newState == PeerConnection.PeerConnectionState.FAILED) { + if (Arrays.asList(PeerConnection.PeerConnectionState.FAILED, PeerConnection.PeerConnectionState.DISCONNECTED).contains(newState)) { if (TERMINATED.contains(this.state)) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); return; @@ -949,9 +954,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void updateOngoingCallNotification() { if (STATES_SHOWING_ONGOING_CALL.contains(this.state)) { - xmppConnectionService.setOngoingCall(id); + xmppConnectionService.setOngoingCall(id, getMedia()); } else { - xmppConnectionService.removeOngoingCall(id); + xmppConnectionService.removeOngoingCall(); } } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index bcb757cac..6cdab665b 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -903,6 +903,7 @@ Application failure Hang up Ongoing call + Ongoing video call Disable Tor to make calls Incoming call Incoming call · %s From ec6bcec849769b8b24359f82ee09925c5c0cf096 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 16 Apr 2020 00:08:58 +0200 Subject: [PATCH 111/182] use different aspect ratio for landscape --- .../conversations/ui/RtpSessionActivity.java | 20 +++++- .../xmpp/jingle/JingleRtpConnection.java | 3 +- src/main/res/layout/activity_rtp_session.xml | 10 +-- src/main/res/values-land/dimens.xml | 4 ++ src/main/res/values/dimens.xml | 62 ++++++++++--------- 5 files changed, 62 insertions(+), 37 deletions(-) create mode 100644 src/main/res/values-land/dimens.xml diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index be8c21862..05fcf035b 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -304,7 +304,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe putScreenInCallMode(); } binding.with.setText(getWith().getDisplayName()); - updateVideoViews(); + updateVideoViews(currentState); updateStateDisplay(currentState); updateButtonConfiguration(currentState); } @@ -508,7 +508,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.binding.inCallActionLeft.setVisibility(View.VISIBLE); } - private void updateVideoViews() { + private void updateVideoViews(final RtpEndUserState state) { + if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) { + binding.localVideo.setVisibility(View.GONE); + binding.remoteVideo.setVisibility(View.GONE); + binding.appBarLayout.setVisibility(View.VISIBLE); + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + return; + } final Optional localVideoTrack = requireRtpConnection().geLocalVideoTrack(); if (localVideoTrack.isPresent()) { ensureSurfaceViewRendererIsSetup(binding.localVideo); @@ -523,7 +530,10 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe if (remoteVideoTrack.isPresent()) { ensureSurfaceViewRendererIsSetup(binding.remoteVideo); remoteVideoTrack.get().addSink(binding.remoteVideo); + binding.appBarLayout.setVisibility(View.GONE); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); } else { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); binding.remoteVideo.setVisibility(View.GONE); } } @@ -590,6 +600,10 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe return; } if (this.rtpConnectionReference == null) { + if (END_CARD.contains(state)) { + Log.d(Config.LOGTAG,"not reinitializing session"); + return; + } //this happens when going from proposed session to actual session reInitializeActivityWithRunningRapSession(account, with, sessionId); return; @@ -607,7 +621,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe updateStateDisplay(state); updateButtonConfiguration(state); //TODO kill video when in final or error stages - updateVideoViews(); + updateVideoViews(state); }); } else { Log.d(Config.LOGTAG, "received update for other rtp session"); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 5e2446788..7682c1b91 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -910,7 +910,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web @Override public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); - updateEndUserState(); if (newState == PeerConnection.PeerConnectionState.CONNECTED && this.rtpConnectionStarted == 0) { this.rtpConnectionStarted = SystemClock.elapsedRealtime(); } @@ -920,6 +919,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return; } sendSessionTerminate(Reason.CONNECTIVITY_ERROR); + } else { + updateEndUserState(); } } diff --git a/src/main/res/layout/activity_rtp_session.xml b/src/main/res/layout/activity_rtp_session.xml index a9b279334..f9eba40b0 100644 --- a/src/main/res/layout/activity_rtp_session.xml +++ b/src/main/res/layout/activity_rtp_session.xml @@ -11,7 +11,8 @@ + android:layout_height="wrap_content" + android:visibility="visible"> + 96dp + 128dp + \ No newline at end of file diff --git a/src/main/res/values/dimens.xml b/src/main/res/values/dimens.xml index 9eb4b102b..ae22105c8 100644 --- a/src/main/res/values/dimens.xml +++ b/src/main/res/values/dimens.xml @@ -1,38 +1,42 @@ - - 8dp - 8dp - 16dp - 8dp - 8dp - 12dp - 48dp - 11sp - 224dp - 16dp + + 8dp + 8dp + 16dp + 8dp + 8dp + 12dp + 48dp + + 11sp + 224dp + 16dp - 80dp - 56dp - 96dp - 4dp + 80dp + 56dp + 96dp + 4dp - 8dp - 96dp - 32dp - 48dp - 56dp - 56dp + 8dp + 96dp + 32dp + 48dp + 56dp + 56dp - 4dp - 4dp + 4dp + 4dp - - 4dp - 8dp + + 4dp + 8dp - 1200dp + 1200dp - 0.12 + 0.12 - 256dp + 256dp + + 128dp + 96dp From b95d406e61fb84bfe5019b22792434845ca634c5 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 16 Apr 2020 08:20:13 +0200 Subject: [PATCH 112/182] use more approriate reason when failing because of parse errors --- .../xmpp/jingle/JingleRtpConnection.java | 19 +++++++++++++++---- .../xmpp/jingle/RtpContentMap.java | 19 +++++++++++++++---- .../xmpp/jingle/stanzas/Reason.java | 14 ++++++++++++++ 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 7682c1b91..f2a877d04 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -266,7 +266,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web contentMap.requireDTLSFingerprint(); } catch (final IllegalArgumentException | IllegalStateException | NullPointerException e) { respondOk(jinglePacket); - sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); + sendSessionTerminate(Reason.of(e), e.getMessage()); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); return; } @@ -315,14 +315,22 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web contentMap = RtpContentMap.of(jinglePacket); contentMap.requireContentDescriptions(); contentMap.requireDTLSFingerprint(); - } catch (IllegalArgumentException | IllegalStateException | NullPointerException e) { + } catch (final IllegalArgumentException | IllegalStateException | NullPointerException e) { respondOk(jinglePacket); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents in session-accept", e); webRTCWrapper.close(); - sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); + sendSessionTerminate(Reason.of(e), e.getMessage()); + return; + } + final Set initiatorMedia = this.initiatorRtpContentMap.getMedia(); + if (!initiatorMedia.equals(contentMap.getMedia())) { + sendSessionTerminate(Reason.SECURITY_ERROR, String.format( + "Your session-included included media %s but our session-initiate was %s", + this.proposedMedia, + contentMap.getMedia() + )); return; } - //TODO check that session accept content media matched ours Log.d(Config.LOGTAG, "processing session-accept with " + contentMap.contents.size() + " contents"); if (transition(State.SESSION_ACCEPTED)) { respondOk(jinglePacket); @@ -913,6 +921,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (newState == PeerConnection.PeerConnectionState.CONNECTED && this.rtpConnectionStarted == 0) { this.rtpConnectionStarted = SystemClock.elapsedRealtime(); } + //TODO 'DISCONNECTED' might be an opportunity to renew the offer and send a transport-replace + //TODO exact syntax is yet to be determined but transport-replace sounds like the most reasonable + //as there is no content-replace if (Arrays.asList(PeerConnection.PeerConnectionState.FAILED, PeerConnection.PeerConnectionState.DISCONNECTED).contains(newState)) { if (TERMINATED.contains(this.state)) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index db9902874..da48d017f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -23,6 +23,7 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; public class RtpContentMap { @@ -130,14 +131,12 @@ public class RtpContentMap { rtpDescription = (RtpDescription) description; } else { Log.d(Config.LOGTAG, "description was " + description); - //todo throw unsupported application - throw new IllegalArgumentException("Content does not contain RtpDescription"); + throw new UnsupportedApplicationException("Content does not contain rtp description"); } if (transportInfo instanceof IceUdpTransportInfo) { iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo; } else { - //TODO throw UNSUPPORTED_TRANSPORT exception - throw new IllegalArgumentException("Content does not contain ICE-UDP transport"); + throw new UnsupportedTransportException("Content does not contain ICE-UDP transport"); } return new DescriptionTransport(rtpDescription, iceUdpTransportInfo); } @@ -158,4 +157,16 @@ public class RtpContentMap { })); } } + + public static class UnsupportedApplicationException extends IllegalArgumentException { + UnsupportedApplicationException(String message) { + super(message); + } + } + + public static class UnsupportedTransportException extends IllegalArgumentException { + UnsupportedTransportException(String message) { + super(message); + } + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java index 8fee7a552..9e4c8d95d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java @@ -4,6 +4,8 @@ import android.support.annotation.NonNull; import com.google.common.base.CaseFormat; +import eu.siacs.conversations.xmpp.jingle.RtpContentMap; + public enum Reason { ALTERNATIVE_SESSION, BUSY, @@ -37,4 +39,16 @@ public enum Reason { public String toString() { return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, super.toString()); } + + public static Reason of(final RuntimeException e) { + if (e instanceof SecurityException) { + return SECURITY_ERROR; + } else if (e instanceof RtpContentMap.UnsupportedTransportException) { + return UNSUPPORTED_TRANSPORTS; + } else if (e instanceof RtpContentMap.UnsupportedApplicationException) { + return UNSUPPORTED_APPLICATIONS; + } else { + return FAILED_APPLICATION; + } + } } \ No newline at end of file From 45d5d1f6353cea845ab6571ca335205b54f2062a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 16 Apr 2020 09:03:39 +0200 Subject: [PATCH 113/182] capture in ~1920 resolution when available --- .../xmpp/jingle/WebRTCWrapper.java | 57 ++++++++++++++++--- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 70c4f8e7b..5bb05675e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -17,6 +17,7 @@ import org.webrtc.AudioSource; import org.webrtc.AudioTrack; import org.webrtc.Camera1Enumerator; import org.webrtc.Camera2Enumerator; +import org.webrtc.CameraEnumerationAndroid; import org.webrtc.CameraEnumerator; import org.webrtc.CameraVideoCapturer; import org.webrtc.CandidatePairChangeEvent; @@ -36,6 +37,9 @@ import org.webrtc.SurfaceTextureHelper; import org.webrtc.VideoSource; import org.webrtc.VideoTrack; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Set; @@ -47,6 +51,9 @@ import eu.siacs.conversations.services.AppRTCAudioManager; public class WebRTCWrapper { + private static final int CAPTURING_RESOLUTION = 1920; + private static final int CAPTURING_MAX_FRAME_RATE = 30; + private final EventCallback eventCallback; private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = new AppRTCAudioManager.AudioManagerEvents() { @Override @@ -139,7 +146,7 @@ public class WebRTCWrapper { private AppRTCAudioManager appRTCAudioManager = null; private Context context = null; private EglBase eglBase = null; - private Optional optionalCapturer; + private Optional optionalCapturer; public WebRTCWrapper(final EventCallback eventCallback) { this.eventCallback = eventCallback; @@ -173,11 +180,13 @@ public class WebRTCWrapper { this.optionalCapturer = media.contains(Media.VIDEO) ? getVideoCapturer() : Optional.absent(); if (this.optionalCapturer.isPresent()) { - final CameraVideoCapturer capturer = this.optionalCapturer.get(); + final CapturerChoice choice = this.optionalCapturer.get(); + final CameraVideoCapturer capturer = choice.cameraVideoCapturer; final VideoSource videoSource = peerConnectionFactory.createVideoSource(false); SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("webrtc", eglBase.getEglBaseContext()); capturer.initialize(surfaceTextureHelper, requireContext(), videoSource.getCapturerObserver()); - capturer.startCapture(320, 240, 30); + Log.d(Config.LOGTAG, String.format("start capturing at %dx%d@%d", choice.captureFormat.width, choice.captureFormat.height, choice.getFrameRate())); + capturer.startCapture(choice.captureFormat.width, choice.captureFormat.height, choice.getFrameRate()); this.localVideoTrack = peerConnectionFactory.createVideoTrack("my-video-track", videoSource); @@ -205,7 +214,7 @@ public class WebRTCWrapper { public void close() { final PeerConnection peerConnection = this.peerConnection; - final Optional optionalCapturer = this.optionalCapturer; + final Optional optionalCapturer = this.optionalCapturer; final AppRTCAudioManager audioManager = this.appRTCAudioManager; final EglBase eglBase = this.eglBase; if (peerConnection != null) { @@ -218,7 +227,7 @@ public class WebRTCWrapper { this.remoteVideoTrack = null; if (optionalCapturer != null && optionalCapturer.isPresent()) { try { - optionalCapturer.get().stopCapture(); + optionalCapturer.get().cameraVideoCapturer.stopCapture(); } catch (InterruptedException e) { Log.e(Config.LOGTAG, "unable to stop capturing"); } @@ -358,26 +367,42 @@ public class WebRTCWrapper { } } - private Optional getVideoCapturer() { + private Optional getVideoCapturer() { final CameraEnumerator enumerator = getCameraEnumerator(); final String[] deviceNames = enumerator.getDeviceNames(); for (final String deviceName : deviceNames) { if (enumerator.isFrontFacing(deviceName)) { - return Optional.fromNullable(enumerator.createCapturer(deviceName, null)); + return Optional.fromNullable(of(enumerator, deviceName)); } } if (deviceNames.length == 0) { return Optional.absent(); } else { - return Optional.fromNullable(enumerator.createCapturer(deviceNames[0], null)); + return Optional.fromNullable(of(enumerator, deviceNames[0])); } } + @Nullable + private static CapturerChoice of(CameraEnumerator enumerator, final String deviceName) { + final CameraVideoCapturer capturer = enumerator.createCapturer(deviceName, null); + if (capturer == null) { + return null; + } + final ArrayList choices = new ArrayList<>(enumerator.getSupportedFormats(deviceName)); + Collections.sort(choices, (a, b) -> b.width - a.width); + for (final CameraEnumerationAndroid.CaptureFormat captureFormat : choices) { + if (captureFormat.width <= CAPTURING_RESOLUTION) { + return new CapturerChoice(capturer, captureFormat); + } + } + return null; + } + public PeerConnection.PeerConnectionState getState() { return requirePeerConnection().connectionState(); } - public EglBase.Context getEglBaseContext() { + EglBase.Context getEglBaseContext() { return this.eglBase.getEglBaseContext(); } @@ -452,4 +477,18 @@ public class WebRTCWrapper { super(message); } } + + private static class CapturerChoice { + private final CameraVideoCapturer cameraVideoCapturer; + private final CameraEnumerationAndroid.CaptureFormat captureFormat; + + public CapturerChoice(CameraVideoCapturer cameraVideoCapturer, CameraEnumerationAndroid.CaptureFormat captureFormat) { + this.cameraVideoCapturer = cameraVideoCapturer; + this.captureFormat = captureFormat; + } + + public int getFrameRate() { + return Math.max(captureFormat.framerate.min, Math.min(CAPTURING_MAX_FRAME_RATE, captureFormat.framerate.max)); + } + } } From 4558b9a7b0d0286a9d2b4436ac5f64924ee70a11 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 16 Apr 2020 10:03:53 +0200 Subject: [PATCH 114/182] select proper media for retry --- .../conversations/ui/RtpSessionActivity.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 05fcf035b..f1d01afc7 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -60,6 +60,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe public static final String EXTRA_WITH = "with"; public static final String EXTRA_SESSION_ID = "session_id"; public static final String EXTRA_LAST_REPORTED_STATE = "last_reported_state"; + public static final String EXTRA_LAST_ACTION = "last_action"; public static final String ACTION_ACCEPT_CALL = "action_accept_call"; public static final String ACTION_MAKE_VOICE_CALL = "action_make_voice_call"; @@ -565,8 +566,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final Intent intent = getIntent(); final Account account = extractAccount(intent); final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH)); + final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION); + final String action = intent.getAction(); + final Set media = actionToMedia(lastAction == null ? action : lastAction); this.rtpConnectionReference = null; - proposeJingleRtpSession(account, with, ImmutableSet.of(Media.AUDIO, Media.VIDEO)); + proposeJingleRtpSession(account, with, media); } private void exit(View view) { @@ -614,13 +618,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe finish(); return; } else if (END_CARD.contains(state)) { - //todo remember if we were video - resetIntent(account, with, state); + resetIntent(account, with, state, requireRtpConnection().getMedia()); } runOnUiThread(() -> { updateStateDisplay(state); updateButtonConfiguration(state); - //TODO kill video when in final or error stages updateVideoViews(state); }); } else { @@ -665,7 +667,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe updateStateDisplay(state); updateButtonConfiguration(state); }); - resetIntent(account, with, state); + resetIntent(account, with, state, actionToMedia(currentIntent.getAction())); } } @@ -675,11 +677,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe setIntent(intent); } - private void resetIntent(final Account account, Jid with, final RtpEndUserState state) { + private void resetIntent(final Account account, Jid with, final RtpEndUserState state, final Set media) { 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_LAST_REPORTED_STATE, state.toString()); + intent.putExtra(EXTRA_LAST_ACTION, media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL); setIntent(intent); } } From 0c4f0c074deb0f03e28b4ffbddb19140f195323a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 16 Apr 2020 10:51:34 +0200 Subject: [PATCH 115/182] improve busy behaviour with multiple devices --- .../xmpp/jingle/JingleConnectionManager.java | 51 +++++++++++++++++-- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index a50dcd70f..886343be6 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.xmpp.jingle; +import android.os.SystemClock; import android.util.Base64; import android.util.Log; @@ -26,6 +27,7 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; @@ -163,8 +165,21 @@ public class JingleConnectionManager extends AbstractConnectionManager { } if (fromSelf) { + if ("proceed".equals(message.getName())) { + final Conversation c = mXmppConnectionService.findOrCreateConversation(account, id.with, false, false); + final Message previousBusy = c.findRtpSession(sessionId, Message.STATUS_RECEIVED); + if (previousBusy != null) { + previousBusy.setBody(new RtpSessionStatus(true, 0).toString()); + if (serverMsgId != null) { + previousBusy.setServerMsgId(serverMsgId); + } + previousBusy.setTime(timestamp); + mXmppConnectionService.updateMessage(previousBusy, true); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": updated previous busy because call got picked up by another device"); + return; + } + } Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignore jingle message from self"); - //TODO proceed from self should maybe dedup/change the busy that we set earlier return; } @@ -182,9 +197,15 @@ public class JingleConnectionManager extends AbstractConnectionManager { return; } if (isBusy()) { //TODO only if no other devices are active - //TODO create busy - final MessagePacket reject = mXmppConnectionService.getMessageGenerator().sessionReject(from, sessionId); - mXmppConnectionService.sendMessagePacket(account, reject); + writeLogMissedIncoming(account, id.with.asBareJid(), id.sessionId, serverMsgId, timestamp); + final int activeDevices = account.countPresences(); + Log.d(Config.LOGTAG, "active devices: " + activeDevices); + if (activeDevices == 0) { + final MessagePacket reject = mXmppConnectionService.getMessageGenerator().sessionReject(from, sessionId); + mXmppConnectionService.sendMessagePacket(account, reject); + } else { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring proposal because busy on this device but there are other devices"); + } } else { final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, from); this.connections.put(id, rtpConnection); @@ -246,6 +267,26 @@ public class JingleConnectionManager extends AbstractConnectionManager { Message.TYPE_RTP_SESSION, sessionId ); + message.setBody(new RtpSessionStatus(false, 0).toString()); + message.setServerMsgId(serverMsgId); + message.setTime(timestamp); + writeMessage(message); + } + + private void writeLogMissedIncoming(final Account account, Jid with, final String sessionId, String serverMsgId, long timestamp) { + final Conversation conversation = mXmppConnectionService.findOrCreateConversation( + account, + with.asBareJid(), + false, + false + ); + final Message message = new Message( + conversation, + Message.STATUS_RECEIVED, + Message.TYPE_RTP_SESSION, + sessionId + ); + message.setBody(new RtpSessionStatus(false, 0).toString()); message.setServerMsgId(serverMsgId); message.setTime(timestamp); writeMessage(message); @@ -490,8 +531,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { public static class RtpSessionProposal { public final Jid with; public final String sessionId; - private final Account account; public final Set media; + private final Account account; private RtpSessionProposal(Account account, Jid with, String sessionId) { this(account, with, sessionId, Collections.emptySet()); From 21e412ef6f69996304df8e4a6ca5baf5f21a11e8 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 16 Apr 2020 11:02:42 +0200 Subject: [PATCH 116/182] only show remote video when connected --- .../eu/siacs/conversations/ui/RtpSessionActivity.java | 9 +++++++-- .../xmpp/jingle/JingleConnectionManager.java | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index f1d01afc7..8883ec9fa 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -531,8 +531,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe if (remoteVideoTrack.isPresent()) { ensureSurfaceViewRendererIsSetup(binding.remoteVideo); remoteVideoTrack.get().addSink(binding.remoteVideo); - binding.appBarLayout.setVisibility(View.GONE); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + if (state == RtpEndUserState.CONNECTED) { + binding.appBarLayout.setVisibility(View.GONE); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } else { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + binding.remoteVideo.setVisibility(View.GONE); + } } else { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); binding.remoteVideo.setVisibility(View.GONE); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 886343be6..4ec48b35b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -196,7 +196,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": encountered unknown media in session proposal. " + propose); return; } - if (isBusy()) { //TODO only if no other devices are active + if (isBusy()) { writeLogMissedIncoming(account, id.with.asBareJid(), id.sessionId, serverMsgId, timestamp); final int activeDevices = account.countPresences(); Log.d(Config.LOGTAG, "active devices: " + activeDevices); From ea2ed85ed7608e7a9f2b8027259dad6dac6cf581 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 16 Apr 2020 17:38:05 +0200 Subject: [PATCH 117/182] support picture in picture for video calls --- src/main/AndroidManifest.xml | 4 +- .../conversations/ui/RtpSessionActivity.java | 108 ++++++++++++------ .../drawable-hdpi/ic_warning_white_48dp.png | Bin 0 -> 714 bytes .../drawable-mdpi/ic_warning_white_48dp.png | Bin 0 -> 364 bytes .../drawable-xhdpi/ic_warning_white_48dp.png | Bin 0 -> 590 bytes .../drawable-xxhdpi/ic_warning_white_48dp.png | Bin 0 -> 843 bytes .../ic_warning_white_48dp.png | Bin 0 -> 1044 bytes src/main/res/layout/activity_rtp_session.xml | 20 +++- 8 files changed, 95 insertions(+), 37 deletions(-) create mode 100644 src/main/res/drawable-hdpi/ic_warning_white_48dp.png create mode 100644 src/main/res/drawable-mdpi/ic_warning_white_48dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_warning_white_48dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_warning_white_48dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_warning_white_48dp.png diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 8b0cbd6f1..29356fb21 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -292,7 +292,9 @@ android:name=".ui.ChannelDiscoveryActivity" android:label="@string/discover_channels" /> + android:supportsPictureInPicture="true" + android:launchMode="singleTask" + android:autoRemoveFromRecents="true"/> diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 8883ec9fa..cff788e04 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -2,6 +2,7 @@ package eu.siacs.conversations.ui; import android.Manifest; import android.annotation.SuppressLint; +import android.app.PictureInPictureParams; import android.content.Context; import android.content.Intent; import android.databinding.DataBindingUtil; @@ -11,6 +12,7 @@ import android.os.PowerManager; import android.support.annotation.NonNull; import android.support.annotation.StringRes; import android.util.Log; +import android.util.Rational; import android.view.View; import android.view.WindowManager; import android.widget.Toast; @@ -19,6 +21,7 @@ import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import org.webrtc.PeerConnection; import org.webrtc.RendererCommon; import org.webrtc.SurfaceViewRenderer; import org.webrtc.VideoTrack; @@ -47,31 +50,33 @@ import static java.util.Arrays.asList; public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate { + public static final String EXTRA_WITH = "with"; + public static final String EXTRA_SESSION_ID = "session_id"; + public static final String EXTRA_LAST_REPORTED_STATE = "last_reported_state"; + public static final String EXTRA_LAST_ACTION = "last_action"; + public static final String ACTION_ACCEPT_CALL = "action_accept_call"; + public static final String ACTION_MAKE_VOICE_CALL = "action_make_voice_call"; + public static final String ACTION_MAKE_VIDEO_CALL = "action_make_video_call"; private static final List END_CARD = Arrays.asList( RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.CONNECTIVITY_ERROR ); - private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session"; - private static final int REQUEST_ACCEPT_CALL = 0x1111; - - public static final String EXTRA_WITH = "with"; - public static final String EXTRA_SESSION_ID = "session_id"; - public static final String EXTRA_LAST_REPORTED_STATE = "last_reported_state"; - public static final String EXTRA_LAST_ACTION = "last_action"; - - public static final String ACTION_ACCEPT_CALL = "action_accept_call"; - public static final String ACTION_MAKE_VOICE_CALL = "action_make_voice_call"; - public static final String ACTION_MAKE_VIDEO_CALL = "action_make_video_call"; - - private WeakReference rtpConnectionReference; private ActivityRtpSessionBinding binding; private PowerManager.WakeLock mProximityWakeLock; + private static Set actionToMedia(final String action) { + if (ACTION_MAKE_VIDEO_CALL.equals(action)) { + return ImmutableSet.of(Media.AUDIO, Media.VIDEO); + } else { + return ImmutableSet.of(Media.AUDIO); + } + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -235,14 +240,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } - private static Set actionToMedia(final String action) { - if (ACTION_MAKE_VIDEO_CALL.equals(action)) { - return ImmutableSet.of(Media.AUDIO, Media.VIDEO); - } else { - return ImmutableSet.of(Media.AUDIO); - } - } - private void proposeJingleRtpSession(final Account account, final Jid with, final Set media) { xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with, media); putScreenInCallMode(media); @@ -284,6 +281,29 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe super.onBackPressed(); } + @Override + public void onUserLeaveHint() { + Log.d(Config.LOGTAG, "user leave hint"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (shouldBePictureInPicture()) { + enterPictureInPictureMode( + new PictureInPictureParams.Builder() + .setAspectRatio(new Rational(10, 16)) + .build() + ); + } + } + + } + + private boolean shouldBePictureInPicture() { + try { + final JingleRtpConnection rtpConnection = requireRtpConnection(); + return rtpConnection.getMedia().contains(Media.VIDEO) && rtpConnection.getEndUserState() == RtpEndUserState.CONNECTED; + } catch (IllegalStateException e) { + return false; + } + } private void initializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) { final WeakReference reference = xmppConnectionService.getJingleConnectionManager() @@ -380,7 +400,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @SuppressLint("RestrictedApi") private void updateButtonConfiguration(final RtpEndUserState state) { - if (state == RtpEndUserState.INCOMING_CALL) { + if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) { + this.binding.rejectCall.setVisibility(View.INVISIBLE); + this.binding.endCall.setVisibility(View.INVISIBLE); + this.binding.acceptCall.setVisibility(View.INVISIBLE); + } else if (state == RtpEndUserState.INCOMING_CALL) { this.binding.rejectCall.setOnClickListener(this::rejectCall); this.binding.rejectCall.setImageResource(R.drawable.ic_call_end_white_48dp); this.binding.rejectCall.setVisibility(View.VISIBLE); @@ -388,10 +412,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.binding.acceptCall.setOnClickListener(this::acceptCall); this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp); this.binding.acceptCall.setVisibility(View.VISIBLE); - } else if (state == RtpEndUserState.ENDING_CALL) { - this.binding.rejectCall.setVisibility(View.INVISIBLE); - this.binding.endCall.setVisibility(View.INVISIBLE); - this.binding.acceptCall.setVisibility(View.INVISIBLE); } else if (state == RtpEndUserState.DECLINED_OR_BUSY) { this.binding.rejectCall.setVisibility(View.INVISIBLE); this.binding.endCall.setOnClickListener(this::exit); @@ -416,13 +436,21 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe updateInCallButtonConfiguration(state); } + private boolean isPictureInPicture() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return isInPictureInPictureMode(); + } else { + return false; + } + } + private void updateInCallButtonConfiguration() { updateInCallButtonConfiguration(requireRtpConnection().getEndUserState()); } @SuppressLint("RestrictedApi") private void updateInCallButtonConfiguration(final RtpEndUserState state) { - if (state == RtpEndUserState.CONNECTED) { + if (state == RtpEndUserState.CONNECTED && !isPictureInPicture()) { if (getMedia().contains(Media.VIDEO)) { updateInCallButtonConfigurationVideo(requireRtpConnection().isVideoEnabled()); } else { @@ -513,12 +541,23 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) { binding.localVideo.setVisibility(View.GONE); binding.remoteVideo.setVisibility(View.GONE); - binding.appBarLayout.setVisibility(View.VISIBLE); + if (isPictureInPicture()) { + binding.appBarLayout.setVisibility(View.GONE); + binding.pipPlaceholder.setVisibility(View.VISIBLE); + if (state == RtpEndUserState.APPLICATION_ERROR || state == RtpEndUserState.CONNECTIVITY_ERROR) { + binding.pipWarning.setVisibility(View.VISIBLE); + } else { + binding.pipWarning.setVisibility(View.GONE); + } + } else { + binding.appBarLayout.setVisibility(View.VISIBLE); + binding.pipPlaceholder.setVisibility(View.GONE); + } getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); return; } final Optional localVideoTrack = requireRtpConnection().geLocalVideoTrack(); - if (localVideoTrack.isPresent()) { + if (localVideoTrack.isPresent() && !isPictureInPicture()) { ensureSurfaceViewRendererIsSetup(binding.localVideo); //paint local view over remote view binding.localVideo.setZOrderMediaOverlay(true); @@ -600,9 +639,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) { Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")"); if (END_CARD.contains(state)) { - Log.d(Config.LOGTAG,"end card reached"); + Log.d(Config.LOGTAG, "end card reached"); releaseProximityWakeLock(); - runOnUiThread(()-> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)); + runOnUiThread(() -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)); } if (with.isBareJid()) { updateRtpSessionProposalState(account, with, state); @@ -610,7 +649,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } if (this.rtpConnectionReference == null) { if (END_CARD.contains(state)) { - Log.d(Config.LOGTAG,"not reinitializing session"); + Log.d(Config.LOGTAG, "not reinitializing session"); return; } //this happens when going from proposed session to actual session @@ -620,6 +659,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final AbstractJingleConnection.Id id = requireRtpConnection().getId(); if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) { if (state == RtpEndUserState.ENDED) { + resetIntent(account, with, state, requireRtpConnection().getMedia()); finish(); return; } else if (END_CARD.contains(state)) { @@ -641,7 +681,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe Log.d(Config.LOGTAG, "onAudioDeviceChanged in activity: selected:" + selectedAudioDevice + ", available:" + availableAudioDevices); try { if (getMedia().contains(Media.VIDEO)) { - Log.d(Config.LOGTAG,"nothing to do; in video mode"); + Log.d(Config.LOGTAG, "nothing to do; in video mode"); return; } final RtpEndUserState endUserState = requireRtpConnection().getEndUserState(); @@ -652,7 +692,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe audioManager.getAudioDevices().size() ); } else if (END_CARD.contains(endUserState)) { - Log.d(Config.LOGTAG,"onAudioDeviceChanged() nothing to do because end card has been reached"); + Log.d(Config.LOGTAG, "onAudioDeviceChanged() nothing to do because end card has been reached"); } else { putProximityWakeLockInProperState(); } diff --git a/src/main/res/drawable-hdpi/ic_warning_white_48dp.png b/src/main/res/drawable-hdpi/ic_warning_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a8889659086685a6fec08677aa94fded88773bd4 GIT binary patch literal 714 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw6p}rHd>I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>XMsm#F#`j)FbFd;%$g$s zRN>_5;uw-~@9nMqeoT%6$3M!eh=hdP%2bcYSa{LzZR@=c3|{#a3|q}@8g5C;ImkJ@ zHHq?2ypg$FRFsu7LZhI*tUT?+ySt*+S2voM|1?p#FHj_$l3gVC#8Ga^Jgo-5G6&WY z!-HyWk&LE=oC`X`%p6X?Sj>{pVEQM8v84C$%~0jq3uf~s{NFE^%)UOeS#E>)uaXIR zEBbEV@U*rK5VT$6a?WmJUt-jP;yIfB`j;5jK9Y4k`cpu(W+`LVV~^_P^K3HYzaQJ( zf3oRQ#j$A*|8bWY`U#aZX%{_Kvkc&rk5DSx7kErTGk|;fnzd<03%KqDB)sX}&{!w5 zfRFFyY}4-xwBE7YHqE@@<;YqRDE8+2l06~VL!6ZyonI~E@LvHn^98DXq(jpm9W(KYNed3KxVy?*z4+P=(qxw^%g~oADVPU6ngQDd(|5@S0m}nupA|jE=AR+u52clF}C{F3fq` zprb3)T$t;&LPsB*4j1OQZP_8bdYziUwqk+z(?i3Jxo>Npa9yJ(vMqSR?1)%}oWJq9 gHYb{aNa-`TWbR`|xA^#tz$C`t>FVdQ&MBb@07(27d;kCd literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_warning_white_48dp.png b/src/main/res/drawable-mdpi/ic_warning_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a43fa3c27d451e56094e5f182d1820b5424f2987 GIT binary patch literal 364 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}cwPFdBQhIEGX(zBy~icQ8Ss{o^vv zcE7ndBxcUsd7zw;?*OX>gZKl6m)_Mj_wyHnbY@Kdom|q>Q!N! zuKrSPN6~>vYs7*aW;d*1iZVJdZ%>)ffeC*S0>n4$oFpH!;WcA$*Q_gyHuprn9=gna zO{M$)$*^4x(>I6+T)N2I7r@of%(q~H3B#+)ak5d297i1l-t1R+#Vl%>6z}j+pLI=# z1bas^mq5_tiscN7s(qFW9-Ya23_*`0*gblaWfe^ON*k_NTw??Z%;)iFP3C6^dU%7m zA;jVv1F{^uN8{r)*^ekp6x00ZyC8Qbxrq8qfR%h9ZNftDnm{r-UW| DcZiV2 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_warning_white_48dp.png b/src/main/res/drawable-xhdpi/ic_warning_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..8683a2ea9ad340635e32632d7b1f5234f567d796 GIT binary patch literal 590 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z$EJF;uuoF_~yJJ-@yV2hmXrV z+x_O=keE4h=YjuBG7a1X40;C`cQCL&U^sK8C&y>5l}YBjm7OM6uU&ZZd(!{+b01hv zOO2csD$m2V{G80IGmp>l`oBLQ(=hW|qnaXvBJW)0=pgA{=31d3PTN}M^nG);@b;XP z%9Q$0IqB&6>Y&O4oUnB8F`s4L4{U3W86LLw+dq4vO2uZi62-ZW~as!d{Nx;Ld# z@Puqc@KLW<4%267C0QA<_l1;But@mP{&P$G@ol^;jsh&An}q`8x*8(wYO7d%q*=jDrtIxvIH0W4kk)?u{C7bU_KKDUhFQ!uP7Vxb8RZle8lExq3kWzA zH@I_fEU-MF%))X(=7As+QwHA$4n{_(^1kEhya^dba-SJm)E*o1J7ieN{bp!d_IM+| z0)&~?b6jqgpJHmt$7n@j4dY6|3>|cN|31qJ1o(pdrEX zWp}jP%-b791MajWpEKlqkf|tmZLTr4`HV@-1z-2NcJLm!`dv)Xj3N9v z1Bh+O02D7h(QtzyM%G9HDBR8i5>{l!C49LcwE2cf%vA+5?r__Vvxh?YlavcZn{If- zv?`d1giChx9t~wlN-h*_ywNeghM&2nFCj)%!AvTgv7`6jW7cV1FRt=NcrTt_{~%Gg zcQwNix5rux8!ztQ`VeEXj!8foS;!#=D6~5QNZhUg67LUY#IQn5Iu^>1lx!y3`qf0Aq!EUXW-csDZc@N&*!Sa(T4x?#1+5jKKCjjO*KFyL2r;Hg%v zAU5L~SDznvYn#o+w@M<8dlr_vKluGu{^8?8u}rQn;}|3l zFc>v3rZ6y{U|^fT$fLk4;lO6lz*CS{e|(1cxtS2ukowxX!KpFKV-)gPp1SI@y)H6Hc@9$IRYn?jUFU zO)zqj;epOy$J$?}G<>;cQ+&E@D&r+Co{Q6inN>EVO8&Glc3|R-{ld@1tn%S#OZ74t zo(adA%#+^CkXC4&*Cipkm+@cxY2l4gQOth=^4--14jYKDocwlw^X0~ejrD2ko0w}o z$QQ=iqTgZY|{xg&xhP7;0obEiht` zZ?{>&bKtllzgF{yLjHzRHZ?L#3yk>X+Zk4zIec7^LF@B}LJpk56*Wf|8p+8Qt~hht zU0Un&$BS*Ja%#RTG?JFDU2*1^yS&!sj~Cm?6Iy;AS>yeKZ=+SX4nwX?ewGNr%YEaaL0 z6ogQ~%>CiF%f0`lX2!q!nXmorcIdz9{olkPTJ{;-Y?=<%f^15_OngFxKPvuxd-tHl zQGsERhF|r!`~AEea`v0@w>2KvzoT+S&7Ap;Pah<6PtE=)7{X__t$fbGkkescKO@v@3&qT3fTEAiBIeLBjpt_{rykh zvwjWO3DfiX<*yDdiw<}rw$$$1KJY} zGWoC93YnUmg+(7Ww!(!;6nK6XY8+S}$R8l{zdL1K*HpJC|1 + + + + + + android:visibility="gone" + app:elevation="4dp" /> Date: Thu, 16 Apr 2020 19:49:34 +0200 Subject: [PATCH 118/182] getMedia() would throw null pointer when called after going from proposed to some error state --- build.gradle | 4 ++-- .../eu/siacs/conversations/ui/RtpSessionActivity.java | 1 - .../conversations/xmpp/jingle/JingleRtpConnection.java | 9 +++++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 606021a97..f19f8e28a 100644 --- a/build.gradle +++ b/build.gradle @@ -92,8 +92,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 28 - versionCode 369 - versionName "2.8.0-alpha.3" + versionCode 370 + versionName "2.8.0-alpha.4" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index cff788e04..b1beee945 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -659,7 +659,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final AbstractJingleConnection.Id id = requireRtpConnection().getId(); if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) { if (state == RtpEndUserState.ENDED) { - resetIntent(account, with, state, requireRtpConnection().getMedia()); finish(); return; } else if (END_CARD.contains(state)) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index f2a877d04..5f0ccb43c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -756,9 +756,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web throw new IllegalStateException("RTP connection has not been initialized yet"); } if (isInState(State.PROPOSED, State.PROCEED)) { - 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; + if (initiatorContentMap != null) { + return initiatorContentMap.getMedia(); + } else { + return Preconditions.checkNotNull(this.proposedMedia, "RTP connection has not been initialized properly"); } - return Preconditions.checkNotNull(initiatorRtpContentMap.getMedia()); } From 8472712b3e2009f47cea6bc2f1e3f585747c19a6 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 16 Apr 2020 20:26:46 +0200 Subject: [PATCH 119/182] play notification sound pre notification categories --- .../services/NotificationService.java | 26 ++++++++++++++++++- .../conversations/utils/Compatibility.java | 4 ++- src/main/res/values/defaults.xml | 1 + src/main/res/values/strings.xml | 6 +++-- src/main/res/xml/preferences.xml | 10 +++++-- 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index b6257cf92..ee35e00a8 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -355,7 +355,9 @@ public class NotificationService { builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); builder.setPriority(NotificationCompat.PRIORITY_HIGH); builder.setCategory(NotificationCompat.CATEGORY_CALL); - builder.setFullScreenIntent(createPendingRtpSession(id, Intent.ACTION_VIEW, 101), true); + PendingIntent pendingIntent = createPendingRtpSession(id, Intent.ACTION_VIEW, 101); + builder.setFullScreenIntent(pendingIntent, true); + builder.setContentIntent(pendingIntent); //old androids need this? builder.setOngoing(true); builder.addAction(new NotificationCompat.Action.Builder( R.drawable.ic_call_end_white_48dp, @@ -367,6 +369,7 @@ public class NotificationService { mXmppConnectionService.getString(R.string.answer_call), createPendingRtpSession(id, RtpSessionActivity.ACTION_ACCEPT_CALL, 103)) .build()); + modifyIncomingCall(builder); final Notification notification = builder.build(); notification.flags = notification.flags | Notification.FLAG_INSISTENT; notify(INCOMING_CALL_NOTIFICATION_ID, notification); @@ -570,6 +573,27 @@ public class NotificationService { } } + private void modifyIncomingCall(Builder mBuilder) { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); + final Resources resources = mXmppConnectionService.getResources(); + final String ringtone = preferences.getString("call_ringtone", resources.getString(R.string.incoming_call_ringtone)); + final int dat = 70; + final long[] pattern = {0, 3 * dat, dat, dat, 3 * dat, dat, dat}; + mBuilder.setVibrate(pattern); + Uri uri = Uri.parse(ringtone); + try { + mBuilder.setSound(fixRingtoneUri(uri)); + } catch (SecurityException e) { + Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString()); + } + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mBuilder.setCategory(Notification.CATEGORY_MESSAGE); + } + mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH); + setNotificationColor(mBuilder); + mBuilder.setLights(LED_COLOR, 2000, 3000); + } + private Uri fixRingtoneUri(Uri uri) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && "file".equals(uri.getScheme())) { return FileBackend.getUriForFile(mXmppConnectionService, new File(uri.getPath())); diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java index 9f7b9c997..3389d7519 100644 --- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java +++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java @@ -30,7 +30,9 @@ public class Compatibility { "led", "notification_ringtone", "notification_headsup", - "vibrate_on_notification"); + "vibrate_on_notification", + "call_ringtone" + ); private static final List UNUESD_SETTINGS_PRE_TWENTYSIX = Collections.singletonList("more_notification_settings"); diff --git a/src/main/res/values/defaults.xml b/src/main/res/values/defaults.xml index 3c1de4ebc..ebfdf5672 100644 --- a/src/main/res/values/defaults.xml +++ b/src/main/res/values/defaults.xml @@ -14,6 +14,7 @@ true false content://settings/system/notification_sound + content://settings/system/ringtone 144 524288 auto diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 6cdab665b..1ac409fdc 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -116,8 +116,10 @@ Vibrate when a new message arrives LED Notification Blink notification light when a new message arrives - Ringtone - Play sound when a new message arrives + Ringtone + Notification sound + Notification sound for new messages + Ringtone for incoming call Grace Period The length of time notifications are silenced after detecting activity on one of your other devices. Advanced diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index 31c2f8b08..3ef7b10b2 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -117,8 +117,14 @@ android:defaultValue="@string/notification_ringtone" android:key="notification_ringtone" android:ringtoneType="notification" - android:summary="@string/pref_sound_summary" - android:title="@string/pref_sound" /> + android:summary="@string/pref_notification_sound_summary" + android:title="@string/pref_notification_sound" /> + Date: Thu, 16 Apr 2020 21:58:30 +0200 Subject: [PATCH 120/182] fixed 215 credential detection --- .../eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 5f0ccb43c..8ad07028d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1002,7 +1002,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (port < 0 || port > 65535) { continue; } - if (Arrays.asList("stun", "turn").contains(type) || Arrays.asList("udp", "tcp").contains(transport)) { + if (Arrays.asList("stun", "turn").contains(type) && Arrays.asList("udp", "tcp").contains(transport)) { //TODO wrap ipv6 addresses PeerConnection.IceServer.Builder iceServerBuilder = PeerConnection.IceServer.builder(String.format("%s:%s:%s?transport=%s", type, host, port, transport)); if (username != null && password != null) { From fa3ef07580a0b8d40e1e2725d26647588c292f45 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 17 Apr 2020 10:29:36 +0200 Subject: [PATCH 121/182] be more strict with ice candidate parsing --- .../xmpp/jingle/JingleRtpConnection.java | 10 ++++++++-- .../xmpp/jingle/stanzas/IceUdpTransportInfo.java | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 8ad07028d..41445c77c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -234,7 +234,13 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web for (final Map.Entry content : contentMap.contents.entrySet()) { final String ufrag = content.getValue().transport.getAttribute("ufrag"); for (final IceUdpTransportInfo.Candidate candidate : content.getValue().transport.getCandidates()) { - final String sdp = candidate.toSdpAttribute(ufrag); + final String sdp; + try { + sdp = candidate.toSdpAttribute(ufrag); + } catch (IllegalArgumentException e) { + Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": ignoring invalid ICE candidate "+e.getMessage()); + continue; + } final String sdpMid = content.getKey(); final int mLineIndex = identificationTags.indexOf(sdpMid); final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp); @@ -418,8 +424,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void addIceCandidatesFromBlackLog() { while (!this.pendingIceCandidates.isEmpty()) { final IceCandidate iceCandidate = this.pendingIceCandidates.poll(); - this.webRTCWrapper.addIceCandidate(iceCandidate); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added ICE candidate from back log " + iceCandidate); + this.webRTCWrapper.addIceCandidate(iceCandidate); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index 467a25490..1e7ada424 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -5,6 +5,7 @@ import android.util.Log; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; @@ -188,11 +189,17 @@ public class IceUdpTransportInfo extends GenericTransportInfo { public String toSdpAttribute(final String ufrag) { final String foundation = this.getAttribute("foundation"); + checkNotNullNoWhitespace(foundation, "foundation"); final String component = this.getAttribute("component"); + checkNotNullNoWhitespace(component, "component"); final String transport = this.getAttribute("protocol"); + checkNotNullNoWhitespace(transport, "protocol"); final String priority = this.getAttribute("priority"); + checkNotNullNoWhitespace(priority, "priority"); final String connectionAddress = this.getAttribute("ip"); + checkNotNullNoWhitespace(connectionAddress, "ip"); final String port = this.getAttribute("port"); + checkNotNullNoWhitespace(port, "port"); final Map additionalParameter = new LinkedHashMap<>(); final String relAddr = this.getAttribute("rel-addr"); final String type = this.getAttribute("type"); @@ -228,6 +235,13 @@ public class IceUdpTransportInfo extends GenericTransportInfo { } } + private static void checkNotNullNoWhitespace(final String value, final String name) { + if (Strings.isNullOrEmpty(value)) { + throw new IllegalArgumentException(String.format("Parameter %s is missing or empty", name)); + } + SessionDescription.checkNoWhitespace(value, String.format("Parameter %s contains white spaces", name)); + } + public static class Fingerprint extends Element { From 2f437ea845a0972437c151100d59be5b7242d0b5 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 17 Apr 2020 10:56:27 +0200 Subject: [PATCH 122/182] ignore iq errors if session has already been terminated --- .../xmpp/jingle/JingleRtpConnection.java | 66 +++++++++++-------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 41445c77c..64da8bc35 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -26,6 +26,7 @@ import java.util.Map; import java.util.Set; import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; @@ -238,7 +239,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { sdp = candidate.toSdpAttribute(ufrag); } catch (IllegalArgumentException e) { - Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": ignoring invalid ICE candidate "+e.getMessage()); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage()); continue; } final String sdpMid = content.getKey(); @@ -658,35 +659,44 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void send(final JinglePacket jinglePacket) { jinglePacket.setTo(id.with); - xmppConnectionService.sendIqPacket(id.account, jinglePacket, (account, response) -> { - if (response.getType() == IqPacket.TYPE.ERROR) { - final String errorCondition = response.getErrorCondition(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ-error from " + response.getFrom() + " in RTP session. " + errorCondition); - this.webRTCWrapper.close(); - final State target; - if (Arrays.asList( - "service-unavailable", - "recipient-unavailable", - "remote-server-not-found", - "remote-server-timeout" - ).contains(errorCondition)) { - target = State.TERMINATED_CONNECTIVITY_ERROR; - } else { - target = State.TERMINATED_APPLICATION_FAILURE; - } - if (transition(target)) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": terminated session with " + id.with); - } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not transitioning because already at state=" + this.state); - } + xmppConnectionService.sendIqPacket(id.account, jinglePacket, this::handleIqResponse); + } - } else if (response.getType() == IqPacket.TYPE.TIMEOUT) { - this.webRTCWrapper.close(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ timeout in RTP session with " + id.with + ". terminating with connectivity error"); - transition(State.TERMINATED_CONNECTIVITY_ERROR); - this.jingleConnectionManager.finishConnection(this); + private synchronized void handleIqResponse(final Account account, final IqPacket response) { + if (response.getType() == IqPacket.TYPE.ERROR) { + final String errorCondition = response.getErrorCondition(); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ-error from " + response.getFrom() + " in RTP session. " + errorCondition); + if (TERMINATED.contains(this.state)) { + Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); + return; } - }); + this.webRTCWrapper.close(); + final State target; + if (Arrays.asList( + "service-unavailable", + "recipient-unavailable", + "remote-server-not-found", + "remote-server-timeout" + ).contains(errorCondition)) { + target = State.TERMINATED_CONNECTIVITY_ERROR; + } else { + target = State.TERMINATED_APPLICATION_FAILURE; + } + if (transition(target)) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": terminated session with " + id.with); + } else { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not transitioning because already at state=" + this.state); + } + } else if (response.getType() == IqPacket.TYPE.TIMEOUT) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ timeout in RTP session with " + id.with + ". terminating with connectivity error"); + if (TERMINATED.contains(this.state)) { + Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); + return; + } + this.webRTCWrapper.close(); + transition(State.TERMINATED_CONNECTIVITY_ERROR); + this.jingleConnectionManager.finishConnection(this); + } } private void terminateWithOutOfOrder(final JinglePacket jinglePacket) { From ab2681640a729ed5b134375d4d0c2ef0e767562e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 17 Apr 2020 11:44:20 +0200 Subject: [PATCH 123/182] allow pip during connecting --- .../conversations/ui/RtpSessionActivity.java | 17 ++++++++++++++++- src/main/res/layout/activity_rtp_session.xml | 12 +++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index b1beee945..4b71a86fc 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -299,7 +299,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private boolean shouldBePictureInPicture() { try { final JingleRtpConnection rtpConnection = requireRtpConnection(); - return rtpConnection.getMedia().contains(Media.VIDEO) && rtpConnection.getEndUserState() == RtpEndUserState.CONNECTED; + return rtpConnection.getMedia().contains(Media.VIDEO) && Arrays.asList( + RtpEndUserState.ACCEPTING_CALL, + RtpEndUserState.CONNECTING, + RtpEndUserState.CONNECTED + ).contains(rtpConnection.getEndUserState()); } catch (IllegalStateException e) { return false; } @@ -546,8 +550,10 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe binding.pipPlaceholder.setVisibility(View.VISIBLE); if (state == RtpEndUserState.APPLICATION_ERROR || state == RtpEndUserState.CONNECTIVITY_ERROR) { binding.pipWarning.setVisibility(View.VISIBLE); + binding.pipWaiting.setVisibility(View.GONE); } else { binding.pipWarning.setVisibility(View.GONE); + binding.pipWaiting.setVisibility(View.GONE); } } else { binding.appBarLayout.setVisibility(View.VISIBLE); @@ -556,6 +562,15 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); return; } + if (isPictureInPicture() && (state == RtpEndUserState.CONNECTING || state == RtpEndUserState.ACCEPTING_CALL)) { + binding.localVideo.setVisibility(View.GONE); + binding.remoteVideo.setVisibility(View.GONE); + binding.appBarLayout.setVisibility(View.GONE); + binding.pipPlaceholder.setVisibility(View.VISIBLE); + binding.pipWarning.setVisibility(View.GONE); + binding.pipWaiting.setVisibility(View.VISIBLE); + return; + } final Optional localVideoTrack = requireRtpConnection().geLocalVideoTrack(); if (localVideoTrack.isPresent() && !isPictureInPicture()) { ensureSurfaceViewRendererIsSetup(binding.localVideo); diff --git a/src/main/res/layout/activity_rtp_session.xml b/src/main/res/layout/activity_rtp_session.xml index 25b5f3a12..ef549aebd 100644 --- a/src/main/res/layout/activity_rtp_session.xml +++ b/src/main/res/layout/activity_rtp_session.xml @@ -17,11 +17,21 @@ android:orientation="horizontal" android:visibility="gone"> + + + android:src="@drawable/ic_warning_white_48dp" + android:visibility="gone" /> Date: Fri, 17 Apr 2020 14:16:39 +0200 Subject: [PATCH 124/182] parse turns and stuns (regression from earlier commit) --- .../xmpp/jingle/JingleRtpConnection.java | 6 +++++- .../xmpp/jingle/WebRTCWrapper.java | 21 +++++++++---------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 64da8bc35..f47eb8f4f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1018,7 +1018,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (port < 0 || port > 65535) { continue; } - if (Arrays.asList("stun", "turn").contains(type) && Arrays.asList("udp", "tcp").contains(transport)) { + if (Arrays.asList("stun", "stuns", "turn", "turns").contains(type) && Arrays.asList("udp", "tcp").contains(transport)) { + if (Arrays.asList("stuns","turns").contains(type) && "udp".equals(transport)) { + Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": skipping invalid combination of udp/tls in external services"); + continue; + } //TODO wrap ipv6 addresses PeerConnection.IceServer.Builder iceServerBuilder = PeerConnection.IceServer.builder(String.format("%s:%s:%s?transport=%s", type, host, port, transport)); if (username != null && password != null) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 5bb05675e..28d0d8fa4 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -39,7 +39,6 @@ import org.webrtc.VideoTrack; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.Set; @@ -146,7 +145,7 @@ public class WebRTCWrapper { private AppRTCAudioManager appRTCAudioManager = null; private Context context = null; private EglBase eglBase = null; - private Optional optionalCapturer; + private CapturerChoice capturerChoice; public WebRTCWrapper(final EventCallback eventCallback) { this.eventCallback = eventCallback; @@ -177,16 +176,16 @@ public class WebRTCWrapper { final MediaStream stream = peerConnectionFactory.createLocalMediaStream("my-media-stream"); - this.optionalCapturer = media.contains(Media.VIDEO) ? getVideoCapturer() : Optional.absent(); + final Optional optionalCapturerChoice = media.contains(Media.VIDEO) ? getVideoCapturer() : Optional.absent(); - if (this.optionalCapturer.isPresent()) { - final CapturerChoice choice = this.optionalCapturer.get(); - final CameraVideoCapturer capturer = choice.cameraVideoCapturer; + if (optionalCapturerChoice.isPresent()) { + this.capturerChoice = optionalCapturerChoice.get(); + final CameraVideoCapturer capturer = this.capturerChoice.cameraVideoCapturer; final VideoSource videoSource = peerConnectionFactory.createVideoSource(false); SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("webrtc", eglBase.getEglBaseContext()); capturer.initialize(surfaceTextureHelper, requireContext(), videoSource.getCapturerObserver()); - Log.d(Config.LOGTAG, String.format("start capturing at %dx%d@%d", choice.captureFormat.width, choice.captureFormat.height, choice.getFrameRate())); - capturer.startCapture(choice.captureFormat.width, choice.captureFormat.height, choice.getFrameRate()); + Log.d(Config.LOGTAG, String.format("start capturing at %dx%d@%d", capturerChoice.captureFormat.width, capturerChoice.captureFormat.height, capturerChoice.getFrameRate())); + capturer.startCapture(capturerChoice.captureFormat.width, capturerChoice.captureFormat.height, capturerChoice.getFrameRate()); this.localVideoTrack = peerConnectionFactory.createVideoTrack("my-video-track", videoSource); @@ -214,7 +213,7 @@ public class WebRTCWrapper { public void close() { final PeerConnection peerConnection = this.peerConnection; - final Optional optionalCapturer = this.optionalCapturer; + final CapturerChoice capturerChoice = this.capturerChoice; final AppRTCAudioManager audioManager = this.appRTCAudioManager; final EglBase eglBase = this.eglBase; if (peerConnection != null) { @@ -225,9 +224,9 @@ public class WebRTCWrapper { } this.localVideoTrack = null; this.remoteVideoTrack = null; - if (optionalCapturer != null && optionalCapturer.isPresent()) { + if (capturerChoice != null) { try { - optionalCapturer.get().cameraVideoCapturer.stopCapture(); + capturerChoice.cameraVideoCapturer.stopCapture(); } catch (InterruptedException e) { Log.e(Config.LOGTAG, "unable to stop capturing"); } From 644e5aa856559173086fe27aaa1e707be6a4b25d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 17 Apr 2020 14:33:56 +0200 Subject: [PATCH 125/182] remove video sinks when calling onStop. otherwise going in and out foreground will give us endless sinks --- .../conversations/ui/RtpSessionActivity.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 4b71a86fc..a27808f3e 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -270,11 +270,26 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe public void onStop() { binding.remoteVideo.release(); binding.localVideo.release(); + final WeakReference weakReference = this.rtpConnectionReference; + final JingleRtpConnection jingleRtpConnection = weakReference == null ? null : weakReference.get(); + if (jingleRtpConnection != null) { + releaseVideoTracks(jingleRtpConnection); + } releaseProximityWakeLock(); - //TODO maybe we want to finish if call had ended super.onStop(); } + private void releaseVideoTracks(final JingleRtpConnection jingleRtpConnection) { + final Optional remoteVideo = jingleRtpConnection.getRemoteVideoTrack(); + if (remoteVideo.isPresent()) { + remoteVideo.get().removeSink(binding.remoteVideo); + } + final Optional localVideo = jingleRtpConnection.geLocalVideoTrack(); + if (localVideo.isPresent()) { + localVideo.get().removeSink(binding.localVideo); + } + } + @Override public void onBackPressed() { endCall(); From 48f752366b85752371b737cb19fd43fb1b45990d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 17 Apr 2020 15:06:13 +0200 Subject: [PATCH 126/182] paint local mic off button in pip --- .../siacs/conversations/ui/RtpSessionActivity.java | 11 ++++++++--- src/main/res/layout/activity_rtp_session.xml | 13 +++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index a27808f3e..7a3545166 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -21,8 +21,6 @@ import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; -import org.webrtc.PeerConnection; -import org.webrtc.RendererCommon; import org.webrtc.SurfaceViewRenderer; import org.webrtc.VideoTrack; @@ -84,7 +82,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); - Log.d(Config.LOGTAG, "RtpSessionActivity.onCreate()"); this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session); setSupportActionBar(binding.toolbar); } @@ -560,6 +557,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) { binding.localVideo.setVisibility(View.GONE); binding.remoteVideo.setVisibility(View.GONE); + binding.pipLocalMicOffIndicator.setVisibility(View.GONE); if (isPictureInPicture()) { binding.appBarLayout.setVisibility(View.GONE); binding.pipPlaceholder.setVisibility(View.VISIBLE); @@ -584,6 +582,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe binding.pipPlaceholder.setVisibility(View.VISIBLE); binding.pipWarning.setVisibility(View.GONE); binding.pipWaiting.setVisibility(View.VISIBLE); + binding.pipLocalMicOffIndicator.setVisibility(View.GONE); return; } final Optional localVideoTrack = requireRtpConnection().geLocalVideoTrack(); @@ -607,9 +606,15 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); binding.remoteVideo.setVisibility(View.GONE); } + if (isPictureInPicture() && !requireRtpConnection().isMicrophoneEnabled()) { + binding.pipLocalMicOffIndicator.setVisibility(View.VISIBLE); + } else { + binding.pipLocalMicOffIndicator.setVisibility(View.GONE); + } } else { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); binding.remoteVideo.setVisibility(View.GONE); + binding.pipLocalMicOffIndicator.setVisibility(View.GONE); } } diff --git a/src/main/res/layout/activity_rtp_session.xml b/src/main/res/layout/activity_rtp_session.xml index ef549aebd..391adba1d 100644 --- a/src/main/res/layout/activity_rtp_session.xml +++ b/src/main/res/layout/activity_rtp_session.xml @@ -62,6 +62,7 @@ + + Date: Fri, 17 Apr 2020 16:39:23 +0200 Subject: [PATCH 127/182] version bump --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index f19f8e28a..8fc50c291 100644 --- a/build.gradle +++ b/build.gradle @@ -92,8 +92,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 28 - versionCode 370 - versionName "2.8.0-alpha.4" + versionCode 371 + versionName "2.8.0-beta" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 934b98d1990e5f70da1fd0cda04888db713849df Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 18 Apr 2020 17:51:21 +0200 Subject: [PATCH 128/182] add microphone availability check --- .../services/AppRTCAudioManager.java | 37 +++++++++++++++++++ .../ui/ConversationFragment.java | 17 ++++++--- .../conversations/ui/RtpSessionActivity.java | 15 +++++++- .../xmpp/jingle/JingleRtpConnection.java | 1 + src/main/res/values/strings.xml | 1 + 5 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java index db1b8e1e1..07d52891b 100644 --- a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java +++ b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java @@ -15,7 +15,10 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.media.AudioDeviceInfo; +import android.media.AudioFormat; import android.media.AudioManager; +import android.media.AudioRecord; +import android.media.MediaRecorder; import android.os.Build; import android.support.annotation.Nullable; import android.util.Log; @@ -111,6 +114,40 @@ public class AppRTCAudioManager { return new AppRTCAudioManager(context, speakerPhonePreference); } + public static boolean isMicrophoneAvailable(final Context context) { + AudioRecord audioRecord = null; + boolean available = true; + try { + final int sampleRate = 44100; + final int channel = AudioFormat.CHANNEL_IN_MONO; + final int format = AudioFormat.ENCODING_PCM_16BIT; + final int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channel, format); + audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channel, format, bufferSize); + audioRecord.startRecording(); + final short[] buffer = new short[bufferSize]; + final int audioStatus = audioRecord.read(buffer, 0, bufferSize); + if (audioStatus == AudioRecord.ERROR_INVALID_OPERATION || audioStatus == AudioRecord.STATE_UNINITIALIZED) + available = false; + } catch (Exception e) { + available = false; + } finally { + release(audioRecord); + + } + return available; + } + + private static void release(final AudioRecord audioRecord) { + if (audioRecord == null) { + return; + } + try { + audioRecord.release(); + } catch (Exception e) { + //ignore + } + } + /** * This method is called when the proximity sensor reports a state change, * e.g. from "NEAR to FAR" or from "FAR to NEAR". diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 03c90320f..8d1a0f1ab 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -82,6 +82,7 @@ import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.TransferablePlaceholder; import eu.siacs.conversations.http.HttpDownloadConnection; import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.XmppConnectionService; @@ -1272,12 +1273,16 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private void triggerRtpSession(final String action) { - final Contact contact = conversation.getContact(); - final Intent intent = new Intent(activity, RtpSessionActivity.class); - intent.setAction(action); - intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, contact.getAccount().getJid().toEscapedString()); - intent.putExtra(RtpSessionActivity.EXTRA_WITH, contact.getJid().asBareJid().toEscapedString()); - startActivity(intent); + if (AppRTCAudioManager.isMicrophoneAvailable(getActivity())) { + final Contact contact = conversation.getContact(); + final Intent intent = new Intent(activity, RtpSessionActivity.class); + intent.setAction(action); + intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, contact.getAccount().getJid().toEscapedString()); + intent.putExtra(RtpSessionActivity.EXTRA_WITH, contact.getJid().asBareJid().toEscapedString()); + startActivity(intent); + } else { + Toast.makeText(getActivity(), R.string.microphone_unavailable, Toast.LENGTH_SHORT).show(); + } } private void handleAttachmentSelection(MenuItem item) { diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 7a3545166..7b6c1fefe 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -9,6 +9,7 @@ import android.databinding.DataBindingUtil; import android.os.Build; import android.os.Bundle; import android.os.PowerManager; +import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.annotation.StringRes; import android.util.Log; @@ -126,7 +127,19 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) { putScreenInCallMode(); + checkRecorderAndAcceptCall(); + } + } + + private void checkRecorderAndAcceptCall() { + final long start = SystemClock.elapsedRealtime(); + final boolean isMicrophoneAvailable = AppRTCAudioManager.isMicrophoneAvailable(this); + final long stop = SystemClock.elapsedRealtime(); + Log.d(Config.LOGTAG, "checking microphone availability took " + (stop - start) + "ms"); + if (isMicrophoneAvailable) { requireRtpConnection().acceptCall(); + } else { + Toast.makeText(this, R.string.microphone_unavailable, Toast.LENGTH_SHORT).show(); } } @@ -247,7 +260,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (PermissionUtils.allGranted(grantResults)) { if (requestCode == REQUEST_ACCEPT_CALL) { - requireRtpConnection().acceptCall(); + checkRecorderAndAcceptCall(); } } else { @StringRes int res; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index f47eb8f4f..3067f18f0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -950,6 +950,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); return; } + //we need to call close sendSessionTerminate(Reason.CONNECTIVITY_ERROR); } else { updateEndUserState(); diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 1ac409fdc..9b05af9e3 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -914,6 +914,7 @@ Missed call Audio call Video call + Microphone unavailable View %1$d Participant View %1$d Participants From 7dfd47a5c423a9ea56b18b3829d5afe81c24ced2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 18 Apr 2020 18:22:10 +0200 Subject: [PATCH 129/182] better crash than leave WebRTCWrapper unclosed --- .../xmpp/jingle/JingleRtpConnection.java | 41 +++++++++++-------- .../xmpp/jingle/WebRTCWrapper.java | 11 +++++ 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 3067f18f0..d6e950692 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -193,7 +193,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web sendSessionTerminate(Reason.CONNECTIVITY_ERROR); } else { transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR); - jingleConnectionManager.finishConnection(this); + finish(); } } @@ -213,7 +213,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (previous == State.PROPOSED || previous == State.SESSION_INITIALIZED) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); } - jingleConnectionManager.finishConnection(this); + finish(); } private void receiveTransportInfo(final JinglePacket jinglePacket) { @@ -466,7 +466,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (transition(State.TERMINATED_CONNECTIVITY_ERROR)) { webRTCWrapper.close(); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into connectivity error"); - this.jingleConnectionManager.finishConnection(this); + this.finish(); } } @@ -481,7 +481,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.message.setCarbon(true); //indicate that call was accepted on other device this.writeLogMessageSuccess(0); this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); - this.jingleConnectionManager.finishConnection(this); + this.finish(); } else { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to transition to accept because already in state=" + this.state); } @@ -496,7 +496,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (originatedFromMyself) { if (transition(State.REJECTED)) { this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); - this.jingleConnectionManager.finishConnection(this); + this.finish(); if (serverMsgId != null) { this.message.setServerMsgId(serverMsgId); } @@ -561,7 +561,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (transition(State.ACCEPTED)) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": moved session with " + id.with + " into state accepted after received carbon copied procced"); this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); - this.jingleConnectionManager.finishConnection(this); + this.finish(); } } else { Log.d(Config.LOGTAG, String.format("%s: ignoring proceed from %s. was expected from %s", id.account.getJid().asBareJid(), from, id.with)); @@ -578,7 +578,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } this.message.setTime(timestamp); writeLogMessageMissed(); - jingleConnectionManager.finishConnection(this); + finish(); } else { Log.d(Config.LOGTAG, "ignoring retract because already in " + this.state); } @@ -641,7 +641,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web jinglePacket.setReason(reason, text); Log.d(Config.LOGTAG, jinglePacket.toString()); send(jinglePacket); - jingleConnectionManager.finishConnection(this); + finish(); } private void sendTransportInfo(final String contentName, IceUdpTransportInfo.Candidate candidate) { @@ -695,16 +695,16 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } this.webRTCWrapper.close(); transition(State.TERMINATED_CONNECTIVITY_ERROR); - this.jingleConnectionManager.finishConnection(this); + this.finish(); } } private void terminateWithOutOfOrder(final JinglePacket jinglePacket) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": terminating session with out-of-order"); - webRTCWrapper.close(); + this.webRTCWrapper.close(); transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE); respondWithOutOfOrder(jinglePacket); - jingleConnectionManager.finishConnection(this); + this.finish(); } private void respondWithOutOfOrder(final JinglePacket jinglePacket) { @@ -820,13 +820,13 @@ 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"); - webRTCWrapper.close(); - jingleConnectionManager.finishConnection(this); + this.webRTCWrapper.close(); + this.finish(); transitionOrThrow(State.TERMINATED_SUCCESS); //arguably this wasn't success; but not a real failure either return; } if (isInitiator() && isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED)) { - webRTCWrapper.close(); + this.webRTCWrapper.close(); sendSessionTerminate(Reason.CANCEL); return; } @@ -835,7 +835,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return; } if (isInState(State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { - webRTCWrapper.close(); + this.webRTCWrapper.close(); sendSessionTerminate(Reason.SUCCESS); return; } @@ -869,7 +869,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web writeLogMessageMissed(); xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); this.sendJingleMessage("reject"); - jingleConnectionManager.finishConnection(this); + finish(); } private void rejectCallFromSessionInitiate() { @@ -1020,8 +1020,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web continue; } if (Arrays.asList("stun", "stuns", "turn", "turns").contains(type) && Arrays.asList("udp", "tcp").contains(transport)) { - if (Arrays.asList("stuns","turns").contains(type) && "udp".equals(transport)) { - Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": skipping invalid combination of udp/tls in external services"); + if (Arrays.asList("stuns", "turns").contains(type) && "udp".equals(transport)) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": skipping invalid combination of udp/tls in external services"); continue; } //TODO wrap ipv6 addresses @@ -1054,6 +1054,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } + private void finish() { + this.webRTCWrapper.verifyClosed(); + this.jingleConnectionManager.finishConnection(this); + } + private void writeLogMessage(final State state) { final long started = this.rtpConnectionStarted; long duration = started <= 0 ? 0 : SystemClock.elapsedRealtime() - started; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 28d0d8fa4..b6ed9aa10 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -218,6 +218,7 @@ public class WebRTCWrapper { final EglBase eglBase = this.eglBase; if (peerConnection != null) { peerConnection.dispose(); + this.peerConnection = null; } if (audioManager != null) { mainHandler.post(audioManager::stop); @@ -233,6 +234,16 @@ public class WebRTCWrapper { } if (eglBase != null) { eglBase.release(); + this.eglBase = null; + } + } + + void verifyClosed() { + if (this.peerConnection != null + || this.eglBase != null + || this.localVideoTrack != null + || this.remoteVideoTrack != null) { + throw new IllegalStateException("WebRTCWrapper hasn't been closed properly"); } } From c20c40a80726a83325509c30daf0feec7ae951c2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 18 Apr 2020 20:57:15 +0200 Subject: [PATCH 130/182] ensure webrtc connection gets closed after connection failure --- .../conversations/xmpp/jingle/JingleRtpConnection.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index d6e950692..1c30ee9a5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -119,7 +119,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private RtpContentMap responderRtpContentMap; private long rtpConnectionStarted = 0; //time of 'connected' - JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { super(jingleConnectionManager, id, initiator); final Conversation conversation = jingleConnectionManager.getXmppConnectionService().findOrCreateConversation( @@ -847,6 +846,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void setupWebRTC(final Set media, final List iceServers) throws WebRTCWrapper.InitializationException { + //TODO ensure registered with connection manager final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference; if (media.contains(Media.VIDEO)) { speakerPhonePreference = AppRTCAudioManager.SpeakerPhonePreference.SPEAKER; @@ -950,13 +950,17 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); return; } - //we need to call close - sendSessionTerminate(Reason.CONNECTIVITY_ERROR); + new Thread(this::closeWebRTCSessionAfterFailedConnection).start(); } else { updateEndUserState(); } } + private void closeWebRTCSessionAfterFailedConnection() { + this.webRTCWrapper.close(); + sendSessionTerminate(Reason.CONNECTIVITY_ERROR); + } + public AppRTCAudioManager getAudioManager() { return webRTCWrapper.getAudioManager(); } From a12760300c2c48bb0a51acf05e05edbd191671e2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 18 Apr 2020 21:27:50 +0200 Subject: [PATCH 131/182] ensure that rtp connection is registered with connection manager --- .../conversations/xmpp/jingle/JingleConnectionManager.java | 7 +++++++ .../conversations/xmpp/jingle/JingleRtpConnection.java | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 4ec48b35b..2846fbfaf 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -513,6 +513,13 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } + void ensureConnectionIsRegistered(final AbstractJingleConnection connection) { + if (connections.containsValue(connection)) { + return; + } + throw new IllegalStateException("JingleConnection has not been registered with connection manager"); + } + public enum DeviceDiscoveryState { SEARCHING, DISCOVERED, FAILED; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 1c30ee9a5..121f894f2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -846,7 +846,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void setupWebRTC(final Set media, final List iceServers) throws WebRTCWrapper.InitializationException { - //TODO ensure registered with connection manager + this.jingleConnectionManager.ensureConnectionIsRegistered(this); final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference; if (media.contains(Media.VIDEO)) { speakerPhonePreference = AppRTCAudioManager.SpeakerPhonePreference.SPEAKER; From 72c551d128e1eb9cbe8c885f5c6ca330e3352afc Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 19 Apr 2020 07:52:45 +0200 Subject: [PATCH 132/182] bump to 2.8.0-beta.2 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 8fc50c291..d2ee2b7de 100644 --- a/build.gradle +++ b/build.gradle @@ -92,8 +92,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 28 - versionCode 371 - versionName "2.8.0-beta" + versionCode 372 + versionName "2.8.0-beta.2" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 31dfb0c7048b6841b1633a544823fca53b661648 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 19 Apr 2020 08:04:36 +0200 Subject: [PATCH 133/182] cache useTor information in activity --- .../siacs/conversations/ui/ConversationFragment.java | 4 ++-- .../conversations/ui/ConversationsActivity.java | 1 - .../java/eu/siacs/conversations/ui/XmppActivity.java | 12 +++++++++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 8d1a0f1ab..e5c1925f7 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1252,7 +1252,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } private void checkPermissionAndTriggerAudioCall() { - if (activity.xmppConnectionService.useTorToConnect() || conversation.getAccount().isOnion()) { + if (activity.mUseTor || conversation.getAccount().isOnion()) { Toast.makeText(activity, R.string.disable_tor_to_make_call, Toast.LENGTH_SHORT).show(); return; } @@ -1262,7 +1262,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } private void checkPermissionAndTriggerVideoCall() { - if (activity.xmppConnectionService.useTorToConnect() || conversation.getAccount().isOnion()) { + if (activity.mUseTor || conversation.getAccount().isOnion()) { Toast.makeText(activity, R.string.disable_tor_to_make_call, Toast.LENGTH_SHORT).show(); return; } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index bc9d78f71..cd6a1d8e6 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -383,7 +383,6 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio if (isCameraFeatureAvailable()) { Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment); boolean visible = getResources().getBoolean(R.bool.show_qr_code_scan) - && fragment != null && fragment instanceof ConversationsOverviewFragment; qrCodeScanMenuItem.setVisible(visible); } else { diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 9ed35190b..b2fee69f9 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -67,6 +67,7 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.BarcodeProvider; +import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; import eu.siacs.conversations.ui.service.EmojiService; @@ -96,6 +97,7 @@ public abstract class XmppActivity extends ActionBarActivity { protected int mTheme; protected boolean mUsingEnterKey = false; + protected boolean mUseTor = false; protected Toast mToast; public Runnable onOpenPGPKeyPublished = () -> Toast.makeText(XmppActivity.this, R.string.openpgp_has_been_published, Toast.LENGTH_SHORT).show(); protected ConferenceInvite mPendingConferenceInvite = null; @@ -211,6 +213,8 @@ public abstract class XmppActivity extends ActionBarActivity { this.registerListeners(); this.onBackendConnected(); } + this.mUsingEnterKey = usingEnterKey(); + this.mUseTor = useTor(); } public void connectToBackend() { @@ -408,8 +412,6 @@ public abstract class XmppActivity extends ActionBarActivity { } this.mTheme = findTheme(); setTheme(this.mTheme); - - this.mUsingEnterKey = usingEnterKey(); } protected boolean isCameraFeatureAvailable() { @@ -451,10 +453,14 @@ public abstract class XmppActivity extends ActionBarActivity { } } - protected boolean usingEnterKey() { + private boolean usingEnterKey() { return getBooleanPreference("display_enter_key", R.bool.display_enter_key); } + private boolean useTor() { + return QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor); + } + protected SharedPreferences getPreferences() { return PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); } From c7269bc0aa9b5b12138add062b50a4c9c65809d6 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 19 Apr 2020 08:49:08 +0200 Subject: [PATCH 134/182] check microphone availability on background thread --- .../services/AppRTCAudioManager.java | 21 +++++++++++++++- .../ui/ConversationFragment.java | 16 +++++-------- .../conversations/ui/RtpSessionActivity.java | 24 ++++++++++++------- src/main/res/values/strings.xml | 2 +- 4 files changed, 43 insertions(+), 20 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java index 07d52891b..cd8a19820 100644 --- a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java +++ b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java @@ -28,6 +28,7 @@ import org.webrtc.ThreadUtils; import java.util.Collections; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.CountDownLatch; import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.AppRTCUtils; @@ -36,6 +37,9 @@ import eu.siacs.conversations.utils.AppRTCUtils; * AppRTCAudioManager manages all audio related parts of the AppRTC demo. */ public class AppRTCAudioManager { + + private static CountDownLatch microphoneLatch; + private final Context apprtcContext; // Contains speakerphone setting: auto, true or false @Nullable @@ -114,7 +118,8 @@ public class AppRTCAudioManager { return new AppRTCAudioManager(context, speakerPhonePreference); } - public static boolean isMicrophoneAvailable(final Context context) { + public static boolean isMicrophoneAvailable() { + microphoneLatch = new CountDownLatch(1); AudioRecord audioRecord = null; boolean available = true; try { @@ -134,6 +139,7 @@ public class AppRTCAudioManager { release(audioRecord); } + microphoneLatch.countDown(); return available; } @@ -181,6 +187,7 @@ public class AppRTCAudioManager { Log.e(Config.LOGTAG, "AudioManager is already active"); return; } + awaitMicrophoneLatch(); // TODO(henrika): perhaps call new method called preInitAudio() here if UNINITIALIZED. Log.d(Config.LOGTAG, "AudioManager starts..."); this.audioManagerEvents = audioManagerEvents; @@ -261,6 +268,18 @@ public class AppRTCAudioManager { Log.d(Config.LOGTAG, "AudioManager started"); } + private void awaitMicrophoneLatch() { + final CountDownLatch latch = microphoneLatch; + if (latch == null) { + return; + } + try { + latch.await(); + } catch (InterruptedException e) { + //ignore + } + } + @SuppressWarnings("deprecation") // TODO(henrika): audioManager.abandonAudioFocus() is deprecated. public void stop() { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index e5c1925f7..2c6e2c863 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1273,16 +1273,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private void triggerRtpSession(final String action) { - if (AppRTCAudioManager.isMicrophoneAvailable(getActivity())) { - final Contact contact = conversation.getContact(); - final Intent intent = new Intent(activity, RtpSessionActivity.class); - intent.setAction(action); - intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, contact.getAccount().getJid().toEscapedString()); - intent.putExtra(RtpSessionActivity.EXTRA_WITH, contact.getJid().asBareJid().toEscapedString()); - startActivity(intent); - } else { - Toast.makeText(getActivity(), R.string.microphone_unavailable, Toast.LENGTH_SHORT).show(); - } + final Contact contact = conversation.getContact(); + final Intent intent = new Intent(activity, RtpSessionActivity.class); + intent.setAction(action); + intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, contact.getAccount().getJid().toEscapedString()); + intent.putExtra(RtpSessionActivity.EXTRA_WITH, contact.getJid().asBareJid().toEscapedString()); + startActivity(intent); } private void handleAttachmentSelection(MenuItem item) { diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 7b6c1fefe..c3ef71c80 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -132,15 +132,22 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void checkRecorderAndAcceptCall() { - final long start = SystemClock.elapsedRealtime(); - final boolean isMicrophoneAvailable = AppRTCAudioManager.isMicrophoneAvailable(this); - final long stop = SystemClock.elapsedRealtime(); - Log.d(Config.LOGTAG, "checking microphone availability took " + (stop - start) + "ms"); - if (isMicrophoneAvailable) { - requireRtpConnection().acceptCall(); - } else { - Toast.makeText(this, R.string.microphone_unavailable, Toast.LENGTH_SHORT).show(); + checkMicrophoneAvailability(); + requireRtpConnection().acceptCall(); + } + + private void checkMicrophoneAvailability() { + new Thread(() -> { + final long start = SystemClock.elapsedRealtime(); + final boolean isMicrophoneAvailable = AppRTCAudioManager.isMicrophoneAvailable(); + final long stop = SystemClock.elapsedRealtime(); + Log.d(Config.LOGTAG, "checking microphone availability took " + (stop - start) + "ms"); + if (isMicrophoneAvailable) { + return; + } + runOnUiThread(() -> Toast.makeText(this, R.string.microphone_unavailable, Toast.LENGTH_SHORT).show()); } + ).start(); } private void putScreenInCallMode() { @@ -251,6 +258,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void proposeJingleRtpSession(final Account account, final Jid with, final Set media) { + checkMicrophoneAvailability(); xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with, media); putScreenInCallMode(media); } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 9b05af9e3..63a991e76 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -914,7 +914,7 @@ Missed call Audio call Video call - Microphone unavailable + Your microphone is unavailable View %1$d Participant View %1$d Participants From f7f0dc99a7ffde5045a6d67ea6ec1fcbd13a779b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 19 Apr 2020 10:12:33 +0200 Subject: [PATCH 135/182] launch calls in new task --- src/main/AndroidManifest.xml | 13 +++++++------ .../conversations/ui/ConversationFragment.java | 6 ++++++ .../siacs/conversations/ui/RtpSessionActivity.java | 10 +++------- .../xmpp/jingle/JingleConnectionManager.java | 5 ++++- src/main/res/values/strings.xml | 1 + 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 29356fb21..95eb5139c 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -34,7 +34,7 @@ - + - + diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 2c6e2c863..0f47f6936 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1273,11 +1273,17 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private void triggerRtpSession(final String action) { + if (activity.xmppConnectionService.getJingleConnectionManager().isBusy()) { + Toast.makeText(getActivity(), R.string.only_one_call_at_a_time, Toast.LENGTH_LONG).show(); + return; + } final Contact contact = conversation.getContact(); final Intent intent = new Intent(activity, RtpSessionActivity.class); intent.setAction(action); intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, contact.getAccount().getJid().toEscapedString()); intent.putExtra(RtpSessionActivity.EXTRA_WITH, contact.getJid().asBareJid().toEscapedString()); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); startActivity(intent); } diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index c3ef71c80..f0cae4e53 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -78,6 +78,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override public void onCreate(Bundle savedInstanceState) { + Log.d(Config.LOGTAG, this.getClass().getName() + ".onCreate()"); super.onCreate(savedInstanceState); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD @@ -87,12 +88,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe setSupportActionBar(binding.toolbar); } - @Override - public void onStart() { - super.onStart(); - Log.d(Config.LOGTAG, "RtpSessionActivity.onStart()"); - } - private void endCall(View view) { endCall(); } @@ -145,7 +140,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe if (isMicrophoneAvailable) { return; } - runOnUiThread(() -> Toast.makeText(this, R.string.microphone_unavailable, Toast.LENGTH_SHORT).show()); + runOnUiThread(() -> Toast.makeText(this, R.string.microphone_unavailable, Toast.LENGTH_LONG).show()); } ).start(); } @@ -206,6 +201,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override public void onNewIntent(final Intent intent) { + Log.d(Config.LOGTAG, this.getClass().getName() + ".onNewIntent()"); super.onNewIntent(intent); setIntent(intent); if (xmppConnectionService == null) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 2846fbfaf..0dfb65c97 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -104,7 +104,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { return account.isOnion() || mXmppConnectionService.useTorToConnect(); } - private boolean isBusy() { + public boolean isBusy() { for (AbstractJingleConnection connection : this.connections.values()) { if (connection instanceof JingleRtpConnection) { return true; @@ -403,6 +403,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } } + if (isBusy()) { + throw new IllegalStateException("There is already a running RTP session. This should have been caught by the UI"); + } final RtpSessionProposal proposal = RtpSessionProposal.of(account, with.asBareJid(), media); this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING); mXmppConnectionService.notifyJingleRtpConnectionUpdate( diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 63a991e76..358d768dd 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -915,6 +915,7 @@ Audio call Video call Your microphone is unavailable + You can only have one call at a time. View %1$d Participant View %1$d Participants From 5a0979b41e3d929154ba550b28b1bf2d584c3c30 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 19 Apr 2020 14:23:52 +0200 Subject: [PATCH 136/182] store 'ended call' when ended from proceed --- .../xmpp/jingle/JingleConnectionManager.java | 43 ++++++++++++++++++- .../xmpp/jingle/JingleRtpConnection.java | 1 + 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 0dfb65c97..468b8d8a2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -7,6 +7,8 @@ import android.util.Log; import com.google.common.base.Function; import com.google.common.base.Objects; import com.google.common.base.Preconditions; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableSet; @@ -21,6 +23,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; @@ -49,6 +52,10 @@ public class JingleConnectionManager extends AbstractConnectionManager { private final HashMap rtpSessionProposals = new HashMap<>(); private final Map connections = new ConcurrentHashMap<>(); + private final Cache endedSessions = CacheBuilder.newBuilder() + .expireAfterWrite(30, TimeUnit.MINUTES) + .build(); + private HashMap primaryCandidates = new HashMap<>(); public JingleConnectionManager(XmppConnectionService service) { @@ -79,7 +86,9 @@ 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)) { - if (isBusy()) { + final boolean sessionEnded = this.endedSessions.asMap().containsKey(PersistableSessionId.of(id)); + if (isBusy() || sessionEnded) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": rejected session with " + id.with + " because busy. sessionEnded=" + sessionEnded); mXmppConnectionService.sendIqPacket(account, packet.generateResponse(IqPacket.TYPE.RESULT), null); final JinglePacket sessionTermination = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); sessionTermination.setTo(id.with); @@ -523,6 +532,38 @@ public class JingleConnectionManager extends AbstractConnectionManager { throw new IllegalStateException("JingleConnection has not been registered with connection manager"); } + public void endSession(AbstractJingleConnection.Id id, final AbstractJingleConnection.State state) { + this.endedSessions.put(PersistableSessionId.of(id), state); + } + + private static class PersistableSessionId { + private final Jid with; + private final String sessionId; + + private PersistableSessionId(Jid with, String sessionId) { + this.with = with; + this.sessionId = sessionId; + } + + public static PersistableSessionId of(AbstractJingleConnection.Id id) { + return new PersistableSessionId(id.with, id.sessionId); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PersistableSessionId that = (PersistableSessionId) o; + return Objects.equal(with, that.with) && + Objects.equal(sessionId, that.sessionId); + } + + @Override + public int hashCode() { + return Objects.hashCode(with, sessionId); + } + } + public enum DeviceDiscoveryState { SEARCHING, DISCOVERED, FAILED; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 121f894f2..313549fb1 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -819,6 +819,7 @@ 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(); this.finish(); transitionOrThrow(State.TERMINATED_SUCCESS); //arguably this wasn't success; but not a real failure either From 7f45f3ab548b5ed3552c44d2919ebc8ddff94302 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 19 Apr 2020 20:21:31 +0200 Subject: [PATCH 137/182] build abi-split apk; use stable libwebrtc --- build.gradle | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index d2ee2b7de..2b7415d37 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,5 @@ +import com.android.build.OutputFile + // Top-level build file where you can add configuration options common to all // sub-projects/modules. buildscript { @@ -6,7 +8,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.1' + classpath 'com.android.tools.build:gradle:3.6.2' } } @@ -78,12 +80,13 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:3.12.10' implementation 'com.google.guava:guava:27.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.11.1' - implementation 'org.webrtc:google-webrtc:1.0.+' + implementation fileTree(include: ['libwebrtc-m79.aar'], dir: 'libs') } ext { travisBuild = System.getenv("TRAVIS") == "true" preDexEnabled = System.getProperty("pre-dex", "true") + abiCodes = ['armeabi-v7a': 1, 'x86': 2, 'x86_64': 3, 'arm64-v8a': 4] } android { @@ -92,8 +95,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 28 - versionCode 372 - versionName "2.8.0-beta.2" + versionCode 374 + versionName "2.8.0-beta.3" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId @@ -101,6 +104,12 @@ android { buildConfigField "String", "LOGTAG", "\"conversations\"" } + splits { + abi { + enable true + } + } + dataBinding { enabled true } @@ -254,4 +263,14 @@ android { exclude 'META-INF/BCKEY.DSA' exclude 'META-INF/BCKEY.SF' } -} + + android.applicationVariants.all { variant -> + variant.outputs.each { output -> + def baseAbiVersionCode = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI)) + if (baseAbiVersionCode != null) { + output.versionCodeOverride = (100 * variant.versionCode) + baseAbiVersionCode + } + } + + } +} \ No newline at end of file From c64779329b7e2d108ec8133f535e3b6b62184209 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 20 Apr 2020 10:43:18 +0200 Subject: [PATCH 138/182] upgrade libwebrtc to m81 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2b7415d37..1109541a0 100644 --- a/build.gradle +++ b/build.gradle @@ -80,7 +80,7 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:3.12.10' implementation 'com.google.guava:guava:27.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.11.1' - implementation fileTree(include: ['libwebrtc-m79.aar'], dir: 'libs') + implementation fileTree(include: ['libwebrtc-m81.aar'], dir: 'libs') } ext { From 1cc0dfad84de6cb6c540ba0b4380402322e02783 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 20 Apr 2020 11:38:02 +0200 Subject: [PATCH 139/182] move sdp logging to different tag --- .../xmpp/jingle/JingleRtpConnection.java | 1 - .../xmpp/jingle/SessionDescription.java | 2 +- .../conversations/xmpp/jingle/WebRTCWrapper.java | 15 +++++++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 313549fb1..39c8bef48 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -606,7 +606,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get(); final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); - Log.d(Config.LOGTAG, "description: " + webRTCSessionDescription.description); final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); sendSessionInitiate(rtpContentMap, targetState); this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index dc4007d69..b7400b5b4 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -20,7 +20,7 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; public class SessionDescription { - private final static String LINE_DIVIDER = "\r\n"; + public final static String LINE_DIVIDER = "\r\n"; private final static String HARDCODED_MEDIA_PROTOCOL = "UDP/TLS/RTP/SAVPF"; //probably only true for DTLS-SRTP aka when we have a fingerprint private final static int HARDCODED_MEDIA_PORT = 9; private final static String HARDCODED_ICE_OPTIONS = "trickle renomination"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index b6ed9aa10..e987be4ef 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -50,6 +50,8 @@ import eu.siacs.conversations.services.AppRTCAudioManager; public class WebRTCWrapper { + private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName(); + private static final int CAPTURING_RESOLUTION = 1920; private static final int CAPTURING_MAX_FRAME_RATE = 30; @@ -110,11 +112,12 @@ public class WebRTCWrapper { @Override public void onAddStream(MediaStream mediaStream) { - Log.d(Config.LOGTAG, "onAddStream"); final List videoTracks = mediaStream.videoTracks; if (videoTracks.size() > 0) { - Log.d(Config.LOGTAG, "more than zero remote video tracks found. using first"); remoteVideoTrack = videoTracks.get(0); + Log.d(Config.LOGTAG, "remote video track enabled?=" + remoteVideoTrack.enabled()); + } else { + Log.d(Config.LOGTAG, "no remote video tracks found"); } } @@ -317,6 +320,10 @@ public class WebRTCWrapper { } public ListenableFuture setLocalDescription(final SessionDescription sessionDescription) { + Log.d(EXTENDED_LOGGING_TAG, "setting local description:"); + for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { + Log.d(EXTENDED_LOGGING_TAG, line); + } return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { final SettableFuture future = SettableFuture.create(); peerConnection.setLocalDescription(new SetSdpObserver() { @@ -337,6 +344,10 @@ public class WebRTCWrapper { } public ListenableFuture setRemoteDescription(final SessionDescription sessionDescription) { + Log.d(EXTENDED_LOGGING_TAG, "setting remote description:"); + for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { + Log.d(EXTENDED_LOGGING_TAG, line); + } return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { final SettableFuture future = SettableFuture.create(); peerConnection.setRemoteDescription(new SetSdpObserver() { From 1016e8d018e9950281615919e3881ce3fe37bc7a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 20 Apr 2020 11:50:21 +0200 Subject: [PATCH 140/182] added note about libwebrtc --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a5ae2fb0e..ab81421eb 100644 --- a/README.md +++ b/README.md @@ -318,7 +318,7 @@ merge it if I don't at least you and like minded people get to enjoy it. #### I need a feature and I need it now! -I am available for hire. Contact me via XMPP: `inputmice@siacs.eu` +I am available for hire. Find contact information on [my website](https://gultsch.de). ### Security @@ -401,6 +401,12 @@ you can get access to the the latest beta version by signing up using [this link #### How do I build Conversations +**Note:** Starting with version 2.8.0 you will need to compile libwebrtc. +[Instructions](https://webrtc.github.io/webrtc-org/native-code/android/) can be found on the WebRTC +website. Place the resulting libwebrtc.aar in the `libs/` directory. The PlayStore release currently +uses the stable M81 release and renamed the file name to `libwebrtc-m81.aar` put potentially you can +reference any file name by modifying `build.gradle`. + Make sure to have ANDROID_HOME point to your Android SDK. Use the Android SDK Manager to install missing dependencies. git clone https://github.com/siacs/Conversations.git From e661d5b7ada261102bea7cbf5950a7d40f64c514 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 20 Apr 2020 12:19:34 +0200 Subject: [PATCH 141/182] provide deep link from settings into call notification settings --- .../services/NotificationService.java | 2 -- .../conversations/utils/Compatibility.java | 7 +++++-- src/main/res/values-ar/strings.xml | 2 +- src/main/res/values-bg/strings.xml | 2 +- src/main/res/values-de/strings.xml | 2 +- src/main/res/values-el/strings.xml | 2 +- src/main/res/values-es/strings.xml | 2 +- src/main/res/values-eu/strings.xml | 2 +- src/main/res/values-fr/strings.xml | 2 +- src/main/res/values-gl/strings.xml | 2 +- src/main/res/values-hu/strings.xml | 2 +- src/main/res/values-it/strings.xml | 2 +- src/main/res/values-nl/strings.xml | 2 +- src/main/res/values-pl/strings.xml | 2 +- src/main/res/values-pt-rBR/strings.xml | 2 +- src/main/res/values-ro-rRO/strings.xml | 2 +- src/main/res/values-ru/strings.xml | 2 +- src/main/res/values-sr/strings.xml | 2 +- src/main/res/values-uk/strings.xml | 2 +- src/main/res/values-zh-rCN/strings.xml | 2 +- src/main/res/values/strings.xml | 5 +++-- src/main/res/xml/preferences.xml | 17 +++++++++++++++-- 22 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index ee35e00a8..c4a0fe2e4 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -404,8 +404,6 @@ public class NotificationService { fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString()); fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString()); fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId); - //fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - //fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); return PendingIntent.getActivity(mXmppConnectionService, requestCode, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT); } diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java index 3389d7519..13e38e487 100644 --- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java +++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java @@ -33,7 +33,10 @@ public class Compatibility { "vibrate_on_notification", "call_ringtone" ); - private static final List UNUESD_SETTINGS_PRE_TWENTYSIX = Collections.singletonList("more_notification_settings"); + private static final List UNUESD_SETTINGS_PRE_TWENTYSIX = Arrays.asList( + "message_notification_settings", + "call_notification_settings" + ); public static boolean hasStoragePermission(Context context) { @@ -133,7 +136,7 @@ public class Compatibility { context.startService(intent); } } catch (RuntimeException e) { - Log.d(Config.LOGTAG, context.getClass().getSimpleName()+" was unable to start service"); + Log.d(Config.LOGTAG, context.getClass().getSimpleName() + " was unable to start service"); } } } diff --git a/src/main/res/values-ar/strings.xml b/src/main/res/values-ar/strings.xml index 5673a3a17..2f8560ad1 100644 --- a/src/main/res/values-ar/strings.xml +++ b/src/main/res/values-ar/strings.xml @@ -652,7 +652,7 @@ مشاكل إتّصال رسائل رسائل - إعدادات الإشعار + إعدادات الإشعار الأهمية ، الصوت ، الإهتزاز ضغط الفيديو اعرض الوسائط diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml index da9c5917d..7990bc0a2 100644 --- a/src/main/res/values-bg/strings.xml +++ b/src/main/res/values-bg/strings.xml @@ -700,7 +700,7 @@ Съобщения Тихи съобщения Тази категория известия се използва за показване на известия, които не бива да изпълняват звук. Това може да се използва, например, докато използвате друго устройство (по време на Период на пренебрегване). - Настройки за известията + Настройки за известията Важност, звук, вибрация Компресия на видеото Преглед на медийното съдържание diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 89bd2c179..66001a36e 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -749,7 +749,7 @@ Nachrichten Lautlose Nachrichten Diese Benachrichtigungsart wird verwendet, um Benachrichtigungen anzuzeigen, die keinen Ton auslösen sollen. Zum Beispiel, wenn du auf einem anderen Gerät aktiv bist (Schonfrist). - Benachrichtigungseinstellungen + Benachrichtigungseinstellungen Wichtigkeit, Klang, Vibrationen Video komprimieren Medien anzeigen diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml index d41cf52e2..419912371 100644 --- a/src/main/res/values-el/strings.xml +++ b/src/main/res/values-el/strings.xml @@ -748,7 +748,7 @@ Μηνύματα Σιωπηρά μηνύματα Αυτή η κατηγορία ειδοποιήσεων χρησιμοποιείται για να εμφανίσει ειδοποιήσεις που δεν θα έπρεπε να παράγουν ήχο. Για παράδειγμα όταν κάποιος είναι ενεργός σε άλλη συσκευή (περίοδος χάριτος). - Ρυθμίσεις ειδοποίησης + Ρυθμίσεις ειδοποίησης Σημασία, Ήχος, Δόνηση Συμπίεση βίντεο Εμφάνιση μέσου diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index d62c8f2a4..c791be90f 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -749,7 +749,7 @@ Mensajes Mensajes sin sonido Este grupo de notificaciones se usa para mostrar notificaciones que no deberían emitir ningún sonido. Por ejemplo, cuando estás activo en otro dispositivo (periodo de gracia). - Ajustes de Notificación + Ajustes de Notificación Importancia, Sonido, Vibración Compresión de video Ver galería diff --git a/src/main/res/values-eu/strings.xml b/src/main/res/values-eu/strings.xml index 2e841f7f3..823d559b3 100644 --- a/src/main/res/values-eu/strings.xml +++ b/src/main/res/values-eu/strings.xml @@ -741,7 +741,7 @@ Mezuak Mezu isilak Jakinarazpen talde hau inolako soinurik egin beharko ez luketen jakinarazpenak erakusteko erabiltze da. Adibidez beste gailu batean aktibo zaudenean (grazia epea). - Jakinarazpenen ezarpenak + Jakinarazpenen ezarpenak Garrantzia, soinua, dardara Bideoen konprimatzea Ikusi multimedia diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 37978425b..71abbac9c 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -749,7 +749,7 @@ Messages Messages silencieux Ce groupe de notifications est utilisé pour afficher les notifications qui ne doivent pas émettre de son. Par exemple, lorsque le son est activé sur un autre appareil (délai de grâce). - Options de notification + Options de notification Importance, son, vibration Compression vidéo Voir les média diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 29250175b..34dc2cfdd 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -749,7 +749,7 @@ Mensaxes Mensaxes acalados Este grupo de notificacións é utilizado para mostrar notificacións que non debería activar ningún son. Por exemplo, cando está activo en outro dispositivo (Período de Graza). - Axustes das notificacións + Axustes das notificacións Importancia, Son, Vibrar Compresión de vídeo Ver medios diff --git a/src/main/res/values-hu/strings.xml b/src/main/res/values-hu/strings.xml index d22157e4f..9c9018e39 100644 --- a/src/main/res/values-hu/strings.xml +++ b/src/main/res/values-hu/strings.xml @@ -749,7 +749,7 @@ Üzenetek Csendes üzenetek Ezt az értesítési csoportot olyan értesítések megjelenítéséhez használják, amelyek nem aktiválhatnak hangot. Például ha aktívvá válik egy másik eszközön (türelmi idő). - Értesítési beállítások + Értesítési beállítások Fontosság, hang, rezgés Videó tömörítése Média megtekintése diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 115a22285..13734de4c 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -749,7 +749,7 @@ Messaggi Messaggi silenziosi Questo gruppo di notifiche è usato per mostrare notifiche che non devono riprodurre alcun suono. Ad esempio mentre si è attivi su un altro dispositivo (Periodo di grazia). - Impostazioni di notifica + Impostazioni di notifica Importanza, suono, vibrazione Compressione video Vedi i media diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index 901135d04..97a0920f4 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -745,7 +745,7 @@ Berichten Stille berichten Deze meldingscategorie wordt gebruikt om meldingen weer te geven die geen geluid mogen maken. Bijvoorbeeld, indien actief op een ander apparaat (uitstelperiode). - Meldingsinstellingen + Meldingsinstellingen Belang, geluid, trillen Videocompressie Media bekijken diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index c20fff766..52ef9bdbf 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -767,7 +767,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Wiadomości Ciche wiadomości Ta kategoria powiadomień jest używana aby wyświetlać powiadomienia które nie powodują żadnych dźwięków. Na przykład w ciągu aktywności na innym urządzeniu (okres karencji). - Ustawienia powiadomień + Ustawienia powiadomień Ważność, Dźwięk, Wibracja Kompresja wideo Pokaż media diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 4d29df038..b01d1c3e5 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -749,7 +749,7 @@ Mensagens Silenciar mensagens Essa categoria de notificação é utilizada para exibir notificações que não deveriam gerar nenhum som. Por exemplo, quando estiver ativo em outro dispositivo (Período de Espera). - Configurações de notificações + Configurações de notificações Importância, som, vibração. Compressão de vídeo Ver mídia diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index df5547c25..dd33608b9 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -759,7 +759,7 @@ sau chiar pierderea mesajelor.\nÎn continuare veți fi rugați să dezactivați Mesaje Mesaje silențioase Acest grup de notificări este folosit pentru a arăta notificări care nu emit sunete. De exemplu atunci când sunteți activi pe un alt dispozitiv (Perioada de grație). - Setări notificări + Setări notificări Importanță, sunete, vibrații Compresie video Vizualizare fișiere media diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 24598898e..e4319355a 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -702,7 +702,7 @@ Название конференции Эта конференция была уничтожена Проблемы с подключением - Настройки уведомлений + Настройки уведомлений Сжатие видео Просмотр медиа Участники diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml index aeabe6c0f..9ca1efb64 100644 --- a/src/main/res/values-sr/strings.xml +++ b/src/main/res/values-sr/strings.xml @@ -620,6 +620,6 @@ Поруке Поруке Тихе поруке - Поставке обавештавања + Поставке обавештавања Видео компресија diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index 41ed517d1..2fc9fae7b 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -756,7 +756,7 @@ Повідомлення Тихі повідомлення Ця група сповіщень показує сповіщення, які не повинні супроводжуватися звуком. Наприклад, у разі активності на іншому пристрої (період очікування). - Налаштування сповіщень + Налаштування сповіщень Важливість, звук, вібрація Стиснення відео Перегляд медіа diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 40013308e..a9ca38fbb 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -742,7 +742,7 @@ 消息 无声消息 此通知组用于显示不应触发任何声音的通知。 例如,当在另一个设备上激活时(宽限期)。 - 通知设置 + 通知设置 重要性,声音,振动 视频压缩 查看媒体文件 diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 358d768dd..6f7b4a551 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -651,7 +651,7 @@ Corresponding conversations closed. Contact blocked. Notifications from strangers - Notify for messages received from strangers. + Notify for messages and calls received from strangers. Received message from stranger Block stranger Block entire domain @@ -756,7 +756,8 @@ Ongoing calls Silent messages This notification group is used to display notifications that should not trigger any sound. For example when being active on another device (Grace Period). - Notification Settings + Message notification settings + Incoming calls notification settings Importance, Sound, Vibrate Video compression View media diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index 3ef7b10b2..f2b2943b1 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -101,9 +101,9 @@ + android:title="@string/pref_message_notification_settings"> + + + + + + Date: Mon, 20 Apr 2020 12:32:56 +0200 Subject: [PATCH 142/182] automatically reject/ignore calls from strangers if the setting is set --- .../services/NotificationService.java | 2 +- .../xmpp/jingle/JingleConnectionManager.java | 22 ++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index c4a0fe2e4..9f1617b56 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -224,7 +224,7 @@ public class NotificationService { && (!conversation.isWithStranger() || notificationsFromStrangers()); } - private boolean notificationsFromStrangers() { + public boolean notificationsFromStrangers() { return mXmppConnectionService.getBooleanPreference("notifications_from_strangers", R.bool.notifications_from_strangers); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 468b8d8a2..5fd89aea2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -27,6 +27,7 @@ import java.util.concurrent.TimeUnit; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; @@ -87,8 +88,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { 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)); - if (isBusy() || sessionEnded) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": rejected session with " + id.with + " because busy. sessionEnded=" + sessionEnded); + 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); mXmppConnectionService.sendIqPacket(account, packet.generateResponse(IqPacket.TYPE.RESULT), null); final JinglePacket sessionTermination = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); sessionTermination.setTo(id.with); @@ -124,6 +126,15 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } + private boolean isWithStrangerAndStrangerNotificationsAreOff(final Account account, Jid with) { + final boolean notifyForStrangers = mXmppConnectionService.getNotificationService().notificationsFromStrangers(); + if (notifyForStrangers) { + return false; + } + final Contact contact = account.getRoster().getContact(with); + return !contact.showInContactList(); + } + public void respondWithJingleError(final Account account, final IqPacket original, String jingleCondition, String condition, String conditionType) { final IqPacket response = original.generateResponse(IqPacket.TYPE.ERROR); final Element error = response.addChild("error"); @@ -205,8 +216,13 @@ public class JingleConnectionManager extends AbstractConnectionManager { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": encountered unknown media in session proposal. " + propose); return; } - if (isBusy()) { + final boolean stranger = isWithStrangerAndStrangerNotificationsAreOff(account, id.with); + if (isBusy() || stranger) { writeLogMissedIncoming(account, id.with.asBareJid(), id.sessionId, serverMsgId, timestamp); + if (stranger) { + Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": ignoring call proposal from stranger "+id.with); + return; + } final int activeDevices = account.countPresences(); Log.d(Config.LOGTAG, "active devices: " + activeDevices); if (activeDevices == 0) { From 187dff3df930dba56c3166e0aa1ef8f62331016c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 20 Apr 2020 13:08:43 +0200 Subject: [PATCH 143/182] put contact picture in incoming call notification --- .../eu/siacs/conversations/services/NotificationService.java | 4 ++++ .../conversations/xmpp/jingle/AbstractJingleConnection.java | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 9f1617b56..ac1efaaca 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -351,6 +351,10 @@ public class NotificationService { builder.setSmallIcon(R.drawable.ic_call_white_24dp); builder.setContentTitle(mXmppConnectionService.getString(R.string.rtp_state_incoming_call)); } + builder.setLargeIcon(mXmppConnectionService.getAvatarService().get( + id.getContact(), + AvatarService.getSystemUiAvatarSize(mXmppConnectionService)) + ); builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName()); builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); builder.setPriority(NotificationCompat.PRIORITY_HIGH); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java index b6e160898..088b4fc17 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -4,6 +4,7 @@ import com.google.common.base.Objects; import com.google.common.base.Preconditions; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; @@ -68,6 +69,10 @@ public abstract class AbstractJingleConnection { ); } + public Contact getContact() { + return account.getRoster().getContact(with); + } + @Override public boolean equals(Object o) { if (this == o) return true; From 23d1ee5e41b400a10d8171ebcb43e5409ac71749 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 20 Apr 2020 13:41:50 +0200 Subject: [PATCH 144/182] =?UTF-8?q?render=20contact=E2=80=99s=20avatar=20d?= =?UTF-8?q?uring=20incoming=20call?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/ContactDetailsActivity.java | 10 +++++----- .../conversations/ui/RtpSessionActivity.java | 19 +++++++++++++++++++ src/main/res/layout/activity_rtp_session.xml | 19 ++++++++++++++++++- src/main/res/values-h360dp/dimens.xml | 1 + src/main/res/values-h500dp/dimens.xml | 1 + src/main/res/values-land/bools.xml | 4 ++++ src/main/res/values/bools.xml | 4 ++++ src/main/res/values/dimens.xml | 1 + 8 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 src/main/res/values-land/bools.xml create mode 100644 src/main/res/values/bools.xml diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 086ab7101..2d4129bbd 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -207,7 +207,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp }); binding.addContactButton.setOnClickListener(v -> showAddToRosterDialog(contact)); - mMediaAdapter = new MediaAdapter(this,R.dimen.media_size); + mMediaAdapter = new MediaAdapter(this, R.dimen.media_size); this.binding.media.setAdapter(mMediaAdapter); GridManager.setupLayoutManager(this, this.binding.media, R.dimen.media_size); } @@ -416,7 +416,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp account = contact.getAccount().getJid().asBareJid().toString(); } binding.detailsAccount.setText(getString(R.string.using_account, account)); - AvatarWorkerTask.loadAvatar(contact,binding.detailsContactBadge,R.dimen.avatar_on_details_screen_size); + AvatarWorkerTask.loadAvatar(contact, binding.detailsContactBadge, R.dimen.avatar_on_details_screen_size); binding.detailsContactBadge.setOnClickListener(this.onBadgeClick); binding.detailsContactKeys.removeAllViews(); @@ -426,7 +426,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp if (Config.supportOmemo() && axolotlService != null) { final Collection sessions = axolotlService.findSessionsForContact(contact); boolean anyActive = false; - for(XmppAxolotlSession session : sessions) { + for (XmppAxolotlSession session : sessions) { anyActive = session.getTrust().isActive(); if (anyActive) { break; @@ -434,7 +434,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp } boolean skippedInactive = false; boolean showsInactive = false; - for (final XmppAxolotlSession session :sessions) { + for (final XmppAxolotlSession session : sessions) { final FingerprintStatus trust = session.getTrust(); hasKeys |= !trust.isCompromised(); if (!trust.isActive() && anyActive) { @@ -537,7 +537,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp public void onMediaLoaded(List attachments) { runOnUiThread(() -> { int limit = GridManager.getCurrentColumnCount(binding.media); - mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit,attachments.size()))); + mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit, attachments.size()))); binding.mediaWrapper.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE); }); diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index f0cae4e53..f85ad3f4e 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -37,6 +37,7 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.util.AvatarWorkerTask; import eu.siacs.conversations.utils.PermissionUtils; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; @@ -248,6 +249,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe RtpEndUserState state = RtpEndUserState.valueOf(extraLastState); updateButtonConfiguration(state); updateStateDisplay(state); + updateProfilePicture(state); } binding.with.setText(account.getRoster().getContact(with).getDisplayName()); } @@ -361,6 +363,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe updateVideoViews(currentState); updateStateDisplay(currentState); updateButtonConfiguration(currentState); + updateProfilePicture(currentState); } private void reInitializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) { @@ -427,6 +430,20 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } + private void updateProfilePicture(final RtpEndUserState state) { + if (state == RtpEndUserState.INCOMING_CALL || state == RtpEndUserState.ACCEPTING_CALL) { + final boolean show = getResources().getBoolean(R.bool.show_avatar_incoming_call); + if (show) { + binding.contactPhoto.setVisibility(View.VISIBLE); + AvatarWorkerTask.loadAvatar(getWith(), binding.contactPhoto, R.dimen.publish_avatar_size); + } else { + binding.contactPhoto.setVisibility(View.GONE); + } + } else { + binding.contactPhoto.setVisibility(View.GONE); + } + } + private Set getMedia() { return requireRtpConnection().getMedia(); } @@ -720,6 +737,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe updateStateDisplay(state); updateButtonConfiguration(state); updateVideoViews(state); + updateProfilePicture(state); }); } else { Log.d(Config.LOGTAG, "received update for other rtp session"); @@ -762,6 +780,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe runOnUiThread(() -> { updateStateDisplay(state); updateButtonConfiguration(state); + updateProfilePicture(state); }); resetIntent(account, with, state, actionToMedia(currentIntent.getAction())); } diff --git a/src/main/res/layout/activity_rtp_session.xml b/src/main/res/layout/activity_rtp_session.xml index 391adba1d..47bfa95ca 100644 --- a/src/main/res/layout/activity_rtp_session.xml +++ b/src/main/res/layout/activity_rtp_session.xml @@ -62,6 +62,22 @@ + + + + + + 16dp 128dp + 64dp diff --git a/src/main/res/values-h500dp/dimens.xml b/src/main/res/values-h500dp/dimens.xml index a153bff54..40e920fd0 100644 --- a/src/main/res/values-h500dp/dimens.xml +++ b/src/main/res/values-h500dp/dimens.xml @@ -1,4 +1,5 @@ 24dp 192dp + 96dp diff --git a/src/main/res/values-land/bools.xml b/src/main/res/values-land/bools.xml new file mode 100644 index 000000000..1aa953fcc --- /dev/null +++ b/src/main/res/values-land/bools.xml @@ -0,0 +1,4 @@ + + + false + diff --git a/src/main/res/values/bools.xml b/src/main/res/values/bools.xml new file mode 100644 index 000000000..0799afb3f --- /dev/null +++ b/src/main/res/values/bools.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file diff --git a/src/main/res/values/dimens.xml b/src/main/res/values/dimens.xml index ae22105c8..215f11a14 100644 --- a/src/main/res/values/dimens.xml +++ b/src/main/res/values/dimens.xml @@ -19,6 +19,7 @@ 8dp 96dp + 48dp 32dp 48dp 56dp From f858412d72e21b65a7ad9fbd3410484350bef80c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 20 Apr 2020 16:19:31 +0200 Subject: [PATCH 145/182] version bump to 2.8.0-rc.1 + changelog --- CHANGELOG.md | 5 +++++ build.gradle | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 091e36daf..3d5d92537 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### Version 2.8.0 + +* Audio/Video calls (Requires server support in form of STUN and TURN servers discoverable via XEP-0215) + + ### Version 2.7.1 * Fix avatar selection on some Android 10 devices diff --git a/build.gradle b/build.gradle index 1109541a0..90218ea16 100644 --- a/build.gradle +++ b/build.gradle @@ -95,8 +95,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 28 - versionCode 374 - versionName "2.8.0-beta.3" + versionCode 375 + versionName "2.8.0-rc.1" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 7898ba65cda7c0fe2339a7cb2c114a0fb2fb4370 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 20 Apr 2020 17:05:27 +0200 Subject: [PATCH 146/182] extend extended webrtcwrapper logging --- .../conversations/xmpp/jingle/WebRTCWrapper.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index e987be4ef..defe85291 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -28,9 +28,11 @@ import org.webrtc.EglBase; import org.webrtc.IceCandidate; import org.webrtc.MediaConstraints; import org.webrtc.MediaStream; +import org.webrtc.MediaStreamTrack; import org.webrtc.PeerConnection; import org.webrtc.PeerConnectionFactory; import org.webrtc.RtpReceiver; +import org.webrtc.RtpTransceiver; import org.webrtc.SdpObserver; import org.webrtc.SessionDescription; import org.webrtc.SurfaceTextureHelper; @@ -68,7 +70,7 @@ public class WebRTCWrapper { private final PeerConnection.Observer peerConnectionObserver = new PeerConnection.Observer() { @Override public void onSignalingChange(PeerConnection.SignalingState signalingState) { - Log.d(Config.LOGTAG, "onSignalingChange(" + signalingState + ")"); + Log.d(EXTENDED_LOGGING_TAG, "onSignalingChange(" + signalingState + ")"); //this is called after removeTrack or addTrack //and should then trigger a content-add or content-remove or something //https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/removeTrack @@ -112,6 +114,7 @@ public class WebRTCWrapper { @Override public void onAddStream(MediaStream mediaStream) { + Log.d(EXTENDED_LOGGING_TAG, "onAddStream(numAudioTracks=" + mediaStream.audioTracks.size() + ",numVideoTracks=" + mediaStream.videoTracks.size() + ")"); final List videoTracks = mediaStream.videoTracks; if (videoTracks.size() > 0) { remoteVideoTrack = videoTracks.get(0); @@ -138,8 +141,13 @@ public class WebRTCWrapper { @Override public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { - Log.d(Config.LOGTAG, "onAddTrack()"); + final MediaStreamTrack track = rtpReceiver.track(); + Log.d(EXTENDED_LOGGING_TAG, "onAddTrack(kind=" + (track == null ? "null" : track.kind()) + ",numMediaStreams=" + mediaStreams.length + ")"); + } + @Override + public void onTrack(RtpTransceiver transceiver) { + Log.d(EXTENDED_LOGGING_TAG, "onTrack(mid=" + transceiver.getMid() + ",media=" + transceiver.getMediaType() + ")"); } }; @Nullable From 8b79808f027ac2278636ab54ce2c71b1318a8df7 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 20 Apr 2020 21:09:37 +0200 Subject: [PATCH 147/182] try to stfu travis --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 7ded45126..fb2c6866e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,8 @@ android: - extra-google-google_play_services licenses: - '.+' +before_script: + - wget -O libs/libwebrtc-m81.aar http://gultsch.de/files/libwebrtc-m81.aar script: - ./gradlew assembleConversationsFreeSystemRelease - ./gradlew assembleQuicksyFreeCompatRelease From d5e3d13158d945e0aabb549e554611580bb91d84 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 21 Apr 2020 09:11:17 +0200 Subject: [PATCH 148/182] do not just assume rtcp-mux --- .../conversations/xmpp/jingle/SessionDescription.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index b7400b5b4..aa33ae41c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -14,6 +14,7 @@ import java.util.Locale; import java.util.Map; import eu.siacs.conversations.Config; +import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; @@ -53,11 +54,6 @@ public class SessionDescription { } } - public static SessionDescription parse(final Map contents) { - final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder(); - return sessionDescriptionBuilder.createSessionDescription(); - } - public static SessionDescription parse(final String input) { final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder(); MediaBuilder currentMediaBuilder = null; @@ -251,7 +247,10 @@ public class SessionDescription { //random additional attributes mediaAttributes.put("rtcp", "9 IN IP4 0.0.0.0"); mediaAttributes.put("sendrecv", ""); - mediaAttributes.put("rtcp-mux", ""); + + if (description.hasChild("rtcp-mux", Namespace.JINGLE_APPS_RTP)) { + mediaAttributes.put("rtcp-mux", ""); + } final MediaBuilder mediaBuilder = new MediaBuilder(); mediaBuilder.setMedia(description.getMedia().toString().toLowerCase(Locale.ROOT)); From eb911b8196400ae97c27f66b52950c15c2bf188a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 21 Apr 2020 11:40:05 +0200 Subject: [PATCH 149/182] show 215 status in server info --- .../conversations/ui/EditAccountActivity.java | 7 ++++-- .../conversations/xmpp/XmppConnection.java | 2 +- .../xmpp/jingle/JingleRtpConnection.java | 2 +- src/main/res/layout/activity_edit_account.xml | 22 +++++++++++++++++++ src/main/res/values/strings.xml | 1 + 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index cc3b22be0..9e829e041 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -3,7 +3,6 @@ package eu.siacs.conversations.ui; import android.app.Activity; import android.app.PendingIntent; import android.content.ActivityNotFoundException; -import android.content.Context; import android.content.Intent; import android.content.IntentSender; import android.content.SharedPreferences; @@ -63,7 +62,6 @@ import eu.siacs.conversations.ui.util.AvatarWorkerTask; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; import eu.siacs.conversations.ui.util.PendingItem; import eu.siacs.conversations.ui.util.SoftKeyboardUtils; -import eu.siacs.conversations.ui.util.StyledAttributes; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.Resolver; import eu.siacs.conversations.utils.SignupUtils; @@ -1060,6 +1058,11 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } else { this.binding.serverInfoSm.setText(R.string.server_info_unavailable); } + if (features.externalServiceDiscovery()) { + this.binding.serverInfoExternalService.setText(R.string.server_info_available); + } else { + this.binding.serverInfoExternalService.setText(R.string.server_info_unavailable); + } if (features.pep()) { AxolotlService axolotlService = this.mAccount.getAxolotlService(); if (axolotlService != null && axolotlService.isPepBroken()) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 61a156f69..73f58a6c8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1903,7 +1903,7 @@ public class XmppConnection implements Runnable { return Config.USE_BOOKMARKS2 /* || hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT)*/; } - public boolean extendedServiceDiscovery() { + public boolean externalServiceDiscovery() { return hasDiscoFeature(Jid.of(account.getServer()),Namespace.EXTERNAL_SERVICE_DISCOVERY); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 39c8bef48..f183f7ee9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -999,7 +999,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void discoverIceServers(final OnIceServersDiscovered onIceServersDiscovered) { - if (id.account.getXmppConnection().getFeatures().extendedServiceDiscovery()) { + if (id.account.getXmppConnection().getFeatures().externalServiceDiscovery()) { final IqPacket request = new IqPacket(IqPacket.TYPE.GET); request.setTo(Jid.of(id.account.getJid().getDomain())); request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); diff --git a/src/main/res/layout/activity_edit_account.xml b/src/main/res/layout/activity_edit_account.xml index 4b7a03e8a..8741740ad 100644 --- a/src/main/res/layout/activity_edit_account.xml +++ b/src/main/res/layout/activity_edit_account.xml @@ -338,6 +338,28 @@ tools:ignore="RtlHardcoded"/> + + + + + + + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 6f7b4a551..32ade6387 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -192,6 +192,7 @@ XEP-0191: Blocking Command XEP-0237: Roster Versioning XEP-0198: Stream Management + XEP-0215: External Service Discovery XEP-0163: PEP (Avatars / OMEMO) XEP-0363: HTTP File Upload XEP-0357: Push From 5b12e23382ced9b7c7ff14a43010c0c368473385 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 21 Apr 2020 12:00:13 +0200 Subject: [PATCH 150/182] improve logging for throws from native callbacks --- .../xmpp/jingle/JingleConnectionManager.java | 8 +++++--- .../eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 5fd89aea2..b25647854 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -90,7 +90,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { final boolean sessionEnded = this.endedSessions.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); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": rejected session with " + id.with + " because busy. sessionEnded=" + sessionEnded + ", stranger=" + stranger); mXmppConnectionService.sendIqPacket(account, packet.generateResponse(IqPacket.TYPE.RESULT), null); final JinglePacket sessionTermination = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); sessionTermination.setTo(id.with); @@ -220,7 +220,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (isBusy() || stranger) { writeLogMissedIncoming(account, id.with.asBareJid(), id.sessionId, serverMsgId, timestamp); if (stranger) { - Log.d(Config.LOGTAG,id.account.getJid().asBareJid()+": ignoring call proposal from stranger "+id.with); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring call proposal from stranger " + id.with); return; } final int activeDevices = account.countPresences(); @@ -545,7 +545,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (connections.containsValue(connection)) { return; } - throw new IllegalStateException("JingleConnection has not been registered with connection manager"); + final IllegalStateException e = new IllegalStateException("JingleConnection has not been registered with connection manager"); + Log.e(Config.LOGTAG, "ensureConnectionIsRegistered() failed. Going to throw", e); + throw e; } public void endSession(AbstractJingleConnection.Id id, final AbstractJingleConnection.State state) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index defe85291..cb4deed2c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -254,7 +254,9 @@ public class WebRTCWrapper { || this.eglBase != null || this.localVideoTrack != null || this.remoteVideoTrack != null) { - throw new IllegalStateException("WebRTCWrapper hasn't been closed properly"); + final IllegalStateException e = new IllegalStateException("WebRTCWrapper hasn't been closed properly"); + Log.e(Config.LOGTAG, "verifyClosed() failed. Going to throw", e); + throw e; } } From 442b952700c7dd4cdce50d0b608f4ac62ab5628a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 21 Apr 2020 14:59:03 +0200 Subject: [PATCH 151/182] add jingle message init namespace to features --- .../generator/AbstractGenerator.java | 222 +++++++++--------- 1 file changed, 110 insertions(+), 112 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index d478a252e..7a3ce765d 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -23,127 +23,125 @@ import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; public abstract class AbstractGenerator { - private final String[] FEATURES = { - Namespace.JINGLE, + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + private final String[] FEATURES = { + Namespace.JINGLE, - //Jingle File Transfer - FileTransferDescription.Version.FT_3.getNamespace(), - FileTransferDescription.Version.FT_4.getNamespace(), - FileTransferDescription.Version.FT_5.getNamespace(), - Namespace.JINGLE_TRANSPORTS_S5B, - Namespace.JINGLE_TRANSPORTS_IBB, - Namespace.JINGLE_ENCRYPTED_TRANSPORT, - Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO, - "http://jabber.org/protocol/muc", - "jabber:x:conference", - Namespace.OOB, - "http://jabber.org/protocol/caps", - "http://jabber.org/protocol/disco#info", - "urn:xmpp:avatar:metadata+notify", - Namespace.NICK+"+notify", - "urn:xmpp:ping", - "jabber:iq:version", - "http://jabber.org/protocol/chatstates" - }; - private final String[] MESSAGE_CONFIRMATION_FEATURES = { - "urn:xmpp:chat-markers:0", - "urn:xmpp:receipts" - }; - private final String[] MESSAGE_CORRECTION_FEATURES = { - "urn:xmpp:message-correct:0" - }; - private final String[] PRIVACY_SENSITIVE = { - "urn:xmpp:time" //XEP-0202: Entity Time leaks time zone - }; + //Jingle File Transfer + FileTransferDescription.Version.FT_3.getNamespace(), + FileTransferDescription.Version.FT_4.getNamespace(), + FileTransferDescription.Version.FT_5.getNamespace(), + Namespace.JINGLE_TRANSPORTS_S5B, + Namespace.JINGLE_TRANSPORTS_IBB, + Namespace.JINGLE_ENCRYPTED_TRANSPORT, + Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO, + "http://jabber.org/protocol/muc", + "jabber:x:conference", + Namespace.OOB, + "http://jabber.org/protocol/caps", + "http://jabber.org/protocol/disco#info", + "urn:xmpp:avatar:metadata+notify", + Namespace.NICK + "+notify", + "urn:xmpp:ping", + "jabber:iq:version", + "http://jabber.org/protocol/chatstates" + }; + private final String[] MESSAGE_CONFIRMATION_FEATURES = { + "urn:xmpp:chat-markers:0", + "urn:xmpp:receipts" + }; + private final String[] MESSAGE_CORRECTION_FEATURES = { + "urn:xmpp:message-correct:0" + }; + private final String[] PRIVACY_SENSITIVE = { + "urn:xmpp:time" //XEP-0202: Entity Time leaks time zone + }; + private final String[] VOIP_NAMESPACES = { + Namespace.JINGLE_TRANSPORT_ICE_UDP, + Namespace.JINGLE_FEATURE_AUDIO, + Namespace.JINGLE_FEATURE_VIDEO, + Namespace.JINGLE_APPS_RTP, + Namespace.JINGLE_APPS_DTLS, + Namespace.JINGLE_MESSAGE + }; + protected XmppConnectionService mXmppConnectionService; + private String mVersion = null; - private final String[] VOIP_NAMESPACES = { - Namespace.JINGLE_TRANSPORT_ICE_UDP, - Namespace.JINGLE_FEATURE_AUDIO, - Namespace.JINGLE_FEATURE_VIDEO, - Namespace.JINGLE_APPS_RTP, - Namespace.JINGLE_APPS_DTLS, - }; - private String mVersion = null; + AbstractGenerator(XmppConnectionService service) { + this.mXmppConnectionService = service; + } - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + public static String getTimestamp(long time) { + DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); + return DATE_FORMAT.format(time); + } - protected XmppConnectionService mXmppConnectionService; + String getIdentityVersion() { + if (mVersion == null) { + this.mVersion = PhoneHelper.getVersionName(mXmppConnectionService); + } + return this.mVersion; + } - AbstractGenerator(XmppConnectionService service) { - this.mXmppConnectionService = service; - } + String getIdentityName() { + return mXmppConnectionService.getString(R.string.app_name); + } - String getIdentityVersion() { - if (mVersion == null) { - this.mVersion = PhoneHelper.getVersionName(mXmppConnectionService); - } - return this.mVersion; - } + public String getUserAgent() { + return mXmppConnectionService.getString(R.string.app_name) + '/' + getIdentityVersion(); + } - String getIdentityName() { - return mXmppConnectionService.getString(R.string.app_name); - } + String getIdentityType() { + if ("chromium".equals(android.os.Build.BRAND)) { + return "pc"; + } else { + return mXmppConnectionService.getString(R.string.default_resource).toLowerCase(); + } + } - public String getUserAgent() { - return mXmppConnectionService.getString(R.string.app_name) + '/' + getIdentityVersion(); - } + String getCapHash(final Account account) { + StringBuilder s = new StringBuilder(); + s.append("client/").append(getIdentityType()).append("//").append(getIdentityName()).append('<'); + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + return null; + } - String getIdentityType() { - if ("chromium".equals(android.os.Build.BRAND)) { - return "pc"; - } else { - return mXmppConnectionService.getString(R.string.default_resource).toLowerCase(); - } - } + for (String feature : getFeatures(account)) { + s.append(feature).append('<'); + } + final byte[] sha1 = md.digest(s.toString().getBytes()); + return Base64.encodeToString(sha1, Base64.NO_WRAP); + } - String getCapHash(final Account account) { - StringBuilder s = new StringBuilder(); - s.append("client/").append(getIdentityType()).append("//").append(getIdentityName()).append('<'); - MessageDigest md; - try { - md = MessageDigest.getInstance("SHA-1"); - } catch (NoSuchAlgorithmException e) { - return null; - } + public List getFeatures(Account account) { + final XmppConnection connection = account.getXmppConnection(); + final ArrayList features = new ArrayList<>(Arrays.asList(FEATURES)); + if (mXmppConnectionService.confirmMessages()) { + features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES)); + } + if (mXmppConnectionService.allowMessageCorrection()) { + features.addAll(Arrays.asList(MESSAGE_CORRECTION_FEATURES)); + } + if (Config.supportOmemo()) { + features.add(AxolotlService.PEP_DEVICE_LIST_NOTIFY); + } + if (!mXmppConnectionService.useTorToConnect() && !account.isOnion()) { + features.addAll(Arrays.asList(PRIVACY_SENSITIVE)); + features.addAll(Arrays.asList(VOIP_NAMESPACES)); + } + if (mXmppConnectionService.broadcastLastActivity()) { + features.add(Namespace.IDLE); + } + if (connection != null && connection.getFeatures().bookmarks2()) { + features.add(Namespace.BOOKMARKS2 + "+notify"); + } else { + features.add(Namespace.BOOKMARKS + "+notify"); + } - for (String feature : getFeatures(account)) { - s.append(feature).append('<'); - } - final byte[] sha1 = md.digest(s.toString().getBytes()); - return Base64.encodeToString(sha1, Base64.NO_WRAP); - } - - public static String getTimestamp(long time) { - DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); - return DATE_FORMAT.format(time); - } - - public List getFeatures(Account account) { - final XmppConnection connection = account.getXmppConnection(); - final ArrayList features = new ArrayList<>(Arrays.asList(FEATURES)); - if (mXmppConnectionService.confirmMessages()) { - features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES)); - } - if (mXmppConnectionService.allowMessageCorrection()) { - features.addAll(Arrays.asList(MESSAGE_CORRECTION_FEATURES)); - } - if (Config.supportOmemo()) { - features.add(AxolotlService.PEP_DEVICE_LIST_NOTIFY); - } - if (!mXmppConnectionService.useTorToConnect() && !account.isOnion()) { - features.addAll(Arrays.asList(PRIVACY_SENSITIVE)); - features.addAll(Arrays.asList(VOIP_NAMESPACES)); - } - if (mXmppConnectionService.broadcastLastActivity()) { - features.add(Namespace.IDLE); - } - if (connection != null && connection.getFeatures().bookmarks2()) { - features.add(Namespace.BOOKMARKS2 +"+notify"); - } else { - features.add(Namespace.BOOKMARKS+"+notify"); - } - - Collections.sort(features); - return features; - } + Collections.sort(features); + return features; + } } From 3c0b3f4b94c83048c8c14844f4d4a41b2b5d422c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 21 Apr 2020 18:13:09 +0200 Subject: [PATCH 152/182] allow dnd overwrite for incoming calls --- .../services/NotificationService.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index ac1efaaca..ce2c8d6c9 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -72,6 +72,9 @@ public class NotificationService { private static final int LED_COLOR = 0xff00ff00; + private static final int CALL_DAT = 120; + private static final long[] CALL_PATTERN = {0, 3 * CALL_DAT, CALL_DAT, CALL_DAT, 3 * CALL_DAT, CALL_DAT, CALL_DAT}; + private static final String CONVERSATIONS_GROUP = "eu.siacs.conversations"; private static final int NOTIFICATION_ID_MULTIPLIER = 1024 * 1024; static final int FOREGROUND_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 4; @@ -167,6 +170,9 @@ public class NotificationService { incomingCallsChannel.setLightColor(LED_COLOR); incomingCallsChannel.enableLights(true); incomingCallsChannel.setGroup("calls"); + incomingCallsChannel.setBypassDnd(true); + incomingCallsChannel.enableVibration(true); + incomingCallsChannel.setVibrationPattern(CALL_PATTERN); notificationManager.createNotificationChannel(incomingCallsChannel); final NotificationChannel ongoingCallsChannel = new NotificationChannel("ongoing_calls", @@ -351,10 +357,15 @@ public class NotificationService { builder.setSmallIcon(R.drawable.ic_call_white_24dp); builder.setContentTitle(mXmppConnectionService.getString(R.string.rtp_state_incoming_call)); } + final Contact contact = id.getContact(); builder.setLargeIcon(mXmppConnectionService.getAvatarService().get( - id.getContact(), + contact, AvatarService.getSystemUiAvatarSize(mXmppConnectionService)) ); + final Uri systemAccount = contact.getSystemAccount(); + if (systemAccount != null) { + builder.addPerson(systemAccount.toString()); + } builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName()); builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); builder.setPriority(NotificationCompat.PRIORITY_HIGH); @@ -579,10 +590,8 @@ public class NotificationService { final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); final Resources resources = mXmppConnectionService.getResources(); final String ringtone = preferences.getString("call_ringtone", resources.getString(R.string.incoming_call_ringtone)); - final int dat = 70; - final long[] pattern = {0, 3 * dat, dat, dat, 3 * dat, dat, dat}; - mBuilder.setVibrate(pattern); - Uri uri = Uri.parse(ringtone); + mBuilder.setVibrate(CALL_PATTERN); + final Uri uri = Uri.parse(ringtone); try { mBuilder.setSound(fixRingtoneUri(uri)); } catch (SecurityException e) { From 62c50d0089fe93976474cbd49169809cae89d231 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 21 Apr 2020 18:19:47 +0200 Subject: [PATCH 153/182] pulled translations from transifex --- src/conversations/res/values-uk/strings.xml | 5 ++- src/main/res/values-ar/strings.xml | 4 --- src/main/res/values-bg/strings.xml | 13 ++++--- src/main/res/values-ca/strings.xml | 3 -- src/main/res/values-cs/strings.xml | 32 +++++++++++++++-- src/main/res/values-de/strings.xml | 7 ++-- src/main/res/values-el/strings.xml | 4 --- src/main/res/values-es/strings.xml | 5 +-- src/main/res/values-eu/strings.xml | 17 ++++++--- src/main/res/values-fa-rIR/strings.xml | 2 -- src/main/res/values-fr/strings.xml | 4 --- src/main/res/values-gl/strings.xml | 9 ++--- src/main/res/values-hu/strings.xml | 4 --- src/main/res/values-id/strings.xml | 2 -- src/main/res/values-it/strings.xml | 5 +-- src/main/res/values-ja/strings.xml | 3 -- src/main/res/values-ko/strings.xml | 3 -- src/main/res/values-nb-rNO/strings.xml | 3 -- src/main/res/values-nl/strings.xml | 4 --- src/main/res/values-pl/strings.xml | 5 +-- src/main/res/values-pt-rBR/strings.xml | 5 +-- src/main/res/values-pt/strings.xml | 2 -- src/main/res/values-ro-rRO/strings.xml | 5 +-- src/main/res/values-ru/strings.xml | 4 --- src/main/res/values-sr/strings.xml | 4 --- src/main/res/values-sv/strings.xml | 3 -- src/main/res/values-tr-rTR/strings.xml | 3 -- src/main/res/values-uk/strings.xml | 39 ++++++++++++++++++--- src/main/res/values-vi/strings.xml | 2 -- src/main/res/values-zh-rCN/strings.xml | 9 ++--- src/main/res/values-zh-rTW/strings.xml | 3 -- src/quicksy/res/values-uk/strings.xml | 6 +++- 32 files changed, 110 insertions(+), 109 deletions(-) diff --git a/src/conversations/res/values-uk/strings.xml b/src/conversations/res/values-uk/strings.xml index 0045a6577..145e2043a 100644 --- a/src/conversations/res/values-uk/strings.xml +++ b/src/conversations/res/values-uk/strings.xml @@ -5,4 +5,7 @@ Створити новий обліковий запис Вже маєте обліковий запис XMPP? Можливо, користуєтеся іншою програмою XMPP або користувалися цією програмою раніше. Якщо ні, можете створити новий обліковий запис XMPP просто зараз.\nЗверніть увагу: Деякі постачальники електронної пошти водночас надають облікові записи XMPP. XMPP — це мережа обміну повідомленнями, незалежна від постачальників. Можете використовувати цю програму з будь-яким XMPP сервером, який оберете.\nПроте, для зручності, ми спростили створення облікового запису на conversations.im¹ — в постачальника, який спеціально налаштований на роботу з цією програмою. - \ No newline at end of file + Вас запросили до %1$s. Ми проведемо Вас крок за кроком, щоб створити обліковий запис.\nОбираючи %1$s в якості свого постачальника, Ви зможете спілкуватися з користувачами інших постачальників, повідомивши їм свою повну адресу XMPP. + Вас запросили до %1$s. Для Вас уже обрали ім\'я користувача. Ми проведемо Вас крок за кроком, щоб створити обліковий запис.\nВи зможете спілкуватися з користувачами інших постачальників, повідомивши їм свою повну адресу XMPP. + Ваше запрошення до сервера + \ No newline at end of file diff --git a/src/main/res/values-ar/strings.xml b/src/main/res/values-ar/strings.xml index 2f8560ad1..0b62c1f26 100644 --- a/src/main/res/values-ar/strings.xml +++ b/src/main/res/values-ar/strings.xml @@ -111,8 +111,6 @@ إعداد الإهتزاز إهتز عند وصول رسالة جديدة إشعار ضوئي - التنبيه الصوتي - أصدر صوتاً حال ما تصل رسالة جديدة فترة السماح متقدم لا ترسل تقارير أخطاء @@ -584,7 +582,6 @@ ضغط الفيديو جهة الاتصال محجوبة. الإشعارات من طرف غرباء - القيام بإخطاري عندما أتلقى رسائل مِن طرف غُرباء. لقد تلقيت رسالة من شخص غريب حظر الغريب حظر إسم النطاق كاملا @@ -652,7 +649,6 @@ مشاكل إتّصال رسائل رسائل - إعدادات الإشعار الأهمية ، الصوت ، الإهتزاز ضغط الفيديو اعرض الوسائط diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml index 7990bc0a2..82d928046 100644 --- a/src/main/res/values-bg/strings.xml +++ b/src/main/res/values-bg/strings.xml @@ -112,8 +112,6 @@ Вибриране при получаване на ново съобщение Известие чрез светодиода Мигане на индикаторния светодиод при получаване на ново съобщение - Тон на звънене - Изпълнение на звук при получаване на ново съобщение Период на пренебрегване Разширени Никога да не се изпращат доклади за сривове @@ -170,8 +168,10 @@ Сигурни ли сте? Ако изтриете профила си, ще загубите цялата история на разговорите си Запис на глас + XMPP адрес username@example.com Парола + Това не е валиден XMPP адрес Няма достатъчно памет. Изображението е твърде голямо. Искате ли да добавите %s към адресния си указател? Инф. за сървъра @@ -343,8 +343,11 @@ Отхвърлен Член Разширен режим + Дай членски привилегии + Премахни членски привилегии Даване на администраторски права Отмяна на администраторските права + Дай права на собственик Премахване от груповия разговор Неуспешна промяна на принадлежността на %s Забраняване на достъпа до груповия разговор @@ -485,7 +488,7 @@ Споделяне на адреса с… Съгласяване и продължаване Създаване на профил - Използване на собствен сървър + Използване на собствен доставчик Изберете потребителското си име Ръчна промяна на присъствието Задайте присъствието си, когато редактирате съобщението за състоянието си. @@ -600,7 +603,6 @@ Съответстващите разговори са затворени. Контактът е блокиран. Известия от непознати - Известяване за съобщения от непознати. Получено е съобщение от непознат Блокиране на непознатия Блокиране на целия домейн @@ -700,7 +702,6 @@ Съобщения Тихи съобщения Тази категория известия се използва за показване на известия, които не бива да изпълняват звук. Това може да се използва, например, докато използвате друго устройство (по време на Период на пренебрегване). - Настройки за известията Важност, звук, вибрация Компресия на видеото Преглед на медийното съдържание @@ -773,4 +774,6 @@ Не използвайте възможността за възстановяване от резервно копие, за да клонирате (да изпълнявате едновременно) инсталацията. Възстановяването от резервно копие е предназначено за мигриране или в случай, че сте загубили устройството си. Не може да се направи възстановяване от резервно копие. Резервното копие не може да бъде дешифрирано. Правилна ли е паролата? + Присъединихте се към съществуващ канал + Добави съществуващ профил diff --git a/src/main/res/values-ca/strings.xml b/src/main/res/values-ca/strings.xml index 1202cb293..7b4c65bba 100644 --- a/src/main/res/values-ca/strings.xml +++ b/src/main/res/values-ca/strings.xml @@ -112,8 +112,6 @@ Vibra quan arribi un missatge nou Notificació LED Fes que la notificació lumínica parpellegi quan arribi un missatge nou - So d\'avís - Reprodueix un so quan arribi un missatge nou Període de gràcia Avançat Mai enviïs informes d\'errors @@ -608,7 +606,6 @@ missatges.\n\nAra se us demanarà que desactiveu-las. S\'han tancat les converses corresponents. Contacte bloquejat. Notificacions d\'estranys - Notifica per als missatges rebuts d\'estrangers. S\'ha rebut un missatge de un desconegut Bloqueja al desconegut Bloqueja tot el domini diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml index 82be2316e..9597b9bc5 100644 --- a/src/main/res/values-cs/strings.xml +++ b/src/main/res/values-cs/strings.xml @@ -3,6 +3,8 @@ Nastavení Nová konverzace Nastavení účtů + Nastavení účtu + Zavřít tuto konverzaci Detaily kontaktu Zabezpečená konverzace Přidat účet @@ -40,6 +42,7 @@ Registrovat nový účet na serveru Změnit heslo na serveru Sdílet s... + Pozvat kontakt Kontakty Zrušit Nastavit @@ -95,13 +98,12 @@ Vibrovat při přijetí nové zprávy LED upozornění Blikat při přijetí nové zprávy - Tón upozornění - Přehrát zvuk při přijetí nové zprávy Časová lhůta Rozšířené Neodesílat detaily o pádu aplikace Zasláním detailů o důvodu selhání pomůžete dalšímu vývoji aplikace Konverzace Potvrzovat zprávy + Nechat kontaky vědět kdy jste dostali a přečetli jejich zprávy UI Přijmout Došlo k chybě @@ -144,6 +146,7 @@ Povolit účet Jste si jisti? Nahrát hlas + Adresa XMPP jmeno@server.cz Heslo Nedostatek paměti. Obrázek je příliš velký @@ -180,6 +183,7 @@ Získávání klíčů... Hotovo Dešifrovat + Záložky Hledat Vložit kontakt Zobrazit detaily kontaktu @@ -189,6 +193,8 @@ Vybrat Kontakt již existuje Vstoupit + kanál@konference.server.cz/jméno + kanál@konference.server.cz Uložit jako záložku Smazat záložku Odejít @@ -239,12 +245,15 @@ Kopírovat originální URL Poslat znovu URL souboru + Skenovat 2D kód + Zobrazit 2D kód Zobrazit seznam blokovaných Detaily účtu Potvrdit Zkusit znovu Ponechat službu v popředí Zamezit operačnímu systému v ukončení připojení + Vytvořit zálohu Vybrat soubor Přijímám %1$s (%2$d%% dokončeno) Stáhnout %s @@ -309,6 +318,7 @@ %s píše... %s přestal(a) psát Upozornění při psaní + Nechat kontaky vědět když jim píšete zprávu Poslat pozici Zobrazit pozici Nebyla nalezena aplikace pro zobrazení pozice @@ -333,6 +343,8 @@ Žádná Naposledy použitá Vybrat rychlou akci + Prohledat kontakty + Prohledat záložky Poslat soukromou zprávu Uživatelské jméno Uživatelské jméno @@ -459,4 +471,20 @@ Tento přístroj byl ověřen Kopírovat identifikátor Všechny OMEMO klíče byly ověřeny + Sdílet jako čárový kód + Sdílet jako XMPP URI + Sdílet jako HTTP odkaz + Neplatný 2D kód + Upravit stavovou zprávu + Upravit stavovou zprávu + Prohledat zprávy + Jméno skupinového chatu + Vaše jméno + Vytvořit skupinový chat + Vytvořit soukromý skupinový chat + Vytvořit veřejný kanál + Jméno kanálu + Najít kanály + Možné porušení soukromí + search.jabber.network.

Používání této služby odešle vaši IP adresu a vyhledávaný termín této službě. Pro více informací konzultujte jejich Zásady ochrany osobních údajů.]]>
diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 66001a36e..9b0df15eb 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -116,8 +116,6 @@ Vibrieren bei Erhalt einer neuen Nachricht LED Benachrichtigung Blinke bei Erhalt einer neuen Nachricht - Benachrichtigungston - Benachrichtigungston wiedergeben Schonfrist Die Zeitspanne, in der Benachrichtigungen nach der Erkennung von Aktivitäten auf einem deiner anderen Geräte unterdrückt werden. Erweitert @@ -647,7 +645,6 @@ Zugehörige Unterhaltung beendet. Kontakt gesperrt. Benachrichtigungen von Unbekannten - Benachrichtigen bei Erhalt einer neuen Nachricht von Unbekannten. Erhaltene Nachricht von einem Unbekannten Unbekannten sperren Gesamte Domain sperren @@ -749,7 +746,6 @@ Nachrichten Lautlose Nachrichten Diese Benachrichtigungsart wird verwendet, um Benachrichtigungen anzuzeigen, die keinen Ton auslösen sollen. Zum Beispiel, wenn du auf einem anderen Gerät aktiv bist (Schonfrist). - Benachrichtigungseinstellungen Wichtigkeit, Klang, Vibrationen Video komprimieren Medien anzeigen @@ -848,7 +844,7 @@ Alle können andere einladen. XMPP-Adressen sind für Administratoren sichtbar. XMPP-Adressen sind für alle sichtbar. - Dieser öffentliche Channel hat keine Teilnehmer. Lade deine Kontakte ein oder benutzt die \"Teilen\"-Schaltfläche, um die XMPP-Adresse zu verteilen. + Dieser öffentliche Channel hat keine Teilnehmer. Lade deine Kontakte ein oder benutze die \"Teilen\"-Schaltfläche, um die XMPP-Adresse zu verteilen. Dieser private Gruppenchat hat keine Teilnehmer. Rechte verwalten Teilnehmer suchen @@ -881,6 +877,7 @@ Channelsuchmethode Sicherungskopie Über + Bitte aktiviere ein Konto %1$d Teilnehmer anzeigen %1$d Teilnehmer anzeigen diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml index 419912371..572a6fd86 100644 --- a/src/main/res/values-el/strings.xml +++ b/src/main/res/values-el/strings.xml @@ -116,8 +116,6 @@ Δόνηση όταν δέχεστε νέο μήνυμα Ειδοποίηση LED Ειδοποίηση μέσω αναβοσβήματος όταν δέχεστε νέο μήνυμα - Ήχος - Κουδούνισμα όταν δέχεστε νέο μήνυμα Περίοδος Χάριτος Ο χρόνος σίγασης ειδοποιήσεων αφότου ανιχνευθεί δραστηριότητα σε μια από τις άλλες συσκευές σας. Προχωρημένος @@ -646,7 +644,6 @@ Οι αντίστοιχες συζητήσεις έκλεισαν. Η επαφή αποκλείστηκε. Ειδοποιήσεις από άγνωστους - Ειδοποίηση για μηνύματα που έρχονται από άγνωστους Λήψη μηνύματος από άγνωστο Αποκλεισμός αγνώστου Αποκλεισμός ολόκληρου τομέα @@ -748,7 +745,6 @@ Μηνύματα Σιωπηρά μηνύματα Αυτή η κατηγορία ειδοποιήσεων χρησιμοποιείται για να εμφανίσει ειδοποιήσεις που δεν θα έπρεπε να παράγουν ήχο. Για παράδειγμα όταν κάποιος είναι ενεργός σε άλλη συσκευή (περίοδος χάριτος). - Ρυθμίσεις ειδοποίησης Σημασία, Ήχος, Δόνηση Συμπίεση βίντεο Εμφάνιση μέσου diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index c791be90f..69229bdac 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -116,8 +116,6 @@ Vibra cuando llega un nuevo mensaje Luz La luz parpadea cuando llega un nuevo mensaje - Tono - Reproduce tono con la notificación Periodo de gracia El periodo de tiempo en el que las notificaciones están silenciadas tras detectar actividad en otro de tus dispositivos. Avanzado @@ -647,7 +645,6 @@ Conversación correspondiente cerrada. Contacto bloqueado. Notificaciones de desconocidos - Notificar mensajes recibidos de contactos desconocidos. Mensaje recibido de un contacto desconocido Bloquear desconocido Bloquear el dominio completo @@ -749,7 +746,6 @@ Mensajes Mensajes sin sonido Este grupo de notificaciones se usa para mostrar notificaciones que no deberían emitir ningún sonido. Por ejemplo, cuando estás activo en otro dispositivo (periodo de gracia). - Ajustes de Notificación Importancia, Sonido, Vibración Compresión de video Ver galería @@ -881,6 +877,7 @@ Método para la búsqueda de Canales Copia de respaldo Acerca de + Por favor, habilita una cuenta Ver %1$d Participante Ver %1$d Participantes diff --git a/src/main/res/values-eu/strings.xml b/src/main/res/values-eu/strings.xml index 823d559b3..c6d818dbb 100644 --- a/src/main/res/values-eu/strings.xml +++ b/src/main/res/values-eu/strings.xml @@ -116,8 +116,6 @@ Mezu berri bat heltzerakoan dardartu LED jakinarazpena Mezu berri bat heltzerakoan jakinarazpenen argia keinu egin - Dei-tonua - Mezu berri bat heltzerakoan dei-tonua jo Grazia epea Aurreratua Gelditze txostenik ez bidali inoiz @@ -506,6 +504,7 @@ Jakinarazpenak gelditu dira Irudiak konprimatu Beti + Irudi handiak soilik Bateriaren optimizazioak gaituta Zure gailua jakinarazpen atzeratuak edota mezuen galera ekar lezaketen bateriaren optimizazio handiak egiten ari da Conversationsen.\nHoriek ezgaitzea gomendatzen da. Zure gailua jakinarazpen atzeratuak edota mezuen galera ekar lezaketen bateriaren optimizazio handiak egiten ari da Conversationsen.\nJarraian hauek ezgaitzea eskatuko zaizu. @@ -550,6 +549,7 @@ Pribatutasuna Gaia Kolore paleta hautatu + Automatikoa Gai argia Gai iluna Atzealde berdea @@ -639,7 +639,6 @@ Dagokion elkarrizketa itxi egin da. Kontaktua blokeatu da. Ezezagunen jakinarazpenak - Ezezagunen mezuak jasotzerakoan jakinarazi. Ezezagun baten mezu bat jaso duzu Ezezaguna blokeatu Domeinu osoa blokeatu @@ -741,7 +740,6 @@ Mezuak Mezu isilak Jakinarazpen talde hau inolako soinurik egin beharko ez luketen jakinarazpenak erakusteko erabiltze da. Adibidez beste gailu batean aktibo zaudenean (grazia epea). - Jakinarazpenen ezarpenak Garrantzia, soinua, dardara Bideoen konprimatzea Ikusi multimedia @@ -865,4 +863,15 @@ Mesedez idatzi ezazu kontu honetarako pasahitza Ezin izan da ekintza hau burutu Kanal publiko batean sartu… + Partekatzen duen aplikazioak ez du baimenik eman fitxategi honetara sartzeko. + jabber.network + Zerbitzari lokala + Kanalak aurkitzeko modua + Babeskopia + Honi buruz + Mesedez kontu bat gaitu + + Parte-hartzaile %1$d ikusi + %1$d parte-hartzaile ikusi + diff --git a/src/main/res/values-fa-rIR/strings.xml b/src/main/res/values-fa-rIR/strings.xml index d70439f87..3cc805356 100644 --- a/src/main/res/values-fa-rIR/strings.xml +++ b/src/main/res/values-fa-rIR/strings.xml @@ -94,8 +94,6 @@ هنگام دریافت پیام جدید بلرز اعلان از طریق LED چشمک زدن چراغ اعلان هنگام رسیدن پیام جدید - آهنگ زنگ - هنگام دریافت پیام جدید صدا پخش کن مهلت پیشرفته هیچ وقت گزارش خرابی را ارسال نکن diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 71abbac9c..b548400a5 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -116,8 +116,6 @@ Vibrer lors de la réception d\'un message Notification LED Faire clignoter la LED lors de la réception d\'un message - Sonnerie - Jouer un son lors de la réception d\'un message Période sans notification La durée pendant laquelle les notifications sont désactivées après la détection d\'une activité sur l\'un de vos autres appareils. Avancé @@ -647,7 +645,6 @@ Conversations correspondantes fermées. Contact bloqué. Notifications d\'inconnus - Notifier pour les messages envoyés par des personnes inconnues Message d\'un inconnu reçu Bloquer l\'inconnu Bloquer le domaine entier @@ -749,7 +746,6 @@ Messages Messages silencieux Ce groupe de notifications est utilisé pour afficher les notifications qui ne doivent pas émettre de son. Par exemple, lorsque le son est activé sur un autre appareil (délai de grâce). - Options de notification Importance, son, vibration Compression vidéo Voir les média diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 34dc2cfdd..518614c26 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -116,8 +116,6 @@ Vibra cando chega unha nova mensaxe Notificación LED Luz pestanexante cando chegue unha nova mensaxe - Ton de aviso - Emitir son cando chegue unha nova mensaxe Período de graza O tempo no que as notificacións son silenciadas tras detectar actividade en algún dos teus outros dispositivos. Avanzado @@ -296,7 +294,7 @@ Comprobando %s no servidor HTTP Non está conectada. Inténteo máis tarde Comprobando o tamaño de %s - Comporbar o tamaño de %1$s en %2$s + Comprobar o tamaño de %1$s en %2$s Opcións da mensaxe Cita Pegar como cita @@ -477,7 +475,7 @@ Introduza o texto da imaxe superior A cadea de certificados non é de confianza Os enderezos XMPP non concordan co certificado - Anvoar certificado + Anovar certificado Fallo obtendo a chave OMEMO! Comprobouse a chave OMEMO co certificado! O seu dispositivo non admite a selección de certificados do cliente! @@ -647,7 +645,6 @@ Conversas correpondentes pechadas. Contacto bloqueado. Notificacións de estraños - Notifica as mensaxes recibidas por parte de estraños. Mensaxe recibida de un estraño Bloquear estraño Bloquear o dominio ao completo @@ -749,7 +746,6 @@ Mensaxes Mensaxes acalados Este grupo de notificacións é utilizado para mostrar notificacións que non debería activar ningún son. Por exemplo, cando está activo en outro dispositivo (Período de Graza). - Axustes das notificacións Importancia, Son, Vibrar Compresión de vídeo Ver medios @@ -881,6 +877,7 @@ Método de descubrimento de canles Respaldo Acerca de + Activa unha conta por favor Ver %1$d Participante Ver %1$d Participantes diff --git a/src/main/res/values-hu/strings.xml b/src/main/res/values-hu/strings.xml index 9c9018e39..a211d6050 100644 --- a/src/main/res/values-hu/strings.xml +++ b/src/main/res/values-hu/strings.xml @@ -116,8 +116,6 @@ Rezegés új üzenet érkezésekor LED értesítés Értesítési fény villogása új üzenet érkezésekor - Csengőhang - Hang lejátszása új üzenet érkezésekor Türelmi idő Az időtartam, amíg az értesítések némítva vannak az egyéb eszközei egyikén történt tevékenység észlelése után. Speciális @@ -647,7 +645,6 @@ A megfelelő beszélgetések lezárultak. Partner tiltva. Értesítések idegenektől - Értesítés az idegenektől kapott üzenetekről. Üzenet érkezett egy idegentől Idegen tiltása Teljes tartomány tiltása @@ -749,7 +746,6 @@ Üzenetek Csendes üzenetek Ezt az értesítési csoportot olyan értesítések megjelenítéséhez használják, amelyek nem aktiválhatnak hangot. Például ha aktívvá válik egy másik eszközön (türelmi idő). - Értesítési beállítások Fontosság, hang, rezgés Videó tömörítése Média megtekintése diff --git a/src/main/res/values-id/strings.xml b/src/main/res/values-id/strings.xml index 009cf7266..1f1c40314 100644 --- a/src/main/res/values-id/strings.xml +++ b/src/main/res/values-id/strings.xml @@ -90,8 +90,6 @@ Getar Aktifkan getar ketika pesan masuk Notifikasi LED - Nada dering - Deringkan ketika pesan masuk Lanjutan Jangan kirim laporan kerusakan Dengan mengirimkan kesalahan Anda membantu pengembangan Aplikasi Conversations diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 13734de4c..fa17d3892 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -116,8 +116,6 @@ Vibra quando arriva un nuovo messaggio Notifica LED Luce di notifica lampeggiante quando arriva un nuovo messaggio - Suono di notifica - Esegui un suono quando arriva un nuovo messaggio Periodo di grazia Il periodo di tempo in cui le notifiche vengono silenziate dopo aver rilevato attività su uno dei tuoi altri dispositivi. Avanzate @@ -647,7 +645,6 @@ Chiuse le relative conversazioni. Contatto bloccato. Notifiche da sconosciuti - Notifica i messaggi ricevuti da sconosciuti. Ricevuto messaggio da uno sconosciuto Blocca sconosciuto Blocca intero dominio @@ -749,7 +746,6 @@ Messaggi Messaggi silenziosi Questo gruppo di notifiche è usato per mostrare notifiche che non devono riprodurre alcun suono. Ad esempio mentre si è attivi su un altro dispositivo (Periodo di grazia). - Impostazioni di notifica Importanza, suono, vibrazione Compressione video Vedi i media @@ -881,6 +877,7 @@ Metodo di scoperta canali Backup Al riguardo + Devi attivare un account Vedi %1$d partecipante Vedi %1$d partecipanti diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 412049c2c..5d43a13db 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -112,8 +112,6 @@ 新しいメッセージが届いたときに振動します LED 通知 新しいメッセージが届いたときに通知ライトを点滅します - 着信音 - 新しいメッセージが届いたときにサウンドを再生します 猶予期間 詳細 クラッシュレポートを送信しない @@ -589,7 +587,6 @@ 対応する会話が閉じられました。 連絡先をブロックしました 知らない人からの通知 - 知らない人から受信したメッセージを通知します。 知らない人からメッセージを受け取りました 見知らぬ人をブロック ドメイン全体をブロック diff --git a/src/main/res/values-ko/strings.xml b/src/main/res/values-ko/strings.xml index ee646b5be..0066c1c73 100644 --- a/src/main/res/values-ko/strings.xml +++ b/src/main/res/values-ko/strings.xml @@ -95,8 +95,6 @@ 새 메세지 도착시 진동 LED 알림 새 메세지 도착시 LED 깜빡이기 - 알림음 - 새 메세지 도착시 알림음 재생 유예기간 고급 충돌 보고서 보내지 않음 @@ -471,5 +469,4 @@ OMEMO 키를 검증 이 장치의 자격 증명을 삭제 하시겠습니까?\n장치와 해당 장치에서 메시지는 신뢰할 수없는 것으로 표시됩니다. 설정된 기간보다 오래된 메시지를 장치에서 자동으로 삭제합니다. - 모르는 사람으로부터 받은 메시지를 알립니다. diff --git a/src/main/res/values-nb-rNO/strings.xml b/src/main/res/values-nb-rNO/strings.xml index f38b28b96..719e6493c 100644 --- a/src/main/res/values-nb-rNO/strings.xml +++ b/src/main/res/values-nb-rNO/strings.xml @@ -102,8 +102,6 @@ Vibrer når en ny melding ankommer LED-merknad Blink merknadslyset når en ny melding ankommer - Ringetone - Spill en lyd når en ny melding ankommer Fristperiode Avansert Aldri send kræsjrapporter @@ -531,7 +529,6 @@ Samsvarende samtaler lukket. Kontakt blokkert. Varslinger fra fremmede - Vis varsel for meldinger mottatt fra fremmede. Mottok melding fra fremmed Blokker fremmed Blokker hele domenet diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index 97a0920f4..5e5bb7357 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -116,8 +116,6 @@ Trillen wanneer een nieuw bericht ontvangen wordt LED-melding Meldingslicht knipperen wanneer een nieuw bericht ontvangen wordt - Meldingstoon - Geluid afspelen wanneer een nieuw bericht ontvangen wordt Uitstelperiode Geavanceerd Verstuur nooit crashrapportages @@ -643,7 +641,6 @@ Bijbehorende gesprekken gesloten. Contact geblokkeerd. Meldingen van onbekenden - Melding bij berichten van onbekenden. Bericht ontvangen van onbekende Vreemde blokkeren Volledig domein blokkeren @@ -745,7 +742,6 @@ Berichten Stille berichten Deze meldingscategorie wordt gebruikt om meldingen weer te geven die geen geluid mogen maken. Bijvoorbeeld, indien actief op een ander apparaat (uitstelperiode). - Meldingsinstellingen Belang, geluid, trillen Videocompressie Media bekijken diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 52ef9bdbf..9fdf041b9 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -116,8 +116,6 @@ Wibruj gdy nadejdzie wiadomość Powiadomienie diodą LED Migaj lampką powiadamiającą gdy nadejdzie wiadomość - Dzwonek - Odtwórz dźwięk gdy nadejdzie wiadomość Czas bez powiadomień Długość czasu kiedy powiadomienia są uśpione po wykryciu aktywności na jednym z twoich innych urządzeń. Zaawansowane @@ -664,7 +662,6 @@ Conversations będzie wciąż ograniczał transfer danych, kiedy tylko to jest m Odpowiadające rozmowy zostały zamknięte. Kontakt zablokowany Powiadomienia od nieznajomych - Powiadamiaj o wiadomościach otrzymanych od nieznajomych Odebrano wiadomość od nieznajomego Zablokuj nieznajomego Zablokuj całą domenę @@ -767,7 +764,6 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Wiadomości Ciche wiadomości Ta kategoria powiadomień jest używana aby wyświetlać powiadomienia które nie powodują żadnych dźwięków. Na przykład w ciągu aktywności na innym urządzeniu (okres karencji). - Ustawienia powiadomień Ważność, Dźwięk, Wibracja Kompresja wideo Pokaż media @@ -899,6 +895,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Metoda odkrywania kanałów Kopia zapasowa O aplikacji + Proszę włączyć konto Pokaż %1$d uczestnika Pokaż %1$d uczestników diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index b01d1c3e5..4379114ae 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -116,8 +116,6 @@ Vibra ao receber uma nova mensagem. Notificação via LED Pisca a luz de notificação ao receber uma nova mensagem. - Toque - Toca um som ao receber uma nova mensagem. Período de espera Espaço de tempo em que as notificações serão silenciadas, após detectar atividade em algum dos seus outros dispositivos. Avançado @@ -647,7 +645,6 @@ As conversas correspondentes foram encerradas. O contato foi bloqueado. Notificações de desconhecidos - Notificar ao receber mensagens de desconhecidos. Foi recebida uma mensagem de um desconhecido Bloquear os desconhecidos Bloquear o domínio inteiro @@ -749,7 +746,6 @@ Mensagens Silenciar mensagens Essa categoria de notificação é utilizada para exibir notificações que não deveriam gerar nenhum som. Por exemplo, quando estiver ativo em outro dispositivo (Período de Espera). - Configurações de notificações Importância, som, vibração. Compressão de vídeo Ver mídia @@ -881,6 +877,7 @@ Método de descoberta de canais Backup Sobre + Por favor, habilite uma conta Ver %1$d participante Ver %1$d participantes diff --git a/src/main/res/values-pt/strings.xml b/src/main/res/values-pt/strings.xml index 2db314122..13c46a1fd 100644 --- a/src/main/res/values-pt/strings.xml +++ b/src/main/res/values-pt/strings.xml @@ -103,8 +103,6 @@ Vibrar quando uma nova mensagem for recebida Notificação LED Piscar luz de notificação quando uma nova mensagem for recebida - Tom de toque - Tocar som quando uma nova mensagem for recebida Avançadas Nunca enviar relatórios de falhas Ao enviar os stack traces você está a ajudar ao desenvolvimento contínuo de Conversations diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index dd33608b9..c1cc05f4a 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -116,8 +116,6 @@ Vibrează când este primit un mesaj nou Notificare LED Clipește lumina de notificare atunci când este primit un mesaj nou - Sunet de notificare - Notificare sonoră atunci când este primit un mesaj nou Perioadă de grație Durata de timp cât notificările sunt ascunse după ce s-a observat activitate pe un alt dispozitiv al dumneavoastră. Opțiuni avansate @@ -657,7 +655,6 @@ sau chiar pierderea mesajelor.\nÎn continuare veți fi rugați să dezactivați Conversațiile corespunzătoare au fost închise. Contact blocat. Notificări de la persoane necunoscute - Primire notificări și pentru mesajele de la persoane care nu sunt în lista de contacte. Mesaj primit de la o persoană necunoscută Blocare contact necunoscut Blocare tot domeniu @@ -759,7 +756,6 @@ sau chiar pierderea mesajelor.\nÎn continuare veți fi rugați să dezactivați Mesaje Mesaje silențioase Acest grup de notificări este folosit pentru a arăta notificări care nu emit sunete. De exemplu atunci când sunteți activi pe un alt dispozitiv (Perioada de grație). - Setări notificări Importanță, sunete, vibrații Compresie video Vizualizare fișiere media @@ -891,6 +887,7 @@ sau chiar pierderea mesajelor.\nÎn continuare veți fi rugați să dezactivați Metoda de descoperire a canalelor Copie de siguranță Despre + Va rugăm să activați un cont Arată %1$d participant Arată %1$d participanți diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index e4319355a..ecd5bd761 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -115,8 +115,6 @@ Вибрировать, когда приходят новые сообщения Светодиодное уведомление Мерцание индикатора при получении нового сообщения - Звук уведомления - Звук при поступлении новых сообщений Грейс-период Дополнительно Не отправлять отчёты об ошибках @@ -634,7 +632,6 @@ Соответствующие беседы закрыты. Контакт заблокирован Уведомления от неизвестных контактов - Уведомлять о сообщениях от незнакомых контактов. Получено сообщение от неизвестного контакта Заблокировать неизвестный контакт Заблокировать весь домен @@ -702,7 +699,6 @@ Название конференции Эта конференция была уничтожена Проблемы с подключением - Настройки уведомлений Сжатие видео Просмотр медиа Участники diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml index 9ca1efb64..825e3469c 100644 --- a/src/main/res/values-sr/strings.xml +++ b/src/main/res/values-sr/strings.xml @@ -105,8 +105,6 @@ Вибрирање кад стигне нова порука ЛЕД светло Трептање ЛЕД светла кад стигне нова порука - Звук - Пуштање звука кад стигне нова порука Период одгоде Напредно Никад не шаљи извештаје о паду @@ -572,7 +570,6 @@ Одговарајуће преписке затворене. Контакт блокиран. Обавештења од непознатих - Обавештења за поруке од непознатих. Примљена порука од незнанца Блокирај странца Блокирај читав домен @@ -620,6 +617,5 @@ Поруке Поруке Тихе поруке - Поставке обавештавања Видео компресија diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index 91ad60332..922453102 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -114,8 +114,6 @@ Vibrera när meddelande tagits emot LED notifieringar Blinka med notifieringsljuset när ett meddelande tagits emot - Meddelandesignal - Spela ljud när meddelande tagits emot Notifieringsfrist Avancerat Skicka aldrig krasch-rapporter @@ -585,7 +583,6 @@ Motsvarande konversationer är stängda. Kontakt blockerad. Notifieringar från främlingar - Notifiera för meddelanden från främlingar. Mottagna meddelanden från främlingar Blockera främling Blockera hel domän diff --git a/src/main/res/values-tr-rTR/strings.xml b/src/main/res/values-tr-rTR/strings.xml index a32aa7c48..c94070f2e 100644 --- a/src/main/res/values-tr-rTR/strings.xml +++ b/src/main/res/values-tr-rTR/strings.xml @@ -95,8 +95,6 @@ Yeni ileti geldiğinde titret LED Bildirimi Yeni bir ileti geldiğinde bildirim ışığı yanıp sönsün - Zil sesi - Yeni bir ileti geldiğinde sesli bildir Mühlet Gelişmiş Asla çöküş raporu gönderme @@ -490,6 +488,5 @@ Konuşma sonlandı Kişi engellendi. Yabancılardan bildirimler - Yabancılardan alınan iletileri bildir. Yabancıdan alınmış ileti diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index 2fc9fae7b..a40413395 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -17,6 +17,8 @@ Розблокувати контакт Заблокувати домен Розблокувати домен + Заблокувати учасника + Розблокувати учасника Впорядкувати облікові записи Налаштування Поділитися в Розмови @@ -28,6 +30,7 @@ щойно 1 хвилину тому %d хвилин тому + %d непрочитаних розмов відправляю… Розшифровую повідомлення. Зачекайте, будь ласка… Повідомлення, зашифроване OpenPGP @@ -113,9 +116,8 @@ Вібрувати, коли приходять нові повідомлення Індикація LED Блимати світловим індикатором, коли надходить нове повідомлення - Мелодія дзвінка - Грати звук, коли надходить нове повідомлення Період очікування + Час, протягом якого не буде сигналу про нові сповіщення, після дій користувача на іншому пристрої. Розширені Не надсилати звіти про збої Надсилаючи траси стеку викликів Ви допомагаєте розробці Розмов, яка продовжується @@ -151,6 +153,7 @@ Ім\'я користувача вже використовується Реєстрацію виконано Сервер не підтримує режстрацію + Недійсний реєстраційний токен Узгодження TLS не відбулося Порушення політики Несумісний сервер @@ -288,6 +291,7 @@ Цю групу закрили Ви більше не берете участь в цій групі використовується обліковий запис %s + розміщений на %s Перевіряю %s на хості HTTP Ви не з\'єднані. Спробуйте ще пізніше. Перевірити %s розмір @@ -329,6 +333,7 @@ %s запропоновано для завантаження Припинити передачу передача файла не вдалася + передачу файлу перервано Файл видалено Не знайдено програми для відкриття файла Не знайдено програми, щоб відкрити посилання @@ -507,7 +512,9 @@ Сповіщення вимкнено Сповіщення призупинено Стиснення зображень + Підказка: Обирайте \"Вибрати файл\" замість \"Вибрати зображення\", щоб надіслати окремі зображення без стиснення в обхід цього налаштування. Завжди + Лише великі зображення Оптимізацію батареї задіяно Ваш пристрій здійснює деяку агресивну оптимізацію Розмов для збереження заряду батареї, яка може призвести до затримки сповіщення або навіть втрати повідомлень.\nРекомендовано відключити цю оптимізацію. Ваш пристрій здійснює деяку агресивну оптимізацію Розмов для збереження заряду батареї, яка може призвести до затримки сповіщення або навіть втрати повідомлень.\nПросимо Вас зараз відключити цю оптимзацію. @@ -553,6 +560,7 @@ Приватність Тема Вибрати палітру кольорів + Автоматично Світла тема Темна тема Зелене тло @@ -654,7 +662,6 @@ Відповідні розмови закрито. Контакт заблоковано. Сповіщення від незнайомців - Сповіщувати про повідомлення від незнайомців. Отримано повідомлення від незнайомця Заблокувати незнайомця Заблокувати весь домен @@ -756,7 +763,6 @@ Повідомлення Тихі повідомлення Ця група сповіщень показує сповіщення, які не повинні супроводжуватися звуком. Наприклад, у разі активності на іншому пристрої (період очікування). - Налаштування сповіщень Важливість, звук, вібрація Стиснення відео Перегляд медіа @@ -864,10 +870,35 @@ Знайти канали Шукати канали Можливе порушення приватності! + search.jabber.network.

Користуючись ним, Ви передає Вашу IP адресу та пошукові запити цьому сервісу. Перегляньте їхню політику конфіденційності, щоб отримати більше інформації.]]>
Я вже маю обліковий запис Додати наявний обліковий запис Зареєструвати новий обліковий запис Це схоже на ім\'я домену Додати все одно Це схоже на адресу каналу + Поділитися резервними копіями + Резервне копіювання розмов + Подія + Відкрити резервну копію + Обраний файл не є резервною копією цієї програми + Цей обліковий запис уже налаштовано + Будь ласка, введіть пароль цього облікового запису + Не можу виконати цю дію + Приєднатися до публічного каналу… + Програма, яка надає доступ, не надала дозволу на доступ до цього файлу. + + jabber.network + Локальний сервер + Пересічному користувачеві слід обрати «jabber.network» для кращих пропозицій з усієї публічної XMPP екосистеми. + Спосіб пошуку каналів + Резервне копіювання + Про + Будь ласка, увімкніть обліковий запис + + Переглянути %1$d учасника + Переглянути %1$d учасників + Переглянути %1$d учасників + Переглянути %1$d учасників + diff --git a/src/main/res/values-vi/strings.xml b/src/main/res/values-vi/strings.xml index 82bbfc97f..92f4a508a 100644 --- a/src/main/res/values-vi/strings.xml +++ b/src/main/res/values-vi/strings.xml @@ -95,8 +95,6 @@ Rung khi có tin nhắn mới Thông báo đèn LED Chớp đèn thông báo khi có tin nhắn mới - Âm báo - Chơi nhạc báo khi có tin nhắn mới Thời gian gia hạn thông báo Nâng cao Không bao giờ gửi báo cáo dừng chạy diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index a9ca38fbb..0daf2fccc 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -116,8 +116,6 @@ 收到新消息时震动 LED 灯提示 收到新消息时闪烁通知灯 - 铃声 - 收到新消息时响铃 静默期限 在您的其他设备之一上检测到活动之后,时间通知的长度将被静音。 高级 @@ -526,6 +524,7 @@ 安全错误:文件访问权限无效 未找到可以分享此链接的应用 分享链接…… + <![CDATA [Quicksy是从受欢迎的XMPP客户端对话中分离出来的,具有自动联系人发现功能。<br><br>您注册了电话号码,Quicksy就会根据您的通讯录中的电话号码自动为您建议可能的联系人。<br><br> 签署即表示您同意我们的<a href="https://quicksy.im/#privacy">隐私政策</a>。]]> 同意 & 继续 此向导将为您在conversations.im¹上创建一个账户。\n您的联系人可以通过您的XMPP完整地址与您聊天。 您的XMPP完整地址将是:%s @@ -640,7 +639,6 @@ 相应的对话已关闭。 联系人已屏蔽 陌生人也通知 - 收到陌生人信息时通知 已收到陌生人的信息 屏蔽陌生人 屏蔽整个域名 @@ -742,7 +740,6 @@ 消息 无声消息 此通知组用于显示不应触发任何声音的通知。 例如,当在另一个设备上激活时(宽限期)。 - 通知设置 重要性,声音,振动 视频压缩 查看媒体文件 @@ -850,6 +847,7 @@ 发现群聊 搜索群聊 可能侵犯隐私! + <![CDATA [频道发现使用了名为<a href="https://search.jabber.network">search.jabber.network</a>。<br><br>的第三方服务。使用此功能会将您的IP地址和搜索字词传输到该服务。 有关更多信息,请参见其<a href="https://search.jabber.network/privacy">Privacy Policy</a>。]]> 我已有账户 添加已有账户 注册新账户 @@ -867,10 +865,13 @@ 加入公开群聊 分享程序没有访问文件的权限 + jabber.network 本地服务器 + 大多数用户应该选择“ jabber.network”以从整个XMPP生态系统中获得更好的建议。 频道发现方法 备份 关于 + 请启用一个帐户 查看%1$d成员 diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index 0c83bd770..84a9ea7ee 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -96,8 +96,6 @@ 收到新訊息時震動 LED 燈通知 收到新訊息時閃爍通知燈 - 鈴聲 - 收到新訊息時響鈴 靜默期限 高級 總不發送崩潰報告 @@ -494,7 +492,6 @@ 關閉相關的對話了。 已經封鎖聯絡人了。 陌生人訊息通知 - 當收到來自陌生人的訊息時顯示通知。 接受來自陌生人的訊息 封鎖陌生人 封鎖整個網域 diff --git a/src/quicksy/res/values-uk/strings.xml b/src/quicksy/res/values-uk/strings.xml index ab585b8a2..62777fe98 100644 --- a/src/quicksy/res/values-uk/strings.xml +++ b/src/quicksy/res/values-uk/strings.xml @@ -19,4 +19,8 @@ Програма потребує доступу до мікрофона Цей вид сповіщень показує постійне сповіщення про те, що ця програма працює. Зображення профілю для Quicksy - + Ця програма не доступна у Вашій країні. + Автентичність сервера не підтверджено + Невідома помилка безпеки. + Вичерпано час для встановлення з\'єднання із сервером. + From 995856ffe0c624352f1e3ed69fa2e734ff3a6d25 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 21 Apr 2020 18:43:53 +0200 Subject: [PATCH 154/182] fixed chinese translation --- src/main/res/values-zh-rCN/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 0daf2fccc..0cfeb7929 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -524,7 +524,7 @@ 安全错误:文件访问权限无效 未找到可以分享此链接的应用 分享链接…… - <![CDATA [Quicksy是从受欢迎的XMPP客户端对话中分离出来的,具有自动联系人发现功能。<br><br>您注册了电话号码,Quicksy就会根据您的通讯录中的电话号码自动为您建议可能的联系人。<br><br> 签署即表示您同意我们的<a href="https://quicksy.im/#privacy">隐私政策</a>。]]> +
您注册了电话号码,Quicksy就会根据您的通讯录中的电话号码自动为您建议可能的联系人

签署即表示您同意我们的隐私政策。]]>
同意 & 继续 此向导将为您在conversations.im¹上创建一个账户。\n您的联系人可以通过您的XMPP完整地址与您聊天。 您的XMPP完整地址将是:%s @@ -847,7 +847,7 @@ 发现群聊 搜索群聊 可能侵犯隐私! - <![CDATA [频道发现使用了名为<a href="https://search.jabber.network">search.jabber.network</a>。<br><br>的第三方服务。使用此功能会将您的IP地址和搜索字词传输到该服务。 有关更多信息,请参见其<a href="https://search.jabber.network/privacy">Privacy Policy</a>。]]> + search.jabber.network。

的第三方服务。使用此功能会将您的IP地址和搜索字词传输到该服务。 有关更多信息,请参见其Privacy Policy。]]>
我已有账户 添加已有账户 注册新账户 From e5282b846f230062e728fae0c77dd0dc5b25ad9e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 21 Apr 2020 22:01:01 +0200 Subject: [PATCH 155/182] pulled translations from transifex --- src/main/res/values-de/strings.xml | 24 ++++++++++++++++ src/main/res/values-pl/strings.xml | 38 ++++++++++++++++++++++++++ src/main/res/values-pt-rBR/strings.xml | 16 +++++++++++ src/main/res/values-ro-rRO/strings.xml | 38 ++++++++++++++++++++++++++ 4 files changed, 116 insertions(+) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 9b0df15eb..078e77d69 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -116,6 +116,10 @@ Vibrieren bei Erhalt einer neuen Nachricht LED Benachrichtigung Blinke bei Erhalt einer neuen Nachricht + Klingelton + Benachrichtigungston + Benachrichtigungston für neue Nachrichten + Klingelton für eingehende Anrufe Schonfrist Die Zeitspanne, in der Benachrichtigungen nach der Erkennung von Aktivitäten auf einem deiner anderen Geräte unterdrückt werden. Erweitert @@ -188,6 +192,7 @@ XEP-0191: Blocking Command XEP-0237: Roster Versioning XEP-0198: Stream Management + XEP-0215: External Service Discovery XEP-0163: PEP (Avatare/OMEMO) XEP-0363: HTTP File Upload XEP-0357: Push @@ -743,7 +748,10 @@ Verbindungsprobleme Diese Benachrichtigungsart wird verwendet, um eine Benachrichtigung anzuzeigen, falls es ein Problem bei der Verbindung zu einem Konto gibt. Nachrichten + Anrufe Nachrichten + Eingehende Anrufe + Laufende Anrufe Lautlose Nachrichten Diese Benachrichtigungsart wird verwendet, um Benachrichtigungen anzuzeigen, die keinen Ton auslösen sollen. Zum Beispiel, wenn du auf einem anderen Gerät aktiv bist (Schonfrist). Wichtigkeit, Klang, Vibrationen @@ -878,6 +886,22 @@ Sicherungskopie Über Bitte aktiviere ein Konto + Anrufen + Eingehender Anruf + Verbinden + Verbunden + Anruf annehmen + Anruf beenden + Klingelt + Auflegen + Laufender Anruf + Deaktiviere Tor, um Anrufe zu tätigen + Eingehender Anruf + Eingehender Anruf · %s + Ausgehender Anruf + Ausgehender Anruf · %s + Dein Mikrofon ist nicht verfügbar + Du kannst immer nur einen Anruf zur gleichen Zeit machen. %1$d Teilnehmer anzeigen %1$d Teilnehmer anzeigen diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 9fdf041b9..18a322e8b 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -116,6 +116,10 @@ Wibruj gdy nadejdzie wiadomość Powiadomienie diodą LED Migaj lampką powiadamiającą gdy nadejdzie wiadomość + Dzwonek + Dźwięk powiadomień + Dźwięk powiadomień dla nowych wiadomości + Dzwonek dla przychodzących połączeń Czas bez powiadomień Długość czasu kiedy powiadomienia są uśpione po wykryciu aktywności na jednym z twoich innych urządzeń. Zaawansowane @@ -188,6 +192,7 @@ XEP-0191: Blocking Command XEP-0237: Roster Versioning XEP-0198: Stream Management + XEP-0215: Wykrywanie Zewnętrznych Usług XEP-0163: PEP (Awatary / OMEMO) XEP-0363: Przesyłanie plików przez HTTP XEP-0357: Push @@ -662,6 +667,7 @@ Conversations będzie wciąż ograniczał transfer danych, kiedy tylko to jest m Odpowiadające rozmowy zostały zamknięte. Kontakt zablokowany Powiadomienia od nieznajomych + Powiadamiaj przy wiadomościach i połączeniach od nieznajomych. Odebrano wiadomość od nieznajomego Zablokuj nieznajomego Zablokuj całą domenę @@ -761,9 +767,14 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Problemy z połączeniem Ta kategoria powiadomień jest używana aby wyświetlać powiadomienia oznaczające, że Conversations ma problemy z połączeniem. Wiadomości + Połączenia Wiadomości + Połączenia przychodzące + Połączenia wychodzące Ciche wiadomości Ta kategoria powiadomień jest używana aby wyświetlać powiadomienia które nie powodują żadnych dźwięków. Na przykład w ciągu aktywności na innym urządzeniu (okres karencji). + Ustawienia powiadomień wiadomości + Ustawienia powiadomień dla przychodzących połączeń Ważność, Dźwięk, Wibracja Kompresja wideo Pokaż media @@ -896,6 +907,33 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Kopia zapasowa O aplikacji Proszę włączyć konto + Zadzwoń + Połączenie przychodzące + Wideorozmowa przychodząca + Łączenie + Połączony + Akceptowanie połączenia + Kończenie połączenia + Odbierz + Odrzuć + Lokalizowanie urządzeń + Dzwonienie + Zajęty + Nie można połączyć rozmowy + Błąd aplikacji + Rozłącz + Połączenie wychodzące + Wideorozmowa wychodząca + Wyłącz Tor aby dzwonić + Połączenie przychodzące + Połączenie przychodzące · %s + Połączenie wychodzące + Połączenie wychodzące · %s + Nieodebrane połączenie + Połączenie audio + Połączenie wideo + Twój mikrofon jest niedostępny + Możesz mieć tylko jedno połączenie na raz. Pokaż %1$d uczestnika Pokaż %1$d uczestników diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 4379114ae..5239def65 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -116,6 +116,10 @@ Vibra ao receber uma nova mensagem. Notificação via LED Pisca a luz de notificação ao receber uma nova mensagem. + Toque + Som de notificação + Som de notificação para novas mensagens + Toque ao receber chamadas Período de espera Espaço de tempo em que as notificações serão silenciadas, após detectar atividade em algum dos seus outros dispositivos. Avançado @@ -188,6 +192,7 @@ XEP-0191: Comando de bloqueio XEP-0237: Versionamento da lista de contatos XEP-0198: Gerenciamento de fluxo + XEP-0215: Descoberta de Serviço Externo XEP-0163: PEP (Avatares / OMEMO) XEP-0363: Envio de arquivos via HTTP XEP-0357: Push @@ -645,6 +650,7 @@ As conversas correspondentes foram encerradas. O contato foi bloqueado. Notificações de desconhecidos + Notificar ao receber mensagens e chamadas de desconhecidos. Foi recebida uma mensagem de um desconhecido Bloquear os desconhecidos Bloquear o domínio inteiro @@ -743,6 +749,7 @@ Problemas de conectividade Essa categoria de notificação é utilizada para exibir uma notificação caso exista algum problema de conectividade com uma conta. Mensagens + Chamadas Mensagens Silenciar mensagens Essa categoria de notificação é utilizada para exibir notificações que não deveriam gerar nenhum som. Por exemplo, quando estiver ativo em outro dispositivo (Período de Espera). @@ -878,6 +885,15 @@ Backup Sobre Por favor, habilite uma conta + Conectando + Conectado + Procurando dispositivos + Tocando + Ocupado + Chamada perdida + Chamada de vídeo + Seu microfone não está disponível + Você só pode ter uma chamada por vez Ver %1$d participante Ver %1$d participantes diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index c1cc05f4a..55dc9eb94 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -116,6 +116,10 @@ Vibrează când este primit un mesaj nou Notificare LED Clipește lumina de notificare atunci când este primit un mesaj nou + Ton de apel + Sunet de notificare + Sunet de notificare pentru mesaje noi + Ton pentru apelul primit Perioadă de grație Durata de timp cât notificările sunt ascunse după ce s-a observat activitate pe un alt dispozitiv al dumneavoastră. Opțiuni avansate @@ -188,6 +192,7 @@ XEP-0191: Comandă blocare XEP-0237: Creare de versiuni listă XEP-0198: Management flux + XEP-0215: Descoperirea serviciilor externe XEP-0163: PEP (Avatare / OMEMO) XEP-0363: Încărcare fișiere prin HTTP XEP-0357: Push @@ -655,6 +660,7 @@ sau chiar pierderea mesajelor.\nÎn continuare veți fi rugați să dezactivați Conversațiile corespunzătoare au fost închise. Contact blocat. Notificări de la persoane necunoscute + Primire notificări pentru mesaje și apeluri de la persoane care nu sunt în lista de contacte. Mesaj primit de la o persoană necunoscută Blocare contact necunoscut Blocare tot domeniu @@ -753,9 +759,14 @@ sau chiar pierderea mesajelor.\nÎn continuare veți fi rugați să dezactivați Probleme de conectare Această categorie de notificări este folosită pentru a arăta o notificare în cazul în care există o problemă la conectarea unui cont. Mesaje + Apeluri Mesaje + Apeluri primite + Apeluri în curs Mesaje silențioase Acest grup de notificări este folosit pentru a arăta notificări care nu emit sunete. De exemplu atunci când sunteți activi pe un alt dispozitiv (Perioada de grație). + Setări de notificare ale mesajelor + Setări de notificare ale apelurilor primite Importanță, sunete, vibrații Compresie video Vizualizare fișiere media @@ -888,6 +899,33 @@ sau chiar pierderea mesajelor.\nÎn continuare veți fi rugați să dezactivați Copie de siguranță Despre Va rugăm să activați un cont + Apelează + Apel primit + Apel video primit + Conectare + Conectat + Se acceptă apelul + Se încheie apelul + Răspunde + Respinge + Localizare dispozitive + Sună + Ocupat + Nu s-a putut conecta apelul + Eroare de aplicație + Închide + Apel în curs + Apel video în curs + Dezactivați Tor pentru a face apeluri + Apel primit + Apel primit · %s + Apel efectuat + Apel efectuat · %s + Apel pierdut + Apel audio + Apel video + Microfonul nu este disponibil + Puteți avea un singur apel simultan. Arată %1$d participant Arată %1$d participanți From e0cb127005fac54904350f2359b573fc80a85f11 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 21 Apr 2020 22:46:46 +0200 Subject: [PATCH 156/182] retract call when pressing home or power button during ringing --- .../conversations/ui/RtpSessionActivity.java | 20 ++++++++++++++----- .../xmpp/jingle/JingleConnectionManager.java | 2 +- .../xmpp/jingle/RtpEndUserState.java | 1 + src/main/res/values/strings.xml | 1 + 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index f85ad3f4e..0b671453a 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -95,16 +95,21 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void endCall() { if (this.rtpConnectionReference == null) { - final Intent intent = getIntent(); - final Account account = extractAccount(intent); - final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH)); - xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid()); + retractSessionProposal(); finish(); } else { requireRtpConnection().endCall(); } } + private void retractSessionProposal() { + final Intent intent = getIntent(); + final Account account = extractAccount(intent); + final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH)); + resetIntent(account, with, RtpEndUserState.RETRACTED, actionToMedia(intent.getAction())); + xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid()); + } + private void rejectCall(View view) { requireRtpConnection().rejectCall(); finish(); @@ -290,6 +295,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final JingleRtpConnection jingleRtpConnection = weakReference == null ? null : weakReference.get(); if (jingleRtpConnection != null) { releaseVideoTracks(jingleRtpConnection); + } else if (!isChangingConfigurations()) { + retractSessionProposal(); } releaseProximityWakeLock(); super.onStop(); @@ -420,6 +427,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe case CONNECTIVITY_ERROR: setTitle(R.string.rtp_state_connectivity_error); break; + case RETRACTED: + setTitle(R.string.rtp_state_retracted); + break; case APPLICATION_ERROR: setTitle(R.string.rtp_state_application_failure); break; @@ -468,7 +478,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.binding.endCall.setImageResource(R.drawable.ic_clear_white_48dp); this.binding.endCall.setVisibility(View.VISIBLE); this.binding.acceptCall.setVisibility(View.INVISIBLE); - } else if (state == RtpEndUserState.CONNECTIVITY_ERROR || state == RtpEndUserState.APPLICATION_ERROR) { + } else if (asList(RtpEndUserState.CONNECTIVITY_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); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index b25647854..07d6a25cb 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -402,11 +402,11 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } if (matchingProposal != null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retracting rtp session proposal with " + with); this.rtpSessionProposals.remove(matchingProposal); final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(matchingProposal); writeLogMissedOutgoing(account, matchingProposal.with, matchingProposal.sessionId, null, System.currentTimeMillis()); mXmppConnectionService.sendMessagePacket(account, messagePacket); - } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java index 4baa0019d..398777cfe 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java @@ -11,5 +11,6 @@ public enum RtpEndUserState { ENDED, //close UI DECLINED_OR_BUSY, //other party declined; no retry button CONNECTIVITY_ERROR, //network error; retry button + 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 } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 32ade6387..48ca72cea 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -904,6 +904,7 @@ Ringing Busy Unable to connect call + Retracted call Application failure Hang up Ongoing call From 876b1149d548e88ef0342e125c676971bcf2fae9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 21 Apr 2020 22:59:54 +0200 Subject: [PATCH 157/182] avoid double termination after failed connection --- .../conversations/xmpp/jingle/JingleRtpConnection.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index f183f7ee9..797993aa5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -958,7 +958,13 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void closeWebRTCSessionAfterFailedConnection() { this.webRTCWrapper.close(); - sendSessionTerminate(Reason.CONNECTIVITY_ERROR); + synchronized (this) { + if (TERMINATED.contains(state)) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": no need to send session-terminate after failed connection. Other party already did"); + return; + } + sendSessionTerminate(Reason.CONNECTIVITY_ERROR); + } } public AppRTCAudioManager getAudioManager() { From 04a7b9da1c25229bf2e48cd3ae8b0ea69b13df0f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 22 Apr 2020 08:54:20 +0200 Subject: [PATCH 158/182] pulled translations from transifex --- src/main/res/values-de/strings.xml | 15 +++ src/main/res/values-gl/strings.xml | 163 +++++++++++++++---------- src/main/res/values-ro-rRO/strings.xml | 1 + src/quicksy/res/values-gl/strings.xml | 10 +- 4 files changed, 122 insertions(+), 67 deletions(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 078e77d69..88f4908c6 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -650,6 +650,7 @@ Zugehörige Unterhaltung beendet. Kontakt gesperrt. Benachrichtigungen von Unbekannten + Benachrichtigen bei Erhalt von Nachrichten und Anrufen von Unbekannten. Erhaltene Nachricht von einem Unbekannten Unbekannten sperren Gesamte Domain sperren @@ -754,6 +755,8 @@ Laufende Anrufe Lautlose Nachrichten Diese Benachrichtigungsart wird verwendet, um Benachrichtigungen anzuzeigen, die keinen Ton auslösen sollen. Zum Beispiel, wenn du auf einem anderen Gerät aktiv bist (Schonfrist). + Benachrichtigungseinstellungen + Anrufeinstellungen Wichtigkeit, Klang, Vibrationen Video komprimieren Medien anzeigen @@ -888,18 +891,30 @@ Bitte aktiviere ein Konto Anrufen Eingehender Anruf + Eingehender Videoanruf Verbinden Verbunden Anruf annehmen Anruf beenden + Annehmen + Ablehnen + Geräte lokalisieren Klingelt + Besetzt + Anruf kann nicht verbunden werden + Rückrufruf + Anwendungsfehler Auflegen Laufender Anruf + Laufender Videoanruf Deaktiviere Tor, um Anrufe zu tätigen Eingehender Anruf Eingehender Anruf · %s Ausgehender Anruf Ausgehender Anruf · %s + Entgangener Anruf + Audioanruf + Videoanruf Dein Mikrofon ist nicht verfügbar Du kannst immer nur einen Anruf zur gleichen Zeit machen. diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 518614c26..07d646240 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -24,7 +24,7 @@ Compartir na conversa Iniciar conversa Escoller Contacto - Escolla contactos + Seleccionar contactos Compartir coa conta Lista de bloqueo agora @@ -72,9 +72,9 @@ Non preguntar de novo Non puido conectarse a conta Erro na conexión a múltiples contas - Pulse aquí para xestionar as súas contas + Preme aquí para xestionar as túas contas Adxuntar - O contacto non está na sua lista. ¿Queres engadilo? + O contacto non está na túa lista. ¿Queres engadilo? Engadir contacto Erro ao enviar Preparando imaxe para enviar @@ -95,7 +95,7 @@ Enviar mensaxe cifrado con OpenPGP Modificouse o teu alcume Enviar sen cifrar - Fallou o descifrado. Quizábeis non teñas a clave privada apropiada. + Fallou o descifrado. Quizais non teñas a chave privada apropiada. OpenKeychain Conversations emprega unha aplicación de terceiros chamada OpenKeychain para cifrar e descifrar mensaxes e xestionar as túas claves públicas.\n\nOpenKeychain está publicado baixo licencia GPLv3 e disponible en F-Droid e Google Play.\n\n(Por favor, reinicie Conversations despois.) Reiniciar @@ -104,9 +104,9 @@ ofrecendo… agardando... Clave OpenPGP non atopada - Conversations non foi quen de cifrar as túas mensaxes porque o teu contactos non está anunciando a súa clave pública.\n\nPor favor, pídelle ao teu contacto que configure OpenPGP. + Conversations non foi quen de cifrar as túas mensaxes porque o teu contacto non está anunciando a súa chave pública.\n\nPor favor, pídelle ao contacto que configure OpenPGP. Non se atoparon chaves OpenPGP - Conversations non pode cifrar a súa mensaxe porque os seus contactos no publicaron a súa chave pública.\n\n Por favor solicite aos seus contactos que configuren OpenPGP. + Conversations non pode cifrar a túa mensaxe porque os teus contactos non publicaron a súa chave pública.\n\n Por favor solicitalle aos teus contactos que configuren OpenPGP. Xeral Aceptar ficheiros De forma automática aceptar ficheiros menores de… @@ -116,13 +116,17 @@ Vibra cando chega unha nova mensaxe Notificación LED Luz pestanexante cando chegue unha nova mensaxe + Ton de chamada + Son da notificación + Son da notificación para novas mensaxes + Ton de chamada para chamadas entrantes Período de graza O tempo no que as notificacións son silenciadas tras detectar actividade en algún dos teus outros dispositivos. Avanzado Nunca enviar informe de erros - Enviando volcados de pilas axudas ao desenvolvemento de Conversations + Enviando trazas do rexistro axudas ao desenvolvemento de Conversations Confirmación de mensaxes - Permitir aos seus contactos saber si recibeu e leu as súas mensaxes + Permitir aos teus contactos saber se recibiches e liches as súas mensaxes Interface OpenKeychain informou de un fallo. Chave incorrecta para cifrar. @@ -130,9 +134,9 @@ Produciuse un erro Fallo A túa conta - Enviar actualizacións de presencia - Recibir actualizacións de presencia - Solicitar actualizacións de presencia + Enviar actualizacións de presenza + Recibir actualizacións de presenza + Solicitar actualizacións de presenza Seleccionar imaxe Facer foto Por defecto otorgar peticiones de suscripción @@ -168,7 +172,7 @@ Publicar avatar Publicar chave pública OpenPGP Eliminar a chave pública OpenPGP - Está seguro de que quere eliminar a súa chave pública OpenPGP do seu anuncio de presencia? \nOs seus contactos non poderán enviarlle mensaxes cifradas con OpenPGP. + Tes a certeza de que queres eliminar a túa chave pública OpenPGP do anuncio de presenza? \nOs teus contactos non poderán enviarche mensaxes cifradas con OpenPGP. Publicouse a chave pública OpenPGP Habilitar Seguro? @@ -180,7 +184,7 @@ Contrasinal Non é un enderezo XMPP válido Exceso de memoria. A imaxe é demasiado grande - Quere engadir a 1%s a súa libreta de enderezos? + Queres engadir a %s a túa libreta de enderezos? Info do servidor XEP-0313: MAM XEP-0280: Copia de mensaxes @@ -188,6 +192,7 @@ XEP-0191: Bloqueo de ordes XEP-0237: Roster Versioning XEP-0198: Stream Management + XEP-0215: Descubrimento de Servizo Externo XEP-0163: PEP (Avatars / OMEMO) XEP-0363: HTTP File Upload XEP-0357: Push @@ -238,7 +243,7 @@ Asunto Entrando na conversa en grupo Saír - Contacto engadido a súa lista de contactos + Contacto engadido a túa lista de contactos Voltar a engadir %s leeu ate este punto %s leu ate este punto @@ -247,8 +252,8 @@ Publicar Toque no avatar para escoller imaxe desde a galería Publicando... - O servidor rexeitou a súa publicación - Algo fallou mentras se convertía a súa imaxe + O servidor rexeitou a túa publicación + Algo fallou mentras se convertía a túa imaxe Non se puido salvar o avatar no disco (ou pulsación longa para voltar ao valor por omisión) O seu servidor non admite a publicación de avatares @@ -264,15 +269,15 @@ Habilitar A conversa en grupo require contrasinal Introducir contrasinal - Por favor, primeiro solicite actualizacións de presencia ao seu contacto.\n\n Esto utilizarase para determinar qué cliente(s) está a usar o seu contacto. + Por favor, primeiro solicita actualizacións de presenza ao teu contacto.\n\n Esto utilizarase para determinar qué cliente(s) está a usar o teu contacto. Solicitar agora Ignorar - Aviso: Enviando esto sen mutuas actualización de presenza podería causar problemas.\n\n Vaia aos detalles do contacto para verificar as súas suscricións de presenza. + Aviso: Ao enviar esto sen mutuas actualizacións de presenza podería causar problemas.\n\n Vaite aos detalles do contacto para verificar as túas suscricións de presenza. Seguridade Permitir a corrección de mensaxes - Permitir aos seus contactos editar as súas mensaxes de xeito retroactivo - Axustes de experto - Por favor teña coidado con estos axustes + Permitir aos teus contactos editar as súas mensaxes de xeito retroactivo + Axustes de experta + Por favor ten tino con estos axustes Acerca de %s Non molestar Hora de inicio @@ -281,7 +286,7 @@ As notificacións serán silenciadas durante estas horas Outro Sincronizar cos marcadores - Unirse e deixar conversas de grupo de acordo coa marca auto-unirse nos seus marcadores. + Unirse e deixar conversas de grupo de acordo coa marca auto-unirse nos teus marcadores. Copiouse a pegada dixital OMEMO ao portapapeis! Non podes acceder a esta conversa en grupo Esta conversa en grupo é so para membros @@ -312,14 +317,14 @@ Confirmar Inténteo de novo Manter servizo en primeiro plano - Evita que o sistema operativo corte a súa conexión - Crear respaldo - Os ficheiros de respaldo gardaranse en %s - Creando ficheiros de respaldo - Creouse o respaldo - Os ficheiros de respaldo gardáronse en %s - Restaurando o respaldo - O seu respaldo foi restablecido + Evita que o sistema operativo corte a conexión + Crear copia de apoio + Os ficheiros de copia gardaranse en %s + Creando ficheiros de apoio + Creouse o ficheiro + Os ficheiros de apoio gardáronse en %s + Restaurando a copia + O copia foi restablecida Non esqueza activar a conta. Escoller ficheiro Recibindo %1$s (%2$d %% completado) @@ -346,9 +351,9 @@ Copiar pegada OMEMO ao portapapeis Rexenerar a chave OMEMO Limplar dispositivos - Está segura de que quere eliminar todos os outros dispositivos da declaración OMEMO? A próxima vez que se conecten os seus dispositivos estos voltarán a anunciarse pero non rebirán mensaxes enviados mentras tanto. + Tes a certeza de que queres eliminar todos os outros dispositivos da declaración OMEMO? A próxima vez que se conecten os teus dispositivos estos voltarán a anunciarse pero non rebirán mensaxes enviados mentras tanto. Non hai chaves dispoñibles que se poidan usar con este contacto.\nNon se puideron obter novas chaves do servidor. Quizáis hai algún problema coas chaves do seu contacto. - Non hai chaves utilizables dispoñibles para este contacto.\nAsegúrese de que ambos teñen mutua suscrición de presenza. + Non hai chaves utilizables dispoñibles para este contacto.\nAsegúrate de que ambos tedes mutua subscrición de presenza. Algo saíu mal Obtendo historial desde o servidor Non hai máis historial no servidor @@ -378,7 +383,7 @@ Non se puido mudar a afiliación de %s Prohibición da conversa en grupo Prohibir no canal - Está a eliminar %s de un canal público. O único xeito de facer esto é vetar esta usuaria para sempre. + Estás a eliminar %s dun canal público. O único xeito de facer isto é vetar esta usuaria para sempre. Rexeitar agora Non se puido mudar o rol de %s Configuración do grupo privado de conversa @@ -414,7 +419,7 @@ %s están escribindo... %s deixaron de escribir Notificacións de escritura - Permita aos seus contactos que saiban cando lles está a escribir + Permitelle aos teus contactos que saiban cando lles estás a escribir Enviar localización Mostrar localización Non se atopou un aplicativo para mostrar a localización @@ -434,11 +439,11 @@ %d certificado eliminado %d certificados eleminados - Substitúa o botón enviar con unha acción rápida + Cambia o botón de enviar por unha acción rápida Acción rápida Ningunha Utilizadas recentemente - Escolla a acción rápida + Escolle a acción rápida Buscar contactos Buscar marcadores Enviar mensaxe privada @@ -500,7 +505,7 @@ Conversations necesita acceso ao almacenamiento externo Conversations necesita acceso á cámara Sincronice con todos os contactos - Conversations quere confrontar a súa lista de contactos no servidor coa súa libreta de enderezos local para mostrarlle os nomes completos e avatares.\n\nConversations só lerá os seus contactos de xeito local e sen subilos ao servidor.\n\nA continuación pediráselle permiso para acceder aos contactos. + Conversations quere confrontar a túa lista de contactos no servidor coa túa libreta de enderezos local para mostrarche os nomes completos e avatares.\n\nConversations só lerá os teus contactos de xeito local e sen subilos ao servidor.\n\nA continuación pediráseche permiso para acceder aos contactos.
Non gardaremos unha copia de esos números de teléfono.\n\nPara máis información lea a nosa política de intimidade.

A continuación pediráselle permiso para acceder aos contactos.]]>
Notificar todas as mensaxes Notificar só cando é mencionada @@ -526,13 +531,13 @@ Compartir URI con...
Podes rexistrarte co teu número de teléfono e Quicksy suxerillache automáticamente —tomando os números da túa libreta de enderezos como referencia— posibles contactos para ti.

Ao rexistrarte aceptas a nosa política de intimidade.]]>
Aceptar & continuar - Guiarémola a través do proceso de creación de unha conta en conversations.im.¹\nAo escoller a conversations.im como fornecedor poderá comunicar con usuarias de outros fornecedores proporcionándolles o seu enderezo XMPP completo. + Guiarémoste a través do proceso de creación dunha conta en conversations.im.¹\nAo escoller a conversations.im como fornecedor poderás comunicar con usuarias de outros fornecedores proporcionándolles o teu enderezo XMPP completo. O seu enderezo XMPP completo será: %s Crear conta Utilizar o meu propio proveedor - Escolla un identificador + Elixe un identificador Xestionar a disponibilidade manualmente - Configure a súa disponibilidade ao editar a mensaxe de estado. + Configura a túa dispoñibilidade ao editar a mensaxe de estado. Mensaxe de estado Dispoñible para conversar En liña @@ -551,10 +556,10 @@ Medio Longo Emitir a última interacción coa usuaria - Permitirlle aos contactos saber cando está a usar Conversations + Permitirlle aos contactos saber cando estás a utilizar Conversations Intimidade Decorado - Escolla a gama de cores + Escolle a gama de cores Automático Decorado claro Decorado escuro @@ -578,7 +583,7 @@ Non se puido actualizar a conta Informe de este JID como emisor de mensaxes non desexadas. Borrar identidades OMEMO - Rexenerar as súas chaves OMEMO. Todos os seus contactos deberán verificalo de novo. Utilice esto como último recurso. + Rexenerar as túas chaves OMEMO. Todos os teus contactos deberán verificarte de novo. Utiliza esto como último recurso. Eliminar as chaves seleccionadas. Precisa estar conectada para publicar o seu avatar. Mostrar mensaxe do fallo @@ -645,6 +650,7 @@ Conversas correpondentes pechadas. Contacto bloqueado. Notificacións de estraños + Notificar as mensaxes e chamadas recibidas por parte de extraños. Mensaxe recibida de un estraño Bloquear estraño Bloquear o dominio ao completo @@ -678,7 +684,7 @@ Detalles do certificado: Unha vez O escaner de código QR necesita acceso á cámara. - Desprazarse ata la parte inferior + Desprazarse ata a parte inferior Desprazarse cara abaixo logo de enviar unha mensaxe Editar a Menxase de Estado Editar a menxase de estado @@ -686,8 +692,8 @@ Conversations non pode enviar mensaxes cifradas a %1$s Isto pode deberse a que o seu contacto utiliza un servidor ou cliente obsoleto que non pode manexar OMEMO. Non se pode recuperar la lista de dispositivos Non se poden recuperar os paquetes de dispositivos - Suxerencia: Nalgúns casos, isto pode solucionarse engadíndose mutuamente as súas listas de contactos. - ¿Está segura de que quere desactivar o cifrado OMEMO para esta conversación? Isto permitirá que o administrador do seu servidor lea as súas mensaxes, pero pode ser a única forma de comunicarse con persoas que usan clientes obsoletos. + Suxestión: Nalgúns casos, isto pode solucionarse engadíndovos mutuamente as vosas listas de contactos. + ¿Tes a certeza de que queres desactivar o cifrado OMEMO para esta conversa? Isto permitirá que o administrador do teu servidor lea as túas mensaxes, pero pode ser a única forma de comunicarse con persoas que usan clientes obsoletos. Desactivar agora Borrador: Cifrado OMEMO @@ -741,11 +747,16 @@ Esta categoría de notificacións utilízase para mostrar unha notificación permanente indicando que Conversations está a funcionar. Información do estado Problemas de conexión - Esta categoría de conexión utilízase para mostrar unha notificación en caso de que houbese un problema ao conectar a conta. + Esta categoría de notificación utilízase para mostrar unha notificación en caso de que houbese un problema ao conectar a conta. Mensaxes + Chamadas Mensaxes + Chamadas recibidas + Chamadas realizadas Mensaxes acalados Este grupo de notificacións é utilizado para mostrar notificacións que non debería activar ningún son. Por exemplo, cando está activo en outro dispositivo (Período de Graza). + Axustes de notificación das mensaxes + Axustes da notificación de chamadas Importancia, Son, Vibrar Compresión de vídeo Ver medios @@ -760,7 +771,7 @@ Xa está a escribir un borrador. Característica non implementada Código de país non válido - Escolla un país + Indica un país número de teléfono Valide o seu número de teléfono Quicsy enviaralle unha mensaxe SMS (poderíanse aplicar cargos) para validar o seu número de teléfono. Introduza o código de país e número de teléfono. @@ -790,14 +801,14 @@ Non se puido conectar co servidor. Non se puido establecer unha conexión segura. Non se atopou o servidor. - Algo fallou ao xestionar a súa solicitude. - Datos inválidos do usuario + Algo fallou ao xestionar a túa solicitude. + Entrada da usuaria non válida Non dispoñible temporalmente. Inténteo máis tarde. Se conexión a rede. Inténteo de novo en %s Taxa de transferencia limitada Demasiados intentos - Está a utilizar unha versión desactualizada de esta app. + Estás a usar unha versión desactualizada desta app. Actualizar Este número de teléfono está actualmente ligado a outro dispositivo. Por favor, escribe o teu nome para permitir que a xente que non te ten na axenda de enderezos sepa quen es. @@ -814,12 +825,12 @@ Abrir con... Imaxe de perfil en Conversations Escoller conta - Restablecer respaldo + Restablecer copia de apoio Restablecer - Introduza o contrasinal da conta %s para restablecer o respaldo. - Non utilice a función de restaurar o respaldo nun intento de clonar (utilizar simultaneamente) unha instalación. Restaurar un respaldo só ten sentido para migrar ou en caso de perda do dispositivo orixinal. - Non se puido restaurar o respaldo. - Non se puido descifrar o respaldo. É correcto o contrasinal? + Escribe o contrasinal da conta %s para restablecer a copia. + Non utilices a función de restaurar a copia nun intento de clonar (utilizar simultaneamente) unha instalación. Restaurar unha copia só ten sentido para migrar ou en caso de perda do dispositivo orixinal. + Non se puido restaurar a copia. + Non se puido descifrar a copia. É correcto o contrasinal? Respaldar & Restaurar Introducir enderezo XMPP Crear grupo de conversa @@ -844,7 +855,7 @@ Calquera pode convidar a outras. Os enderezos XMPP son visibles para a administración. Os enderezos XMPP son visibles para calquera. - Este canal público non ten participantes. Convide aos seus contactos ou utilice o botón compartir para distribuír o seu enderezo XMPP. + Este canal público non ten participantes. Convida aos teus contactos ou utiliza o botón compartir para distribuír o teu enderezo XMPP. Este grupo privado non ten participantes. Xestionar privilexios Buscar participantes @@ -853,18 +864,18 @@ Descubrir canales Buscar canales Posible intrusión na intimidade! - search.jabber.network.

Ao utilizar esta función transmitirá o seu enderezo IP e termos de busca a ese servizo. Lea a súa Política de Intimidade para máis información.]]>
+ search.jabber.network.

Ao utilizar esta función transmitirá o teu enderezo IP e termos de busca a ese servizo. Le a súa Política de Privacidade para máis información.]]>
Xa teño unha conta Engadir conta existente Rexistrar unha nova conta Esto semella un enderezo de dominio Engadir igualmente Esto semella o enderezo de un canal - Compartir ficheiros de respaldo + Compartir ficheiros de apoio Respaldar Conversations Evento - Abrir respaldo - O ficheiro seleccionado non é un ficheiro de respaldo Conversations + Abrir copia de apoio + O ficheiro seleccionado non é un ficheiro de apoio Conversations Esta conta xa foi configurada Introduza o contrasinal de esta conta Non se puido completar a acción @@ -875,9 +886,37 @@ Servidor local A maioría das usuarias debería escoller \'jabber.network\' para obter mellores suxestións desde o ecosistema público XMPP. Método de descubrimento de canles - Respaldo + Copia de apoio Acerca de Activa unha conta por favor + Facer unha chamada + Chamada entrante + Videochamada entrante + Conectando + Conectado + Aceptando a chamada + Rematando a chamada + Responder + Rexeitar + Localizando dispositivos + Sonando + Ocupado + Non se pode establecer a chamada + Chamada cortada + Fallo na aplicación + Colgar + Chamada en curso + Videochamada en curso + Desactivar Tor para facer chamadas + Chamada entrante + Conversa de · %s + Chamada realizada + Conversa de · %s + Chamada perdida + Chamada de audio + Chamada de vídeo + O micrófono non está dispoñible + Só podes manter unha chamada en cada momento. Ver %1$d Participante Ver %1$d Participantes diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 55dc9eb94..b08db01dc 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -912,6 +912,7 @@ sau chiar pierderea mesajelor.\nÎn continuare veți fi rugați să dezactivați Sună Ocupat Nu s-a putut conecta apelul + Apel anulat Eroare de aplicație Închide Apel în curs diff --git a/src/quicksy/res/values-gl/strings.xml b/src/quicksy/res/values-gl/strings.xml index 9641086fe..99701c741 100644 --- a/src/quicksy/res/values-gl/strings.xml +++ b/src/quicksy/res/values-gl/strings.xml @@ -1,12 +1,12 @@ Quicksy fallou - Ao enviar lotes de rexistro estás a axudar no desenvolvemento de Quicksy\nAviso: vas utilizar a tua conta XMPP para enviar o rexistro ao equipo de desenvolvemento. - Quicksy utiliza unha app de terceiros chamada OpenKeychain para cifrar e descifrar as mensaxes e xestionar a tuas chaves públicas.\n\nOpenKeychain ten licenza GPLv3 e está dispoñible en F-Droid e Google Play.\n\n(Por favor, reinicia Quicksy ao rematar). - Quicksy non pode cifrar as tuas mensaxes porque o teu contacto non publicou as sua chave pública.\n\nPor favor, solicita ao contacto que configure OpenPGP. - Quicksy non pode cifrar as tuas mensaxes porque os teus contactos non están a publicar a súa chave pública.\n\nPor favor, pídelle aos teus contactos que configuren OpenPGP. + Ao enviar trazas do rexistro estás a axudar no desenvolvemento de Quicksy\nAviso: vas utilizar a túa conta XMPP para enviar o rexistro ao equipo de desenvolvemento. + Quicksy utiliza unha app de terceiros chamada OpenKeychain para cifrar e descifrar as mensaxes e xestionar a túas chaves públicas.\n\nOpenKeychain ten licenza GPLv3 e está dispoñible en F-Droid e Google Play.\n\n(Por favor, reinicia Quicksy ao rematar). + Quicksy non pode cifrar as túas mensaxes porque o teu contacto non publicou as súa chave pública.\n\nPor favor, solicita ao contacto que configure OpenPGP. + Quicksy non pode cifrar as túas mensaxes porque os teus contactos non están a publicar a súa chave pública.\n\nPor favor, pídelle aos teus contactos que configuren OpenPGP. O período de tempo que Quicksy permanece acalado tras ver actividade en outro dispositivo - Enviando trazas de rexistro estás axudando ao desenvolvemento de Quicksy + Enviando trazas do rexistro estás axudando ao desenvolvemento de Quicksy Quicksy precisa acceso ao almacenamento externo Quicksy precisa acceso a cámara O teu dispositivo está a realizar optimizacións de batería intensivas con Quicksy que poderían levar a que as notificacións tarden en chegar ou que as mensaxes se perdan.\nRecomendamos desactivalas. From 9afac21b0b46746779b7fc7d547d763039a08f22 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 22 Apr 2020 14:49:48 +0200 Subject: [PATCH 159/182] =?UTF-8?q?don=E2=80=99t=20throw=20when=20user=20d?= =?UTF-8?q?ouble=20taps=20accept=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../conversations/xmpp/jingle/JingleRtpConnection.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 797993aa5..1a02f2af7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -789,6 +789,13 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web case SESSION_INITIALIZED: acceptCallFromSessionInitialized(); break; + case ACCEPTED: + Log.w(Config.LOGTAG,id.account.getJid().asBareJid()+": the call has already been accepted with another client. UI was just lagging behind"); + break; + case PROCEED: + case SESSION_ACCEPTED: + Log.w(Config.LOGTAG,id.account.getJid().asBareJid()+": the call has already been accepted. user probably double tapped the UI"); + break; default: throw new IllegalStateException("Can not accept call from " + this.state); } From 1fc98c0c118b90ce447766a5a0acb81330e70c45 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 22 Apr 2020 14:52:14 +0200 Subject: [PATCH 160/182] version bump to 2.8.0-rc.2 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 90218ea16..7bcf04232 100644 --- a/build.gradle +++ b/build.gradle @@ -95,8 +95,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 28 - versionCode 375 - versionName "2.8.0-rc.1" + versionCode 376 + versionName "2.8.0-rc.2" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From fa45422fa8a2931131039d45f9571993a44b7bb4 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 22 Apr 2020 14:59:23 +0200 Subject: [PATCH 161/182] pulled translations from transifex --- src/main/res/values-hu/strings.xml | 40 ++++++++++++++++++++++++++ src/main/res/values-it/strings.xml | 39 +++++++++++++++++++++++++ src/main/res/values-pt-rBR/strings.xml | 29 +++++++++++++++++-- 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/src/main/res/values-hu/strings.xml b/src/main/res/values-hu/strings.xml index a211d6050..c7f8d7f28 100644 --- a/src/main/res/values-hu/strings.xml +++ b/src/main/res/values-hu/strings.xml @@ -116,6 +116,10 @@ Rezegés új üzenet érkezésekor LED értesítés Értesítési fény villogása új üzenet érkezésekor + Csengőhang + Értesítési hang + Értesítési hang új üzeneteknél + Csengőhang bejövő hívásnál Türelmi idő Az időtartam, amíg az értesítések némítva vannak az egyéb eszközei egyikén történt tevékenység észlelése után. Speciális @@ -188,6 +192,7 @@ XEP-0191: Tiltóparancs XEP-0237: Névsorverziózás XEP-0198: Adatfolyam-kezelés + XEP-0215: Külső szolgáltatás felderítése XEP-0163: PEP (profilképek / OMEMO) XEP-0363: HTTP fájlfeltöltés XEP-0357: Leküldés @@ -645,6 +650,7 @@ A megfelelő beszélgetések lezárultak. Partner tiltva. Értesítések idegenektől + Értesítés az idegenektől fogadott üzenetekről és hívásokról. Üzenet érkezett egy idegentől Idegen tiltása Teljes tartomány tiltása @@ -743,9 +749,14 @@ Kapcsolódási problémák Ezt az értesítési kategóriát egy értesítés megjelenítéséhez használják abban az esetben, ha probléma merül fel a fiókhoz való kapcsolódásnál. Üzenetek + Hívások Üzenetek + Bejövő hívások + Kimenő hívások Csendes üzenetek Ezt az értesítési csoportot olyan értesítések megjelenítéséhez használják, amelyek nem aktiválhatnak hangot. Például ha aktívvá válik egy másik eszközön (türelmi idő). + Üzenet értesítésének beállításai + Bejövő hívások értesítésnek beállításai Fontosság, hang, rezgés Videó tömörítése Média megtekintése @@ -877,6 +888,35 @@ Csatornafelderítés módszere Biztonsági mentés Névjegy + Engedélyezzen egy fiókot + Hívás indítása + Bejövő hívás + Bejövő videohívás + Kapcsolódás + Kapcsolódva + Hívás elfogadása + Hívás befejezése + Válasz + Elutasítás + Eszközök keresése + Csörgetés + Elfoglalt + Nem lehet kapcsolódni a híváshoz + Visszavont hívás + Alkalmazáshiba + Lerakás + Kimenő hívás + Kimenő videohívás + Tor letiltása a hívások indításához + Bejövő hívás + Bejövő hívás · %s + Kimenő hívás + Kimenő hívás · %s + Nem fogadott hívás + Hanghívás + Videohívás + A mikrofonja nem érhető el + Egyszerre csak egy hívásban vehet részt. %1$d résztvevő megtekintése %1$d résztvevő megtekintése diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index fa17d3892..28234b2ce 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -116,6 +116,10 @@ Vibra quando arriva un nuovo messaggio Notifica LED Luce di notifica lampeggiante quando arriva un nuovo messaggio + Suoneria + Suono di notifica + Suono di notifica per i nuovi messaggi + Suoneria per chiamate in arrivo Periodo di grazia Il periodo di tempo in cui le notifiche vengono silenziate dopo aver rilevato attività su uno dei tuoi altri dispositivi. Avanzate @@ -188,6 +192,7 @@ XEP-0191: Blocking Command XEP-0237: Roster Versioning XEP-0198: Stream Management + XEP-0215: Scoperta di servizi esterni XEP-0163: PEP (Avatars / OMEMO) XEP-0363: HTTP File Upload XEP-0357: Push @@ -645,6 +650,7 @@ Chiuse le relative conversazioni. Contatto bloccato. Notifiche da sconosciuti + Notifica messaggi e chiamate ricevuti da sconosciuti. Ricevuto messaggio da uno sconosciuto Blocca sconosciuto Blocca intero dominio @@ -743,9 +749,14 @@ Problemi di connettività Questa categoria di notifiche è usata per mostrare un notifica in caso si verifichi un problema nella connessione ad un account. Messaggi + Chiamate Messaggi + Chiamate in arrivo + Chiamate in uscita Messaggi silenziosi Questo gruppo di notifiche è usato per mostrare notifiche che non devono riprodurre alcun suono. Ad esempio mentre si è attivi su un altro dispositivo (Periodo di grazia). + Impostazioni di notifica dei messaggi + Impostazioni di notifica delle chiamate in arrivo Importanza, suono, vibrazione Compressione video Vedi i media @@ -878,6 +889,34 @@ Backup Al riguardo Devi attivare un account + Chiama + Chiamata in arrivo + Chiamata video in arrivo + Connessione + Connesso + Accettazione chiamata + Chiusura chiamata + Rispondi + Rifiuta + Localizzazione dispositivi + Sta squillando + Occupato + Impossibile connettere la chiamata + Chiamata ritirata + Errore dell\'applicazione + Riaggancia + Chiamata in corso + Chiamata video in corso + Disattiva Tor per le chiamate + Chiamata in arrivo + Chiamata in arrivo · %s + Chiamata in uscita + Chiamata in uscita · %s + Chiamata persa + Chiamata vocale + Chiamata video + Il tuo microfono non è disponibile + Puoi fare solo una chiamata alla volta. Vedi %1$d partecipante Vedi %1$d partecipanti diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 5239def65..c90490a8f 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -119,7 +119,7 @@ Toque Som de notificação Som de notificação para novas mensagens - Toque ao receber chamadas + Toque para chamadas recebidas Período de espera Espaço de tempo em que as notificações serão silenciadas, após detectar atividade em algum dos seus outros dispositivos. Avançado @@ -192,7 +192,7 @@ XEP-0191: Comando de bloqueio XEP-0237: Versionamento da lista de contatos XEP-0198: Gerenciamento de fluxo - XEP-0215: Descoberta de Serviço Externo + XEP-0215: Descoberta de serviço externo XEP-0163: PEP (Avatares / OMEMO) XEP-0363: Envio de arquivos via HTTP XEP-0357: Push @@ -751,8 +751,12 @@ Mensagens Chamadas Mensagens + Chamadas recebidas + Chamadas em andamento Silenciar mensagens Essa categoria de notificação é utilizada para exibir notificações que não deveriam gerar nenhum som. Por exemplo, quando estiver ativo em outro dispositivo (Período de Espera). + Configurações das notificações de mensagens + configurações das notificações de chamadas recebidas Importância, som, vibração. Compressão de vídeo Ver mídia @@ -885,15 +889,34 @@ Backup Sobre Por favor, habilite uma conta + Fazer chamada + Recebendo chamada + Recebendo chamada de vídeo Conectando Conectado + Atendendo chamada + Encerrando chamada + Atender + Dispensar Procurando dispositivos Tocando Ocupado + Não foi possível conectar a chamada + Chamada rejeitada + Falha no aplicativo + Desligar + Atendendo chamada + Atendendo vídeo chamada + Desabilitar o Tor para fazer chamadas + Chamada recebida + Chamada recebida · %s + Chamada realizada + Chamada realizada · %s Chamada perdida + Chamada de áudio Chamada de vídeo Seu microfone não está disponível - Você só pode ter uma chamada por vez + Você só pode ter uma chamada de cada vez Ver %1$d participante Ver %1$d participantes From 9fa9ca9cbcf4626bad2318f1955901667b1b4813 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 22 Apr 2020 16:35:08 +0200 Subject: [PATCH 162/182] catch securityException when parsing rtp description --- .../siacs/conversations/xmpp/jingle/JingleRtpConnection.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 1a02f2af7..18bdcf1f5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -270,7 +270,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web contentMap = RtpContentMap.of(jinglePacket); contentMap.requireContentDescriptions(); contentMap.requireDTLSFingerprint(); - } catch (final IllegalArgumentException | IllegalStateException | NullPointerException e) { + } catch (final RuntimeException e) { respondOk(jinglePacket); sendSessionTerminate(Reason.of(e), e.getMessage()); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", e); @@ -321,7 +321,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web contentMap = RtpContentMap.of(jinglePacket); contentMap.requireContentDescriptions(); contentMap.requireDTLSFingerprint(); - } catch (final IllegalArgumentException | IllegalStateException | NullPointerException e) { + } catch (final RuntimeException e) { respondOk(jinglePacket); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents in session-accept", e); webRTCWrapper.close(); From 892d913e2cf4835eb1dd35a0608ac84c9174b058 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 22 Apr 2020 18:42:07 +0200 Subject: [PATCH 163/182] parsing iq erros also need to finish the connection --- .../conversations/xmpp/jingle/JingleRtpConnection.java | 9 +++------ .../conversations/xmpp/jingle/stanzas/JinglePacket.java | 2 -- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 18bdcf1f5..476c463d7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -680,11 +680,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } else { target = State.TERMINATED_APPLICATION_FAILURE; } - if (transition(target)) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": terminated session with " + id.with); - } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not transitioning because already at state=" + this.state); - } + transitionOrThrow(target); + this.finish(); } else if (response.getType() == IqPacket.TYPE.TIMEOUT) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ timeout in RTP session with " + id.with + ". terminating with connectivity error"); if (TERMINATED.contains(this.state)) { @@ -692,7 +689,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return; } this.webRTCWrapper.close(); - transition(State.TERMINATED_CONNECTIVITY_ERROR); + transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR); this.finish(); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java index 99fa3dc60..31ee95bbf 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -16,8 +16,6 @@ import rocks.xmpp.addr.Jid; public class JinglePacket extends IqPacket { - //TODO add support for groups: https://xmpp.org/extensions/xep-0338.html - private JinglePacket() { super(); } From fc7ecca1b32900eac7023cfeba9b885867407432 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 22 Apr 2020 18:42:42 +0200 Subject: [PATCH 164/182] build universal apk (easier to give to people manually) --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7bcf04232..648a9c77a 100644 --- a/build.gradle +++ b/build.gradle @@ -106,6 +106,7 @@ android { splits { abi { + universalApk true enable true } } @@ -273,4 +274,4 @@ android { } } -} \ No newline at end of file +} From 22e93e4169e98b850df1a31d98f95d7aac289644 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 22 Apr 2020 20:23:13 +0200 Subject: [PATCH 165/182] fix direct share for cases where the application id was changed --- src/main/AndroidManifest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 95eb5139c..c78d32a98 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -225,9 +225,10 @@ + + android:value="eu.siacs.conversations.services.ContactChooserTargetService" />
Date: Wed, 22 Apr 2020 21:59:20 +0200 Subject: [PATCH 166/182] add 20s busy timeout to incoming calls --- .../xmpp/jingle/JingleConnectionManager.java | 8 +++++ .../xmpp/jingle/JingleRtpConnection.java | 32 +++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 07d6a25cb..df05e631b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -23,6 +23,9 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import eu.siacs.conversations.Config; @@ -50,6 +53,7 @@ import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import rocks.xmpp.addr.Jid; public class JingleConnectionManager extends AbstractConnectionManager { + private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); private final HashMap rtpSessionProposals = new HashMap<>(); private final Map connections = new ConcurrentHashMap<>(); @@ -135,6 +139,10 @@ public class JingleConnectionManager extends AbstractConnectionManager { return !contact.showInContactList(); } + public ScheduledFuture schedule(final Runnable runnable, final long delay, final TimeUnit timeUnit) { + return this.scheduledExecutorService.schedule(runnable, delay, timeUnit); + } + public void respondWithJingleError(final Account account, final IqPacket original, String jingleCondition, String condition, String conditionType) { final IqPacket response = original.generateResponse(IqPacket.TYPE.ERROR); final Element error = response.addChild("error"); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 476c463d7..adc4c43e4 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -24,6 +24,8 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; @@ -46,6 +48,8 @@ import rocks.xmpp.addr.Jid; public class JingleRtpConnection extends AbstractJingleConnection implements WebRTCWrapper.EventCallback { + private static final long BUSY_TIME_OUT = 20; + public static final List STATES_SHOWING_ONGOING_CALL = Arrays.asList( State.PROCEED, State.SESSION_INITIALIZED, @@ -118,6 +122,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private RtpContentMap initiatorRtpContentMap; private RtpContentMap responderRtpContentMap; private long rtpConnectionStarted = 0; //time of 'connected' + private ScheduledFuture ringingTimeoutFuture; JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { super(jingleConnectionManager, id, initiator); @@ -536,9 +541,29 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void startRinging() { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received call from " + id.with + ". start ringing"); + ringingTimeoutFuture = jingleConnectionManager.schedule(this::ringingTimeout, BUSY_TIME_OUT, TimeUnit.SECONDS); xmppConnectionService.getNotificationService().showIncomingCallNotification(id, getMedia()); } + private synchronized void ringingTimeout() { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": timeout reached for ringing"); + switch (this.state) { + case PROPOSED: + rejectCallFromProposed(); + break; + case SESSION_INITIALIZED: + rejectCallFromSessionInitiate(); + break; + } + } + + private void cancelRingingTimeout() { + final ScheduledFuture future = this.ringingTimeoutFuture; + if (future != null && !future.isCancelled()) { + future.cancel(false); + } + } + private void receiveProceed(final Jid from, final String serverMsgId, final long timestamp) { final Set media = Preconditions.checkNotNull(this.proposedMedia, "Proposed media has to be set before handling proceed"); Preconditions.checkState(media.size() > 0, "Proposed media should not be empty"); @@ -781,17 +806,19 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web public synchronized void acceptCall() { switch (this.state) { case PROPOSED: + cancelRingingTimeout(); acceptCallFromProposed(); break; case SESSION_INITIALIZED: + cancelRingingTimeout(); acceptCallFromSessionInitialized(); break; case ACCEPTED: - Log.w(Config.LOGTAG,id.account.getJid().asBareJid()+": the call has already been accepted with another client. UI was just lagging behind"); + Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": the call has already been accepted with another client. UI was just lagging behind"); break; case PROCEED: case SESSION_ACCEPTED: - Log.w(Config.LOGTAG,id.account.getJid().asBareJid()+": the call has already been accepted. user probably double tapped the UI"); + Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": the call has already been accepted. user probably double tapped the UI"); break; default: throw new IllegalStateException("Can not accept call from " + this.state); @@ -1069,6 +1096,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void finish() { + this.cancelRingingTimeout(); this.webRTCWrapper.verifyClosed(); this.jingleConnectionManager.finishConnection(this); } From 60cea03dce5fc9235bac7b607741ae1f5482e6b9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 23 Apr 2020 10:13:10 +0200 Subject: [PATCH 167/182] do not attempt retract if onStop was faster than backend connect --- .../java/eu/siacs/conversations/ui/RtpSessionActivity.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 0b671453a..73e30bbd5 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -296,7 +296,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe if (jingleRtpConnection != null) { releaseVideoTracks(jingleRtpConnection); } else if (!isChangingConfigurations()) { - retractSessionProposal(); + if (xmppConnectionService != null) { + retractSessionProposal(); + } } releaseProximityWakeLock(); super.onStop(); From cfb9368edbb41b9a219546fd6a5d7e124eddc71b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 23 Apr 2020 12:14:45 +0200 Subject: [PATCH 168/182] check if pip feature is available on top of doing version check --- .../siacs/conversations/ui/RtpSessionActivity.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 73e30bbd5..888044f7a 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -5,6 +5,7 @@ import android.annotation.SuppressLint; import android.app.PictureInPictureParams; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.databinding.DataBindingUtil; import android.os.Build; import android.os.Bundle; @@ -238,6 +239,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH)); final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID); if (sessionId != null) { + //TODO this should probably return true/false so we don’t continue to accept the call after calling finish() initializeActivityWithRunningRtpSession(account, with, sessionId); if (ACTION_ACCEPT_CALL.equals(intent.getAction())) { Log.d(Config.LOGTAG, "intent action was accept"); @@ -323,8 +325,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override public void onUserLeaveHint() { - Log.d(Config.LOGTAG, "user leave hint"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && deviceSupportsPictureInPicture()) { if (shouldBePictureInPicture()) { enterPictureInPictureMode( new PictureInPictureParams.Builder() @@ -333,7 +334,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe ); } } + } + private boolean deviceSupportsPictureInPicture() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE); + } else { + return false; + } } private boolean shouldBePictureInPicture() { From d7a8519ad6d03f3c14adafea647529e8c6ac6798 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 23 Apr 2020 12:52:02 +0200 Subject: [PATCH 169/182] do not continue to accept call if reinit() caused activity to finish --- .../conversations/ui/RtpSessionActivity.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 888044f7a..33a82e977 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -220,7 +220,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID); if (sessionId != null) { Log.d(Config.LOGTAG, "reinitializing from onNewIntent()"); - initializeActivityWithRunningRtpSession(account, with, sessionId); + if (initializeActivityWithRunningRtpSession(account, with, sessionId)) { + return; + } if (ACTION_ACCEPT_CALL.equals(intent.getAction())) { Log.d(Config.LOGTAG, "accepting call from onNewIntent()"); requestPermissionsAndAcceptCall(); @@ -239,8 +241,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH)); final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID); if (sessionId != null) { - //TODO this should probably return true/false so we don’t continue to accept the call after calling finish() - initializeActivityWithRunningRtpSession(account, with, sessionId); + if (initializeActivityWithRunningRtpSession(account, with, sessionId)) { + return; + } if (ACTION_ACCEPT_CALL.equals(intent.getAction())) { Log.d(Config.LOGTAG, "intent action was accept"); requestPermissionsAndAcceptCall(); @@ -357,18 +360,18 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } - private void initializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) { + private boolean initializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) { final WeakReference reference = xmppConnectionService.getJingleConnectionManager() .findJingleRtpConnection(account, with, sessionId); if (reference == null || reference.get() == null) { finish(); - return; + return true; } this.rtpConnectionReference = reference; final RtpEndUserState currentState = requireRtpConnection().getEndUserState(); if (currentState == RtpEndUserState.ENDED) { finish(); - return; + return true; } if (currentState == RtpEndUserState.INCOMING_CALL) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); @@ -381,6 +384,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe updateStateDisplay(currentState); updateButtonConfiguration(currentState); updateProfilePicture(currentState); + return false; } private void reInitializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) { From c88d736cee5c25b191af9faac6fb89c76577b0cf Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 23 Apr 2020 13:01:00 +0200 Subject: [PATCH 170/182] pulled translations from transifex --- src/main/res/values-es/strings.xml | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 69229bdac..7ea8adf8e 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -116,6 +116,10 @@ Vibra cuando llega un nuevo mensaje Luz La luz parpadea cuando llega un nuevo mensaje + Tono de llamada + Sonido de notificación + Sonido de notificación para nuevos mensajes + Tono para las nuevas llamadas Periodo de gracia El periodo de tiempo en el que las notificaciones están silenciadas tras detectar actividad en otro de tus dispositivos. Avanzado @@ -188,6 +192,7 @@ XEP-0191: Blocking Command XEP-0237: Roster Versioning XEP-0198: Stream Management + XEP-0215: External Service Discovery XEP-0163: PEP (Avatars / OMEMO) XEP-0363: HTTP File Upload XEP-0357: Push @@ -645,6 +650,7 @@ Conversación correspondiente cerrada. Contacto bloqueado. Notificaciones de desconocidos + Notificar de nuevos mensajes y llamadas recibidas de contactos desconocidos. Mensaje recibido de un contacto desconocido Bloquear desconocido Bloquear el dominio completo @@ -743,9 +749,14 @@ Problemas de conectividad Esta categoría de notificación se usa para mostrar una notificación en caso de que exista un problema conectándose a una cuenta. Mensajes + Llamadas Mensajes + Llamadas entrantes + Llamadas salientes Mensajes sin sonido Este grupo de notificaciones se usa para mostrar notificaciones que no deberían emitir ningún sonido. Por ejemplo, cuando estás activo en otro dispositivo (periodo de gracia). + Ajustes de notificación de mensajes + Ajustes de notificación de llamadas Importancia, Sonido, Vibración Compresión de video Ver galería @@ -878,6 +889,34 @@ Copia de respaldo Acerca de Por favor, habilita una cuenta + Hacer una llamada + Llamada entrante + Videollamada entrante + Conectando + Conectado + Aceptar llamada + Terminar llamada + Contestar + Descartar + Localizando dispositivos + Llamando + Ocupado + No se ha podido realizar la llamada + Llamada rechazada + Fallo en la aplicación + Colgar + Llamada saliente + Video llamada saliente + Deshabilitar Tor para hacer llamadas + Llamada entrante + Llamada entrante · %s + Llamada saliente + Video llamada saliente · %s + Llamada perdida + Audio llamada + Video llamada + Tu micrófono no está disponible + Solo puedes hacer una llamada a la vez Ver %1$d Participante Ver %1$d Participantes From adad683b200500336fa8f6f6b49a64851ccbe358 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 23 Apr 2020 14:07:05 +0200 Subject: [PATCH 171/182] version bump to 2.8.0-rc.3 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 648a9c77a..975021e6a 100644 --- a/build.gradle +++ b/build.gradle @@ -95,8 +95,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 28 - versionCode 376 - versionName "2.8.0-rc.2" + versionCode 377 + versionName "2.8.0-rc.3" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId From 80cac3bd6973e72a8cbd11cb11bb3e7ad79f2697 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 23 Apr 2020 19:51:58 +0200 Subject: [PATCH 172/182] disable tcp candidates --- .../eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index cb4deed2c..a5fc8429a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -99,7 +99,7 @@ public class WebRTCWrapper { @Override public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { - + Log.d(EXTENDED_LOGGING_TAG, "onIceGatheringChange(" + iceGatheringState + ")"); } @Override @@ -212,7 +212,9 @@ public class WebRTCWrapper { } - final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(iceServers, peerConnectionObserver); + final PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers); + rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; //XEP-0176 doesn't support tcp + final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, peerConnectionObserver); if (peerConnection == null) { throw new InitializationException("Unable to create PeerConnection"); } From 96f6ae2b4956ac69afd245f89c4dd1f6e37305a3 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 23 Apr 2020 20:11:45 +0200 Subject: [PATCH 173/182] additional null check in case to is null --- .../conversations/xmpp/jingle/JingleConnectionManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index df05e631b..eff4f7edb 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -174,7 +174,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { final boolean fromSelf = from.asBareJid().equals(account.getJid().asBareJid()); final AbstractJingleConnection.Id id; if (fromSelf) { - if (to.isFullJid()) { + if (to != null && to.isFullJid()) { id = AbstractJingleConnection.Id.of(account, to, sessionId); } else { return; From 02a74b10a14cdd2adbadfbfee6a54d02975c8629 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 23 Apr 2020 20:32:52 +0200 Subject: [PATCH 174/182] use better version code for universal to allow people to upgrade from abi to univerals as long is the base version is higher --- build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index 975021e6a..357c8632c 100644 --- a/build.gradle +++ b/build.gradle @@ -270,6 +270,8 @@ android { def baseAbiVersionCode = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI)) if (baseAbiVersionCode != null) { output.versionCodeOverride = (100 * variant.versionCode) + baseAbiVersionCode + } else { + output.versionCodeOverride = (100 * variant.versionCode) } } From c0036b4ca6bcfdef75e60f0f2e90f302207aac92 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 24 Apr 2020 09:16:59 +0200 Subject: [PATCH 175/182] increase busy timeout to 30s --- .../conversations/xmpp/jingle/JingleRtpConnection.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index adc4c43e4..aaa77a937 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -48,7 +48,7 @@ import rocks.xmpp.addr.Jid; public class JingleRtpConnection extends AbstractJingleConnection implements WebRTCWrapper.EventCallback { - private static final long BUSY_TIME_OUT = 20; + private static final long BUSY_TIME_OUT = 30; public static final List STATES_SHOWING_ONGOING_CALL = Arrays.asList( State.PROCEED, @@ -954,7 +954,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - public void transitionOrThrow(final State target) { + void transitionOrThrow(final State target) { if (!transition(target)) { throw new IllegalStateException(String.format("Unable to transition from %s to %s", this.state, target)); } @@ -1149,7 +1149,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return webRTCWrapper.getEglBaseContext(); } - public void setProposedMedia(final Set media) { + void setProposedMedia(final Set media) { this.proposedMedia = media; } From 4f5415ecbae26f9aa52c08c84720ab8fd5cb62d6 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 24 Apr 2020 09:41:54 +0200 Subject: [PATCH 176/182] terminated rtp connection do not count as busy --- .../xmpp/jingle/JingleConnectionManager.java | 7 +++++-- .../xmpp/jingle/JingleRtpConnection.java | 20 +++++++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index eff4f7edb..4c410a5be 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -122,6 +122,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { public boolean isBusy() { for (AbstractJingleConnection connection : this.connections.values()) { if (connection instanceof JingleRtpConnection) { + if (((JingleRtpConnection) connection).isTerminated()) { + continue; + } return true; } } @@ -139,11 +142,11 @@ public class JingleConnectionManager extends AbstractConnectionManager { return !contact.showInContactList(); } - public ScheduledFuture schedule(final Runnable runnable, final long delay, final TimeUnit timeUnit) { + ScheduledFuture schedule(final Runnable runnable, final long delay, final TimeUnit timeUnit) { return this.scheduledExecutorService.schedule(runnable, delay, timeUnit); } - public void respondWithJingleError(final Account account, final IqPacket original, String jingleCondition, String condition, String conditionType) { + void respondWithJingleError(final Account account, final IqPacket original, String jingleCondition, String condition, String conditionType) { final IqPacket response = original.generateResponse(IqPacket.TYPE.ERROR); final Element error = response.addChild("error"); error.setAttribute("type", conditionType); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index aaa77a937..0052228b1 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -185,7 +185,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web @Override synchronized void notifyRebound() { - if (TERMINATED.contains(this.state)) { + if (isTerminated()) { return; } webRTCWrapper.close(); @@ -398,7 +398,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private synchronized void sendSessionAccept(final Set media, final SessionDescription offer, final List iceServers) { - if (TERMINATED.contains(this.state)) { + if (isTerminated()) { Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": ICE servers got discovered when session was already terminated. nothing to do."); return; } @@ -617,7 +617,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private synchronized void sendSessionInitiate(final Set media, final State targetState, final List iceServers) { - if (TERMINATED.contains(this.state)) { + if (isTerminated()) { Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": ICE servers got discovered when session was already terminated. nothing to do."); return; } @@ -689,7 +689,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (response.getType() == IqPacket.TYPE.ERROR) { final String errorCondition = response.getErrorCondition(); Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ-error from " + response.getFrom() + " in RTP session. " + errorCondition); - if (TERMINATED.contains(this.state)) { + if (isTerminated()) { Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); return; } @@ -709,7 +709,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.finish(); } else if (response.getType() == IqPacket.TYPE.TIMEOUT) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ timeout in RTP session with " + id.with + ". terminating with connectivity error"); - if (TERMINATED.contains(this.state)) { + if (isTerminated()) { Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); return; } @@ -839,7 +839,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } public synchronized void endCall() { - if (TERMINATED.contains(this.state)) { + if (isTerminated()) { Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": received endCall() when session has already been terminated. nothing to do"); return; } @@ -977,7 +977,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web //TODO exact syntax is yet to be determined but transport-replace sounds like the most reasonable //as there is no content-replace if (Arrays.asList(PeerConnection.PeerConnectionState.FAILED, PeerConnection.PeerConnectionState.DISCONNECTED).contains(newState)) { - if (TERMINATED.contains(this.state)) { + if (isTerminated()) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); return; } @@ -990,7 +990,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void closeWebRTCSessionAfterFailedConnection() { this.webRTCWrapper.close(); synchronized (this) { - if (TERMINATED.contains(state)) { + if (isTerminated()) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": no need to send session-terminate after failed connection. Other party already did"); return; } @@ -1136,6 +1136,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return this.state; } + public boolean isTerminated() { + return TERMINATED.contains(this.state); + } + public Optional geLocalVideoTrack() { return webRTCWrapper.getLocalVideoTrack(); } From cacd85b4f1b4b056772de43dedda913157bf170b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 24 Apr 2020 10:37:46 +0200 Subject: [PATCH 177/182] catch ISE when entering PIP --- .../conversations/ui/RtpSessionActivity.java | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 33a82e977..4758cdca9 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -12,6 +12,7 @@ import android.os.Bundle; import android.os.PowerManager; import android.os.SystemClock; import android.support.annotation.NonNull; +import android.support.annotation.RequiresApi; import android.support.annotation.StringRes; import android.util.Log; import android.util.Rational; @@ -330,15 +331,26 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe public void onUserLeaveHint() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && deviceSupportsPictureInPicture()) { if (shouldBePictureInPicture()) { - enterPictureInPictureMode( - new PictureInPictureParams.Builder() - .setAspectRatio(new Rational(10, 16)) - .build() - ); + startPictureInPicture(); } } } + + @RequiresApi(api = Build.VERSION_CODES.O) + private void startPictureInPicture() { + try { + enterPictureInPictureMode( + new PictureInPictureParams.Builder() + .setAspectRatio(new Rational(10, 16)) + .build() + ); + } catch (IllegalStateException e) { + //this sometimes happens on Samsung phones (possibly when Knox is enabled) + Log.w(Config.LOGTAG, "unable to enter picture in picture mode", e); + } + } + private boolean deviceSupportsPictureInPicture() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE); From 99d2b6a2680bf475f82dc01d0e22e24c427d0fbe Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 24 Apr 2020 10:39:15 +0200 Subject: [PATCH 178/182] add a/v calls to features --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ab81421eb..f118b6b54 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ * End-to-end encryption with [OMEMO](http://conversations.im/omemo/) or [OpenPGP](http://openpgp.org/about/) * Send and receive images as well as other kind of files +* Make audio and video calls * Share your location * Send voice messages * Indication when your contact has read your message From 32ab7775d729ae5109a8d54df9e528bb4f8eb02b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 24 Apr 2020 10:42:19 +0200 Subject: [PATCH 179/182] pulled translations from transifex --- src/main/res/values-pl/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 18a322e8b..cf7068329 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -920,6 +920,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Dzwonienie Zajęty Nie można połączyć rozmowy + Anulowane połączenie Błąd aplikacji Rozłącz Połączenie wychodzące From 07ba70aef705f5cd0178d76daff076d14e668258 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 24 Apr 2020 20:53:47 +0200 Subject: [PATCH 180/182] update fastlane metadata --- fastlane/metadata/android/en-US/changelogs/378.txt | 1 + fastlane/metadata/android/en-US/full_description.txt | 1 + 2 files changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/378.txt diff --git a/fastlane/metadata/android/en-US/changelogs/378.txt b/fastlane/metadata/android/en-US/changelogs/378.txt new file mode 100644 index 000000000..a99adee16 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/378.txt @@ -0,0 +1 @@ +* Audio/Video calls (Requires server support in form of STUN and TURN servers discoverable via XEP-0215) diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 0830b69b9..d03cefd88 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -11,6 +11,7 @@ Features: * End-to-end encryption with either OMEMO or OpenPGP * Sending and receiving images +* Make audio and video calls * Intuitive UI that follows Android Design guidelines * Pictures / Avatars for your Contacts * Syncs with desktop client From a5beaaed9de10a85f6c21166c65a60848c1e9f09 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 24 Apr 2020 21:27:00 +0200 Subject: [PATCH 181/182] null reference to rtpconnection when end card is reached this will make re-init work if for example retry end card had been reached and we get another call --- .../java/eu/siacs/conversations/ui/RtpSessionActivity.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 4758cdca9..acd9d151e 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -766,8 +766,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe if (state == RtpEndUserState.ENDED) { finish(); return; - } else if (END_CARD.contains(state)) { - resetIntent(account, with, state, requireRtpConnection().getMedia()); } runOnUiThread(() -> { updateStateDisplay(state); @@ -775,9 +773,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe updateVideoViews(state); updateProfilePicture(state); }); + if (END_CARD.contains(state)) { + resetIntent(account, with, state, requireRtpConnection().getMedia()); + this.rtpConnectionReference = null; + } } else { Log.d(Config.LOGTAG, "received update for other rtp session"); - //TODO if we only ever have one; we might just switch over? Maybe! } } From 45bb86c0f6da5682e00898eeb1dc2e8e8880338d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 24 Apr 2020 22:01:48 +0200 Subject: [PATCH 182/182] version bump for release --- build.gradle | 4 ++-- .../metadata/android/en-US/changelogs/{378.txt => 379.txt} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename fastlane/metadata/android/en-US/changelogs/{378.txt => 379.txt} (100%) diff --git a/build.gradle b/build.gradle index 357c8632c..162dc26ee 100644 --- a/build.gradle +++ b/build.gradle @@ -95,8 +95,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 28 - versionCode 377 - versionName "2.8.0-rc.3" + versionCode 379 + versionName "2.8.0" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/en-US/changelogs/378.txt b/fastlane/metadata/android/en-US/changelogs/379.txt similarity index 100% rename from fastlane/metadata/android/en-US/changelogs/378.txt rename to fastlane/metadata/android/en-US/changelogs/379.txt