export() throws Exception {
- NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
+ NotificationCompat.Builder mBuilder =
+ new NotificationCompat.Builder(getBaseContext(), "backup");
mBuilder.setContentTitle(getString(R.string.notification_create_backup_title))
.setSmallIcon(R.drawable.ic_archive_white_24dp)
.setProgress(1, 0, false);
@@ -286,17 +277,38 @@ public class ExportBackupService extends Service {
for (final Account account : this.mAccounts) {
final String password = account.getPassword();
if (Strings.nullToEmpty(password).trim().isEmpty()) {
- Log.d(Config.LOGTAG, String.format("skipping backup for %s because password is empty. unable to encrypt", account.getJid().asBareJid()));
+ Log.d(
+ Config.LOGTAG,
+ String.format(
+ "skipping backup for %s because password is empty. unable to encrypt",
+ account.getJid().asBareJid()));
continue;
}
- Log.d(Config.LOGTAG, String.format("exporting data for account %s (%s)", account.getJid().asBareJid(), account.getUuid()));
+ Log.d(
+ Config.LOGTAG,
+ String.format(
+ "exporting data for account %s (%s)",
+ account.getJid().asBareJid(), account.getUuid()));
final byte[] IV = new byte[12];
final byte[] salt = new byte[16];
secureRandom.nextBytes(IV);
secureRandom.nextBytes(salt);
- final BackupFileHeader backupFileHeader = new BackupFileHeader(getString(R.string.app_name), account.getJid(), System.currentTimeMillis(), IV, salt);
+ final BackupFileHeader backupFileHeader =
+ new BackupFileHeader(
+ getString(R.string.app_name),
+ account.getJid(),
+ System.currentTimeMillis(),
+ IV,
+ salt);
final Progress progress = new Progress(mBuilder, max, count);
- final File file = new File(FileBackend.getBackupDirectory(this), account.getJid().asBareJid().toEscapedString() + ".ceb");
+ final String filename =
+ String.format(
+ "%s.%s.ceb",
+ account.getJid().asBareJid().toEscapedString(),
+ DATE_FORMAT.format(new Date()));
+ final File file =
+ new File(
+ FileBackend.getBackupDirectory(this), filename);
files.add(file);
final File directory = file.getParentFile();
if (directory != null && directory.mkdirs()) {
@@ -307,25 +319,38 @@ public class ExportBackupService extends Service {
backupFileHeader.write(dataOutputStream);
dataOutputStream.flush();
- final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
+ final Cipher cipher =
+ Compatibility.twentyEight()
+ ? Cipher.getInstance(CIPHERMODE)
+ : Cipher.getInstance(CIPHERMODE, PROVIDER);
final byte[] key = getKey(password, salt);
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
IvParameterSpec ivSpec = new IvParameterSpec(IV);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
- CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher);
+ CipherOutputStream cipherOutputStream =
+ new CipherOutputStream(fileOutputStream, cipher);
- GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream);
- PrintWriter writer = new PrintWriter(gzipOutputStream);
- SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase();
+ final GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream);
+ final JsonWriter jsonWriter =
+ new JsonWriter(
+ new OutputStreamWriter(gzipOutputStream, StandardCharsets.UTF_8));
+ jsonWriter.beginArray();
+ final SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase();
final String uuid = account.getUuid();
- accountExport(db, uuid, writer);
- simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, writer);
- messageExport(db, uuid, writer, progress);
- for (String table : Arrays.asList(SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) {
- simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, writer);
+ accountExport(db, uuid, jsonWriter);
+ simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, jsonWriter);
+ messageExport(db, uuid, jsonWriter, progress);
+ for (final String table :
+ Arrays.asList(
+ SQLiteAxolotlStore.PREKEY_TABLENAME,
+ SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
+ SQLiteAxolotlStore.SESSION_TABLENAME,
+ SQLiteAxolotlStore.IDENTITIES_TABLENAME)) {
+ simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, jsonWriter);
}
- writer.flush();
- writer.close();
+ jsonWriter.endArray();
+ jsonWriter.flush();
+ jsonWriter.close();
mediaScannerScanFile(file);
Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
count++;
@@ -346,9 +371,15 @@ public class ExportBackupService extends Service {
for (final Intent intent : getPossibleFileOpenIntents(this, path)) {
if (intent.resolveActivityInfo(getPackageManager(), 0) != null) {
- openFolderIntent = PendingIntent.getActivity(this, 189, intent, s()
- ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
- : PendingIntent.FLAG_UPDATE_CURRENT);
+ openFolderIntent =
+ PendingIntent.getActivity(
+ this,
+ 189,
+ intent,
+ s()
+ ? PendingIntent.FLAG_IMMUTABLE
+ | PendingIntent.FLAG_UPDATE_CURRENT
+ : PendingIntent.FLAG_UPDATE_CURRENT);
break;
}
}
@@ -363,22 +394,39 @@ public class ExportBackupService extends Service {
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setType(MIME_TYPE);
- final Intent chooser = Intent.createChooser(intent, getString(R.string.share_backup_files));
- shareFilesIntent = PendingIntent.getActivity(this, 190, chooser, s()
- ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
- : PendingIntent.FLAG_UPDATE_CURRENT);
+ final Intent chooser =
+ Intent.createChooser(intent, getString(R.string.share_backup_files));
+ shareFilesIntent =
+ PendingIntent.getActivity(
+ this,
+ 190,
+ chooser,
+ s()
+ ? PendingIntent.FLAG_IMMUTABLE
+ | PendingIntent.FLAG_UPDATE_CURRENT
+ : PendingIntent.FLAG_UPDATE_CURRENT);
}
- NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
+ NotificationCompat.Builder mBuilder =
+ new NotificationCompat.Builder(getBaseContext(), "backup");
mBuilder.setContentTitle(getString(R.string.notification_backup_created_title))
.setContentText(getString(R.string.notification_backup_created_subtitle, path))
- .setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_backup_created_subtitle, FileBackend.getBackupDirectory(this).getAbsolutePath())))
+ .setStyle(
+ new NotificationCompat.BigTextStyle()
+ .bigText(
+ getString(
+ R.string.notification_backup_created_subtitle,
+ FileBackend.getBackupDirectory(this)
+ .getAbsolutePath())))
.setAutoCancel(true)
.setContentIntent(openFolderIntent)
.setSmallIcon(R.drawable.ic_archive_white_24dp);
if (shareFilesIntent != null) {
- mBuilder.addAction(R.drawable.ic_share_white_24dp, getString(R.string.share_backup_files), shareFilesIntent);
+ mBuilder.addAction(
+ R.drawable.ic_share_white_24dp,
+ getString(R.string.share_backup_files),
+ shareFilesIntent);
}
notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
diff --git a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java
index 520348943..81cfb951f 100644
--- a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java
+++ b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java
@@ -33,6 +33,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
+import android.os.Build;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.util.Base64;
@@ -43,9 +44,21 @@ import androidx.appcompat.app.AppCompatActivity;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.BundledTrustManager;
+import eu.siacs.conversations.crypto.CombiningTrustManager;
+import eu.siacs.conversations.crypto.TrustManagers;
+import eu.siacs.conversations.crypto.XmppDomainVerifier;
+import eu.siacs.conversations.entities.MTMDecision;
+import eu.siacs.conversations.http.HttpConnectionManager;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.ui.MemorizingActivity;
+
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -78,39 +91,40 @@ import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.crypto.XmppDomainVerifier;
-import eu.siacs.conversations.entities.MTMDecision;
-import eu.siacs.conversations.http.HttpConnectionManager;
-import eu.siacs.conversations.persistance.FileBackend;
-import eu.siacs.conversations.ui.MemorizingActivity;
-
/**
- * A X509 trust manager implementation which asks the user about invalid
- * certificates and memorizes their decision.
- *
- * The certificate validity is checked using the system default X509
- * TrustManager, creating a query Dialog if the check fails.
- *
- * WARNING: This only works if a dedicated thread is used for
- * opening sockets!
+ * A X509 trust manager implementation which asks the user about invalid certificates and memorizes
+ * their decision.
+ *
+ *
The certificate validity is checked using the system default X509 TrustManager, creating a
+ * query Dialog if the check fails.
+ *
+ *
WARNING: This only works if a dedicated thread is used for opening sockets!
*/
public class MemorizingTrustManager {
- private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
+ private static final SimpleDateFormat DATE_FORMAT =
+ new SimpleDateFormat("yyyy-MM-dd", Locale.US);
- 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 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());
+ static final String DECISION_INTENT = "de.duenndns.ssl.DECISION";
+ public static final String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId";
+ public static final String DECISION_INTENT_CERT = DECISION_INTENT + ".cert";
+ public static final String DECISION_TITLE_ID = DECISION_INTENT + ".titleId";
+ static final 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 static final Logger LOGGER = Logger.getLogger(MemorizingTrustManager.class.getName());
static String KEYSTORE_DIR = "KeyStore";
static String KEYSTORE_FILE = "KeyStore.bks";
private static int decisionId = 0;
@@ -126,54 +140,76 @@ public class MemorizingTrustManager {
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.
+ * Creates an instance of the MemorizingTrustManager class that falls back to a custom
+ * TrustManager.
*
- * @param m Context for the application.
- * @param defaultTrustManager Delegate trust management to this TM. If null, the user must accept every certificate.
+ *
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 context 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);
+ public MemorizingTrustManager(
+ final Context context, final X509TrustManager defaultTrustManager) {
+ init(context);
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.
+ *
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 context Context for the application.
*/
- public MemorizingTrustManager(Context m) {
- init(m);
+ public MemorizingTrustManager(final Context context) {
+ init(context);
this.appTrustManager = getTrustManager(appKeyStore);
- this.defaultTrustManager = getTrustManager(null);
+ try {
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
+ this.defaultTrustManager = defaultWithBundledLetsEncrypt(context);
+ } else {
+ this.defaultTrustManager = TrustManagers.createDefaultTrustManager();
+ }
+ } catch (final NoSuchAlgorithmException
+ | KeyStoreException
+ | CertificateException
+ | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static X509TrustManager defaultWithBundledLetsEncrypt(final Context context)
+ throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException {
+ final BundledTrustManager bundleTrustManager =
+ BundledTrustManager.builder()
+ .loadKeyStore(
+ context.getResources().openRawResource(R.raw.letsencrypt),
+ "letsencrypt")
+ .build();
+ return CombiningTrustManager.combineWithDefault(bundleTrustManager);
}
private static boolean isIp(final String server) {
- return server != null && (
- PATTERN_IPV4.matcher(server).matches()
+ 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 {
+ private static String getBase64Hash(X509Certificate certificate, String digest)
+ throws CertificateEncodingException {
MessageDigest md;
try {
md = MessageDigest.getInstance(digest);
@@ -188,8 +224,7 @@ public class MemorizingTrustManager {
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(":");
+ if (i < data.length - 1) si.append(":");
}
return si.toString();
}
@@ -220,20 +255,22 @@ public class MemorizingTrustManager {
}
}
- void init(final Context m) {
- master = m;
- masterHandler = new Handler(m.getMainLooper());
- notificationManager = (NotificationManager) master.getSystemService(Context.NOTIFICATION_SERVICE);
+ void init(final Context context) {
+ master = context;
+ masterHandler = new Handler(context.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();
+ if (context instanceof Application) {
+ app = (Application) context;
+ } else if (context instanceof Service) {
+ app = ((Service) context).getApplication();
+ } else if (context instanceof AppCompatActivity) {
+ app = ((AppCompatActivity) context).getApplication();
} else
- throw new ClassCastException("MemorizingTrustManager context must be either Activity or Service!");
+ 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);
@@ -260,12 +297,9 @@ public class MemorizingTrustManager {
/**
* 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.
- *
+ * 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.
@@ -275,20 +309,21 @@ public class MemorizingTrustManager {
keyStoreUpdated();
}
- X509TrustManager getTrustManager(KeyStore ks) {
+ private X509TrustManager getTrustManager(final KeyStore keyStore) {
+ Preconditions.checkNotNull(keyStore);
try {
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
- tmf.init(ks);
+ tmf.init(keyStore);
for (TrustManager t : tmf.getTrustManagers()) {
if (t instanceof X509TrustManager) {
return (X509TrustManager) t;
}
}
- } catch (Exception e) {
+ } catch (final 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);
+ LOGGER.log(Level.SEVERE, "getTrustManager(" + keyStore + ")", e);
}
return null;
}
@@ -361,45 +396,60 @@ public class MemorizingTrustManager {
}
}
-
- private void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive)
+ private void checkCertTrusted(
+ X509Certificate[] chain,
+ String authType,
+ String domain,
+ boolean isServer,
+ boolean interactive)
throws CertificateException {
- LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
+ 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);
+ if (isServer) appTrustManager.checkServerTrusted(chain, authType);
+ else appTrustManager.checkClientTrusted(chain, authType);
} catch (final CertificateException ae) {
LOGGER.log(Level.FINER, "checkCertTrusted: appTrustManager failed", ae);
if (isCertKnown(chain[0])) {
- LOGGER.log(Level.INFO, "checkCertTrusted: accepting cert already stored in keystore");
+ LOGGER.log(
+ Level.INFO, "checkCertTrusted: accepting cert already stored in keystore");
return;
}
try {
- if (defaultTrustManager == null)
- throw ae;
+ if (defaultTrustManager == null) throw ae;
LOGGER.log(Level.FINE, "checkCertTrusted: trying defaultTrustManager");
- if (isServer)
- defaultTrustManager.checkServerTrusted(chain, authType);
- else
- defaultTrustManager.checkClientTrusted(chain, authType);
+ if (isServer) defaultTrustManager.checkServerTrusted(chain, authType);
+ else defaultTrustManager.checkClientTrusted(chain, authType);
} catch (final CertificateException e) {
- final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(master);
- final boolean trustSystemCAs = !preferences.getBoolean("dont_trust_system_cas", false);
- if (domain != null && isServer && trustSystemCAs && !isIp(domain) && !domain.endsWith(".onion")) {
+ final SharedPreferences preferences =
+ PreferenceManager.getDefaultSharedPreferences(master);
+ final boolean trustSystemCAs =
+ !preferences.getBoolean("dont_trust_system_cas", false);
+ if (domain != null
+ && isServer
+ && trustSystemCAs
+ && !isIp(domain)
+ && !domain.endsWith(".onion")) {
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(Config.LOGTAG, "trusted cert fingerprint of " + domain + " via posh");
+ Log.d(
+ Config.LOGTAG,
+ "trusted cert fingerprint of " + domain + " via posh");
return;
} else {
- Log.d(Config.LOGTAG, "fingerprint " + hash + " not found in " + fingerprints);
+ Log.d(
+ Config.LOGTAG,
+ "fingerprint " + hash + " not found in " + fingerprints);
}
if (getPoshCacheFile(domain).delete()) {
- Log.d(Config.LOGTAG, "deleted posh file for " + domain + " after not being able to verify");
+ Log.d(
+ Config.LOGTAG,
+ "deleted posh file for "
+ + domain
+ + " after not being able to verify");
}
}
}
@@ -422,17 +472,25 @@ public class MemorizingTrustManager {
}
private List getPoshFingerprintsFromServer(String domain) {
- return getPoshFingerprintsFromServer(domain, "https://" + domain + "/.well-known/posh/xmpp-client.json", -1, true);
+ return getPoshFingerprintsFromServer(
+ domain, "https://" + domain + "/.well-known/posh/xmpp-client.json", -1, true);
}
- private List getPoshFingerprintsFromServer(String domain, String url, int maxTtl, boolean followUrl) {
+ private List getPoshFingerprintsFromServer(
+ String domain, String url, int maxTtl, boolean followUrl) {
Log.d(Config.LOGTAG, "downloading json for " + domain + " from " + url);
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(master);
- final boolean useTor = QuickConversationsService.isConversations() && preferences.getBoolean("use_tor", master.getResources().getBoolean(R.bool.use_tor));
+ final boolean useTor =
+ QuickConversationsService.isConversations()
+ && preferences.getBoolean(
+ "use_tor", master.getResources().getBoolean(R.bool.use_tor));
try {
final List results = new ArrayList<>();
final InputStream inputStream = HttpConnectionManager.open(url, useTor);
- final String body = CharStreams.toString(new InputStreamReader(ByteStreams.limit(inputStream,10_000), Charsets.UTF_8));
+ final String body =
+ CharStreams.toString(
+ new InputStreamReader(
+ ByteStreams.limit(inputStream, 10_000), Charsets.UTF_8));
final JSONObject jsonObject = new JSONObject(body);
int expires = jsonObject.getInt("expires");
if (expires <= 0) {
@@ -459,7 +517,7 @@ public class MemorizingTrustManager {
writeFingerprintsToCache(domain, results, 1000L * expires + System.currentTimeMillis());
return results;
} catch (final Exception e) {
- Log.d(Config.LOGTAG, "error fetching posh",e);
+ Log.d(Config.LOGTAG, "error fetching posh", e);
return new ArrayList<>();
}
}
@@ -489,7 +547,8 @@ public class MemorizingTrustManager {
final File file = getPoshCacheFile(domain);
try {
final InputStream inputStream = new FileInputStream(file);
- final String json = CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8));
+ final String json =
+ CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8));
final JSONObject jsonObject = new JSONObject(json);
long expires = jsonObject.getLong("expires");
long expiresIn = expires - System.currentTimeMillis();
@@ -514,7 +573,9 @@ public class MemorizingTrustManager {
}
private X509Certificate[] getAcceptedIssuers() {
- return defaultTrustManager == null ? new X509Certificate[0] : defaultTrustManager.getAcceptedIssuers();
+ return defaultTrustManager == null
+ ? new X509Certificate[0]
+ : defaultTrustManager.getAcceptedIssuers();
}
private int createDecisionId(MTMDecision d) {
@@ -527,7 +588,8 @@ public class MemorizingTrustManager {
return myId;
}
- private void certDetails(final StringBuffer si, final X509Certificate c, final boolean showValidFor) {
+ private void certDetails(
+ final StringBuffer si, final X509Certificate c, final boolean showValidFor) {
si.append("\n");
if (showValidFor) {
@@ -564,8 +626,7 @@ public class MemorizingTrustManager {
// 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());
+ } else si.append(e.getLocalizedMessage());
si.append("\n");
}
si.append("\n");
@@ -573,7 +634,7 @@ public class MemorizingTrustManager {
si.append("\n\n");
si.append(master.getString(R.string.mtm_cert_details));
si.append('\n');
- for(int i = 0; i < chain.length; ++i) {
+ for (int i = 0; i < chain.length; ++i) {
certDetails(si, chain[i], i == 0);
}
return si.toString();
@@ -593,24 +654,25 @@ public class MemorizingTrustManager {
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);
+ 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);
- }
- }
- });
+ // 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 {
@@ -661,7 +723,8 @@ public class MemorizingTrustManager {
}
@Override
- public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, false);
}
@@ -675,7 +738,6 @@ public class MemorizingTrustManager {
public X509Certificate[] getAcceptedIssuers() {
return MemorizingTrustManager.this.getAcceptedIssuers();
}
-
}
private class InteractiveMemorizingTrustManager implements X509TrustManager {
@@ -686,7 +748,8 @@ public class MemorizingTrustManager {
}
@Override
- public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+ public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, true);
}
diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java
index ce302c92e..aae2a968d 100644
--- a/src/main/java/eu/siacs/conversations/services/NotificationService.java
+++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java
@@ -10,6 +10,7 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
+import android.content.pm.ShortcutManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Typeface;
@@ -21,6 +22,7 @@ import android.os.Build;
import android.os.SystemClock;
import android.os.Vibrator;
import android.preference.PreferenceManager;
+import android.provider.Settings;
import android.text.SpannableString;
import android.text.style.StyleSpan;
import android.util.DisplayMetrics;
@@ -34,10 +36,12 @@ import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.Person;
import androidx.core.app.RemoteInput;
import androidx.core.content.ContextCompat;
+import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.graphics.drawable.IconCompat;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import java.io.File;
@@ -111,6 +115,7 @@ public class NotificationService {
private long mLastNotification;
private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL = "incoming_calls_channel";
+ private static final String MESSAGES_NOTIFICATION_CHANNEL = "messages";
private Ringtone currentlyPlayingRingtone = null;
private ScheduledFuture> vibrationFuture;
@@ -240,7 +245,7 @@ public class NotificationService {
final NotificationChannel messagesChannel =
new NotificationChannel(
- "messages",
+ MESSAGES_NOTIFICATION_CHANNEL,
c.getString(R.string.messages_channel_name),
NotificationManager.IMPORTANCE_HIGH);
messagesChannel.setShowBadge(true);
@@ -640,6 +645,7 @@ public class NotificationService {
createCallAction(
id.sessionId, XmppConnectionService.ACTION_END_CALL, 104))
.build());
+ builder.setLocalOnly(true);
return builder.build();
}
@@ -770,6 +776,25 @@ public class NotificationService {
}
}
+ public void clearMissedCall(final Message message) {
+ synchronized (mMissedCalls) {
+ final Iterator> iterator = mMissedCalls.entrySet().iterator();
+ while (iterator.hasNext()) {
+ final Map.Entry entry = iterator.next();
+ final Conversational conversational = entry.getKey();
+ final MissedCallsInfo missedCallsInfo = entry.getValue();
+ if (conversational.getUuid().equals(message.getConversation().getUuid())) {
+ if (missedCallsInfo.removeMissedCall()) {
+ cancel(conversational.getUuid(), MISSED_CALL_NOTIFICATION_ID);
+ Log.d(Config.LOGTAG,conversational.getAccount().getJid().asBareJid()+": dismissed missed call because call was picked up on other device");
+ iterator.remove();
+ }
+ }
+ }
+ updateMissedCallNotifications(null);
+ }
+ }
+
public void clearMissedCalls() {
synchronized (mMissedCalls) {
for (final Conversational conversation : mMissedCalls.keySet()) {
@@ -1088,7 +1113,7 @@ public class NotificationService {
final Builder mBuilder =
new NotificationCompat.Builder(
mXmppConnectionService,
- quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
+ quietHours ? "quiet_hours" : (notify ? MESSAGES_NOTIFICATION_CHANNEL : "silent_messages"));
final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
style.setBigContentTitle(
mXmppConnectionService
@@ -1154,147 +1179,161 @@ public class NotificationService {
private Builder buildSingleConversations(
final ArrayList messages, final boolean notify, final boolean quietHours) {
- final Builder mBuilder =
- new NotificationCompat.Builder(
- mXmppConnectionService,
- quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
- if (messages.size() >= 1) {
- final Conversation conversation = (Conversation) messages.get(0).getConversation();
- mBuilder.setLargeIcon(
+ final var channel = quietHours ? "quiet_hours" : (notify ? MESSAGES_NOTIFICATION_CHANNEL : "silent_messages");
+ final Builder notificationBuilder =
+ new NotificationCompat.Builder(mXmppConnectionService, channel);
+ if (messages.isEmpty()) {
+ return notificationBuilder;
+ }
+ final Conversation conversation = (Conversation) messages.get(0).getConversation();
+ notificationBuilder.setLargeIcon(
+ mXmppConnectionService
+ .getAvatarService()
+ .get(
+ conversation,
+ AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
+ notificationBuilder.setContentTitle(conversation.getName());
+ if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
+ int count = messages.size();
+ notificationBuilder.setContentText(
mXmppConnectionService
- .getAvatarService()
- .get(
- conversation,
- AvatarService.getSystemUiAvatarSize(mXmppConnectionService)));
- mBuilder.setContentTitle(conversation.getName());
- if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
- int count = messages.size();
- mBuilder.setContentText(
- mXmppConnectionService
- .getResources()
- .getQuantityString(R.plurals.x_messages, count, count));
+ .getResources()
+ .getQuantityString(R.plurals.x_messages, count, count));
+ } else {
+ final Message message;
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P
+ && (message = getImage(messages)) != null) {
+ modifyForImage(notificationBuilder, message, messages);
} else {
- Message message;
- // TODO starting with Android 9 we might want to put images in MessageStyle
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P
- && (message = getImage(messages)) != null) {
- modifyForImage(mBuilder, message, messages);
- } else {
- modifyForTextOnly(mBuilder, messages);
- }
- RemoteInput remoteInput =
- new RemoteInput.Builder("text_reply")
- .setLabel(
- UIHelper.getMessageHint(
- mXmppConnectionService, conversation))
- .build();
- PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation);
- NotificationCompat.Action markReadAction =
- new NotificationCompat.Action.Builder(
- R.drawable.ic_drafts_white_24dp,
- mXmppConnectionService.getString(R.string.mark_as_read),
- markAsReadPendingIntent)
- .setSemanticAction(
- NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
- .setShowsUserInterface(false)
- .build();
- final String replyLabel = mXmppConnectionService.getString(R.string.reply);
- final String lastMessageUuid = Iterables.getLast(messages).getUuid();
- final NotificationCompat.Action replyAction =
- new NotificationCompat.Action.Builder(
- R.drawable.ic_send_text_offline,
- replyLabel,
- createReplyIntent(conversation, lastMessageUuid, false))
- .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
- .setShowsUserInterface(false)
- .addRemoteInput(remoteInput)
- .build();
- final NotificationCompat.Action wearReplyAction =
- new NotificationCompat.Action.Builder(
- R.drawable.ic_wear_reply,
- replyLabel,
- createReplyIntent(conversation, lastMessageUuid, true))
- .addRemoteInput(remoteInput)
- .build();
- mBuilder.extend(
- new NotificationCompat.WearableExtender().addAction(wearReplyAction));
- int addedActionsCount = 1;
- mBuilder.addAction(markReadAction);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- mBuilder.addAction(replyAction);
- ++addedActionsCount;
- }
+ modifyForTextOnly(notificationBuilder, messages);
+ }
+ RemoteInput remoteInput =
+ new RemoteInput.Builder("text_reply")
+ .setLabel(UIHelper.getMessageHint(mXmppConnectionService, conversation))
+ .build();
+ PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation);
+ NotificationCompat.Action markReadAction =
+ new NotificationCompat.Action.Builder(
+ R.drawable.ic_drafts_white_24dp,
+ mXmppConnectionService.getString(R.string.mark_as_read),
+ markAsReadPendingIntent)
+ .setSemanticAction(
+ NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
+ .setShowsUserInterface(false)
+ .build();
+ final String replyLabel = mXmppConnectionService.getString(R.string.reply);
+ final String lastMessageUuid = Iterables.getLast(messages).getUuid();
+ final NotificationCompat.Action replyAction =
+ new NotificationCompat.Action.Builder(
+ R.drawable.ic_send_text_offline,
+ replyLabel,
+ createReplyIntent(conversation, lastMessageUuid, false))
+ .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
+ .setShowsUserInterface(false)
+ .addRemoteInput(remoteInput)
+ .build();
+ final NotificationCompat.Action wearReplyAction =
+ new NotificationCompat.Action.Builder(
+ R.drawable.ic_wear_reply,
+ replyLabel,
+ createReplyIntent(conversation, lastMessageUuid, true))
+ .addRemoteInput(remoteInput)
+ .build();
+ notificationBuilder.extend(
+ new NotificationCompat.WearableExtender().addAction(wearReplyAction));
+ int addedActionsCount = 1;
+ notificationBuilder.addAction(markReadAction);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ notificationBuilder.addAction(replyAction);
+ ++addedActionsCount;
+ }
- if (displaySnoozeAction(messages)) {
- String label = mXmppConnectionService.getString(R.string.snooze);
- PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation);
- NotificationCompat.Action snoozeAction =
- new NotificationCompat.Action.Builder(
- R.drawable.ic_notifications_paused_white_24dp,
- label,
- pendingSnoozeIntent)
- .build();
- mBuilder.addAction(snoozeAction);
- ++addedActionsCount;
- }
- if (addedActionsCount < 3) {
- final Message firstLocationMessage = getFirstLocationMessage(messages);
- if (firstLocationMessage != null) {
- final PendingIntent pendingShowLocationIntent =
- createShowLocationIntent(firstLocationMessage);
- if (pendingShowLocationIntent != null) {
- final String label =
- mXmppConnectionService
- .getResources()
- .getString(R.string.show_location);
- NotificationCompat.Action locationAction =
- new NotificationCompat.Action.Builder(
- R.drawable.ic_room_white_24dp,
- label,
- pendingShowLocationIntent)
- .build();
- mBuilder.addAction(locationAction);
- ++addedActionsCount;
- }
- }
- }
- if (addedActionsCount < 3) {
- Message firstDownloadableMessage = getFirstDownloadableMessage(messages);
- if (firstDownloadableMessage != null) {
- String label =
+ if (displaySnoozeAction(messages)) {
+ String label = mXmppConnectionService.getString(R.string.snooze);
+ PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation);
+ NotificationCompat.Action snoozeAction =
+ new NotificationCompat.Action.Builder(
+ R.drawable.ic_notifications_paused_white_24dp,
+ label,
+ pendingSnoozeIntent)
+ .build();
+ notificationBuilder.addAction(snoozeAction);
+ ++addedActionsCount;
+ }
+ if (addedActionsCount < 3) {
+ final Message firstLocationMessage = getFirstLocationMessage(messages);
+ if (firstLocationMessage != null) {
+ final PendingIntent pendingShowLocationIntent =
+ createShowLocationIntent(firstLocationMessage);
+ if (pendingShowLocationIntent != null) {
+ final String label =
mXmppConnectionService
.getResources()
- .getString(
- R.string.download_x_file,
- UIHelper.getFileDescriptionString(
- mXmppConnectionService,
- firstDownloadableMessage));
- PendingIntent pendingDownloadIntent =
- createDownloadIntent(firstDownloadableMessage);
- NotificationCompat.Action downloadAction =
+ .getString(R.string.show_location);
+ NotificationCompat.Action locationAction =
new NotificationCompat.Action.Builder(
- R.drawable.ic_file_download_white_24dp,
+ R.drawable.ic_room_white_24dp,
label,
- pendingDownloadIntent)
+ pendingShowLocationIntent)
.build();
- mBuilder.addAction(downloadAction);
+ notificationBuilder.addAction(locationAction);
++addedActionsCount;
}
}
}
- if (conversation.getMode() == Conversation.MODE_SINGLE) {
- Contact contact = conversation.getContact();
- Uri systemAccount = contact.getSystemAccount();
- if (systemAccount != null) {
- mBuilder.addPerson(systemAccount.toString());
+ if (addedActionsCount < 3) {
+ Message firstDownloadableMessage = getFirstDownloadableMessage(messages);
+ if (firstDownloadableMessage != null) {
+ String label =
+ mXmppConnectionService
+ .getResources()
+ .getString(
+ R.string.download_x_file,
+ UIHelper.getFileDescriptionString(
+ mXmppConnectionService,
+ firstDownloadableMessage));
+ PendingIntent pendingDownloadIntent =
+ createDownloadIntent(firstDownloadableMessage);
+ NotificationCompat.Action downloadAction =
+ new NotificationCompat.Action.Builder(
+ R.drawable.ic_file_download_white_24dp,
+ label,
+ pendingDownloadIntent)
+ .build();
+ notificationBuilder.addAction(downloadAction);
+ ++addedActionsCount;
}
}
- mBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
- mBuilder.setSmallIcon(R.drawable.ic_notification);
- mBuilder.setDeleteIntent(createDeleteIntent(conversation));
- mBuilder.setContentIntent(createContentIntent(conversation));
}
- return mBuilder;
+ final ShortcutInfoCompat info;
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ final Contact contact = conversation.getContact();
+ final Uri systemAccount = contact.getSystemAccount();
+ if (systemAccount != null) {
+ notificationBuilder.addPerson(systemAccount.toString());
+ }
+ info = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact);
+ } else {
+ info =
+ mXmppConnectionService
+ .getShortcutService()
+ .getShortcutInfoCompat(conversation.getMucOptions());
+ }
+ notificationBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
+ notificationBuilder.setSmallIcon(R.drawable.ic_notification);
+ notificationBuilder.setDeleteIntent(createDeleteIntent(conversation));
+ notificationBuilder.setContentIntent(createContentIntent(conversation));
+ if (channel.equals(MESSAGES_NOTIFICATION_CHANNEL)) {
+ // when do not want 'customized' notifications for silent notifications in their
+ // respective channels
+ notificationBuilder.setShortcutInfo(info);
+ if (Build.VERSION.SDK_INT >= 30) {
+ mXmppConnectionService
+ .getSystemService(ShortcutManager.class)
+ .pushDynamicShortcut(info.toShortcutInfo());
+ }
+ }
+ return notificationBuilder;
}
private void modifyForImage(
@@ -1626,12 +1665,25 @@ public class NotificationService {
}
private PendingIntent createCallAction(String sessionId, final String action, int requestCode) {
- final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
+ return pendingServiceIntent(mXmppConnectionService, action, requestCode, ImmutableMap.of(RtpSessionActivity.EXTRA_SESSION_ID, sessionId));
+ }
+
+ private PendingIntent createSnoozeIntent(final Conversation conversation) {
+ return pendingServiceIntent(mXmppConnectionService, XmppConnectionService.ACTION_SNOOZE, generateRequestCode(conversation,22),ImmutableMap.of("uuid",conversation.getUuid()));
+ }
+
+ private static PendingIntent pendingServiceIntent(final Context context, final String action, final int requestCode) {
+ return pendingServiceIntent(context, action, requestCode, ImmutableMap.of());
+ }
+
+ private static PendingIntent pendingServiceIntent(final Context context, final String action, final int requestCode, final Map extras) {
+ final Intent intent = new Intent(context, XmppConnectionService.class);
intent.setAction(action);
- intent.setPackage(mXmppConnectionService.getPackageName());
- intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId);
+ for(final Map.Entry entry : extras.entrySet()) {
+ intent.putExtra(entry.getKey(), entry.getValue());
+ }
return PendingIntent.getService(
- mXmppConnectionService,
+ context,
requestCode,
intent,
s()
@@ -1639,44 +1691,6 @@ public class NotificationService {
: PendingIntent.FLAG_UPDATE_CURRENT);
}
- private PendingIntent createSnoozeIntent(Conversation conversation) {
- final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
- intent.setAction(XmppConnectionService.ACTION_SNOOZE);
- intent.putExtra("uuid", conversation.getUuid());
- intent.setPackage(mXmppConnectionService.getPackageName());
- return PendingIntent.getService(
- mXmppConnectionService,
- generateRequestCode(conversation, 22),
- intent,
- s()
- ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
- : PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- private PendingIntent createTryAgainIntent() {
- final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
- intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN);
- return PendingIntent.getService(
- mXmppConnectionService,
- 45,
- intent,
- s()
- ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
- : PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
- private PendingIntent createDismissErrorIntent() {
- final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
- intent.setAction(XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS);
- return PendingIntent.getService(
- mXmppConnectionService,
- 69,
- intent,
- s()
- ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
- : PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
private boolean wasHighlightedOrPrivate(final Message message) {
if (message.getConversation() instanceof Conversation) {
Conversation conversation = (Conversation) message.getConversation();
@@ -1721,17 +1735,15 @@ public class NotificationService {
final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService);
mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name));
final List accounts = mXmppConnectionService.getAccounts();
- int enabled = 0;
- int connected = 0;
- if (accounts != null) {
- for (Account account : accounts) {
- if (account.isOnlineAndConnected()) {
- connected++;
- enabled++;
- } else if (account.isEnabled()) {
- enabled++;
- }
- }
+ final int enabled;
+ final int connected;
+ if (accounts == null) {
+ enabled = 0;
+ connected = 0;
+ } else {
+ enabled = Iterables.size(Iterables.filter(accounts, Account::isEnabled));
+ connected =
+ Iterables.size(Iterables.filter(accounts, Account::isOnlineAndConnected));
}
mBuilder.setContentText(
mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled));
@@ -1749,11 +1761,36 @@ public class NotificationService {
if (Compatibility.runsTwentySix()) {
mBuilder.setChannelId("foreground");
+ mBuilder.addAction(
+ R.drawable.ic_logout_white_24dp,
+ mXmppConnectionService.getString(R.string.log_out),
+ pendingServiceIntent(
+ mXmppConnectionService,
+ XmppConnectionService.ACTION_TEMPORARILY_DISABLE,
+ 87));
+ mBuilder.addAction(
+ R.drawable.ic_notifications_off_white_24dp,
+ mXmppConnectionService.getString(R.string.hide_notification),
+ pendingNotificationSettingsIntent(mXmppConnectionService));
}
return mBuilder.build();
}
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ private static PendingIntent pendingNotificationSettingsIntent(final Context context) {
+ final Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
+ intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName());
+ intent.putExtra(Settings.EXTRA_CHANNEL_ID, "foreground");
+ return PendingIntent.getActivity(
+ context,
+ 89,
+ intent,
+ s()
+ ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
+ : PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
private PendingIntent createOpenConversationsIntent() {
try {
return PendingIntent.getActivity(
@@ -1804,7 +1841,7 @@ public class NotificationService {
mBuilder.addAction(
R.drawable.ic_autorenew_white_24dp,
mXmppConnectionService.getString(R.string.try_again),
- createTryAgainIntent());
+ pendingServiceIntent(mXmppConnectionService, XmppConnectionService.ACTION_TRY_AGAIN, 45));
if (torNotAvailable) {
if (TorServiceUtils.isOrbotInstalled(mXmppConnectionService)) {
mBuilder.addAction(
@@ -1832,7 +1869,7 @@ public class NotificationService {
: PendingIntent.FLAG_UPDATE_CURRENT));
}
}
- mBuilder.setDeleteIntent(createDismissErrorIntent());
+ mBuilder.setDeleteIntent(pendingServiceIntent(mXmppConnectionService,XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS, 69));
mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE);
mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp);
mBuilder.setLocalOnly(true);
@@ -1927,6 +1964,11 @@ public class NotificationService {
lastTime = time;
}
+ public boolean removeMissedCall() {
+ --numberOfCalls;
+ return numberOfCalls <= 0;
+ }
+
public int getNumberOfCalls() {
return numberOfCalls;
}
diff --git a/src/main/java/eu/siacs/conversations/services/ShortcutService.java b/src/main/java/eu/siacs/conversations/services/ShortcutService.java
index 6b7106f74..c6ae77b63 100644
--- a/src/main/java/eu/siacs/conversations/services/ShortcutService.java
+++ b/src/main/java/eu/siacs/conversations/services/ShortcutService.java
@@ -11,6 +11,9 @@ import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.core.content.pm.ShortcutInfoCompat;
+import androidx.core.graphics.drawable.IconCompat;
import java.util.ArrayList;
import java.util.HashMap;
@@ -19,6 +22,7 @@ import java.util.List;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.ui.StartConversationActivity;
import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
import eu.siacs.conversations.xmpp.Jid;
@@ -88,13 +92,45 @@ public class ShortcutService {
}
}
- @TargetApi(Build.VERSION_CODES.N_MR1)
- private ShortcutInfo getShortcutInfo(Contact contact) {
- return new ShortcutInfo.Builder(xmppConnectionService, getShortcutId(contact))
+ public ShortcutInfoCompat getShortcutInfoCompat(final Contact contact) {
+ final ShortcutInfoCompat.Builder builder =
+ new ShortcutInfoCompat.Builder(xmppConnectionService, getShortcutId(contact))
.setShortLabel(contact.getDisplayName())
.setIntent(getShortcutIntent(contact))
- .setIcon(Icon.createWithBitmap(xmppConnectionService.getAvatarService().getRoundedShortcut(contact)))
- .build();
+ .setIsConversation();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ builder.setIcon(
+ IconCompat.createFromIcon(
+ xmppConnectionService,
+ Icon.createWithBitmap(
+ xmppConnectionService
+ .getAvatarService()
+ .getRoundedShortcut(contact))));
+ }
+ return builder.build();
+ }
+
+ public ShortcutInfoCompat getShortcutInfoCompat(final MucOptions mucOptions) {
+ final ShortcutInfoCompat.Builder builder =
+ new ShortcutInfoCompat.Builder(xmppConnectionService, getShortcutId(mucOptions))
+ .setShortLabel(mucOptions.getConversation().getName())
+ .setIntent(getShortcutIntent(mucOptions))
+ .setIsConversation();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ builder.setIcon(
+ IconCompat.createFromIcon(
+ xmppConnectionService,
+ Icon.createWithBitmap(
+ xmppConnectionService
+ .getAvatarService()
+ .getRoundedShortcut(mucOptions))));
+ }
+ return builder.build();
+ }
+
+ @TargetApi(Build.VERSION_CODES.N_MR1)
+ private ShortcutInfo getShortcutInfo(final Contact contact) {
+ return getShortcutInfoCompat(contact).toShortcutInfo();
}
private static boolean contactsChanged(List needles, List haystack) {
@@ -120,12 +156,40 @@ public class ShortcutService {
return contact.getAccount().getJid().asBareJid().toEscapedString()+"#"+contact.getJid().asBareJid().toEscapedString();
}
- private Intent getShortcutIntent(Contact contact) {
+ private static String getShortcutId(final MucOptions mucOptions) {
+ final Account account = mucOptions.getAccount();
+ final Jid jid = mucOptions.getConversation().getJid();
+ return account.getJid().asBareJid().toEscapedString()
+ + "#"
+ + jid.asBareJid().toEscapedString();
+ }
+
+ private Intent getShortcutIntent(final MucOptions mucOptions) {
+ final Account account = mucOptions.getAccount();
+ return getShortcutIntent(
+ account,
+ Uri.parse(
+ String.format(
+ "xmpp:%s?join",
+ mucOptions
+ .getConversation()
+ .getJid()
+ .asBareJid()
+ .toEscapedString())));
+ }
+
+ private Intent getShortcutIntent(final Contact contact) {
+ return getShortcutIntent(
+ contact.getAccount(),
+ Uri.parse("xmpp:" + contact.getJid().asBareJid().toEscapedString()));
+ }
+
+ private Intent getShortcutIntent(final Account account, final Uri uri) {
Intent intent = new Intent(xmppConnectionService, StartConversationActivity.class);
intent.setAction(Intent.ACTION_VIEW);
- intent.setData(Uri.parse("xmpp:"+contact.getJid().asBareJid().toEscapedString()));
- intent.putExtra("account",contact.getAccount().getJid().asBareJid().toString());
- intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP| Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ intent.setData(uri);
+ intent.putExtra("account", account.getJid().asBareJid().toString());
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
return intent;
}
diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java
index 7d2d90dd5..bfa1785f1 100644
--- a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java
+++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java
@@ -1,15 +1,28 @@
package eu.siacs.conversations.services;
+import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.io.BaseEncoding;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
@@ -62,7 +75,7 @@ public class UnifiedPushBroker {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid() + ": trigger endpoint renewal on bind");
- renewUnifiedEndpoint(transportOptional.get());
+ renewUnifiedEndpoint(transportOptional.get(), null);
}
}
}
@@ -73,22 +86,44 @@ public class UnifiedPushBroker {
service.sendPresencePacket(account, presence);
}
- public Optional renewUnifiedPushEndpoints() {
+ public void renewUnifiedPushEndpoints() {
+ renewUnifiedPushEndpoints(null);
+ }
+
+ public Optional renewUnifiedPushEndpoints(@Nullable final PushTargetMessenger pushTargetMessenger) {
final Optional transportOptional = getTransport();
if (transportOptional.isPresent()) {
final Transport transport = transportOptional.get();
if (transport.account.isEnabled()) {
- renewUnifiedEndpoint(transportOptional.get());
+ renewUnifiedEndpoint(transportOptional.get(), pushTargetMessenger);
} else {
+ if (pushTargetMessenger != null && pushTargetMessenger.messenger != null) {
+ sendRegistrationDelayed(pushTargetMessenger.messenger,"account is disabled");
+ }
Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. Account is disabled");
}
} else {
+ if (pushTargetMessenger != null && pushTargetMessenger.messenger != null) {
+ sendRegistrationDelayed(pushTargetMessenger.messenger,"no transport selected");
+ }
Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. No transport selected");
}
return transportOptional;
}
- private void renewUnifiedEndpoint(final Transport transport) {
+ private void sendRegistrationDelayed(final Messenger messenger, final String error) {
+ final Intent intent = new Intent(UnifiedPushDistributor.ACTION_REGISTRATION_DELAYED);
+ intent.putExtra(UnifiedPushDistributor.EXTRA_MESSAGE, error);
+ final var message = new Message();
+ message.obj = intent;
+ try {
+ messenger.send(message);
+ } catch (final RemoteException e) {
+ Log.d(Config.LOGTAG,"unable to tell messenger of delayed registration",e);
+ }
+ }
+
+ private void renewUnifiedEndpoint(final Transport transport, final PushTargetMessenger pushTargetMessenger) {
final Account account = transport.account;
final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
final List renewals =
@@ -105,6 +140,7 @@ public class UnifiedPushBroker {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid() + ": try to renew UnifiedPush " + renewal);
+ UnifiedPushDistributor.quickLog(service,String.format("%s: try to renew UnifiedPush %s", account.getJid(), renewal.toString()));
final String hashedApplication =
UnifiedPushDistributor.hash(account.getUuid(), renewal.application);
final String hashedInstance =
@@ -114,16 +150,23 @@ public class UnifiedPushBroker {
final Element register = registration.addChild("register", Namespace.UNIFIED_PUSH);
register.setAttribute("application", hashedApplication);
register.setAttribute("instance", hashedInstance);
+ final Messenger messenger;
+ if (pushTargetMessenger != null && renewal.equals(pushTargetMessenger.pushTarget)) {
+ messenger = pushTargetMessenger.messenger;
+ } else {
+ messenger = null;
+ }
this.service.sendIqPacket(
account,
registration,
- (a, response) -> processRegistration(transport, renewal, response));
+ (a, response) -> processRegistration(transport, renewal, messenger, response));
}
}
private void processRegistration(
final Transport transport,
final UnifiedPushDatabase.PushTarget renewal,
+ final Messenger messenger,
final IqPacket response) {
if (response.getType() == IqPacket.TYPE.RESULT) {
final Element registered = response.findChild("registered", Namespace.UNIFIED_PUSH);
@@ -142,7 +185,7 @@ public class UnifiedPushBroker {
Log.d(Config.LOGTAG, "could not parse expiration", e);
return;
}
- renewUnifiedPushEndpoint(transport, renewal, endpoint, expiration);
+ renewUnifiedPushEndpoint(transport, renewal, messenger, endpoint, expiration);
} else {
Log.d(Config.LOGTAG, "could not register UP endpoint " + response.getErrorCondition());
}
@@ -151,6 +194,7 @@ public class UnifiedPushBroker {
private void renewUnifiedPushEndpoint(
final Transport transport,
final UnifiedPushDatabase.PushTarget renewal,
+ final Messenger messenger,
final String endpoint,
final long expiration) {
Log.d(Config.LOGTAG, "registered endpoint " + endpoint + " expiration=" + expiration);
@@ -171,15 +215,42 @@ public class UnifiedPushBroker {
+ renewal.instance
+ " was updated to "
+ endpoint);
- broadcastEndpoint(
- renewal.instance,
- new UnifiedPushDatabase.ApplicationEndpoint(renewal.application, endpoint));
+ UnifiedPushDistributor.quickLog(
+ service,
+ "endpoint for "
+ + renewal.application
+ + "/"
+ + renewal.instance
+ + " was updated to "
+ + endpoint);
+ final UnifiedPushDatabase.ApplicationEndpoint applicationEndpoint =
+ new UnifiedPushDatabase.ApplicationEndpoint(renewal.application, endpoint);
+ sendEndpoint(messenger, renewal.instance, applicationEndpoint);
+ }
+ }
+
+ private void sendEndpoint(final Messenger messenger, String instance, final UnifiedPushDatabase.ApplicationEndpoint applicationEndpoint) {
+ if (messenger != null) {
+ Log.d(Config.LOGTAG,"using messenger instead of broadcast to communicate endpoint to "+applicationEndpoint.application);
+ final Message message = new Message();
+ message.obj = endpointIntent(instance, applicationEndpoint);
+ try {
+ messenger.send(message);
+ } catch (final RemoteException e) {
+ Log.d(Config.LOGTAG,"messenger failed. falling back to broadcast");
+ broadcastEndpoint(instance, applicationEndpoint);
+ }
+ } else {
+ broadcastEndpoint(instance, applicationEndpoint);
}
}
public boolean reconfigurePushDistributor() {
final boolean enabled = getTransport().isPresent();
setUnifiedPushDistributorEnabled(enabled);
+ if (!enabled) {
+ unregisterCurrentPushTargets();
+ }
return enabled;
}
@@ -202,6 +273,43 @@ public class UnifiedPushBroker {
}
}
+ private void unregisterCurrentPushTargets() {
+ final var future = deletePushTargets();
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(
+ final List pushTargets) {
+ broadcastUnregistered(pushTargets);
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ Log.d(
+ Config.LOGTAG,
+ "could not delete endpoints after UnifiedPushDistributor was disabled");
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture> deletePushTargets() {
+ return Futures.submit(() -> UnifiedPushDatabase.getInstance(service).deletePushTargets(),SCHEDULER);
+ }
+
+ private void broadcastUnregistered(final List pushTargets) {
+ for(final UnifiedPushDatabase.PushTarget pushTarget : pushTargets) {
+ Log.d(Config.LOGTAG,"sending unregistered to "+pushTarget);
+ broadcastUnregistered(pushTarget);
+ }
+ }
+
+ private void broadcastUnregistered(final UnifiedPushDatabase.PushTarget pushTarget) {
+ final var intent = unregisteredIntent(pushTarget);
+ service.sendBroadcast(intent);
+ }
+
public boolean processPushMessage(
final Account account, final Jid transport, final Element push) {
final String instance = push.getAttribute("instance");
@@ -296,20 +404,50 @@ public class UnifiedPushBroker {
updateIntent.putExtra("token", target.instance);
updateIntent.putExtra("bytesMessage", payload);
updateIntent.putExtra("message", new String(payload, StandardCharsets.UTF_8));
+ final var distributorVerificationIntent = new Intent();
+ distributorVerificationIntent.setPackage(service.getPackageName());
+ final var pendingIntent =
+ PendingIntent.getBroadcast(
+ service, 0, distributorVerificationIntent, PendingIntent.FLAG_IMMUTABLE);
+ updateIntent.putExtra("distributor", pendingIntent);
service.sendBroadcast(updateIntent);
}
private void broadcastEndpoint(
final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) {
Log.d(Config.LOGTAG, "broadcasting endpoint to " + endpoint.application);
- final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_NEW_ENDPOINT);
- updateIntent.setPackage(endpoint.application);
- updateIntent.putExtra("token", instance);
- updateIntent.putExtra("endpoint", endpoint.endpoint);
+ final Intent updateIntent = endpointIntent(instance, endpoint);
service.sendBroadcast(updateIntent);
}
- public void rebroadcastEndpoint(final String instance, final Transport transport) {
+ private Intent endpointIntent(final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) {
+ final Intent intent = new Intent(UnifiedPushDistributor.ACTION_NEW_ENDPOINT);
+ intent.setPackage(endpoint.application);
+ intent.putExtra("token", instance);
+ intent.putExtra("endpoint", endpoint.endpoint);
+ final var distributorVerificationIntent = new Intent();
+ distributorVerificationIntent.setPackage(service.getPackageName());
+ final var pendingIntent =
+ PendingIntent.getBroadcast(
+ service, 0, distributorVerificationIntent, PendingIntent.FLAG_IMMUTABLE);
+ intent.putExtra("distributor", pendingIntent);
+ return intent;
+ }
+
+ private Intent unregisteredIntent(final UnifiedPushDatabase.PushTarget pushTarget) {
+ final Intent intent = new Intent(UnifiedPushDistributor.ACTION_UNREGISTERED);
+ intent.setPackage(pushTarget.application);
+ intent.putExtra("token", pushTarget.instance);
+ final var distributorVerificationIntent = new Intent();
+ distributorVerificationIntent.setPackage(service.getPackageName());
+ final var pendingIntent =
+ PendingIntent.getBroadcast(
+ service, 0, distributorVerificationIntent, PendingIntent.FLAG_IMMUTABLE);
+ intent.putExtra("distributor", pendingIntent);
+ return intent;
+ }
+
+ public void rebroadcastEndpoint(final Messenger messenger, final String instance, final Transport transport) {
final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service);
final UnifiedPushDatabase.ApplicationEndpoint endpoint =
unifiedPushDatabase.getEndpoint(
@@ -317,7 +455,7 @@ public class UnifiedPushBroker {
transport.transport.toEscapedString(),
instance);
if (endpoint != null) {
- broadcastEndpoint(instance, endpoint);
+ sendEndpoint(messenger, instance, endpoint);
}
}
@@ -330,4 +468,14 @@ public class UnifiedPushBroker {
this.transport = transport;
}
}
+
+ public static class PushTargetMessenger {
+ private final UnifiedPushDatabase.PushTarget pushTarget;
+ public final Messenger messenger;
+
+ public PushTargetMessenger(UnifiedPushDatabase.PushTarget pushTarget, Messenger messenger) {
+ this.pushTarget = pushTarget;
+ this.messenger = messenger;
+ }
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java
index 64c16dbcd..b47a61a53 100644
--- a/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java
+++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java
@@ -1,10 +1,15 @@
package eu.siacs.conversations.services;
+import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.net.Uri;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.Parcelable;
+import android.os.RemoteException;
import android.util.Log;
import com.google.common.base.Charsets;
@@ -24,16 +29,30 @@ import eu.siacs.conversations.utils.Compatibility;
public class UnifiedPushDistributor extends BroadcastReceiver {
+ // distributor actions (these are actios used for connector->distributor broadcasts)
+ // we, the distributor, have a broadcast receiver listening for those actions
+
public static final String ACTION_REGISTER = "org.unifiedpush.android.distributor.REGISTER";
public static final String ACTION_UNREGISTER = "org.unifiedpush.android.distributor.UNREGISTER";
+
+
+ // connector actions (these are actions used for distributor->connector broadcasts)
+ public static final String ACTION_UNREGISTERED = "org.unifiedpush.android.connector.UNREGISTERED";
public static final String ACTION_BYTE_MESSAGE =
"org.unifiedpush.android.distributor.feature.BYTES_MESSAGE";
public static final String ACTION_REGISTRATION_FAILED =
"org.unifiedpush.android.connector.REGISTRATION_FAILED";
+
+ // this action is only used in 'messenger' communication to tell the app that a registration is
+ // probably fine but can not be processed right now; for example due to spotty internet
+ public static final String ACTION_REGISTRATION_DELAYED =
+ "org.unifiedpush.android.connector.REGISTRATION_DELAYED";
public static final String ACTION_MESSAGE = "org.unifiedpush.android.connector.MESSAGE";
public static final String ACTION_NEW_ENDPOINT =
"org.unifiedpush.android.connector.NEW_ENDPOINT";
+ public static final String EXTRA_MESSAGE = "message";
+
public static final String PREFERENCE_ACCOUNT = "up_push_account";
public static final String PREFERENCE_PUSH_SERVER = "up_push_server";
@@ -46,22 +65,24 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
return;
}
final String action = intent.getAction();
- final String application = intent.getStringExtra("application");
+ final String application;
+ final Parcelable appVerification = intent.getParcelableExtra("app");
+ if (appVerification instanceof PendingIntent pendingIntent) {
+ application = pendingIntent.getIntentSender().getCreatorPackage();
+ Log.d(Config.LOGTAG,"received application name via pending intent "+ application);
+ } else {
+ application = intent.getStringExtra("application");
+ }
+ final Parcelable messenger = intent.getParcelableExtra("messenger");
final String instance = intent.getStringExtra("token");
final List features = intent.getStringArrayListExtra("features");
switch (Strings.nullToEmpty(action)) {
- case ACTION_REGISTER:
- register(context, application, instance, features);
- break;
- case ACTION_UNREGISTER:
- unregister(context, instance);
- break;
- case Intent.ACTION_PACKAGE_FULLY_REMOVED:
- unregisterApplication(context, intent.getData());
- break;
- default:
- Log.d(Config.LOGTAG, "UnifiedPushDistributor received unknown action " + action);
- break;
+ case ACTION_REGISTER -> register(context, application, instance, features, messenger);
+ case ACTION_UNREGISTER -> unregister(context, instance);
+ case Intent.ACTION_PACKAGE_FULLY_REMOVED ->
+ unregisterApplication(context, intent.getData());
+ default ->
+ Log.d(Config.LOGTAG, "UnifiedPushDistributor received unknown action " + action);
}
}
@@ -69,7 +90,8 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
final Context context,
final String application,
final String instance,
- final Collection features) {
+ final Collection features,
+ final Parcelable messenger) {
if (Strings.isNullOrEmpty(application) || Strings.isNullOrEmpty(instance)) {
Log.w(Config.LOGTAG, "ignoring invalid UnifiedPush registration");
return;
@@ -89,18 +111,37 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
Log.d(
Config.LOGTAG,
"successfully created UnifiedPush entry. waking up XmppConnectionService");
+ quickLog(context, String.format("successfully registered %s (token = %s) for UnifiedPushed", application, instance));
final Intent serviceIntent = new Intent(context, XmppConnectionService.class);
serviceIntent.setAction(XmppConnectionService.ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS);
serviceIntent.putExtra("instance", instance);
+ serviceIntent.putExtra("application", application);
+ if (messenger instanceof Messenger) {
+ serviceIntent.putExtra("messenger", messenger);
+ }
Compatibility.startService(context, serviceIntent);
} else {
Log.d(Config.LOGTAG, "not successful. sending error message back to application");
final Intent registrationFailed = new Intent(ACTION_REGISTRATION_FAILED);
+ registrationFailed.putExtra(EXTRA_MESSAGE, "instance already exits");
registrationFailed.setPackage(application);
registrationFailed.putExtra("token", instance);
- context.sendBroadcast(registrationFailed);
+ if (messenger instanceof Messenger m) {
+ final var message = new Message();
+ message.obj = registrationFailed;
+ try {
+ m.send(message);
+ } catch (final RemoteException e) {
+ context.sendBroadcast(registrationFailed);
+ }
+ } else {
+ context.sendBroadcast(registrationFailed);
+ }
}
} else {
+ if (messenger instanceof Messenger m) {
+ sendRegistrationFailed(m,"Your application is not registered to receive messages");
+ }
Log.d(
Config.LOGTAG,
"ignoring invalid UnifiedPush registration. Unknown application "
@@ -108,6 +149,18 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
}
}
+ private void sendRegistrationFailed(final Messenger messenger, final String error) {
+ final Intent intent = new Intent(ACTION_REGISTRATION_FAILED);
+ intent.putExtra(EXTRA_MESSAGE, error);
+ final var message = new Message();
+ message.obj = intent;
+ try {
+ messenger.send(message);
+ } catch (final RemoteException e) {
+ Log.d(Config.LOGTAG,"unable to tell messenger of failed registration",e);
+ }
+ }
+
private List getBroadcastReceivers(final Context context, final String application) {
final Intent messageIntent = new Intent(ACTION_MESSAGE);
messageIntent.setPackage(application);
@@ -124,7 +177,9 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
}
final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(context);
if (unifiedPushDatabase.deleteInstance(instance)) {
+ quickLog(context, String.format("successfully unregistered token %s from UnifiedPushed (application requested unregister)", instance));
Log.d(Config.LOGTAG, "successfully removed " + instance + " from UnifiedPush");
+ // TODO send UNREGISTERED broadcast back to app?!
}
}
@@ -137,6 +192,7 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
Log.d(Config.LOGTAG, "app " + application + " has been removed from the system");
final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(context);
if (database.deleteApplication(application)) {
+ quickLog(context, String.format("successfully removed %s from UnifiedPushed (ACTION_PACKAGE_FULLY_REMOVED)", application));
Log.d(Config.LOGTAG, "successfully removed " + application + " from UnifiedPush");
}
}
@@ -149,4 +205,11 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
.hashString(Joiner.on('\0').join(components), Charsets.UTF_8)
.asBytes());
}
+
+ public static void quickLog(final Context context, final String message) {
+ final Intent intent = new Intent(context, XmppConnectionService.class);
+ intent.setAction(XmppConnectionService.ACTION_QUICK_LOG);
+ intent.putExtra("message", message);
+ context.startService(intent);
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index d3722053f..584237156 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -19,6 +19,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
import android.database.ContentObserver;
import android.graphics.Bitmap;
import android.media.AudioManager;
@@ -32,6 +33,7 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
+import android.os.Messenger;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.SystemClock;
@@ -51,12 +53,14 @@ import androidx.annotation.IntegerRes;
import androidx.annotation.NonNull;
import androidx.core.app.RemoteInput;
import androidx.core.content.ContextCompat;
+import androidx.core.util.Consumer;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import org.conscrypt.Conscrypt;
+import org.jxmpp.stringprep.libidn.LibIdnXmppStringprep;
import org.openintents.openpgp.IOpenPgpService2;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpServiceConnection;
@@ -65,7 +69,6 @@ import java.io.File;
import java.security.Security;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
-import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -82,6 +85,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
@@ -121,6 +126,7 @@ import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.persistance.UnifiedPushDatabase;
import eu.siacs.conversations.ui.ChooseAccountForProfilePictureActivity;
+import eu.siacs.conversations.ui.ConversationsActivity;
import eu.siacs.conversations.ui.RtpSessionActivity;
import eu.siacs.conversations.ui.SettingsActivity;
import eu.siacs.conversations.ui.UiCallback;
@@ -182,7 +188,11 @@ public class XmppConnectionService extends Service {
public static final String ACTION_CLEAR_MISSED_CALL_NOTIFICATION = "clear_missed_call_notification";
public static final String ACTION_DISMISS_ERROR_NOTIFICATIONS = "dismiss_error";
public static final String ACTION_TRY_AGAIN = "try_again";
+
+ public static final String ACTION_TEMPORARILY_DISABLE = "temporarily_disable";
+ public static final String ACTION_PING = "ping";
public static final String ACTION_IDLE_PING = "idle_ping";
+ public static final String ACTION_INTERNAL_PING = "internal_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";
@@ -190,12 +200,15 @@ public class XmppConnectionService extends Service {
public static final String ACTION_PROVISION_ACCOUNT = "provision_account";
private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE";
public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS = "eu.siacs.conversations.UNIFIED_PUSH_RENEW";
+ public static final String ACTION_QUICK_LOG = "eu.siacs.conversations.QUICK_LOG";
private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);
private final static Executor FILE_OBSERVER_EXECUTOR = Executors.newSingleThreadExecutor();
private final static Executor FILE_ATTACHMENT_EXECUTOR = Executors.newSingleThreadExecutor();
+
+ private final ScheduledExecutorService internalPingExecutor = Executors.newSingleThreadScheduledExecutor();
private final static SerialSingleThreadExecutor VIDEO_COMPRESSION_EXECUTOR = new SerialSingleThreadExecutor("VideoCompression");
private final SerialSingleThreadExecutor mDatabaseWriterExecutor = new SerialSingleThreadExecutor("DatabaseWriter");
private final SerialSingleThreadExecutor mDatabaseReaderExecutor = new SerialSingleThreadExecutor("DatabaseReader");
@@ -450,9 +463,9 @@ public class XmppConnectionService extends Service {
joinMuc(conversation);
}
scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
- } else if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED) {
+ } else if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED || account.getStatus() == Account.State.LOGGED_OUT) {
resetSendingToWaiting(account);
- if (account.isEnabled() && isInLowPingTimeoutMode(account)) {
+ if (account.isConnectionEnabled() && isInLowPingTimeoutMode(account)) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": went into offline state during low ping mode. reconnecting now");
reconnectAccount(account, true, false);
} else {
@@ -465,15 +478,24 @@ public class XmppConnectionService extends Service {
} else if (account.getStatus() != Account.State.CONNECTING && account.getStatus() != Account.State.NO_INTERNET) {
resetSendingToWaiting(account);
if (connection != null && account.getStatus().isAttemptReconnect()) {
- final int next = connection.getTimeToNextAttempt();
+ final boolean aggressive = account.getStatus() == Account.State.SEE_OTHER_HOST
+ || hasJingleRtpConnection(account);
+ final int next = connection.getTimeToNextAttempt(aggressive);
final boolean lowPingTimeoutMode = isInLowPingTimeoutMode(account);
if (next <= 0) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error connecting account. reconnecting now. lowPingTimeout=" + lowPingTimeoutMode);
reconnectAccount(account, true, false);
} else {
final int attempt = connection.getAttempt() + 1;
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error connecting account. try again in " + next + "s for the " + attempt + " time. lowPingTimeout=" + lowPingTimeoutMode);
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error connecting account. try again in " + next + "s for the " + attempt + " time. lowPingTimeout=" + lowPingTimeoutMode+", aggressive="+aggressive);
scheduleWakeUpCall(next, account.getUuid().hashCode());
+ if (aggressive) {
+ internalPingExecutor.schedule(
+ XmppConnectionService.this::manageAccountConnectionStatesInternal,
+ (next * 1000L) + 50,
+ TimeUnit.MILLISECONDS
+ );
+ }
}
}
}
@@ -485,6 +507,7 @@ public class XmppConnectionService extends Service {
private WakeLock wakeLock;
private LruCache mBitmapCache;
private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver();
+ private final BroadcastReceiver mInternalRestrictedEventReceiver = new RestrictedEventReceiver(Arrays.asList(TorServiceUtils.ACTION_STATUS));
private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver();
private static String generateFetchKey(Account account, final Avatar avatar) {
@@ -647,223 +670,293 @@ public class XmppConnectionService extends Service {
}
@Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- final String action = intent == null ? null : intent.getAction();
+ public int onStartCommand(final Intent intent, int flags, int startId) {
+ final String action = Strings.nullToEmpty(intent == null ? null : intent.getAction());
final boolean needsForegroundService = intent != null && intent.getBooleanExtra(EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE, false);
if (needsForegroundService) {
Log.d(Config.LOGTAG, "toggle forced foreground service after receiving event (action=" + action + ")");
toggleForegroundService(true);
}
- String pushedAccountHash = null;
- boolean interactive = false;
- if (action != null) {
- final String uuid = intent.getStringExtra("uuid");
- switch (action) {
- case QuickConversationsService.SMS_RETRIEVED_ACTION:
- mQuickConversationsService.handleSmsReceived(intent);
- break;
- case ConnectivityManager.CONNECTIVITY_ACTION:
- if (hasInternetConnection()) {
- if (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0) {
- schedulePostConnectivityChange();
- }
- if (Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
- resetAllAttemptCounts(true, false);
- }
- Resolver.clearCache();
+ final String uuid = intent == null ? null : intent.getStringExtra("uuid");
+ switch (action) {
+ case QuickConversationsService.SMS_RETRIEVED_ACTION:
+ mQuickConversationsService.handleSmsReceived(intent);
+ break;
+ case ConnectivityManager.CONNECTIVITY_ACTION:
+ if (hasInternetConnection()) {
+ if (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0) {
+ schedulePostConnectivityChange();
}
- break;
- case Intent.ACTION_SHUTDOWN:
- logoutAndSave(true);
- return START_NOT_STICKY;
- case ACTION_CLEAR_MESSAGE_NOTIFICATION:
- mNotificationExecutor.execute(() -> {
- try {
- final Conversation c = findConversationByUuid(uuid);
- if (c != null) {
- mNotificationService.clearMessages(c);
- } else {
- mNotificationService.clearMessages();
- }
- restoredFromDatabaseLatch.await();
-
- } catch (InterruptedException e) {
- Log.d(Config.LOGTAG, "unable to process clear message notification");
- }
- });
- break;
- case ACTION_CLEAR_MISSED_CALL_NOTIFICATION:
- mNotificationExecutor.execute(() -> {
- try {
- final Conversation c = findConversationByUuid(uuid);
- if (c != null) {
- mNotificationService.clearMissedCalls(c);
- } else {
- mNotificationService.clearMissedCalls();
- }
- restoredFromDatabaseLatch.await();
-
- } catch (InterruptedException e) {
- Log.d(Config.LOGTAG, "unable to process clear missed call notification");
- }
- });
- 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);
- mJingleConnectionManager.rejectRtpSession(sessionId);
- break;
- }
- case TorServiceUtils.ACTION_STATUS:
- final String status = intent.getStringExtra(TorServiceUtils.EXTRA_STATUS);
- //TODO port and host are in 'extras' - but this may not be a reliable source?
- if ("ON".equals(status)) {
- handleOrbotStartedEvent();
- return START_STICKY;
+ if (Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
+ resetAllAttemptCounts(true, false);
}
- 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);
+ Resolver.clearCache();
}
break;
- case ACTION_PROVISION_ACCOUNT: {
- final String address = intent.getStringExtra("address");
- final String password = intent.getStringExtra("password");
- if (QuickConversationsService.isQuicksy() || Strings.isNullOrEmpty(address) || Strings.isNullOrEmpty(password)) {
- break;
- }
- provisionAccount(address, password);
- break;
- }
- case ACTION_DISMISS_ERROR_NOTIFICATIONS:
- dismissErrorNotifications();
- break;
- case ACTION_TRY_AGAIN:
- resetAllAttemptCounts(false, true);
- interactive = true;
- break;
- case ACTION_REPLY_TO_CONVERSATION:
- Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
- if (remoteInput == null) {
- break;
- }
- final CharSequence body = remoteInput.getCharSequence("text_reply");
- final boolean dismissNotification = intent.getBooleanExtra("dismiss_notification", false);
- final String lastMessageUuid = intent.getStringExtra("last_message_uuid");
- if (body == null || body.length() <= 0) {
- break;
- }
- mNotificationExecutor.execute(() -> {
- try {
- restoredFromDatabaseLatch.await();
- final Conversation c = findConversationByUuid(uuid);
- if (c != null) {
- directReply(c, body.toString(), lastMessageUuid, dismissNotification);
- }
- } catch (InterruptedException e) {
- Log.d(Config.LOGTAG, "unable to process direct reply");
- }
- });
- break;
- case ACTION_MARK_AS_READ:
- mNotificationExecutor.execute(() -> {
+ case Intent.ACTION_SHUTDOWN:
+ logoutAndSave(true);
+ return START_NOT_STICKY;
+ case ACTION_CLEAR_MESSAGE_NOTIFICATION:
+ mNotificationExecutor.execute(() -> {
+ try {
final Conversation c = findConversationByUuid(uuid);
- if (c == null) {
- Log.d(Config.LOGTAG, "received mark read intent for unknown conversation (" + uuid + ")");
- return;
- }
- try {
- restoredFromDatabaseLatch.await();
- sendReadMarker(c, null);
- } catch (InterruptedException e) {
- Log.d(Config.LOGTAG, "unable to process notification read marker for conversation " + c.getName());
+ if (c != null) {
+ mNotificationService.clearMessages(c);
+ } else {
+ mNotificationService.clearMessages();
}
+ restoredFromDatabaseLatch.await();
- });
- break;
- case ACTION_SNOOZE:
- mNotificationExecutor.execute(() -> {
+ } catch (InterruptedException e) {
+ Log.d(Config.LOGTAG, "unable to process clear message notification");
+ }
+ });
+ break;
+ case ACTION_CLEAR_MISSED_CALL_NOTIFICATION:
+ mNotificationExecutor.execute(() -> {
+ try {
final Conversation c = findConversationByUuid(uuid);
- if (c == null) {
- Log.d(Config.LOGTAG, "received snooze intent for unknown conversation (" + uuid + ")");
- return;
+ if (c != null) {
+ mNotificationService.clearMissedCalls(c);
+ } else {
+ mNotificationService.clearMissedCalls();
}
- c.setMutedTill(System.currentTimeMillis() + 30 * 60 * 1000);
- mNotificationService.clearMessages(c);
- updateConversation(c);
- });
- case AudioManager.RINGER_MODE_CHANGED_ACTION:
- case NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED:
- if (dndOnSilentMode()) {
- refreshAllPresences();
+ restoredFromDatabaseLatch.await();
+
+ } catch (InterruptedException e) {
+ Log.d(Config.LOGTAG, "unable to process clear missed call notification");
}
+ });
+ break;
+ case ACTION_DISMISS_CALL: {
+ if (intent == null) {
break;
- case Intent.ACTION_SCREEN_ON:
- deactivateGracePeriod();
- case Intent.ACTION_USER_PRESENT:
- case Intent.ACTION_SCREEN_OFF:
- if (awayWhenScreenLocked()) {
- refreshAllPresences();
- }
- break;
- case ACTION_FCM_TOKEN_REFRESH:
- refreshAllFcmTokens();
- break;
- case ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS:
- final String instance = intent.getStringExtra("instance");
- final Optional transport = renewUnifiedPushEndpoints();
- if (instance != null && transport.isPresent()) {
- unifiedPushBroker.rebroadcastEndpoint(instance, transport.get());
- }
- break;
- case ACTION_IDLE_PING:
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- scheduleNextIdlePing();
- }
- break;
- case ACTION_FCM_MESSAGE_RECEIVED:
- pushedAccountHash = intent.getStringExtra("account");
- Log.d(Config.LOGTAG, "push message arrived in service. account=" + pushedAccountHash);
- break;
- case Intent.ACTION_SEND:
- Uri uri = intent.getData();
- if (uri != null) {
- Log.d(Config.LOGTAG, "received uri permission for " + uri);
- }
- return START_STICKY;
- }
- }
- synchronized (this) {
- WakeLockHelper.acquire(wakeLock);
- boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action) || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0 && ACTION_POST_CONNECTIVITY_CHANGE.equals(action));
- final HashSet pingCandidates = new HashSet<>();
- final String androidId = PhoneHelper.getAndroidId(this);
- for (Account account : accounts) {
- final boolean pushWasMeantForThisAccount = CryptoHelper.getAccountFingerprint(account, androidId).equals(pushedAccountHash);
- pingNow |= processAccountState(account,
- interactive,
- "ui".equals(action),
- pushWasMeantForThisAccount,
- pingCandidates);
- }
- if (pingNow) {
- for (Account account : pingCandidates) {
- final boolean lowTimeout = isInLowPingTimeoutMode(account);
- account.getXmppConnection().sendPing();
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + " send ping (action=" + action + ",lowTimeout=" + lowTimeout + ")");
- scheduleWakeUpCall(lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT, account.getUuid().hashCode());
}
+ 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;
}
- WakeLockHelper.release(wakeLock);
+ case TorServiceUtils.ACTION_STATUS:
+ final String status = intent == null ? null : intent.getStringExtra(TorServiceUtils.EXTRA_STATUS);
+ //TODO port and host are in 'extras' - but this may not be a reliable source?
+ if ("ON".equals(status)) {
+ handleOrbotStartedEvent();
+ return START_STICKY;
+ }
+ break;
+ case ACTION_END_CALL: {
+ if (intent == null) {
+ break;
+ }
+ 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_PROVISION_ACCOUNT: {
+ if (intent == null) {
+ break;
+ }
+ final String address = intent.getStringExtra("address");
+ final String password = intent.getStringExtra("password");
+ if (QuickConversationsService.isQuicksy() || Strings.isNullOrEmpty(address) || Strings.isNullOrEmpty(password)) {
+ break;
+ }
+ provisionAccount(address, password);
+ break;
+ }
+ case ACTION_DISMISS_ERROR_NOTIFICATIONS:
+ dismissErrorNotifications();
+ break;
+ case ACTION_TRY_AGAIN:
+ resetAllAttemptCounts(false, true);
+ break;
+ case ACTION_REPLY_TO_CONVERSATION:
+ final Bundle remoteInput = intent == null ? null : RemoteInput.getResultsFromIntent(intent);
+ if (remoteInput == null) {
+ break;
+ }
+ final CharSequence body = remoteInput.getCharSequence("text_reply");
+ final boolean dismissNotification = intent.getBooleanExtra("dismiss_notification", false);
+ final String lastMessageUuid = intent.getStringExtra("last_message_uuid");
+ if (body == null || body.length() <= 0) {
+ break;
+ }
+ mNotificationExecutor.execute(() -> {
+ try {
+ restoredFromDatabaseLatch.await();
+ final Conversation c = findConversationByUuid(uuid);
+ if (c != null) {
+ directReply(c, body.toString(), lastMessageUuid, dismissNotification);
+ }
+ } catch (InterruptedException e) {
+ Log.d(Config.LOGTAG, "unable to process direct reply");
+ }
+ });
+ break;
+ case ACTION_MARK_AS_READ:
+ mNotificationExecutor.execute(() -> {
+ final Conversation c = findConversationByUuid(uuid);
+ if (c == null) {
+ Log.d(Config.LOGTAG, "received mark read intent for unknown conversation (" + uuid + ")");
+ return;
+ }
+ try {
+ restoredFromDatabaseLatch.await();
+ sendReadMarker(c, null);
+ } catch (InterruptedException e) {
+ Log.d(Config.LOGTAG, "unable to process notification read marker for conversation " + c.getName());
+ }
+
+ });
+ break;
+ case ACTION_SNOOZE:
+ mNotificationExecutor.execute(() -> {
+ final Conversation c = findConversationByUuid(uuid);
+ if (c == null) {
+ Log.d(Config.LOGTAG, "received snooze intent for unknown conversation (" + uuid + ")");
+ return;
+ }
+ c.setMutedTill(System.currentTimeMillis() + 30 * 60 * 1000);
+ mNotificationService.clearMessages(c);
+ updateConversation(c);
+ });
+ case AudioManager.RINGER_MODE_CHANGED_ACTION:
+ case NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED:
+ if (dndOnSilentMode()) {
+ refreshAllPresences();
+ }
+ break;
+ case Intent.ACTION_SCREEN_ON:
+ deactivateGracePeriod();
+ case Intent.ACTION_USER_PRESENT:
+ case Intent.ACTION_SCREEN_OFF:
+ if (awayWhenScreenLocked()) {
+ refreshAllPresences();
+ }
+ break;
+ case ACTION_FCM_TOKEN_REFRESH:
+ refreshAllFcmTokens();
+ break;
+ case ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS:
+ if (intent == null) {
+ break;
+ }
+ final String instance = intent.getStringExtra("instance");
+ final String application = intent.getStringExtra("application");
+ final Messenger messenger = intent.getParcelableExtra("messenger");
+ final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger;
+ if (messenger != null && application != null && instance != null) {
+ pushTargetMessenger = new UnifiedPushBroker.PushTargetMessenger(new UnifiedPushDatabase.PushTarget(application, instance),messenger);
+ Log.d(Config.LOGTAG,"found push target messenger");
+ } else {
+ pushTargetMessenger = null;
+ }
+ final Optional transport = renewUnifiedPushEndpoints(pushTargetMessenger);
+ if (instance != null && transport.isPresent()) {
+ unifiedPushBroker.rebroadcastEndpoint(messenger, instance, transport.get());
+ }
+ break;
+ case ACTION_IDLE_PING:
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ scheduleNextIdlePing();
+ }
+ break;
+ case ACTION_FCM_MESSAGE_RECEIVED:
+ Log.d(Config.LOGTAG, "push message arrived in service. account");
+ break;
+ case ACTION_QUICK_LOG:
+ final String message = intent == null ? null : intent.getStringExtra("message");
+ if (message != null && Config.QUICK_LOG) {
+ quickLog(message);
+ }
+ break;
+ case Intent.ACTION_SEND:
+ final Uri uri = intent == null ? null : intent.getData();
+ if (uri != null) {
+ Log.d(Config.LOGTAG, "received uri permission for " + uri);
+ }
+ return START_STICKY;
+ case ACTION_TEMPORARILY_DISABLE:
+ toggleSoftDisabled(true);
+ if (checkListeners()) {
+ stopSelf();
+ }
+ return START_NOT_STICKY;
}
+ manageAccountConnectionStates(action, intent == null ? null : intent.getExtras());
if (SystemClock.elapsedRealtime() - mLastExpiryRun.get() >= Config.EXPIRY_INTERVAL) {
expireOldMessages();
}
return START_STICKY;
}
+ private void quickLog(final String message) {
+ if (Strings.isNullOrEmpty(message)) {
+ return;
+ }
+ final Account account = AccountUtils.getFirstEnabled(this);
+ if (account == null) {
+ return;
+ }
+ final Conversation conversation =
+ findOrCreateConversation(account, Config.BUG_REPORTS, false, true);
+ final Message report = new Message(conversation, message, Message.ENCRYPTION_NONE);
+ report.setStatus(Message.STATUS_RECEIVED);
+ conversation.add(report);
+ databaseBackend.createMessage(report);
+ updateConversationUi();
+ }
+
+ private void manageAccountConnectionStatesInternal() {
+ manageAccountConnectionStates(ACTION_INTERNAL_PING, null);
+ }
+
+ private synchronized void manageAccountConnectionStates(
+ final String action, final Bundle extras) {
+ final String pushedAccountHash = extras == null ? null : extras.getString("account");
+ final boolean interactive = java.util.Objects.equals(ACTION_TRY_AGAIN, action);
+ WakeLockHelper.acquire(wakeLock);
+ boolean pingNow =
+ ConnectivityManager.CONNECTIVITY_ACTION.equals(action)
+ || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0
+ && ACTION_POST_CONNECTIVITY_CHANGE.equals(action));
+ final HashSet pingCandidates = new HashSet<>();
+ final String androidId = pushedAccountHash == null ? null : PhoneHelper.getAndroidId(this);
+ for (final Account account : accounts) {
+ final boolean pushWasMeantForThisAccount =
+ androidId != null
+ && CryptoHelper.getAccountFingerprint(account, androidId)
+ .equals(pushedAccountHash);
+ pingNow |=
+ processAccountState(
+ account,
+ interactive,
+ "ui".equals(action),
+ pushWasMeantForThisAccount,
+ pingCandidates);
+ }
+ if (pingNow) {
+ for (final Account account : pingCandidates) {
+ final boolean lowTimeout = isInLowPingTimeoutMode(account);
+ account.getXmppConnection().sendPing();
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + " send ping (action="
+ + action
+ + ",lowTimeout="
+ + lowTimeout
+ + ")");
+ scheduleWakeUpCall(
+ lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT,
+ account.getUuid().hashCode());
+ }
+ }
+ WakeLockHelper.release(wakeLock);
+ }
+
private void handleOrbotStartedEvent() {
for (final Account account : accounts) {
if (account.getStatus() == Account.State.TOR_NOT_AVAILABLE) {
@@ -873,78 +966,85 @@ public class XmppConnectionService extends Service {
}
private boolean processAccountState(Account account, boolean interactive, boolean isUiAction, boolean isAccountPushed, HashSet pingCandidates) {
- boolean pingNow = false;
- if (account.getStatus().isAttemptReconnect()) {
- if (!hasInternetConnection()) {
- account.setStatus(Account.State.NO_INTERNET);
- if (statusListener != null) {
- statusListener.onStatusChanged(account);
- }
- } else {
- if (account.getStatus() == Account.State.NO_INTERNET) {
- account.setStatus(Account.State.OFFLINE);
- if (statusListener != null) {
- statusListener.onStatusChanged(account);
- }
- }
- if (account.getStatus() == Account.State.ONLINE) {
- synchronized (mLowPingTimeoutMode) {
- long lastReceived = account.getXmppConnection().getLastPacketReceived();
- long lastSent = account.getXmppConnection().getLastPingSent();
- long pingInterval = isUiAction ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
- long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime();
- int pingTimeout = mLowPingTimeoutMode.contains(account.getJid().asBareJid()) ? Config.LOW_PING_TIMEOUT * 1000 : Config.PING_TIMEOUT * 1000;
- long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime();
- if (lastSent > lastReceived) {
- if (pingTimeoutIn < 0) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping timeout");
- this.reconnectAccount(account, true, interactive);
- } else {
- int secs = (int) (pingTimeoutIn / 1000);
- this.scheduleWakeUpCall(secs, account.getUuid().hashCode());
- }
+ if (!account.getStatus().isAttemptReconnect()) {
+ return false;
+ }
+ if (!hasInternetConnection()) {
+ account.setStatus(Account.State.NO_INTERNET);
+ statusListener.onStatusChanged(account);
+ } else {
+ if (account.getStatus() == Account.State.NO_INTERNET) {
+ account.setStatus(Account.State.OFFLINE);
+ statusListener.onStatusChanged(account);
+ }
+ if (account.getStatus() == Account.State.ONLINE) {
+ synchronized (mLowPingTimeoutMode) {
+ long lastReceived = account.getXmppConnection().getLastPacketReceived();
+ long lastSent = account.getXmppConnection().getLastPingSent();
+ long pingInterval = isUiAction ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
+ long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime();
+ int pingTimeout = mLowPingTimeoutMode.contains(account.getJid().asBareJid()) ? Config.LOW_PING_TIMEOUT * 1000 : Config.PING_TIMEOUT * 1000;
+ long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime();
+ if (lastSent > lastReceived) {
+ if (pingTimeoutIn < 0) {
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping timeout");
+ this.reconnectAccount(account, true, interactive);
} else {
- pingCandidates.add(account);
- if (isAccountPushed) {
- pingNow = true;
- if (mLowPingTimeoutMode.add(account.getJid().asBareJid())) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": entering low ping timeout mode");
- }
- } else if (msToNextPing <= 0) {
- pingNow = true;
- } else {
- this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode());
- if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": leaving low ping timeout mode");
- }
+ int secs = (int) (pingTimeoutIn / 1000);
+ this.scheduleWakeUpCall(secs, account.getUuid().hashCode());
+ }
+ } else {
+ pingCandidates.add(account);
+ if (isAccountPushed) {
+ if (mLowPingTimeoutMode.add(account.getJid().asBareJid())) {
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": entering low ping timeout mode");
+ }
+ return true;
+ } else if (msToNextPing <= 0) {
+ return true;
+ } else {
+ this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode());
+ if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) {
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": leaving low ping timeout mode");
}
}
}
- } else if (account.getStatus() == Account.State.OFFLINE) {
+ }
+ } else if (account.getStatus() == Account.State.OFFLINE) {
+ reconnectAccount(account, true, interactive);
+ } else if (account.getStatus() == Account.State.CONNECTING) {
+ long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000;
+ long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000;
+ long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco;
+ long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect;
+ if (timeout < 0) {
+ Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting (secondsSinceLast=" + secondsSinceLastConnect + ")");
+ account.getXmppConnection().resetAttemptCount(false);
reconnectAccount(account, true, interactive);
- } else if (account.getStatus() == Account.State.CONNECTING) {
- long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000;
- long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000;
- long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco;
- long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect;
- if (timeout < 0) {
- Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting (secondsSinceLast=" + secondsSinceLastConnect + ")");
- account.getXmppConnection().resetAttemptCount(false);
- reconnectAccount(account, true, interactive);
- } else if (discoTimeout < 0) {
- account.getXmppConnection().sendDiscoTimeout();
- scheduleWakeUpCall((int) Math.min(timeout, discoTimeout), account.getUuid().hashCode());
- } else {
- scheduleWakeUpCall((int) Math.min(timeout, discoTimeout), account.getUuid().hashCode());
- }
+ } else if (discoTimeout < 0) {
+ account.getXmppConnection().sendDiscoTimeout();
+ scheduleWakeUpCall((int) Math.min(timeout, discoTimeout), account.getUuid().hashCode());
} else {
- if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
- reconnectAccount(account, true, interactive);
- }
+ scheduleWakeUpCall((int) Math.min(timeout, discoTimeout), account.getUuid().hashCode());
+ }
+ } else {
+ final boolean aggressive = account.getStatus() == Account.State.SEE_OTHER_HOST || hasJingleRtpConnection(account);
+ if (account.getXmppConnection().getTimeToNextAttempt(aggressive) <= 0) {
+ reconnectAccount(account, true, interactive);
+ }
+ }
+ }
+ return false;
+ }
+
+ private void toggleSoftDisabled(final boolean softDisabled) {
+ for(final Account account : this.accounts) {
+ if (account.isEnabled()) {
+ if (account.setOption(Account.OPTION_SOFT_DISABLED, softDisabled)) {
+ updateAccount(account);
}
}
}
- return pingNow;
}
public boolean processUnifiedPushMessage(final Account account, final Jid transport, final Element push) {
@@ -1144,6 +1244,7 @@ public class XmppConnectionService extends Service {
@SuppressLint("TrulyRandom")
@Override
public void onCreate() {
+ LibIdnXmppStringprep.setup();
if (Compatibility.runsTwentySix()) {
mNotificationService.initializeChannels();
}
@@ -1189,7 +1290,11 @@ public class XmppConnectionService extends Service {
restoreFromDatabase();
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
+ if (QuickConversationsService.isContactListIntegration(this)
+ && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
+ || ContextCompat.checkSelfPermission(
+ this, Manifest.permission.READ_CONTACTS)
+ == PackageManager.PERMISSION_GRANTED)) {
startContactObserver();
}
FILE_OBSERVER_EXECUTOR.execute(fileBackend::deleteHistoricAvatarPath);
@@ -1201,7 +1306,7 @@ public class XmppConnectionService extends Service {
if (Config.supportOpenPgp()) {
this.pgpServiceConnection = new OpenPgpServiceConnection(this, "org.sufficientlysecure.keychain", new OpenPgpServiceConnection.OnBound() {
@Override
- public void onBound(IOpenPgpService2 service) {
+ public void onBound(final IOpenPgpService2 service) {
for (Account account : accounts) {
final PgpDecryptionService pgp = account.getPgpDecryptionService();
if (pgp != null) {
@@ -1211,7 +1316,8 @@ public class XmppConnectionService extends Service {
}
@Override
- public void onError(Exception e) {
+ public void onError(final Exception exception) {
+ Log.e(Config.LOGTAG,"could not bind to OpenKeyChain", exception);
}
});
this.pgpServiceConnection.bindToService();
@@ -1223,19 +1329,30 @@ public class XmppConnectionService extends Service {
toggleForegroundService();
updateUnreadCountBadge();
toggleScreenEventReceiver();
- final IntentFilter intentFilter = new IntentFilter();
- intentFilter.addAction(TorServiceUtils.ACTION_STATUS);
+ final IntentFilter systemBroadcastFilter = new IntentFilter();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
scheduleNextIdlePing();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+ systemBroadcastFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
}
- intentFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
+ systemBroadcastFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
}
- registerReceiver(this.mInternalEventReceiver, intentFilter);
+ ContextCompat.registerReceiver(
+ this,
+ this.mInternalEventReceiver,
+ systemBroadcastFilter,
+ ContextCompat.RECEIVER_NOT_EXPORTED);
+ final IntentFilter exportedBroadcastFilter = new IntentFilter();
+ exportedBroadcastFilter.addAction(TorServiceUtils.ACTION_STATUS);
+ ContextCompat.registerReceiver(
+ this,
+ this.mInternalRestrictedEventReceiver,
+ exportedBroadcastFilter,
+ ContextCompat.RECEIVER_EXPORTED);
mForceDuringOnCreate.set(false);
toggleForegroundService();
setupPhoneStateListener();
+ internalPingExecutor.scheduleAtFixedRate(this::manageAccountConnectionStatesInternal,10,10,TimeUnit.SECONDS);
}
@@ -1302,12 +1419,14 @@ public class XmppConnectionService extends Service {
public void onDestroy() {
try {
unregisterReceiver(this.mInternalEventReceiver);
+ unregisterReceiver(this.mInternalRestrictedEventReceiver);
unregisterReceiver(this.mInternalScreenEventReceiver);
} catch (final IllegalArgumentException e) {
//ignored
}
destroyed = false;
fileObserver.stopWatching();
+ internalPingExecutor.shutdown();
super.onDestroy();
}
@@ -1347,7 +1466,7 @@ public class XmppConnectionService extends Service {
toggleForegroundService(false);
}
- private void toggleForegroundService(boolean force) {
+ private void toggleForegroundService(final boolean force) {
final boolean status;
final OngoingCall ongoing = ongoingCall.get();
if (force || mForceDuringOnCreate.get() || mForceForegroundService.get() || ongoing != null || (Compatibility.keepForegroundService(this) && hasEnabledAccounts())) {
@@ -1356,12 +1475,12 @@ public class XmppConnectionService extends Service {
if (ongoing != null) {
notification = this.mNotificationService.getOngoingCallNotification(ongoing);
id = NotificationService.ONGOING_CALL_NOTIFICATION_ID;
- startForeground(id, notification);
+ startForegroundOrCatch(id, notification, true);
mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID);
} else {
notification = this.mNotificationService.createForegroundNotification();
id = NotificationService.FOREGROUND_NOTIFICATION_ID;
- startForeground(id, notification);
+ startForegroundOrCatch(id, notification, false);
}
if (!mForceForegroundService.get()) {
@@ -1381,6 +1500,38 @@ public class XmppConnectionService extends Service {
Log.d(Config.LOGTAG, "ForegroundService: " + (status ? "on" : "off"));
}
+ private void startForegroundOrCatch(
+ final int id, final Notification notification, final boolean requireMicrophone) {
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ final int foregroundServiceType;
+ if (requireMicrophone
+ && ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
+ == PackageManager.PERMISSION_GRANTED) {
+ foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
+ Log.d(Config.LOGTAG, "defaulting to microphone foreground service type");
+ } else if (getSystemService(PowerManager.class)
+ .isIgnoringBatteryOptimizations(getPackageName())) {
+ foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED;
+ } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
+ == PackageManager.PERMISSION_GRANTED) {
+ foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
+ } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
+ == PackageManager.PERMISSION_GRANTED) {
+ foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
+ } else {
+ foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE;
+ Log.w(Config.LOGTAG, "falling back to special use foreground service type");
+ }
+ startForeground(id, notification, foregroundServiceType);
+ } else {
+ startForeground(id, notification);
+ }
+ } catch (final IllegalStateException | SecurityException e) {
+ Log.e(Config.LOGTAG, "Could not start foreground service", e);
+ }
+ }
+
public boolean foregroundNotificationNeedsUpdatingWhenErrorStateChanges() {
return !mForceForegroundService.get() && ongoingCall.get() == null && Compatibility.keepForegroundService(this) && hasEnabledAccounts();
}
@@ -1398,7 +1549,7 @@ public class XmppConnectionService extends Service {
private void logoutAndSave(boolean stop) {
int activeAccounts = 0;
for (final Account account : accounts) {
- if (account.getStatus() != Account.State.DISABLED) {
+ if (account.isConnectionEnabled()) {
databaseBackend.writeRoster(account.getRoster());
activeAccounts++;
}
@@ -1434,25 +1585,18 @@ public class XmppConnectionService extends Service {
}
}
- public void scheduleWakeUpCall(int seconds, int requestCode) {
+ public void scheduleWakeUpCall(final int seconds, final int requestCode) {
final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000L;
final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
if (alarmManager == null) {
return;
}
final Intent intent = new Intent(this, EventReceiver.class);
- intent.setAction("ping");
+ intent.setAction(ACTION_PING);
try {
- final PendingIntent pendingIntent;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- pendingIntent =
- PendingIntent.getBroadcast(
- this, requestCode, intent, PendingIntent.FLAG_IMMUTABLE);
- } else {
- pendingIntent =
- PendingIntent.getBroadcast(
- this, requestCode, intent, 0);
- }
+ final PendingIntent pendingIntent =
+ PendingIntent.getBroadcast(
+ this, requestCode, intent, PendingIntent.FLAG_IMMUTABLE);
alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent);
} catch (RuntimeException e) {
Log.e(Config.LOGTAG, "unable to schedule alarm for ping", e);
@@ -1846,6 +1990,7 @@ public class XmppConnectionService extends Service {
if (connection == null) {
Log.d(Config.LOGTAG, account.getJid().asBareJid()+": no connection. ignoring bookmark creation");
} else if (connection.getFeatures().bookmarks2()) {
+ Log.d(Config.LOGTAG,account.getJid().asBareJid() + ": pushing bookmark via Bookmarks 2");
final Element item = mIqGenerator.publishBookmarkItem(bookmark);
pushNodeAndEnforcePublishOptions(account, Namespace.BOOKMARKS2, item, bookmark.getJid().asBareJid().toEscapedString(), PublishOptions.persistentWhitelistAccessMaxItems());
} else if (connection.getFeatures().bookmarksConversion()) {
@@ -1859,7 +2004,8 @@ public class XmppConnectionService extends Service {
account.removeBookmark(bookmark);
final XmppConnection connection = account.getXmppConnection();
if (connection.getFeatures().bookmarks2()) {
- IqPacket request = mIqGenerator.deleteItem(Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toEscapedString());
+ final IqPacket request = mIqGenerator.deleteItem(Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toEscapedString());
+ Log.d(Config.LOGTAG,account.getJid().asBareJid() + ": removing bookmark via Bookmarks 2");
sendIqPacket(account, request, (a, response) -> {
if (response.getType() == IqPacket.TYPE.ERROR) {
Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": unable to delete bookmark " + response.getErrorCondition());
@@ -2361,8 +2507,12 @@ public class XmppConnectionService extends Service {
return this.unifiedPushBroker.reconfigurePushDistributor();
}
+ private Optional renewUnifiedPushEndpoints(final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger) {
+ return this.unifiedPushBroker.renewUnifiedPushEndpoints(pushTargetMessenger);
+ }
+
public Optional renewUnifiedPushEndpoints() {
- return this.unifiedPushBroker.renewUnifiedPushEndpoints();
+ return this.unifiedPushBroker.renewUnifiedPushEndpoints(null);
}
private void provisionAccount(final String address, final String password) {
@@ -2473,6 +2623,20 @@ public class XmppConnectionService extends Service {
});
}
+ public void unregisterAccount(final Account account, final Consumer callback) {
+ final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET);
+ final Element query = iqPacket.addChild("query",Namespace.REGISTER);
+ query.addChild("remove");
+ sendIqPacket(account, iqPacket, (a, response) -> {
+ if (response.getType() == IqPacket.TYPE.RESULT) {
+ deleteAccount(a);
+ callback.accept(true);
+ } else {
+ callback.accept(false);
+ }
+ });
+ }
+
public void deleteAccount(final Account account) {
final boolean connected = account.getStatus() == Account.State.ONLINE;
synchronized (this.conversations) {
@@ -2739,6 +2903,7 @@ public class XmppConnectionService extends Service {
}
private void switchToForeground() {
+ toggleSoftDisabled(false);
final boolean broadcastLastActivity = broadcastLastActivity();
for (Conversation conversation : getConversations()) {
if (conversation.getMode() == Conversation.MODE_MULTI) {
@@ -3119,8 +3284,8 @@ public class XmppConnectionService extends Service {
if (this.accounts == null) {
return false;
}
- for (Account account : this.accounts) {
- if (account.isEnabled()) {
+ for (final Account account : this.accounts) {
+ if (account.isConnectionEnabled()) {
return true;
}
}
@@ -3518,23 +3683,23 @@ public class XmppConnectionService extends Service {
});
}
- private void disconnect(Account account, boolean force) {
- if ((account.getStatus() == Account.State.ONLINE)
- || (account.getStatus() == Account.State.DISABLED)) {
- final XmppConnection connection = account.getXmppConnection();
- if (!force) {
- List conversations = getConversations();
- for (Conversation conversation : conversations) {
- if (conversation.getAccount() == account) {
- if (conversation.getMode() == Conversation.MODE_MULTI) {
- leaveMuc(conversation, true);
- }
+ private void disconnect(final Account account, boolean force) {
+ final XmppConnection connection = account.getXmppConnection();
+ if (connection == null) {
+ return;
+ }
+ if (!force) {
+ final List conversations = getConversations();
+ for (Conversation conversation : conversations) {
+ if (conversation.getAccount() == account) {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ leaveMuc(conversation, true);
}
}
- sendOfflinePresence(account);
}
- connection.disconnect(force);
+ sendOfflinePresence(account);
}
+ connection.disconnect(force);
}
@Override
@@ -4018,13 +4183,18 @@ public class XmppConnectionService extends Service {
private void reconnectAccount(final Account account, final boolean force, final boolean interactive) {
synchronized (account) {
- XmppConnection connection = account.getXmppConnection();
- if (connection == null) {
+ final XmppConnection existingConnection = account.getXmppConnection();
+ final XmppConnection connection;
+ if (existingConnection != null) {
+ connection = existingConnection;
+ } else if (account.isConnectionEnabled()) {
connection = createConnection(account);
account.setXmppConnection(connection);
+ } else {
+ return;
}
- boolean hasInternet = hasInternetConnection();
- if (account.isEnabled() && hasInternet) {
+ final boolean hasInternet = hasInternetConnection();
+ if (account.isConnectionEnabled() && hasInternet) {
if (!force) {
disconnect(account, false);
}
@@ -4512,7 +4682,7 @@ public class XmppConnectionService extends Service {
public void refreshAllPresences() {
boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity();
for (Account account : getAccounts()) {
- if (account.isEnabled()) {
+ if (account.isConnectionEnabled()) {
sendPresence(account, includeIdleTimestamp);
}
}
@@ -4553,6 +4723,10 @@ public class XmppConnectionService extends Service {
return this.mJingleConnectionManager;
}
+ private boolean hasJingleRtpConnection(final Account account) {
+ return this.mJingleConnectionManager.hasJingleRtpConnection(account);
+ }
+
public MessageArchiveService getMessageArchiveService() {
return this.mMessageArchiveService;
}
@@ -4635,10 +4809,10 @@ public class XmppConnectionService extends Service {
mDatabaseWriterExecutor.execute(runnable);
}
- public boolean sendBlockRequest(final Blockable blockable, boolean reportSpam) {
+ public boolean sendBlockRequest(final Blockable blockable, final boolean reportSpam, final String serverMsgId) {
if (blockable != null && blockable.getBlockedJid() != null) {
final Jid jid = blockable.getBlockedJid();
- this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid, reportSpam), (a, response) -> {
+ this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid, reportSpam, serverMsgId), (a, response) -> {
if (response.getType() == IqPacket.TYPE.RESULT) {
a.getBlocklist().add(jid);
updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
@@ -4984,11 +5158,30 @@ public class XmppConnectionService extends Service {
private class InternalEventReceiver extends BroadcastReceiver {
@Override
- public void onReceive(Context context, Intent intent) {
+ public void onReceive(final Context context, final Intent intent) {
onStartCommand(intent, 0, 0);
}
}
+ private class RestrictedEventReceiver extends BroadcastReceiver {
+
+ private final Collection allowedActions;
+
+ private RestrictedEventReceiver(final Collection allowedActions) {
+ this.allowedActions = allowedActions;
+ }
+
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ final String action = intent == null ? null : intent.getAction();
+ if (allowedActions.contains(action)) {
+ onStartCommand(intent,0,0);
+ } else {
+ Log.e(Config.LOGTAG,"restricting broadcast of event "+action);
+ }
+ }
+ }
+
public static class OngoingCall {
public final AbstractJingleConnection.Id id;
public final Set media;
@@ -5013,4 +5206,18 @@ public class XmppConnectionService extends Service {
return Objects.hashCode(id, media, reconnecting);
}
}
+
+ public static void toggleForegroundService(final XmppConnectionService service) {
+ if (service == null) {
+ return;
+ }
+ service.toggleForegroundService();
+ }
+
+ public static void toggleForegroundService(final ConversationsActivity activity) {
+ if (activity == null) {
+ return;
+ }
+ toggleForegroundService(activity.xmppConnectionService);
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java b/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java
index 6f4d77c51..986aeb563 100644
--- a/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java
+++ b/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java
@@ -14,13 +14,27 @@ import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.ui.util.JidDialog;
public final class BlockContactDialog {
+
public static void show(final XmppActivity xmppActivity, final Blockable blockable) {
+ show(xmppActivity, blockable, null);
+ }
+ public static void show(final XmppActivity xmppActivity, final Blockable blockable, final String serverMsgId) {
final AlertDialog.Builder builder = new AlertDialog.Builder(xmppActivity);
final boolean isBlocked = blockable.isBlocked();
builder.setNegativeButton(R.string.cancel, null);
DialogBlockContactBinding binding = DataBindingUtil.inflate(xmppActivity.getLayoutInflater(), R.layout.dialog_block_contact, null, false);
final boolean reporting = blockable.getAccount().getXmppConnection().getFeatures().spamReporting();
- binding.reportSpam.setVisibility(!isBlocked && reporting ? View.VISIBLE : View.GONE);
+ if (reporting && !isBlocked) {
+ binding.reportSpam.setVisibility(View.VISIBLE);
+ if (serverMsgId != null) {
+ binding.reportSpam.setChecked(true);
+ binding.reportSpam.setEnabled(false);
+ } else {
+ binding.reportSpam.setEnabled(true);
+ }
+ } else {
+ binding.reportSpam.setVisibility(View.GONE);
+ }
builder.setView(binding.getRoot());
final String value;
@@ -34,8 +48,18 @@ public final class BlockContactDialog {
value =blockable.getJid().getDomain().toEscapedString();
res = isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text;
} else {
- int resBlockAction = blockable instanceof Conversation && ((Conversation) blockable).isWithStranger() ? R.string.block_stranger : R.string.action_block_contact;
- builder.setTitle(isBlocked ? R.string.action_unblock_contact : resBlockAction);
+ if (isBlocked) {
+ builder.setTitle(R.string.action_unblock_contact);
+ } else if (serverMsgId != null) {
+ builder.setTitle(R.string.report_spam_and_block);
+ } else {
+ final int resBlockAction =
+ blockable instanceof Conversation
+ && ((Conversation) blockable).isWithStranger()
+ ? R.string.block_stranger
+ : R.string.action_block_contact;
+ builder.setTitle(resBlockAction);
+ }
value = blockable.getJid().asBareJid().toEscapedString();
res = isBlocked ? R.string.unblock_contact_text : R.string.block_contact_text;
}
@@ -45,7 +69,7 @@ public final class BlockContactDialog {
xmppActivity.xmppConnectionService.sendUnblockRequest(blockable);
} else {
boolean toastShown = false;
- if (xmppActivity.xmppConnectionService.sendBlockRequest(blockable, binding.reportSpam.isChecked())) {
+ if (xmppActivity.xmppConnectionService.sendBlockRequest(blockable, binding.reportSpam.isChecked(), serverMsgId)) {
Toast.makeText(xmppActivity, R.string.corresponding_conversations_closed, Toast.LENGTH_SHORT).show();
toastShown = true;
}
diff --git a/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java b/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java
index 55cf9ba03..17a9853b2 100644
--- a/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java
@@ -87,7 +87,7 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem
dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid) -> {
Blockable blockable = new RawBlockable(account, contactJid);
- if (xmppConnectionService.sendBlockRequest(blockable, false)) {
+ if (xmppConnectionService.sendBlockRequest(blockable, false, null)) {
Toast.makeText(BlocklistActivity.this, R.string.corresponding_conversations_closed, Toast.LENGTH_SHORT).show();
}
return true;
diff --git a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java
index 9da5c7546..246e39855 100644
--- a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java
@@ -270,7 +270,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im
return;
}
for (final Account account : xmppConnectionService.getAccounts()) {
- if (account.getStatus() != Account.State.DISABLED) {
+ if (account.isEnabled()) {
for (final Contact contact : account.getRoster().getContacts()) {
if (contact.showInContactList() &&
!filterContacts.contains(contact.getJid().asBareJid().toString())
@@ -362,7 +362,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im
filterContacts();
this.mActivatedAccounts.clear();
for (Account account : xmppConnectionService.getAccounts()) {
- if (account.getStatus() != Account.State.DISABLED) {
+ if (account.isEnabled()) {
if (Config.DOMAIN_LOCK != null) {
this.mActivatedAccounts.add(account.getJid().getEscapedLocal());
} else {
@@ -382,6 +382,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
ScanActivity.onRequestPermissionResult(this, requestCode, grantResults);
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
index fb716044c..f5fb9c7f5 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java
@@ -137,8 +137,9 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
builder.setMultiChoiceItems(configuration.names, values, (dialog, which, isChecked) -> values[which] = isChecked);
builder.setNegativeButton(R.string.cancel, null);
builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
- Bundle options = configuration.toBundle(values);
+ final Bundle options = configuration.toBundle(values);
options.putString("muc#roomconfig_persistentroom", "1");
+ options.putString("{http://prosody.im/protocol/muc}roomconfig_allowmemberinvites", options.getString("muc#roomconfig_allowinvites"));
xmppConnectionService.pushConferenceConfiguration(mConversation,
options,
ConferenceDetailsActivity.this);
diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
index 3b234c19e..394331452 100644
--- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java
@@ -46,6 +46,7 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.ListItem;
import eu.siacs.conversations.services.AbstractQuickConversationsService;
+import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
import eu.siacs.conversations.ui.adapter.MediaAdapter;
@@ -119,13 +120,13 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
private void checkContactPermissionAndShowAddDialog() {
if (hasContactsPermission()) {
showAddToPhoneBookDialog();
- } else if (Config.CONTACTS_INTEGRATION && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ } else if (QuickConversationsService.isContactListIntegration(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
}
}
private boolean hasContactsPermission() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (QuickConversationsService.isContactListIntegration(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED;
} else {
return true;
@@ -455,6 +456,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
}
boolean skippedInactive = false;
boolean showsInactive = false;
+ boolean showUnverifiedWarning = false;
for (final XmppAxolotlSession session : sessions) {
final FingerprintStatus trust = session.getTrust();
hasKeys |= !trust.isCompromised();
@@ -470,7 +472,11 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
boolean highlight = session.getFingerprint().equals(messageFingerprint);
addFingerprintRow(binding.detailsContactKeys, session, highlight);
}
+ if (trust.isUnverified()) {
+ showUnverifiedWarning = true;
+ }
}
+ binding.unverifiedWarning.setVisibility(showUnverifiedWarning ? View.VISIBLE : View.GONE);
if (showsInactive || skippedInactive) {
binding.showInactiveDevices.setText(showsInactive ? R.string.hide_inactive_devices : R.string.show_inactive_devices);
binding.showInactiveDevices.setVisibility(View.VISIBLE);
@@ -480,7 +486,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
} else {
binding.showInactiveDevices.setVisibility(View.GONE);
}
- binding.scanButton.setVisibility(hasKeys && isCameraFeatureAvailable() ? View.VISIBLE : View.GONE);
+ final boolean isCameraFeatureAvailable = isCameraFeatureAvailable();
+ binding.scanButton.setVisibility(hasKeys && isCameraFeatureAvailable ? View.VISIBLE : View.GONE);
if (hasKeys) {
binding.scanButton.setOnClickListener((v) -> ScanActivity.scan(this));
}
@@ -517,18 +524,30 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
}
}
- private void onBadgeClick(View view) {
- final Uri systemAccount = contact.getSystemAccount();
- if (systemAccount == null) {
- checkContactPermissionAndShowAddDialog();
- } else {
- final Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setData(systemAccount);
- try {
- startActivity(intent);
- } catch (final ActivityNotFoundException e) {
- Toast.makeText(this, R.string.no_application_found_to_view_contact, Toast.LENGTH_SHORT).show();
+ private void onBadgeClick(final View view) {
+ if (QuickConversationsService.isContactListIntegration(this)) {
+ final Uri systemAccount = contact.getSystemAccount();
+ if (systemAccount == null) {
+ checkContactPermissionAndShowAddDialog();
+ } else {
+ final Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setData(systemAccount);
+ try {
+ startActivity(intent);
+ } catch (final ActivityNotFoundException e) {
+ Toast.makeText(
+ this,
+ R.string.no_application_found_to_view_contact,
+ Toast.LENGTH_SHORT)
+ .show();
+ }
}
+ } else {
+ Toast.makeText(
+ this,
+ R.string.contact_list_integration_not_available,
+ Toast.LENGTH_SHORT)
+ .show();
}
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
index 0f7e9073c..d19bc4a55 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
@@ -4,6 +4,8 @@ import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT;
import static eu.siacs.conversations.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSATION;
import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard;
import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
+import static eu.siacs.conversations.utils.PermissionUtils.audioGranted;
+import static eu.siacs.conversations.utils.PermissionUtils.cameraGranted;
import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
@@ -64,19 +66,6 @@ import androidx.databinding.DataBindingUtil;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
-import org.jetbrains.annotations.NotNull;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.atomic.AtomicBoolean;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
@@ -123,13 +112,11 @@ import eu.siacs.conversations.utils.Compatibility;
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;
import eu.siacs.conversations.utils.UIHelper;
-import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
@@ -140,6 +127,19 @@ import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
import eu.siacs.conversations.xmpp.jingle.RtpCapability;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+
public class ConversationFragment extends XmppFragment
implements EditMessage.KeyboardListener,
MessageAdapter.OnContactPictureLongClicked,
@@ -418,6 +418,7 @@ public class ConversationFragment extends XmppFragment
public void onClick(View v) {
final Account account = conversation == null ? null : conversation.getAccount();
if (account != null) {
+ account.setOption(Account.OPTION_SOFT_DISABLED, false);
account.setOption(Account.OPTION_DISABLED, false);
activity.xmppConnectionService.updateAccount(account);
}
@@ -480,7 +481,8 @@ public class ConversationFragment extends XmppFragment
null,
0,
0,
- 0);
+ 0,
+ Compatibility.pgpStartIntentSenderOptions());
} catch (SendIntentException e) {
Toast.makeText(
getActivity(),
@@ -1306,6 +1308,7 @@ public class ConversationFragment extends XmppFragment
|| t instanceof HttpDownloadConnection);
activity.getMenuInflater().inflate(R.menu.message_context, menu);
menu.setHeaderTitle(R.string.message_options);
+ final MenuItem reportAndBlock = menu.findItem(R.id.action_report_and_block);
MenuItem openWith = menu.findItem(R.id.open_with);
MenuItem copyMessage = menu.findItem(R.id.copy_message);
MenuItem copyLink = menu.findItem(R.id.copy_link);
@@ -1324,6 +1327,17 @@ public class ConversationFragment extends XmppFragment
m.getStatus() == Message.STATUS_SEND_FAILED
&& m.getErrorMessage() != null
&& !Message.ERROR_MESSAGE_CANCELLED.equals(m.getErrorMessage());
+ final Conversational conversational = m.getConversation();
+ if (m.getStatus() == Message.STATUS_RECEIVED && conversational instanceof Conversation c) {
+ final XmppConnection connection = c.getAccount().getXmppConnection();
+ if (c.isWithStranger()
+ && m.getServerMsgId() != null
+ && !c.isBlocked()
+ && connection != null
+ && connection.getFeatures().spamReporting()) {
+ reportAndBlock.setVisible(true);
+ }
+ }
if (!m.isFileOrImage()
&& !encrypted
&& !m.isGeoUri()
@@ -1447,6 +1461,9 @@ public class ConversationFragment extends XmppFragment
case R.id.open_with:
openWith(selectedMessage);
return true;
+ case R.id.action_report_and_block:
+ reportMessage(selectedMessage);
+ return true;
default:
return super.onContextItemSelected(item);
}
@@ -1610,8 +1627,12 @@ public class ConversationFragment extends XmppFragment
.show();
return;
}
+ final Account account = conversation.getAccount();
+ if (account.setOption(Account.OPTION_SOFT_DISABLED, false)) {
+ activity.xmppConnectionService.updateAccount(account);
+ }
final Contact contact = conversation.getContact();
- if (contact.getPresences().anySupport(Namespace.JINGLE_MESSAGE)) {
+ if (RtpCapability.jmiSupport(contact)) {
triggerRtpSession(contact.getAccount(), contact.getJid().asBareJid(), action);
} else {
final RtpCapability.Capability capability;
@@ -1866,6 +1887,9 @@ public class ConversationFragment extends XmppFragment
}
refresh();
}
+ if (cameraGranted(grantResults, permissions) || audioGranted(grantResults, permissions)) {
+ XmppConnectionService.toggleForegroundService(activity);
+ }
}
public void startDownloadable(Message message) {
@@ -1965,8 +1989,7 @@ public class ConversationFragment extends XmppFragment
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
final List missingPermissions = new ArrayList<>();
for (String permission : permissions) {
- if (Config.ONLY_INTERNAL_STORAGE
- && permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+ if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU || Config.ONLY_INTERNAL_STORAGE) && permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
continue;
}
if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
@@ -2106,6 +2129,10 @@ public class ConversationFragment extends XmppFragment
}
}
+ private void reportMessage(final Message message) {
+ BlockContactDialog.show(activity, conversation.getContact(), message.getServerMsgId());
+ }
+
private void showErrorMessage(final Message message) {
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
builder.setTitle(R.string.error_message);
@@ -2663,6 +2690,8 @@ public class ConversationFragment extends XmppFragment
R.string.this_account_is_disabled,
R.string.enable,
this.mEnableAccountListener);
+ } else if (account.getStatus() == Account.State.LOGGED_OUT) {
+ showSnackbar(R.string.this_account_is_logged_out,R.string.log_in,this.mEnableAccountListener);
} else if (conversation.isBlocked()) {
showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener);
} else if (contact != null
@@ -3391,7 +3420,7 @@ public class ConversationFragment extends XmppFragment
try {
getActivity()
.startIntentSenderForResult(
- pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0);
+ pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions());
} catch (final SendIntentException ignored) {
}
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java
index c31c3464b..94801c688 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java
@@ -32,6 +32,7 @@ package eu.siacs.conversations.ui;
import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP;
+import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Fragment;
@@ -42,6 +43,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
@@ -54,12 +56,14 @@ import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
+import androidx.core.app.ActivityCompat;
import androidx.databinding.DataBindingUtil;
import org.openintents.openpgp.util.OpenPgpApi;
import java.util.Arrays;
import java.util.List;
+import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import eu.siacs.conversations.Config;
@@ -207,7 +211,9 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
if (ExceptionHelper.checkForCrash(this)) {
return;
}
- openBatteryOptimizationDialogIfNeeded();
+ if (openBatteryOptimizationDialogIfNeeded()) {
+ return;
+ }
}
}
@@ -220,16 +226,16 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
getPreferences().edit().putBoolean(getBatteryOptimizationPreferenceKey(), false).apply();
}
- private void openBatteryOptimizationDialogIfNeeded() {
+ private boolean openBatteryOptimizationDialogIfNeeded() {
if (isOptimizingBattery()
&& android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M
&& getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true)) {
- AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.battery_optimizations_enabled);
builder.setMessage(getString(R.string.battery_optimizations_enabled_dialog, getString(R.string.app_name)));
builder.setPositiveButton(R.string.next, (dialog, which) -> {
- Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
- Uri uri = Uri.parse("package:" + getPackageName());
+ final Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
+ final Uri uri = Uri.parse("package:" + getPackageName());
intent.setData(uri);
try {
startActivityForResult(intent, REQUEST_BATTERY_OP);
@@ -241,6 +247,14 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
final AlertDialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(false);
dialog.show();
+ return true;
+ }
+ return false;
+ }
+
+ private void requestNotificationPermissionIfNeeded() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
+ requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, REQUEST_POST_NOTIFICATION);
}
}
@@ -299,12 +313,17 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
}
}
- private void handleActivityResult(ActivityResult activityResult) {
+ private void handleActivityResult(final ActivityResult activityResult) {
if (activityResult.resultCode == Activity.RESULT_OK) {
handlePositiveActivityResult(activityResult.requestCode, activityResult.data);
} else {
handleNegativeActivityResult(activityResult.requestCode);
}
+ if (activityResult.requestCode == REQUEST_BATTERY_OP) {
+ // the result code is always 0 even when battery permission were granted
+ requestNotificationPermissionIfNeeded();
+ XmppConnectionService.toggleForegroundService(xmppConnectionService);
+ }
}
private void handleNegativeActivityResult(int requestCode) {
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java
index eebd94df5..b673260ad 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java
@@ -57,12 +57,14 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
+import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.FragmentConversationsOverviewBinding;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
+import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.ui.adapter.ConversationAdapter;
import eu.siacs.conversations.ui.interfaces.OnConversationArchived;
import eu.siacs.conversations.ui.interfaces.OnConversationSelected;
@@ -304,13 +306,19 @@ public class ConversationsOverviewFragment extends XmppFragment {
return binding.getRoot();
}
- @Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
- menuInflater.inflate(R.menu.fragment_conversations_overview, menu);
- AccountUtils.showHideMenuItems(menu);
- final MenuItem easyOnboardInvite = menu.findItem(R.id.action_easy_invite);
- easyOnboardInvite.setVisible(EasyOnboardingInvite.anyHasSupport(activity == null ? null : activity.xmppConnectionService));
- }
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+ menuInflater.inflate(R.menu.fragment_conversations_overview, menu);
+ AccountUtils.showHideMenuItems(menu);
+ final MenuItem easyOnboardInvite = menu.findItem(R.id.action_easy_invite);
+ easyOnboardInvite.setVisible(
+ EasyOnboardingInvite.anyHasSupport(
+ activity == null ? null : activity.xmppConnectionService));
+ final MenuItem privacyPolicyMenuItem = menu.findItem(R.id.action_privacy_policy);
+ privacyPolicyMenuItem.setVisible(
+ BuildConfig.PRIVACY_POLICY != null
+ && QuickConversationsService.isPlayStoreFlavor());
+ }
@Override
public void onBackendConnected() {
diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
index c0c43dda7..741cbcce5 100644
--- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java
@@ -49,6 +49,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.databinding.ActivityEditAccountBinding;
import eu.siacs.conversations.databinding.DialogPresenceBinding;
@@ -66,6 +67,7 @@ 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.utils.Compatibility;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.Resolver;
import eu.siacs.conversations.utils.SignupUtils;
@@ -149,7 +151,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
if (mInitMode && mAccount != null) {
mAccount.setOption(Account.OPTION_DISABLED, false);
}
- if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !accountInfoEdited) {
+ if (mAccount != null && Arrays.asList(Account.State.DISABLED, Account.State.LOGGED_OUT).contains(mAccount.getStatus()) && !accountInfoEdited) {
+ mAccount.setOption(Account.OPTION_SOFT_DISABLED, false);
mAccount.setOption(Account.OPTION_DISABLED, false);
if (!xmppConnectionService.updateAccount(mAccount)) {
Toast.makeText(EditAccountActivity.this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
@@ -471,6 +474,10 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
if (requestCode == REQUEST_BATTERY_OP || requestCode == REQUEST_DATA_SAVER) {
updateAccountInformation(mAccount == null);
}
+ if (requestCode == REQUEST_BATTERY_OP) {
+ // the result code is always 0 even when battery permission were granted
+ XmppConnectionService.toggleForegroundService(xmppConnectionService);
+ }
if (requestCode == REQUEST_CHANGE_STATUS) {
PresenceTemplate template = mPendingPresenceTemplate.pop();
if (template != null && resultCode == Activity.RESULT_OK) {
@@ -624,6 +631,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
this.binding.accountRegisterNew.setVisibility(View.GONE);
}
this.binding.actionEditYourName.setOnClickListener(this::onEditYourNameClicked);
+ this.binding.scanButton.setOnClickListener((v) -> ScanActivity.scan(this));
}
private void onEditYourNameClicked(View view) {
@@ -648,6 +656,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
final MenuItem showBlocklist = menu.findItem(R.id.action_show_block_list);
final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more);
final MenuItem changePassword = menu.findItem(R.id.action_change_password_on_server);
+ final MenuItem deleteAccount = menu.findItem(R.id.action_delete_account);
final MenuItem renewCertificate = menu.findItem(R.id.action_renew_certificate);
final MenuItem mamPrefs = menu.findItem(R.id.action_mam_prefs);
final MenuItem changePresence = menu.findItem(R.id.action_change_presence);
@@ -663,6 +672,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
if (!mAccount.getXmppConnection().getFeatures().register()) {
changePassword.setVisible(false);
+ deleteAccount.setVisible(false);
}
mamPrefs.setVisible(mAccount.getXmppConnection().getFeatures().mam());
changePresence.setVisible(!mInitMode);
@@ -670,6 +680,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
showBlocklist.setVisible(false);
showMoreInfo.setVisible(false);
changePassword.setVisible(false);
+ deleteAccount.setVisible(false);
mamPrefs.setVisible(false);
changePresence.setVisible(false);
}
@@ -875,6 +886,9 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
case R.id.action_change_password_on_server:
gotoChangePassword(null);
break;
+ case R.id.action_delete_account:
+ deleteAccount();
+ break;
case R.id.action_mam_prefs:
editMamPrefs();
break;
@@ -888,6 +902,12 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
return super.onOptionsItemSelected(item);
}
+ private void deleteAccount() {
+ this.deleteAccount(mAccount,()->{
+ finish();
+ });
+ }
+
private boolean inNeedOfSaslAccept() {
return mAccount != null && mAccount.getLastErrorStatus() == Account.State.DOWNGRADE_ATTACK && mAccount.getPinnedMechanismPriority() >= 0 && !accountInfoEdited();
}
@@ -965,7 +985,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
public void userInputRequired(PendingIntent pi, String object) {
mPendingPresenceTemplate.push(template);
try {
- startIntentSenderForResult(pi.getIntentSender(), REQUEST_CHANGE_STATUS, null, 0, 0, 0);
+ startIntentSenderForResult(pi.getIntentSender(), REQUEST_CHANGE_STATUS, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions());
} catch (final IntentSender.SendIntentException ignored) {
}
}
@@ -1013,7 +1033,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
final boolean togglePassword = mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) || !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY);
- final boolean editPassword = !mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) || (!mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY) && QuickConversationsService.isConversations()) || mAccount.getLastErrorStatus() == Account.State.UNAUTHORIZED;
+ final boolean neverLoggedIn = !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY) && QuickConversationsService.isConversations();
+ final boolean editPassword = mAccount.unauthorized() || neverLoggedIn;
this.binding.accountPasswordLayout.setPasswordVisibilityToggleEnabled(togglePassword);
@@ -1141,19 +1162,24 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
this.binding.ownFingerprintDesc.setText(R.string.omemo_fingerprint);
}
this.binding.axolotlFingerprint.setText(CryptoHelper.prettifyFingerprint(ownAxolotlFingerprint.substring(2)));
- this.binding.actionCopyAxolotlToClipboard.setVisibility(View.VISIBLE);
- this.binding.actionCopyAxolotlToClipboard.setOnClickListener(v -> copyOmemoFingerprint(ownAxolotlFingerprint));
+ this.binding.showQrCodeButton.setVisibility(View.VISIBLE);
+ this.binding.showQrCodeButton.setOnClickListener(v -> showQrCode());
} else {
this.binding.axolotlFingerprintBox.setVisibility(View.GONE);
}
boolean hasKeys = false;
+ boolean showUnverifiedWarning = false;
binding.otherDeviceKeys.removeAllViews();
- for (XmppAxolotlSession session : mAccount.getAxolotlService().findOwnSessions()) {
- if (!session.getTrust().isCompromised()) {
+ for (final XmppAxolotlSession session : mAccount.getAxolotlService().findOwnSessions()) {
+ final FingerprintStatus trust = session.getTrust();
+ if (!trust.isCompromised()) {
boolean highlight = session.getFingerprint().equals(messageFingerprint);
addFingerprintRow(binding.otherDeviceKeys, session, highlight);
hasKeys = true;
}
+ if (trust.isUnverified()) {
+ showUnverifiedWarning = true;
+ }
}
if (hasKeys && Config.supportOmemo()) { //TODO: either the button should be visible if we print an active device or the device list should be fed with reactived devices
this.binding.otherDeviceKeysCard.setVisibility(View.VISIBLE);
@@ -1163,6 +1189,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
} else {
binding.clearDevices.setVisibility(View.VISIBLE);
}
+ binding.unverifiedWarning.setVisibility(showUnverifiedWarning ? View.VISIBLE : View.GONE);
+ binding.scanButton.setVisibility(showUnverifiedWarning ? View.VISIBLE : View.GONE);
} else {
this.binding.otherDeviceKeysCard.setVisibility(View.GONE);
}
diff --git a/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java b/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java
index 2f05be90f..e759ee18e 100644
--- a/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java
@@ -13,8 +13,13 @@ import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.Toast;
+import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.Locale;
@@ -56,30 +61,38 @@ public class MucUsersActivity extends XmppActivity implements XmppConnectionServ
private void loadAndSubmitUsers() {
if (mConversation != null) {
allUsers = mConversation.getMucOptions().getUsers();
- Collections.sort(allUsers);
submitFilteredList(mSearchEditText != null ? mSearchEditText.getText().toString() : null);
}
}
- private void submitFilteredList(String search) {
+ private void submitFilteredList(final String search) {
if (TextUtils.isEmpty(search)) {
- userAdapter.submitList(allUsers);
+ userAdapter.submitList(Ordering.natural().immutableSortedCopy(allUsers));
} else {
final String needle = search.toLowerCase(Locale.getDefault());
- ArrayList filtered = new ArrayList<>();
- for(MucOptions.User user : allUsers) {
- final String name = user.getName();
- final Contact contact = user.getContact();
- if (name != null && name.toLowerCase(Locale.getDefault()).contains(needle) || contact != null && contact.getDisplayName().toLowerCase(Locale.getDefault()).contains(needle)) {
- filtered.add(user);
- }
- }
- userAdapter.submitList(filtered);
+ userAdapter.submitList(
+ Ordering.natural()
+ .immutableSortedCopy(
+ Collections2.filter(
+ this.allUsers,
+ user -> {
+ final String name = user.getName();
+ final Contact contact = user.getContact();
+ return name != null
+ && name.toLowerCase(
+ Locale.getDefault())
+ .contains(needle)
+ || contact != null
+ && contact.getDisplayName()
+ .toLowerCase(
+ Locale.getDefault())
+ .contains(needle);
+ })));
}
}
@Override
- public boolean onContextItemSelected(MenuItem item) {
+ public boolean onContextItemSelected(@NonNull MenuItem item) {
if (!MucDetailsContextMenuHelper.onContextItemSelected(item, userAdapter.getSelectedUser(), this)) {
return super.onContextItemSelected(item);
}
diff --git a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java
index 89fdae333..44af0d0b2 100644
--- a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java
@@ -205,6 +205,7 @@ public abstract class OmemoActivity extends XmppActivity {
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
ScanActivity.onRequestPermissionResult(this, requestCode, grantResults);
}
}
diff --git a/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java
index 81b0ae15c..658567aa6 100644
--- a/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java
@@ -40,8 +40,6 @@ import android.widget.Toast;
import androidx.annotation.StringRes;
import androidx.databinding.DataBindingUtil;
-import com.theartofdev.edmodo.cropper.CropImage;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityPublishProfilePictureBinding;
@@ -51,6 +49,8 @@ import eu.siacs.conversations.ui.util.PendingItem;
import static eu.siacs.conversations.ui.PublishProfilePictureActivity.REQUEST_CHOOSE_PICTURE;
+import com.canhub.cropper.CropImage;
+
public class PublishGroupChatProfilePictureActivity extends XmppActivity implements OnAvatarPublication {
private final PendingItem pendingConversationUuid = new PendingItem<>();
private ActivityPublishProfilePictureBinding binding;
diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java
index 16607b81e..b6822b301 100644
--- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java
@@ -19,7 +19,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
-import com.theartofdev.edmodo.cropper.CropImage;
+import com.canhub.cropper.CropImage;
import java.util.concurrent.atomic.AtomicBoolean;
diff --git a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java
index ad8684b72..d3b3a96b2 100644
--- a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java
@@ -17,6 +17,8 @@ import android.widget.Toast;
import androidx.databinding.DataBindingUtil;
+import com.google.common.collect.ImmutableSet;
+
import java.io.File;
import java.lang.ref.WeakReference;
import java.text.SimpleDateFormat;
@@ -25,6 +27,7 @@ import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
@@ -95,14 +98,40 @@ public class RecordingActivity extends Activity implements View.OnClickListener
}
}
+ private static final Set AAC_SENSITIVE_DEVICES =
+ new ImmutableSet.Builder()
+ .add("FP4") // Fairphone 4 https://codeberg.org/monocles/monocles_chat/issues/133
+ .add("ONEPLUS A6000") // OnePlus 6 https://github.com/iNPUTmice/Conversations/issues/4329
+ .add("ONEPLUS A6003") // OnePlus 6 https://github.com/iNPUTmice/Conversations/issues/4329
+ .add("ONEPLUS A6010") // OnePlus 6T https://codeberg.org/monocles/monocles_chat/issues/133
+ .add("ONEPLUS A6013") // OnePlus 6T https://codeberg.org/monocles/monocles_chat/issues/133
+ .add("Pixel 4a") // Pixel 4a https://github.com/iNPUTmice/Conversations/issues/4223
+ .build();
+
private boolean startRecording() {
mRecorder = new MediaRecorder();
mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
- mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
- mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
- mRecorder.setAudioEncodingBitRate(96000);
- mRecorder.setAudioSamplingRate(22050);
- setupOutputFile();
+ final int outputFormat;
+ if (Config.USE_OPUS_VOICE_MESSAGES && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ outputFormat = MediaRecorder.OutputFormat.OGG;
+ mRecorder.setOutputFormat(outputFormat);
+ mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS);
+ mRecorder.setAudioEncodingBitRate(32_000);
+ } else {
+ outputFormat = MediaRecorder.OutputFormat.MPEG_4;
+ mRecorder.setOutputFormat(outputFormat);
+ if (AAC_SENSITIVE_DEVICES.contains(Build.MODEL)) {
+ // Changing these three settings for AAC sensitive devices might lead to sporadically truncated (cut-off) voice messages.
+ mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC);
+ mRecorder.setAudioSamplingRate(24_000);
+ mRecorder.setAudioEncodingBitRate(28_000);
+ } else {
+ mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
+ mRecorder.setAudioSamplingRate(22_050);
+ mRecorder.setAudioEncodingBitRate(64_000);
+ }
+ }
+ setupOutputFile(outputFormat);
mRecorder.setOutputFile(mOutputFile.getAbsolutePath());
try {
@@ -110,10 +139,10 @@ public class RecordingActivity extends Activity implements View.OnClickListener
mRecorder.start();
mStartTime = SystemClock.elapsedRealtime();
mHandler.postDelayed(mTickExecutor, 100);
- Log.d("Voice Recorder", "started recording to " + mOutputFile.getAbsolutePath());
+ Log.d(Config.LOGTAG, "started recording to " + mOutputFile.getAbsolutePath());
return true;
} catch (Exception e) {
- Log.e("Voice Recorder", "prepare() failed " + e.getMessage());
+ Log.e(Config.LOGTAG, "prepare() failed ", e);
return false;
}
}
@@ -175,9 +204,18 @@ public class RecordingActivity extends Activity implements View.OnClickListener
}
}
- private File generateOutputFilename() {
+ private File generateOutputFilename(final int outputFormat) {
final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US);
- final String filename = "RECORDING_" + dateFormat.format(new Date()) + ".m4a";
+ final String extension;
+ if (outputFormat == MediaRecorder.OutputFormat.MPEG_4) {
+ extension = "m4a";
+ } else if (outputFormat == MediaRecorder.OutputFormat.OGG) {
+ extension = "oga";
+ } else {
+ throw new IllegalStateException("Unrecognized output format");
+ }
+ final String filename =
+ String.format("RECORDING_%s.%s", dateFormat.format(new Date()), extension);
final File parentDirectory;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
parentDirectory =
@@ -190,8 +228,8 @@ public class RecordingActivity extends Activity implements View.OnClickListener
return new File(conversationsDirectory, filename);
}
- private void setupOutputFile() {
- mOutputFile = generateOutputFilename();
+ private void setupOutputFile(final int outputFormat) {
+ mOutputFile = generateOutputFilename(outputFormat);
final File parentDirectory = mOutputFile.getParentFile();
if (Objects.requireNonNull(parentDirectory).mkdirs()) {
Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath());
diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java
index 75079c585..fee4d9a1c 100644
--- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java
@@ -1,8 +1,9 @@
package eu.siacs.conversations.ui;
-import static java.util.Arrays.asList;
import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
+import static java.util.Arrays.asList;
+
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
@@ -10,7 +11,9 @@ import android.app.PictureInPictureParams;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
+import android.opengl.GLException;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@@ -39,7 +42,6 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
-import org.jetbrains.annotations.NotNull;
import org.webrtc.RendererCommon;
import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoTrack;
@@ -63,15 +65,27 @@ import eu.siacs.conversations.ui.util.MainThreadExecutor;
import eu.siacs.conversations.ui.util.Rationals;
import eu.siacs.conversations.utils.PermissionUtils;
import eu.siacs.conversations.utils.TimeFrameUtils;
-import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
import eu.siacs.conversations.xmpp.jingle.ContentAddition;
import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
+import eu.siacs.conversations.xmpp.jingle.RtpCapability;
import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
+import org.jetbrains.annotations.NotNull;
+
+import org.webrtc.RendererCommon;
+import org.webrtc.SurfaceViewRenderer;
+import org.webrtc.VideoTrack;
+
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
public class RtpSessionActivity extends XmppActivity
implements XmppConnectionService.OnJingleRtpConnectionUpdate,
eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged {
@@ -106,9 +120,7 @@ public class RtpSessionActivity extends XmppActivity
RtpEndUserState.RECONNECTING,
RtpEndUserState.INCOMING_CONTENT_ADD);
private static final List STATES_CONSIDERED_CONNECTED =
- Arrays.asList(
- RtpEndUserState.CONNECTED,
- RtpEndUserState.RECONNECTING);
+ Arrays.asList(RtpEndUserState.CONNECTED, RtpEndUserState.RECONNECTING);
private static final List STATES_SHOWING_PIP_PLACEHOLDER =
Arrays.asList(
RtpEndUserState.ACCEPTING_CALL,
@@ -277,21 +289,22 @@ public class RtpSessionActivity extends XmppActivity
}
public boolean onOptionsItemSelected(final MenuItem item) {
- switch (item.getItemId()) {
- case R.id.action_help:
- launchHelpInBrowser();
- return true;
- case R.id.action_goto_chat:
- switchToConversation();
- return true;
- case R.id.action_switch_to_video:
- requestPermissionAndSwitchToVideo();
- return true;
- case R.id.action_dialpad:
- toggleDialpadVisibility();
- break;
+ final var itemItem = item.getItemId();
+ if (itemItem == R.id.action_help) {
+ launchHelpInBrowser();
+ return true;
+ } else if (itemItem == R.id.action_goto_chat) {
+ switchToConversation();
+ return true;
+ } else if (itemItem == R.id.action_switch_to_video) {
+ requestPermissionAndSwitchToVideo();
+ return true;
+ } else if (itemItem == R.id.action_dialpad) {
+ toggleDialpadVisibility();
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
}
- return super.onOptionsItemSelected(item);
}
private void launchHelpInBrowser() {
@@ -368,8 +381,9 @@ public class RtpSessionActivity extends XmppActivity
}
private void acceptContentAdd(final ContentAddition contentAddition) {
- if (contentAddition == null || contentAddition.direction != ContentAddition.Direction.INCOMING) {
- Log.d(Config.LOGTAG,"ignore press on content-accept button");
+ if (contentAddition == null
+ || contentAddition.direction != ContentAddition.Direction.INCOMING) {
+ Log.d(Config.LOGTAG, "ignore press on content-accept button");
return;
}
requestPermissionAndAcceptContentAdd(contentAddition);
@@ -378,7 +392,11 @@ public class RtpSessionActivity extends XmppActivity
private void requestPermissionAndAcceptContentAdd(final ContentAddition contentAddition) {
final List permissions = permissions(contentAddition.media());
if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CONTENT)) {
- requireRtpConnection().acceptContentAdd(contentAddition.summary);
+ try {
+ requireRtpConnection().acceptContentAdd(contentAddition.summary);
+ } catch (final IllegalStateException e) {
+ Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
+ }
}
}
@@ -457,7 +475,7 @@ public class RtpSessionActivity extends XmppActivity
private void putScreenInCallMode(final Set media) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- if (!media.contains(Media.VIDEO)) {
+ if (Media.audioOnly(media)) {
final JingleRtpConnection rtpConnection =
rtpConnectionReference != null ? rtpConnectionReference.get() : null;
final AppRTCAudioManager audioManager =
@@ -468,6 +486,15 @@ public class RtpSessionActivity extends XmppActivity
acquireProximityWakeLock();
}
}
+ lockOrientation(media);
+ }
+
+ private void lockOrientation(final Set media) {
+ if (Media.audioOnly(media)) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ } else {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+ }
}
@SuppressLint("WakelockTimeout")
@@ -721,8 +748,10 @@ public class RtpSessionActivity extends XmppActivity
private boolean isConnected() {
final JingleRtpConnection connection =
this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
- final RtpEndUserState endUserState = connection == null ? null : connection.getEndUserState();
- return STATES_CONSIDERED_CONNECTED.contains(endUserState) || endUserState == RtpEndUserState.INCOMING_CONTENT_ADD;
+ final RtpEndUserState endUserState =
+ connection == null ? null : connection.getEndUserState();
+ return STATES_CONSIDERED_CONNECTED.contains(endUserState)
+ || endUserState == RtpEndUserState.INCOMING_CONTENT_ADD;
}
private boolean switchToPictureInPicture() {
@@ -870,8 +899,12 @@ public class RtpSessionActivity extends XmppActivity
surfaceViewRenderer.setVisibility(View.VISIBLE);
try {
surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null);
- } catch (final IllegalStateException e) {
- // Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized");
+ } catch (final IllegalStateException ignored) {
+ // SurfaceViewRenderer was already initialized
+ } catch (final RuntimeException e) {
+ if (Throwables.getRootCause(e) instanceof GLException glException) {
+ Log.w(Config.LOGTAG, "could not set up hardware renderer", glException);
+ }
}
surfaceViewRenderer.setEnableHardwareScaler(true);
}
@@ -880,68 +913,43 @@ public class RtpSessionActivity extends XmppActivity
updateStateDisplay(state, Collections.emptySet(), null);
}
- private void updateStateDisplay(final RtpEndUserState state, final Set media, final ContentAddition contentAddition) {
+ private void updateStateDisplay(
+ final RtpEndUserState state,
+ final Set media,
+ final ContentAddition contentAddition) {
switch (state) {
- case INCOMING_CALL:
+ case INCOMING_CALL -> {
Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
if (media.contains(Media.VIDEO)) {
setTitle(R.string.rtp_state_incoming_video_call);
} else {
setTitle(R.string.rtp_state_incoming_call);
}
- break;
- case INCOMING_CONTENT_ADD:
+ }
+ case INCOMING_CONTENT_ADD -> {
if (contentAddition != null && contentAddition.media().contains(Media.VIDEO)) {
setTitle(R.string.rtp_state_content_add_video);
} else {
setTitle(R.string.rtp_state_content_add);
}
- break;
- case CONNECTING:
- setTitle(R.string.rtp_state_connecting);
- break;
- case CONNECTED:
- setTitle(R.string.rtp_state_connected);
- break;
- case RECONNECTING:
- setTitle(R.string.rtp_state_reconnecting);
- break;
- case ACCEPTING_CALL:
- setTitle(R.string.rtp_state_accepting_call);
- break;
- case ENDING_CALL:
- setTitle(R.string.rtp_state_ending_call);
- break;
- case FINDING_DEVICE:
- setTitle(R.string.rtp_state_finding_device);
- break;
- case RINGING:
- setTitle(R.string.rtp_state_ringing);
- break;
- case DECLINED_OR_BUSY:
- setTitle(R.string.rtp_state_declined_or_busy);
- break;
- case CONNECTIVITY_ERROR:
- setTitle(R.string.rtp_state_connectivity_error);
- break;
- case CONNECTIVITY_LOST_ERROR:
- setTitle(R.string.rtp_state_connectivity_lost_error);
- break;
- case RETRACTED:
- setTitle(R.string.rtp_state_retracted);
- break;
- case APPLICATION_ERROR:
- setTitle(R.string.rtp_state_application_failure);
- break;
- case SECURITY_ERROR:
- setTitle(R.string.rtp_state_security_error);
- break;
- case ENDED:
- throw new IllegalStateException(
- "Activity should have called finishAndReleaseWakeLock();");
- default:
- throw new IllegalStateException(
- String.format("State %s has not been handled in UI", state));
+ }
+ case CONNECTING -> setTitle(R.string.rtp_state_connecting);
+ case CONNECTED -> setTitle(R.string.rtp_state_connected);
+ case RECONNECTING -> setTitle(R.string.rtp_state_reconnecting);
+ case ACCEPTING_CALL -> setTitle(R.string.rtp_state_accepting_call);
+ case ENDING_CALL -> setTitle(R.string.rtp_state_ending_call);
+ case FINDING_DEVICE -> setTitle(R.string.rtp_state_finding_device);
+ case RINGING -> setTitle(R.string.rtp_state_ringing);
+ case DECLINED_OR_BUSY -> setTitle(R.string.rtp_state_declined_or_busy);
+ case CONNECTIVITY_ERROR -> setTitle(R.string.rtp_state_connectivity_error);
+ case CONNECTIVITY_LOST_ERROR -> setTitle(R.string.rtp_state_connectivity_lost_error);
+ case RETRACTED -> setTitle(R.string.rtp_state_retracted);
+ case APPLICATION_ERROR -> setTitle(R.string.rtp_state_application_failure);
+ case SECURITY_ERROR -> setTitle(R.string.rtp_state_security_error);
+ case ENDED -> throw new IllegalStateException(
+ "Activity should have called finishAndReleaseWakeLock();");
+ default -> throw new IllegalStateException(
+ String.format("State %s has not been handled in UI", state));
}
}
@@ -997,7 +1005,10 @@ public class RtpSessionActivity extends XmppActivity
}
@SuppressLint("RestrictedApi")
- private void updateButtonConfiguration(final RtpEndUserState state, final Set media, final ContentAddition contentAddition) {
+ private void updateButtonConfiguration(
+ final RtpEndUserState state,
+ final Set media,
+ final ContentAddition contentAddition) {
if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) {
this.binding.rejectCall.setVisibility(View.INVISIBLE);
this.binding.endCall.setVisibility(View.INVISIBLE);
@@ -1013,7 +1024,8 @@ public class RtpSessionActivity extends XmppActivity
this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp);
this.binding.acceptCall.setVisibility(View.VISIBLE);
} else if (state == RtpEndUserState.INCOMING_CONTENT_ADD) {
- this.binding.rejectCall.setContentDescription(getString(R.string.reject_switch_to_video));
+ this.binding.rejectCall.setContentDescription(
+ getString(R.string.reject_switch_to_video));
this.binding.rejectCall.setOnClickListener(this::rejectContentAdd);
this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
this.binding.rejectCall.setVisibility(View.VISIBLE);
@@ -1105,7 +1117,7 @@ public class RtpSessionActivity extends XmppActivity
private void updateInCallButtonConfigurationSpeaker(
final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices) {
switch (selectedAudioDevice) {
- case EARPIECE:
+ case EARPIECE -> {
this.binding.inCallActionRight.setImageResource(
R.drawable.ic_volume_off_black_24dp);
if (numberOfChoices >= 2) {
@@ -1114,13 +1126,13 @@ public class RtpSessionActivity extends XmppActivity
this.binding.inCallActionRight.setOnClickListener(null);
this.binding.inCallActionRight.setClickable(false);
}
- break;
- case WIRED_HEADSET:
+ }
+ case WIRED_HEADSET -> {
this.binding.inCallActionRight.setImageResource(R.drawable.ic_headset_black_24dp);
this.binding.inCallActionRight.setOnClickListener(null);
this.binding.inCallActionRight.setClickable(false);
- break;
- case SPEAKER_PHONE:
+ }
+ case SPEAKER_PHONE -> {
this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_up_black_24dp);
if (numberOfChoices >= 2) {
this.binding.inCallActionRight.setOnClickListener(this::switchToEarpiece);
@@ -1128,13 +1140,13 @@ public class RtpSessionActivity extends XmppActivity
this.binding.inCallActionRight.setOnClickListener(null);
this.binding.inCallActionRight.setClickable(false);
}
- break;
- case BLUETOOTH:
+ }
+ case BLUETOOTH -> {
this.binding.inCallActionRight.setImageResource(
R.drawable.ic_bluetooth_audio_black_24dp);
this.binding.inCallActionRight.setOnClickListener(null);
this.binding.inCallActionRight.setClickable(false);
- break;
+ }
}
this.binding.inCallActionRight.setVisibility(View.VISIBLE);
}
@@ -1163,10 +1175,10 @@ public class RtpSessionActivity extends XmppActivity
private void switchCamera(final View view) {
Futures.addCallback(
requireRtpConnection().switchCamera(),
- new FutureCallback() {
+ new FutureCallback<>() {
@Override
public void onSuccess(@Nullable Boolean isFrontCamera) {
- binding.localVideo.setMirror(isFrontCamera);
+ binding.localVideo.setMirror(Boolean.TRUE.equals(isFrontCamera));
}
@Override
@@ -1430,6 +1442,7 @@ public class RtpSessionActivity extends XmppActivity
final AbstractJingleConnection.Id id = requireRtpConnection().getId();
final boolean verified = requireRtpConnection().isVerified();
final Set media = getMedia();
+ lockOrientation(media);
final ContentAddition contentAddition = getPendingContentAddition();
final Contact contact = getWith();
if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
@@ -1531,10 +1544,7 @@ public class RtpSessionActivity extends XmppActivity
final Account account, Jid with, final RtpEndUserState state, final Set media) {
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
- if (account.getRoster()
- .getContact(with)
- .getPresences()
- .anySupport(Namespace.JINGLE_MESSAGE)) {
+ if (RtpCapability.jmiSupport(account.getRoster().getContact(with))) {
intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString());
} else {
intent.putExtra(EXTRA_WITH, with.toEscapedString());
diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
index 2b2f8110a..b9eaf32af 100644
--- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
@@ -28,6 +28,8 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.security.KeyStoreException;
import java.util.ArrayList;
import java.util.Arrays;
@@ -47,6 +49,7 @@ import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.ui.util.StyledAttributes;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.TimeFrameUtils;
+import eu.siacs.conversations.xmpp.InvalidJid;
import eu.siacs.conversations.xmpp.Jid;
public class SettingsActivity extends XmppActivity implements OnSharedPreferenceChangeListener {
@@ -131,6 +134,7 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
changeOmemoSettingSummary();
if (QuickConversationsService.isQuicksy()
+ || QuickConversationsService.isPlayStoreFlavor()
|| Strings.isNullOrEmpty(Config.CHANNEL_DISCOVERY)) {
final PreferenceCategory groupChats =
(PreferenceCategory) mSettingsFragment.findPreference("group_chats");
@@ -505,12 +509,41 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
} else if (name.equals(PREVENT_SCREENSHOTS)) {
SettingsUtils.applyScreenshotPreventionSetting(this);
} else if (UnifiedPushDistributor.PREFERENCES.contains(name)) {
+ final String pushServerPreference =
+ Strings.nullToEmpty(preferences.getString(
+ UnifiedPushDistributor.PREFERENCE_PUSH_SERVER,
+ getString(R.string.default_push_server))).trim();
+ if (isJidInvalid(pushServerPreference) || isHttpUri(pushServerPreference)) {
+ Toast.makeText(this,R.string.invalid_jid,Toast.LENGTH_LONG).show();
+ }
if (xmppConnectionService.reconfigurePushDistributor()) {
xmppConnectionService.renewUnifiedPushEndpoints();
}
}
}
+ private static boolean isJidInvalid(final String input) {
+ if (Strings.isNullOrEmpty(input)) {
+ return true;
+ }
+ try {
+ Jid.ofEscaped(input);
+ return false;
+ } catch (final IllegalArgumentException e) {
+ return true;
+ }
+ }
+
+ private static boolean isHttpUri(final String input) {
+ final URI uri;
+ try {
+ uri = new URI(input);
+ } catch (final URISyntaxException e) {
+ return false;
+ }
+ return Arrays.asList("http","https").contains(uri.getScheme());
+ }
+
@Override
public void onResume() {
super.onResume();
diff --git a/src/main/java/eu/siacs/conversations/ui/ShortcutActivity.java b/src/main/java/eu/siacs/conversations/ui/ShortcutActivity.java
index 33164c95c..ff935beae 100644
--- a/src/main/java/eu/siacs/conversations/ui/ShortcutActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ShortcutActivity.java
@@ -61,7 +61,7 @@ public class ShortcutActivity extends AbstractSearchableListItemActivity {
return;
}
for (final Account account : xmppConnectionService.getAccounts()) {
- if (account.getStatus() != Account.State.DISABLED) {
+ if (account.isEnabled()) {
for (final Contact contact : account.getRoster().getContacts()) {
if (contact.showInContactList()
&& contact.match(this, needle)) {
diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
index 7a2ffc0d8..8b94dd440 100644
--- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java
@@ -6,12 +6,14 @@ import android.app.Dialog;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.Context;
+import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
+import android.preference.PreferenceManager;
import android.text.Editable;
import android.text.Html;
import android.text.TextWatcher;
@@ -62,6 +64,7 @@ import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
+import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityStartConversationBinding;
@@ -90,6 +93,8 @@ import eu.siacs.conversations.xmpp.XmppConnection;
public class StartConversationActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, CreatePrivateGroupChatDialog.CreateConferenceDialogListener, JoinConferenceDialog.JoinConferenceDialogListener, SwipeRefreshLayout.OnRefreshListener, CreatePublicChannelDialog.CreatePublicChannelDialogListener {
+ private static final String PREF_KEY_CONTACT_INTEGRATION_CONSENT = "contact_list_integration_consent";
+
public static final String EXTRA_INVITE_URI = "eu.siacs.conversations.invite_uri";
private final int REQUEST_SYNC_CONTACTS = 0x28cf;
@@ -323,7 +328,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
}
switch (actionItem.getId()) {
case R.id.discover_public_channels:
- startActivity(new Intent(this, ChannelDiscoveryActivity.class));
+ if (QuickConversationsService.isPlayStoreFlavor()) {
+ throw new IllegalStateException("Channel discovery is not available on Google Play flavor");
+ } else {
+ startActivity(new Intent(this, ChannelDiscoveryActivity.class));
+ }
break;
case R.id.join_public_channel:
showJoinConferenceDialog(prefilled);
@@ -349,6 +358,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
final Menu menu = popupMenu.getMenu();
for (int i = 0; i < menu.size(); i++) {
final MenuItem menuItem = menu.getItem(i);
+ if (QuickConversationsService.isPlayStoreFlavor() && menuItem.getItemId() == R.id.discover_public_channels) {
+ continue;
+ }
final SpeedDialActionItem actionItem = new SpeedDialActionItem.Builder(menuItem.getItemId(), menuItem.getIcon())
.setLabel(menuItem.getTitle() != null ? menuItem.getTitle().toString() : null)
.setFabImageTintColor(ContextCompat.getColor(this, R.color.white))
@@ -645,17 +657,21 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
mSearchEditText.setHint(R.string.search_contacts);
mSearchEditText.setContentDescription(getString(R.string.search_contacts));
} else {
- mSearchEditText.setHint(R.string.search_bookmarks);
- mSearchEditText.setContentDescription(getString(R.string.search_bookmarks));
+ mSearchEditText.setHint(R.string.search_group_chats);
+ mSearchEditText.setContentDescription(getString(R.string.search_group_chats));
}
}
@Override
- public boolean onCreateOptionsMenu(Menu menu) {
+ public boolean onCreateOptionsMenu(final Menu menu) {
getMenuInflater().inflate(R.menu.start_conversation, menu);
AccountUtils.showHideMenuItems(menu);
- MenuItem menuHideOffline = menu.findItem(R.id.action_hide_offline);
- MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code);
+ final MenuItem menuHideOffline = menu.findItem(R.id.action_hide_offline);
+ final MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code);
+ final MenuItem privacyPolicyMenuItem = menu.findItem(R.id.action_privacy_policy);
+ privacyPolicyMenuItem.setVisible(
+ BuildConfig.PRIVACY_POLICY != null
+ && QuickConversationsService.isPlayStoreFlavor());
qrCodeScanMenuItem.setVisible(isCameraFeatureAvailable());
if (QuickConversationsService.isQuicksy()) {
menuHideOffline.setVisible(false);
@@ -749,47 +765,96 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
}
private void askForContactsPermissions() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
+ if (QuickConversationsService.isContactListIntegration(this)
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (checkSelfPermission(Manifest.permission.READ_CONTACTS)
+ != PackageManager.PERMISSION_GRANTED) {
if (mRequestedContactsPermission.compareAndSet(false, true)) {
- if (QuickConversationsService.isQuicksy() || shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
+ final String consent =
+ PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
+ .getString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, null);
+ final boolean requiresConsent =
+ (QuickConversationsService.isQuicksy()
+ || QuickConversationsService.isPlayStoreFlavor())
+ && !"agreed".equals(consent);
+ if (requiresConsent && "declined".equals(consent)) {
+ Log.d(Config.LOGTAG,"not asking for contacts permission because consent has been declined");
+ return;
+ }
+ if (requiresConsent
+ || shouldShowRequestPermissionRationale(
+ Manifest.permission.READ_CONTACTS)) {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
final AtomicBoolean requestPermission = new AtomicBoolean(false);
- builder.setTitle(R.string.sync_with_contacts);
if (QuickConversationsService.isQuicksy()) {
- builder.setMessage(Html.fromHtml(getString(R.string.sync_with_contacts_quicksy)));
+ builder.setTitle(R.string.quicksy_wants_your_consent);
+ builder.setMessage(
+ Html.fromHtml(
+ getString(R.string.sync_with_contacts_quicksy_static)));
} else {
- builder.setMessage(getString(R.string.sync_with_contacts_long, getString(R.string.app_name)));
+ builder.setTitle(R.string.sync_with_contacts);
+ builder.setMessage(
+ getString(
+ R.string.sync_with_contacts_long,
+ getString(R.string.app_name)));
}
@StringRes int confirmButtonText;
- if (QuickConversationsService.isConversations()) {
- confirmButtonText = R.string.next;
+ if (requiresConsent) {
+ confirmButtonText = R.string.agree_and_continue;
} else {
- confirmButtonText = R.string.confirm;
+ confirmButtonText = R.string.next;
}
- builder.setPositiveButton(confirmButtonText, (dialog, which) -> {
- if (requestPermission.compareAndSet(false, true)) {
- requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
- }
- });
- builder.setOnDismissListener(dialog -> {
- if (QuickConversationsService.isConversations() && requestPermission.compareAndSet(false, true)) {
- requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
-
- }
- });
- builder.setCancelable(QuickConversationsService.isQuicksy());
+ builder.setPositiveButton(
+ confirmButtonText,
+ (dialog, which) -> {
+ if (requiresConsent) {
+ PreferenceManager.getDefaultSharedPreferences(
+ getApplicationContext())
+ .edit()
+ .putString(
+ PREF_KEY_CONTACT_INTEGRATION_CONSENT, "agreed")
+ .apply();
+ }
+ if (requestPermission.compareAndSet(false, true)) {
+ requestPermissions(
+ new String[] {Manifest.permission.READ_CONTACTS},
+ REQUEST_SYNC_CONTACTS);
+ }
+ });
+ if (requiresConsent) {
+ builder.setNegativeButton(R.string.decline, (dialog, which) -> PreferenceManager.getDefaultSharedPreferences(
+ getApplicationContext())
+ .edit()
+ .putString(
+ PREF_KEY_CONTACT_INTEGRATION_CONSENT, "declined")
+ .apply());
+ } else {
+ builder.setOnDismissListener(
+ dialog -> {
+ if (requestPermission.compareAndSet(false, true)) {
+ requestPermissions(
+ new String[] {
+ Manifest.permission.READ_CONTACTS
+ },
+ REQUEST_SYNC_CONTACTS);
+ }
+ });
+ }
+ builder.setCancelable(requiresConsent);
final AlertDialog dialog = builder.create();
- dialog.setCanceledOnTouchOutside(QuickConversationsService.isQuicksy());
- dialog.setOnShowListener(dialogInterface -> {
- final TextView tv = dialog.findViewById(android.R.id.message);
- if (tv != null) {
- tv.setMovementMethod(LinkMovementMethod.getInstance());
- }
- });
+ dialog.setCanceledOnTouchOutside(requiresConsent);
+ dialog.setOnShowListener(
+ dialogInterface -> {
+ final TextView tv = dialog.findViewById(android.R.id.message);
+ if (tv != null) {
+ tv.setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ });
dialog.show();
} else {
- requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
+ requestPermissions(
+ new String[] {Manifest.permission.READ_CONTACTS},
+ REQUEST_SYNC_CONTACTS);
}
}
}
@@ -798,6 +863,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantResults.length > 0)
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
ScanActivity.onRequestPermissionResult(this, requestCode, grantResults);
@@ -824,8 +890,10 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
@Override
protected void onBackendConnected() {
-
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
+ if (QuickConversationsService.isContactListIntegration(this)
+ && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
+ || checkSelfPermission(Manifest.permission.READ_CONTACTS)
+ == PackageManager.PERMISSION_GRANTED)) {
xmppConnectionService.getQuickConversationsService().considerSyncBackground(false);
}
if (mPostponedActivityResult != null) {
@@ -961,8 +1029,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
protected void filterContacts(String needle) {
this.contacts.clear();
final List accounts = xmppConnectionService.getAccounts();
- for (Account account : accounts) {
- if (account.getStatus() != Account.State.DISABLED) {
+ for (final Account account : accounts) {
+ if (account.isEnabled()) {
for (Contact contact : account.getRoster().getContacts()) {
Presence.Status s = contact.getShownStatus();
if (contact.showInContactList() && contact.match(this, needle)
@@ -981,7 +1049,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
protected void filterConferences(String needle) {
this.conferences.clear();
for (final Account account : xmppConnectionService.getAccounts()) {
- if (account.getStatus() != Account.State.DISABLED) {
+ if (account.isEnabled()) {
for (final Bookmark bookmark : account.getBookmarks()) {
if (bookmark.match(this, needle)) {
this.conferences.add(bookmark);
@@ -1297,7 +1365,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
case 0:
return getResources().getString(R.string.contacts);
case 1:
- return getResources().getString(R.string.bookmarks);
+ return getResources().getString(R.string.group_chats);
default:
return super.getPageTitle(position);
}
diff --git a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java
index 1e0cf41d3..de284264d 100644
--- a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java
@@ -18,13 +18,6 @@ import androidx.databinding.DataBindingUtil;
import com.google.common.base.Strings;
-import org.jetbrains.annotations.NotNull;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityUriHandlerBinding;
@@ -35,12 +28,20 @@ import eu.siacs.conversations.utils.ProvisioningUtils;
import eu.siacs.conversations.utils.SignupUtils;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.Jid;
+
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.HttpUrl;
import okhttp3.Request;
import okhttp3.Response;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
public class UriHandlerActivity extends AppCompatActivity {
public static final String ACTION_SCAN_QR_CODE = "scan_qr_code";
@@ -58,7 +59,9 @@ public class UriHandlerActivity extends AppCompatActivity {
}
public static void scan(final Activity activity, final boolean provisioning) {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
+ || ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)
+ == PackageManager.PERMISSION_GRANTED) {
final Intent intent = new Intent(activity, UriHandlerActivity.class);
intent.setAction(UriHandlerActivity.ACTION_SCAN_QR_CODE);
if (provisioning) {
@@ -68,14 +71,17 @@ public class UriHandlerActivity extends AppCompatActivity {
activity.startActivity(intent);
} else {
activity.requestPermissions(
- new String[]{Manifest.permission.CAMERA},
- provisioning ? REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION : REQUEST_CAMERA_PERMISSIONS_TO_SCAN
- );
+ new String[] {Manifest.permission.CAMERA},
+ provisioning
+ ? REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION
+ : REQUEST_CAMERA_PERMISSIONS_TO_SCAN);
}
}
- public static void onRequestPermissionResult(Activity activity, int requestCode, int[] grantResults) {
- if (requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN && requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION) {
+ public static void onRequestPermissionResult(
+ Activity activity, int requestCode, int[] grantResults) {
+ if (requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN
+ && requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION) {
return;
}
if (grantResults.length > 0) {
@@ -86,7 +92,11 @@ public class UriHandlerActivity extends AppCompatActivity {
scan(activity);
}
} else {
- Toast.makeText(activity, R.string.qr_code_scanner_needs_access_to_camera, Toast.LENGTH_SHORT).show();
+ Toast.makeText(
+ activity,
+ R.string.qr_code_scanner_needs_access_to_camera,
+ Toast.LENGTH_SHORT)
+ .show();
}
}
}
@@ -116,7 +126,7 @@ public class UriHandlerActivity extends AppCompatActivity {
private boolean handleUri(final Uri uri, final boolean scanned) {
final Intent intent;
final XmppUri xmppUri = new XmppUri(uri);
- final List accounts = DatabaseBackend.getInstance(this).getAccountJids(true);
+ final List accounts = DatabaseBackend.getInstance(this).getAccountJids(false);
if (SignupUtils.isSupportTokenRegistry() && xmppUri.isValidJid()) {
final String preAuth = xmppUri.getParameter(XmppUri.PARAMETER_PRE_AUTH);
@@ -130,7 +140,12 @@ public class UriHandlerActivity extends AppCompatActivity {
startActivity(intent);
return true;
}
- if (accounts.size() == 0 && xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) {
+ if (accounts.size() == 0
+ && xmppUri.isAction(XmppUri.ACTION_ROSTER)
+ && "y"
+ .equalsIgnoreCase(
+ Strings.nullToEmpty(xmppUri.getParameter(XmppUri.PARAMETER_IBR))
+ .trim())) {
intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth);
intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString());
startActivity(intent);
@@ -197,29 +212,28 @@ public class UriHandlerActivity extends AppCompatActivity {
private void checkForLinkHeader(final HttpUrl url) {
Log.d(Config.LOGTAG, "checking for link header on " + url);
- this.call = HttpConnectionManager.OK_HTTP_CLIENT.newCall(new Request.Builder()
- .url(url)
- .head()
- .build());
- this.call.enqueue(new Callback() {
- @Override
- public void onFailure(@NotNull Call call, @NotNull IOException e) {
- Log.d(Config.LOGTAG, "unable to check HTTP url", e);
- showError(R.string.no_xmpp_adddress_found);
- }
-
- @Override
- public void onResponse(@NotNull Call call, @NotNull Response response) {
- if (response.isSuccessful()) {
- final String linkHeader = response.header("Link");
- if (linkHeader != null && processLinkHeader(linkHeader)) {
- return;
+ this.call =
+ HttpConnectionManager.OK_HTTP_CLIENT.newCall(
+ new Request.Builder().url(url).head().build());
+ this.call.enqueue(
+ new Callback() {
+ @Override
+ public void onFailure(@NotNull Call call, @NotNull IOException e) {
+ Log.d(Config.LOGTAG, "unable to check HTTP url", e);
+ showError(R.string.no_xmpp_adddress_found);
}
- }
- showError(R.string.no_xmpp_adddress_found);
- }
- });
+ @Override
+ public void onResponse(@NotNull Call call, @NotNull Response response) {
+ if (response.isSuccessful()) {
+ final String linkHeader = response.header("Link");
+ if (linkHeader != null && processLinkHeader(linkHeader)) {
+ return;
+ }
+ }
+ showError(R.string.no_xmpp_adddress_found);
+ }
+ });
}
private boolean processLinkHeader(final String header) {
@@ -256,7 +270,8 @@ public class UriHandlerActivity extends AppCompatActivity {
}
switch (action) {
case Intent.ACTION_MAIN:
- binding.progress.setVisibility(call != null && !call.isCanceled() ? View.VISIBLE : View.INVISIBLE);
+ binding.progress.setVisibility(
+ call != null && !call.isCanceled() ? View.VISIBLE : View.INVISIBLE);
break;
case Intent.ACTION_VIEW:
case Intent.ACTION_SENDTO:
@@ -280,7 +295,8 @@ public class UriHandlerActivity extends AppCompatActivity {
private boolean allowProvisioning() {
final Intent launchIntent = getIntent();
- return launchIntent != null && launchIntent.getBooleanExtra(EXTRA_ALLOW_PROVISIONING, false);
+ return launchIntent != null
+ && launchIntent.getBooleanExtra(EXTRA_ALLOW_PROVISIONING, false);
}
@Override
@@ -303,13 +319,17 @@ public class UriHandlerActivity extends AppCompatActivity {
showError(R.string.no_xmpp_adddress_found);
}
return;
- } else if (QuickConversationsService.isConversations() && looksLikeJsonObject(result) && allowProvisioning) {
+ } else if (QuickConversationsService.isConversations()
+ && looksLikeJsonObject(result)
+ && allowProvisioning) {
ProvisioningUtils.provision(this, result);
finish();
return;
}
final Uri uri = Uri.parse(result.trim());
- if (allowProvisioning && "https".equalsIgnoreCase(uri.getScheme()) && !XmppUri.INVITE_DOMAIN.equalsIgnoreCase(uri.getHost())) {
+ if (allowProvisioning
+ && "https".equalsIgnoreCase(uri.getScheme())
+ && !XmppUri.INVITE_DOMAIN.equalsIgnoreCase(uri.getHost())) {
final HttpUrl httpUrl = HttpUrl.parse(uri.toString());
if (httpUrl != null) {
checkForLinkHeader(httpUrl);
diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
index 644bd7ec5..9190697a8 100644
--- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
@@ -41,12 +41,13 @@ import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.annotation.BoolRes;
import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AlertDialog.Builder;
@@ -55,12 +56,7 @@ import androidx.databinding.DataBindingUtil;
import com.google.common.base.Strings;
-import java.io.IOException;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.RejectedExecutionException;
-
+import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpEngine;
@@ -78,16 +74,23 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
import eu.siacs.conversations.ui.util.PresenceSelector;
+import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.ExceptionHelper;
-import eu.siacs.conversations.ui.util.SettingsUtils;
+import eu.siacs.conversations.utils.SignupUtils;
import eu.siacs.conversations.utils.ThemeHelper;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
public abstract class XmppActivity extends ActionBarActivity {
public static final String EXTRA_ACCOUNT = "account";
@@ -95,6 +98,7 @@ public abstract class XmppActivity extends ActionBarActivity {
protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102;
protected static final int REQUEST_CHOOSE_PGP_ID = 0x0103;
protected static final int REQUEST_BATTERY_OP = 0x49ff;
+ protected static final int REQUEST_POST_NOTIFICATION = 0x50ff;
public XmppConnectionService xmppConnectionService;
public boolean xmppConnectionServiceBound = false;
@@ -289,6 +293,68 @@ public abstract class XmppActivity extends ActionBarActivity {
builder.create().show();
}
+ protected void deleteAccount(final Account account) {
+ this.deleteAccount(account, null);
+ }
+
+ protected void deleteAccount(final Account account, final Runnable postDelete) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ final View dialogView = getLayoutInflater().inflate(R.layout.dialog_delete_account, null);
+ final CheckBox deleteFromServer =
+ dialogView.findViewById(R.id.delete_from_server);
+ builder.setView(dialogView);
+ builder.setTitle(R.string.mgmt_account_delete);
+ builder.setPositiveButton(getString(R.string.delete),null);
+ builder.setNegativeButton(getString(R.string.cancel), null);
+ final AlertDialog dialog = builder.create();
+ dialog.setOnShowListener(dialogInterface->{
+ final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
+ button.setOnClickListener(v -> {
+ final boolean unregister = deleteFromServer.isChecked();
+ if (unregister) {
+ if (account.isOnlineAndConnected()) {
+ deleteFromServer.setEnabled(false);
+ button.setText(R.string.please_wait);
+ button.setEnabled(false);
+ xmppConnectionService.unregisterAccount(account, result -> {
+ runOnUiThread(()->{
+ if (result) {
+ dialog.dismiss();
+ if (postDelete != null) {
+ postDelete.run();
+ }
+ if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) {
+ final Intent intent = SignupUtils.getSignUpIntent(this);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ startActivity(intent);
+ }
+ } else {
+ deleteFromServer.setEnabled(true);
+ button.setText(R.string.delete);
+ button.setEnabled(true);
+ Toast.makeText(this,R.string.could_not_delete_account_from_server,Toast.LENGTH_LONG).show();
+ }
+ });
+ });
+ } else {
+ Toast.makeText(this,R.string.not_connected_try_again,Toast.LENGTH_LONG).show();
+ }
+ } else {
+ xmppConnectionService.deleteAccount(account);
+ dialog.dismiss();
+ if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) {
+ final Intent intent = SignupUtils.getSignUpIntent(this);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ startActivity(intent);
+ } else if (postDelete != null) {
+ postDelete.run();
+ }
+ }
+ });
+ });
+ dialog.show();
+ }
+
abstract void onBackendConnected();
protected void registerListeners() {
@@ -357,6 +423,9 @@ public abstract class XmppActivity extends ActionBarActivity {
case R.id.action_settings:
startActivity(new Intent(this, SettingsActivity.class));
break;
+ case R.id.action_privacy_policy:
+ openPrivacyPolicy();
+ break;
case R.id.action_accounts:
AccountUtils.launchManageAccounts(this);
break;
@@ -373,6 +442,20 @@ public abstract class XmppActivity extends ActionBarActivity {
return super.onOptionsItemSelected(item);
}
+ private void openPrivacyPolicy() {
+ if (BuildConfig.PRIVACY_POLICY == null) {
+ return;
+ }
+ final var viewPolicyIntent = new Intent(Intent.ACTION_VIEW);
+ viewPolicyIntent.setData(Uri.parse(BuildConfig.PRIVACY_POLICY));
+ try {
+ startActivity(viewPolicyIntent);
+ } catch (final ActivityNotFoundException e) {
+ Toast.makeText(this, R.string.no_application_found_to_open_link, Toast.LENGTH_SHORT)
+ .show();
+ }
+ }
+
public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
final Contact contact = conversation.getContact();
if (contact.showInRoster() || contact.isSelf()) {
@@ -578,9 +661,9 @@ public abstract class XmppActivity extends ActionBarActivity {
xmppConnectionService.getPgpEngine().generateSignature(intent, account, status, new UiCallback() {
@Override
- public void userInputRequired(PendingIntent pi, String signature) {
+ public void userInputRequired(final PendingIntent pi, final String signature) {
try {
- startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
+ startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0,Compatibility.pgpStartIntentSenderOptions());
} catch (final SendIntentException ignored) {
}
}
@@ -641,7 +724,7 @@ public abstract class XmppActivity extends ActionBarActivity {
public void userInputRequired(PendingIntent pi, Account object) {
try {
startIntentSenderForResult(pi.getIntentSender(),
- REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0);
+ REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions());
} catch (final SendIntentException ignored) {
}
}
@@ -746,7 +829,7 @@ public abstract class XmppActivity extends ActionBarActivity {
}
protected boolean hasStoragePermission(int requestCode) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
return false;
@@ -815,8 +898,9 @@ public abstract class XmppActivity extends ActionBarActivity {
try {
startIntentSenderForResult(
pgp.getIntentForKey(keyId).getIntentSender(), 0, null, 0,
- 0, 0);
- } catch (Throwable e) {
+ 0, 0, Compatibility.pgpStartIntentSenderOptions());
+ } catch (final Throwable e) {
+ Log.d(Config.LOGTAG,"could not launch OpenKeyChain", e);
Toast.makeText(XmppActivity.this, R.string.openpgp_error, Toast.LENGTH_SHORT).show();
}
}
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java
index b070a63bb..038f255ea 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java
@@ -59,6 +59,7 @@ public class AccountAdapter extends ArrayAdapter {
viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorOnline));
break;
case DISABLED:
+ case LOGGED_OUT:
case CONNECTING:
viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, android.R.attr.textColorSecondary));
break;
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java
index 2733e7b8b..2683876c7 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java
@@ -74,6 +74,8 @@ public class MediaAdapter extends RecyclerView.Adapter {
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)));
+ viewHolder.status_message.setText(activity.getString(R.string.incoming_call_duration_timestamp, TimeFrameUtils.resolve(activity, duration), UIHelper.readableTimeDifferenceFull(activity, message.getTimeSent())));
} else if (rtpSessionStatus.successful) {
viewHolder.status_message.setText(R.string.incoming_call);
} else {
@@ -704,9 +705,9 @@ public class MessageAdapter extends ArrayAdapter {
}
} else {
if (duration > 0) {
- viewHolder.status_message.setText(activity.getString(R.string.outgoing_call_duration, TimeFrameUtils.resolve(activity, duration)));
+ viewHolder.status_message.setText(activity.getString(R.string.outgoing_call_duration_timestamp, TimeFrameUtils.resolve(activity, duration), UIHelper.readableTimeDifferenceFull(activity, message.getTimeSent())));
} else {
- viewHolder.status_message.setText(R.string.outgoing_call);
+ viewHolder.status_message.setText(activity.getString(R.string.outgoing_call_timestamp, UIHelper.readableTimeDifferenceFull(activity, message.getTimeSent())));
}
}
viewHolder.indicatorReceived.setImageResource(RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful, isDarkTheme));
@@ -873,7 +874,7 @@ public class MessageAdapter extends ArrayAdapter {
}
public void openDownloadable(Message message) {
- if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && 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;
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java
index 41bfb24a1..de3216908 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java
@@ -25,6 +25,7 @@ import eu.siacs.conversations.ui.ConferenceDetailsActivity;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
+import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.xmpp.Jid;
public class UserAdapter extends ListAdapter implements View.OnCreateContextMenuListener {
@@ -104,7 +105,7 @@ public class UserAdapter extends ListAdapter(playPause));
ActivityCompat.requestPermissions(messageAdapter.getActivity(), new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, ConversationsActivity.REQUEST_PLAY_PAUSE);
return;
diff --git a/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java b/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java
index 684fd8094..a5fba62a7 100644
--- a/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java
+++ b/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java
@@ -75,9 +75,7 @@ public class FixedURLSpan extends URLSpan {
}
}
final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
- }
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
//intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
try {
context.startActivity(intent);
diff --git a/src/main/java/eu/siacs/conversations/ui/widget/SwipeRefreshListFragment.java b/src/main/java/eu/siacs/conversations/ui/widget/SwipeRefreshListFragment.java
index b1ef6165a..5f5d6a8cf 100644
--- a/src/main/java/eu/siacs/conversations/ui/widget/SwipeRefreshListFragment.java
+++ b/src/main/java/eu/siacs/conversations/ui/widget/SwipeRefreshListFragment.java
@@ -26,7 +26,6 @@ import android.widget.ListView;
import androidx.fragment.app.ListFragment;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
-import eu.siacs.conversations.R;
import eu.siacs.conversations.ui.util.StyledAttributes;
/**
@@ -57,7 +56,7 @@ public class SwipeRefreshListFragment extends ListFragment {
final Context context = getActivity();
if (context != null) {
- mSwipeRefreshLayout.setColorSchemeColors(StyledAttributes.getColor(context, R.attr.colorAccent));
+ mSwipeRefreshLayout.setColorSchemeColors(StyledAttributes.getColor(context, androidx.appcompat.R.attr.colorAccent));
}
if (onRefreshListener != null) {
diff --git a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java b/src/main/java/eu/siacs/conversations/utils/AccountUtils.java
index b8f4855d0..7b7397123 100644
--- a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java
+++ b/src/main/java/eu/siacs/conversations/utils/AccountUtils.java
@@ -6,6 +6,10 @@ import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
+import com.google.common.primitives.Bytes;
+import com.google.common.primitives.Longs;
+
+import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@@ -24,10 +28,9 @@ public class AccountUtils {
MANAGE_ACCOUNT_ACTIVITY = getManageAccountActivityClass();
}
-
public static boolean hasEnabledAccounts(final XmppConnectionService service) {
final List accounts = service.getAccounts();
- for(Account account : accounts) {
+ for (Account account : accounts) {
if (account.isOptionSet(Account.OPTION_DISABLED)) {
return false;
}
@@ -42,20 +45,22 @@ public class AccountUtils {
} catch (final IllegalArgumentException e) {
return account.getUuid();
}
- final UUID publicDeviceId = getUuid(uuid.getLeastSignificantBits(), uuid.getLeastSignificantBits());
- return publicDeviceId.toString();
- }
-
- protected static UUID getUuid(final long msb, final long lsb) {
- final long msb0 = (msb & 0xffffffffffff0fffL) | 4; // set version
- final long lsb0 = (lsb & 0x3fffffffffffffffL) | 0x8000000000000000L; // set variant
- return new UUID(msb0, lsb0);
+ final byte[] bytes =
+ Bytes.concat(
+ Longs.toByteArray(uuid.getLeastSignificantBits()),
+ Longs.toByteArray(uuid.getLeastSignificantBits()));
+ bytes[6] &= 0x0f; /* clear version */
+ bytes[6] |= 0x40; /* set to version 4 */
+ bytes[8] &= 0x3f; /* clear variant */
+ bytes[8] |= 0x80; /* set to IETF variant */
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+ return new UUID(byteBuffer.getLong(), byteBuffer.getLong()).toString();
}
public static List getEnabledAccounts(final XmppConnectionService service) {
- ArrayList accounts = new ArrayList<>();
- for (Account account : service.getAccounts()) {
- if (account.getStatus() != Account.State.DISABLED) {
+ final ArrayList accounts = new ArrayList<>();
+ for (final Account account : service.getAccounts()) {
+ if (account.isEnabled()) {
if (Config.DOMAIN_LOCK != null) {
accounts.add(account.getJid().getEscapedLocal());
} else {
@@ -68,7 +73,7 @@ public class AccountUtils {
public static Account getFirstEnabled(XmppConnectionService service) {
final List accounts = service.getAccounts();
- for(Account account : accounts) {
+ for (Account account : accounts) {
if (!account.isOptionSet(Account.OPTION_DISABLED)) {
return account;
}
@@ -78,7 +83,7 @@ public class AccountUtils {
public static Account getFirst(XmppConnectionService service) {
final List accounts = service.getAccounts();
- for(Account account : accounts) {
+ for (Account account : accounts) {
return account;
}
return null;
diff --git a/src/main/java/eu/siacs/conversations/utils/BackupFileHeader.java b/src/main/java/eu/siacs/conversations/utils/BackupFileHeader.java
index 54387a8ee..3b536c27a 100644
--- a/src/main/java/eu/siacs/conversations/utils/BackupFileHeader.java
+++ b/src/main/java/eu/siacs/conversations/utils/BackupFileHeader.java
@@ -1,5 +1,7 @@
package eu.siacs.conversations.utils;
+import androidx.annotation.NonNull;
+
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
@@ -8,7 +10,7 @@ import eu.siacs.conversations.xmpp.Jid;
public class BackupFileHeader {
- private static final int VERSION = 1;
+ private static final int VERSION = 2;
private final String app;
private final Jid jid;
@@ -17,6 +19,7 @@ public class BackupFileHeader {
private final byte[] salt;
+ @NonNull
@Override
public String toString() {
return "BackupFileHeader{" +
@@ -47,17 +50,19 @@ public class BackupFileHeader {
public static BackupFileHeader read(DataInputStream inputStream) throws IOException {
final int version = inputStream.readInt();
- if (version > VERSION) {
- throw new IllegalArgumentException("Backup File version was " + version + " but app only supports up to version " + VERSION);
- }
- String app = inputStream.readUTF();
- String jid = inputStream.readUTF();
+ final String app = inputStream.readUTF();
+ final String jid = inputStream.readUTF();
long timestamp = inputStream.readLong();
- byte[] iv = new byte[12];
+ final byte[] iv = new byte[12];
inputStream.readFully(iv);
- byte[] salt = new byte[16];
+ final byte[] salt = new byte[16];
inputStream.readFully(salt);
-
+ if (version < VERSION) {
+ throw new OutdatedBackupFileVersion();
+ }
+ if (version != VERSION) {
+ throw new IllegalArgumentException("Backup File version was " + version + " but app only supports version " + VERSION);
+ }
return new BackupFileHeader(app, Jid.of(jid), timestamp, iv, salt);
}
@@ -81,4 +86,8 @@ public class BackupFileHeader {
public long getTimestamp() {
return timestamp;
}
+
+ public static class OutdatedBackupFileVersion extends RuntimeException {
+
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/utils/Checksum.java b/src/main/java/eu/siacs/conversations/utils/Checksum.java
deleted file mode 100644
index 407cb1944..000000000
--- a/src/main/java/eu/siacs/conversations/utils/Checksum.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (c) 2018, Daniel Gultsch All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation and/or
- * other materials provided with the distribution.
- *
- * 3. Neither the name of the copyright holder nor the names of its contributors
- * may be used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package eu.siacs.conversations.utils;
-
-import android.util.Base64;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-
-public class Checksum {
-
- public static String md5(InputStream inputStream) throws IOException {
- byte[] buffer = new byte[4096];
- MessageDigest messageDigest;
- try {
- messageDigest = MessageDigest.getInstance("MD5");
- } catch (NoSuchAlgorithmException e) {
- throw new AssertionError(e);
- }
-
- int count;
- do {
- count = inputStream.read(buffer);
- if (count > 0) {
- messageDigest.update(buffer, 0, count);
- }
- } while (count != -1);
- inputStream.close();
- return Base64.encodeToString(messageDigest.digest(), Base64.NO_WRAP);
- }
-}
diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java
index b1145794d..23c935a2b 100644
--- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java
+++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java
@@ -3,6 +3,7 @@ package eu.siacs.conversations.utils;
import static eu.siacs.conversations.services.EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE;
import android.annotation.SuppressLint;
+import android.app.ActivityOptions;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -10,6 +11,7 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.os.Build;
+import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceManager;
@@ -20,15 +22,15 @@ import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.ui.SettingsActivity;
import eu.siacs.conversations.ui.SettingsFragment;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
public class Compatibility {
private static final List UNUSED_SETTINGS_POST_TWENTYSIX =
@@ -40,8 +42,9 @@ public class Compatibility {
private static final List UNUSED_SETTINGS_PRE_TWENTYSIX =
Collections.singletonList("message_notification_settings");
- public static boolean hasStoragePermission(Context context) {
+ public static boolean hasStoragePermission(final Context context) {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M
+ || Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|| ContextCompat.checkSelfPermission(
context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
@@ -177,4 +180,15 @@ public class Compatibility {
return ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
}
}
+
+ public static Bundle pgpStartIntentSenderOptions() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ return ActivityOptions.makeBasic()
+ .setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
+ .toBundle();
+ } else {
+ return null;
+ }
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/utils/Emoticons.java b/src/main/java/eu/siacs/conversations/utils/Emoticons.java
index 266d9d081..8f2001ce3 100644
--- a/src/main/java/eu/siacs/conversations/utils/Emoticons.java
+++ b/src/main/java/eu/siacs/conversations/utils/Emoticons.java
@@ -43,8 +43,8 @@ public class Emoticons {
private static final UnicodeRange MISC_SYMBOLS_AND_PICTOGRAPHS = new UnicodeRange(0x1F300, 0x1F5FF);
private static final UnicodeRange SUPPLEMENTAL_SYMBOLS = new UnicodeRange(0x1F900, 0x1F9FF);
- private static final UnicodeRange EMOTICONS = new UnicodeRange(0x1F600, 0x1F64F);
- private static final UnicodeRange TRANSPORT_SYMBOLS = new UnicodeRange(0x1F680, 0x1F6FF);
+ private static final UnicodeRange EMOTICONS = new UnicodeRange(0x1F600, 0x1FAF6);
+ //private static final UnicodeRange TRANSPORT_SYMBOLS = new UnicodeRange(0x1F680, 0x1F6FF);
private static final UnicodeRange MISC_SYMBOLS = new UnicodeRange(0x2600, 0x26FF);
private static final UnicodeRange DINGBATS = new UnicodeRange(0x2700, 0x27BF);
private static final UnicodeRange ENCLOSED_ALPHANUMERIC_SUPPLEMENT = new UnicodeRange(0x1F100, 0x1F1FF);
@@ -69,7 +69,7 @@ public class Emoticons {
MISC_SYMBOLS_AND_PICTOGRAPHS,
SUPPLEMENTAL_SYMBOLS,
EMOTICONS,
- TRANSPORT_SYMBOLS,
+ //TRANSPORT_SYMBOLS,
MISC_SYMBOLS,
DINGBATS,
ENCLOSED_ALPHANUMERIC_SUPPLEMENT,
diff --git a/src/main/java/eu/siacs/conversations/utils/FileUtils.java b/src/main/java/eu/siacs/conversations/utils/FileUtils.java
index 43e3da118..e439ab030 100644
--- a/src/main/java/eu/siacs/conversations/utils/FileUtils.java
+++ b/src/main/java/eu/siacs/conversations/utils/FileUtils.java
@@ -34,10 +34,8 @@ public class FileUtils {
return null;
}
- final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
-
// DocumentProvider
- if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
+ if (DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
diff --git a/src/main/java/eu/siacs/conversations/utils/IP.java b/src/main/java/eu/siacs/conversations/utils/IP.java
index a7182e207..948f7537a 100644
--- a/src/main/java/eu/siacs/conversations/utils/IP.java
+++ b/src/main/java/eu/siacs/conversations/utils/IP.java
@@ -1,5 +1,7 @@
package eu.siacs.conversations.utils;
+import com.google.common.net.InetAddresses;
+
import java.util.regex.Pattern;
public class IP {
@@ -27,4 +29,14 @@ public class IP {
}
}
+ public static String unwrapIPv6(final String host) {
+ if (host.length() > 2 && host.charAt(0) == '[' && host.charAt(host.length() - 1) == ']') {
+ final String ip = host.substring(1,host.length() -1);
+ if (InetAddresses.isInetAddress(ip)) {
+ return ip;
+ }
+ }
+ return host;
+ }
+
}
diff --git a/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java b/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java
index d3aade355..04573ef4c 100644
--- a/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java
+++ b/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java
@@ -60,7 +60,7 @@ public class IrregularUnicodeDetector {
private static final Map NORMALIZATION_MAP;
private static final LruCache CACHE = new LruCache<>(4096);
- private static final List AMBIGUOUS_CYRILLIC = Arrays.asList("а","г","е","ѕ","і","q","о","р","с","у");
+ private static final List AMBIGUOUS_CYRILLIC = Arrays.asList("а","г","е","ѕ","і","ј","ķ","ԛ","о","р","с","у","х");
static {
Map temp = new HashMap<>();
diff --git a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java
index d112a9224..adf3841d3 100644
--- a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java
+++ b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java
@@ -28,6 +28,8 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -66,6 +68,7 @@ public final class MimeUtils {
// by guessExtensionFromMimeType.
add("application/andrew-inset", "ez");
add("application/dsptype", "tsp");
+ add("application/json", "json");
add("application/epub+zip", "epub");
add("application/gpx+xml", "gpx");
add("application/hta", "hta");
@@ -260,7 +263,8 @@ public final class MimeUtils {
add("audio/mpeg", "mpga");
add("audio/mpeg", "mpega");
add("audio/mpeg", "mp2");
- add("audio/mpeg", "m4a");
+ add("audio/mp4", "m4a");
+ add("audio/x-m4b", "m4b");
add("audio/mpegurl", "m3u");
add("audio/ogg", "oga");
add("audio/opus", "opus");
@@ -413,6 +417,9 @@ public final class MimeUtils {
applyOverrides();
}
+ // mime types that are more reliant by path
+ private static final Collection PATH_PRECEDENCE_MIME_TYPE = Arrays.asList("audio/x-m4b");
+
private static void add(String mimeType, String extension) {
// If we have an existing x -> y mapping, we do not want to
// override it with another mapping x -> y2.
@@ -537,44 +544,49 @@ public final class MimeUtils {
}
public static String guessMimeTypeFromUriAndMime(final Context context, final Uri uri, final String mime) {
- Log.d(Config.LOGTAG, "guessMimeTypeFromUriAndMime " + uri + " and mime=" + mime);
- if (mime == null || mime.equals("application/octet-stream")) {
- final String guess = guessMimeTypeFromUri(context, uri);
- if (guess != null) {
- return guess;
- } else {
- return mime;
- }
+ Log.d(Config.LOGTAG, "guessMimeTypeFromUriAndMime(" + uri + "," + mime+")");
+ final String mimeFromUri = guessMimeTypeFromUri(context, uri);
+ Log.d(Config.LOGTAG,"mimeFromUri:"+mimeFromUri);
+ if (PATH_PRECEDENCE_MIME_TYPE.contains(mimeFromUri)) {
+ return mimeFromUri;
+ } else if (mime == null || mime.equals("application/octet-stream")) {
+ return mimeFromUri;
+ } else {
+ return mime;
}
- return guessMimeTypeFromUri(context, uri);
}
- public static String guessMimeTypeFromUri(Context context, Uri uri) {
- // try the content resolver
- String mimeType;
+ public static String guessMimeTypeFromUri(final Context context, final Uri uri) {
+ final String mimeTypeContentResolver = guessFromContentResolver(context, uri);
+ final String mimeTypeFromQueryParameter = uri.getQueryParameter("mimeType");
+ final String name = "content".equals(uri.getScheme()) ? getDisplayName(context, uri) : null;
+ final String mimeTypeFromName = Strings.isNullOrEmpty(name) ? null : guessFromPath(name);
+ final String path = uri.getPath();
+ final String mimeTypeFromPath = Strings.isNullOrEmpty(path) ? null : guessFromPath(path);
+ if (PATH_PRECEDENCE_MIME_TYPE.contains(mimeTypeFromName)) {
+ return mimeTypeFromName;
+ }
+ if (PATH_PRECEDENCE_MIME_TYPE.contains(mimeTypeFromPath)) {
+ return mimeTypeFromPath;
+ }
+ if (mimeTypeContentResolver != null && !"application/octet-stream".equals(mimeTypeContentResolver)) {
+ return mimeTypeContentResolver;
+ }
+ if (mimeTypeFromName != null) {
+ return mimeTypeFromName;
+ }
+ if (mimeTypeFromQueryParameter != null) {
+ return mimeTypeFromQueryParameter;
+ }
+ return mimeTypeFromPath;
+ }
+
+ private static String guessFromContentResolver(final Context context, final Uri uri) {
try {
- mimeType = context.getContentResolver().getType(uri);
- } catch (final Throwable throwable) {
- mimeType = null;
+ return context.getContentResolver().getType(uri);
+ } catch (final Throwable e) {
+ return null;
}
- // try the extension
- if (mimeType == null || mimeType.equals("application/octet-stream")) {
- final String path = uri.getPath();
- if (path != null) {
- mimeType = guessFromPath(path);
- }
- }
- if (mimeType == null && "content".equals(uri.getScheme())) {
- final String name = getDisplayName(context, uri);
- if (name != null) {
- mimeType = guessFromPath(name);
- }
- }
- // sometimes this works (as with the commit content api)
- if (mimeType == null) {
- mimeType = uri.getQueryParameter("mimeType");
- }
- return mimeType;
}
private static String getDisplayName(final Context context, final Uri uri) {
diff --git a/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java b/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java
index 004676156..21d7f42a1 100644
--- a/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java
+++ b/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java
@@ -24,9 +24,23 @@ public class PermissionUtils {
return true;
}
- public static boolean writeGranted(int[] grantResults, String[] permission) {
+ public static boolean writeGranted(final int[] grantResults, final String[] permissions) {
+ return permissionGranted(
+ Manifest.permission.WRITE_EXTERNAL_STORAGE, grantResults, permissions);
+ }
+
+ public static boolean audioGranted(final int[] grantResults, final String[] permissions) {
+ return permissionGranted(Manifest.permission.RECORD_AUDIO, grantResults, permissions);
+ }
+
+ public static boolean cameraGranted(final int[] grantResults, final String[] permissions) {
+ return permissionGranted(Manifest.permission.CAMERA, grantResults, permissions);
+ }
+
+ private static boolean permissionGranted(
+ final String permission, final int[] grantResults, final String[] permissions) {
for (int i = 0; i < grantResults.length; ++i) {
- if (Manifest.permission.WRITE_EXTERNAL_STORAGE.equals(permission[i])) {
+ if (permission.equals(permissions[i])) {
return grantResults[i] == PackageManager.PERMISSION_GRANTED;
}
}
diff --git a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java
index 9ff492578..98924a262 100644
--- a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java
@@ -10,34 +10,37 @@ import android.os.Build;
import android.provider.ContactsContract.Profile;
import android.provider.Settings;
+import com.google.common.base.Strings;
+
+import eu.siacs.conversations.services.QuickConversationsService;
+
public class PhoneHelper {
@SuppressLint("HardwareIds")
- public static String getAndroidId(Context context) {
+ public static String getAndroidId(final Context context) {
return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
}
- public static Uri getProfilePictureUri(Context context) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
- && context.checkSelfPermission(Manifest.permission.READ_CONTACTS)
- != PackageManager.PERMISSION_GRANTED) {
+ public static Uri getProfilePictureUri(final Context context) {
+ if (!QuickConversationsService.isContactListIntegration(context)
+ || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+ && context.checkSelfPermission(Manifest.permission.READ_CONTACTS)
+ != PackageManager.PERMISSION_GRANTED)) {
return null;
}
final String[] projection = new String[] {Profile._ID, Profile.PHOTO_URI};
- final Cursor cursor;
- try {
- cursor =
- context.getContentResolver()
- .query(Profile.CONTENT_URI, projection, null, null, null);
- } catch (Throwable e) {
- return null;
+ try (final Cursor cursor =
+ context.getContentResolver()
+ .query(Profile.CONTENT_URI, projection, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ final var photoUri = cursor.getString(1);
+ if (Strings.isNullOrEmpty(photoUri)) {
+ return null;
+ }
+ return Uri.parse(photoUri);
+ }
}
- if (cursor == null) {
- return null;
- }
- final String uri = cursor.moveToFirst() ? cursor.getString(1) : null;
- cursor.close();
- return uri == null ? null : Uri.parse(uri);
+ return null;
}
public static boolean isEmulator() {
diff --git a/src/main/java/eu/siacs/conversations/utils/Resolver.java b/src/main/java/eu/siacs/conversations/utils/Resolver.java
index 463d6eb73..6746f3ea9 100644
--- a/src/main/java/eu/siacs/conversations/utils/Resolver.java
+++ b/src/main/java/eu/siacs/conversations/utils/Resolver.java
@@ -6,6 +6,11 @@ import android.util.Log;
import androidx.annotation.NonNull;
+import com.google.common.base.Strings;
+import com.google.common.base.Throwables;
+import com.google.common.net.InetAddresses;
+import com.google.common.primitives.Ints;
+
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.Inet4Address;
@@ -15,6 +20,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import de.gultsch.minidns.AndroidDNSClient;
import de.measite.minidns.AbstractDNSClient;
import de.measite.minidns.DNSCache;
import de.measite.minidns.DNSClient;
@@ -112,7 +118,7 @@ public class Resolver {
return port == 443 || port == 5223;
}
- public static List resolve(String domain) {
+ public static List resolve(final String domain) {
final List ipResults = fromIpAddress(domain);
if (ipResults.size() > 0) {
return ipResults;
@@ -126,8 +132,10 @@ public class Resolver {
synchronized (results) {
results.addAll(list);
}
- } catch (Throwable throwable) {
- Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (direct TLS)", throwable);
+ } catch (final Throwable throwable) {
+ if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
+ Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (direct TLS)", throwable);
+ }
}
});
threads[1] = new Thread(() -> {
@@ -136,8 +144,10 @@ public class Resolver {
synchronized (results) {
results.addAll(list);
}
- } catch (Throwable throwable) {
- Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (STARTTLS)", throwable);
+ } catch (final Throwable throwable) {
+ if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
+ Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (STARTTLS)", throwable);
+ }
}
});
threads[2] = new Thread(() -> {
@@ -156,15 +166,15 @@ public class Resolver {
threads[2].interrupt();
synchronized (results) {
Collections.sort(results);
- Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results.toString());
- return new ArrayList<>(results);
+ Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results);
+ return results;
}
} else {
threads[2].join();
synchronized (fallbackResults) {
Collections.sort(fallbackResults);
- Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + fallbackResults.toString());
- return new ArrayList<>(fallbackResults);
+ Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + fallbackResults);
+ return fallbackResults;
}
}
} catch (InterruptedException e) {
@@ -247,7 +257,7 @@ public class Resolver {
}
private static List resolveNoSrvRecords(DNSName dnsName, boolean withCnames) {
- List results = new ArrayList<>();
+ final List results = new ArrayList<>();
try {
for (A a : resolveWithFallback(dnsName, A.class, false).getAnswersOrEmptySet()) {
results.add(Result.createDefault(dnsName, a.getInetAddress()));
@@ -260,8 +270,10 @@ public class Resolver {
results.addAll(resolveNoSrvRecords(cname.name, false));
}
}
- } catch (Throwable throwable) {
- Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable);
+ } catch (final Throwable throwable) {
+ if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) {
+ Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable);
+ }
}
results.add(Result.createDefault(dnsName));
return results;
@@ -274,7 +286,9 @@ public class Resolver {
private static ResolverResult resolveWithFallback(DNSName dnsName, Class type, boolean validateHostname) throws IOException {
final Question question = new Question(dnsName, Record.TYPE.getType(type));
if (!validateHostname) {
- return ResolverApi.INSTANCE.resolve(question);
+ final AndroidDNSClient androidDNSClient = new AndroidDNSClient(SERVICE);
+ final ResolverApi resolverApi = new ResolverApi(androidDNSClient);
+ return resolverApi.resolve(question);
}
try {
return DnssecResolverApi.INSTANCE.resolveDnssecReliable(question);
@@ -435,6 +449,65 @@ public class Resolver {
contentValues.put(AUTHENTICATED, authenticated ? 1 : 0);
return contentValues;
}
+
+ public Result seeOtherHost(final String seeOtherHost) {
+ final String hostname = seeOtherHost.trim();
+ if (hostname.isEmpty()) {
+ return null;
+ }
+ final Result result = new Result();
+ result.directTls = this.directTls;
+ final int portSegmentStart = hostname.lastIndexOf(':');
+ if (hostname.charAt(hostname.length() - 1) != ']'
+ && portSegmentStart >= 0
+ && hostname.length() >= portSegmentStart + 1) {
+ final String hostPart = hostname.substring(0, portSegmentStart);
+ final String portPart = hostname.substring(portSegmentStart + 1);
+ final Integer port = Ints.tryParse(portPart);
+ if (port == null || Strings.isNullOrEmpty(hostPart)) {
+ return null;
+ }
+ final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostPart);
+ result.port = port;
+ if (InetAddresses.isInetAddress(host)) {
+ final InetAddress inetAddress;
+ try {
+ inetAddress = InetAddresses.forString(host);
+ } catch (final IllegalArgumentException e) {
+ return null;
+ }
+ result.ip = inetAddress;
+ } else {
+ if (hostPart.trim().isEmpty()) {
+ return null;
+ }
+ try {
+ result.hostname = DNSName.from(hostPart.trim());
+ } catch (final Exception e) {
+ return null;
+ }
+ }
+ } else {
+ final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostname);
+ if (InetAddresses.isInetAddress(host)) {
+ final InetAddress inetAddress;
+ try {
+ inetAddress = InetAddresses.forString(host);
+ } catch (final IllegalArgumentException e) {
+ return null;
+ }
+ result.ip = inetAddress;
+ } else {
+ try {
+ result.hostname = DNSName.from(hostname);
+ } catch (final Exception e) {
+ return null;
+ }
+ }
+ result.port = port;
+ }
+ return result;
+ }
}
}
diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
index b70bfc558..da72ba9bd 100644
--- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java
@@ -481,6 +481,8 @@ public class UIHelper {
return context.getString(R.string.file);
} else if (MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime)) {
return context.getString(R.string.multimedia_file);
+ } else if (mime.equals("audio/x-m4b")) {
+ return context.getString(R.string.audiobook);
} else if (mime.startsWith("audio/")) {
return context.getString(R.string.audio);
} else if (mime.startsWith("video/")) {
diff --git a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java
index 57a2f3dba..635afd145 100644
--- a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java
+++ b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java
@@ -29,7 +29,7 @@ public class LocalizedContent {
final String childLanguage = child.getAttribute("xml:lang");
final String lang = childLanguage == null ? parentLanguage : childLanguage;
final String content = child.getContent();
- if (content != null && (namespace == null || "jabber:client".equals(namespace))) {
+ if (content != null && (namespace == null || Namespace.JABBER_CLIENT.equals(namespace))) {
if (contents.put(lang, content) != null) {
//anything that has multiple contents for the same language is invalid
return null;
diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java
index b614251bd..85714c765 100644
--- a/src/main/java/eu/siacs/conversations/xml/Namespace.java
+++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java
@@ -2,6 +2,7 @@ package eu.siacs.conversations.xml;
public final class Namespace {
public static final String STREAMS = "http://etherx.jabber.org/streams";
+ public static final String JABBER_CLIENT = "jabber:client";
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";
@@ -46,7 +47,11 @@ 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_TRANSPORT_WEBRTC_DATA_CHANNEL = "urn:xmpp:jingle:transports:webrtc-datachannel:1";
+ public static final String JINGLE_TRANSPORT = "urn:xmpp:jingle:transports:dtls-sctp:1";
public static final String JINGLE_APPS_RTP = "urn:xmpp:jingle:apps:rtp:1";
+
+ public static final String JINGLE_APPS_FILE_TRANSFER = "urn:xmpp:jingle:apps:file-transfer:5";
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";
@@ -59,11 +64,16 @@ public final class Namespace {
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 = "urn:xmpp:bookmarks:1";
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 EASY_ONBOARDING_INVITE = "urn:xmpp:invite#invite";
public static final String OMEMO_DTLS_SRTP_VERIFICATION = "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification";
+ public static final String JINGLE_TRANSPORT_ICE_OPTION = "http://gultsch.de/xmpp/drafts/jingle/transports/ice-udp/option";
public static final String UNIFIED_PUSH = "http://gultsch.de/xmpp/drafts/unified-push";
+ public static final String REPORTING = "urn:xmpp:reporting:1";
+ public static final String REPORTING_REASON_SPAM = "urn:xmpp:reporting:spam";
+ public static final String SDP_OFFER_ANSWER = "urn:ietf:rfc:3264";
+ public static final String HASHES = "urn:xmpp:hashes:2";
}
diff --git a/src/main/java/eu/siacs/conversations/xml/Tag.java b/src/main/java/eu/siacs/conversations/xml/Tag.java
index db2b11172..d8d98348e 100644
--- a/src/main/java/eu/siacs/conversations/xml/Tag.java
+++ b/src/main/java/eu/siacs/conversations/xml/Tag.java
@@ -43,6 +43,10 @@ public class Tag {
return name;
}
+ public String identifier() {
+ return String.format("%s#%s", name, this.attributes.get("xmlns"));
+ }
+
public String getAttribute(final String attrName) {
return this.attributes.get(attrName);
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
index 010cb76c7..9141e1c70 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
@@ -16,55 +16,18 @@ import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.google.common.base.MoreObjects;
import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
-
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.ConnectException;
-import java.net.IDN;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.Socket;
-import java.net.UnknownHostException;
-import java.security.KeyManagementException;
-import java.security.NoSuchAlgorithmException;
-import java.security.Principal;
-import java.security.PrivateKey;
-import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Hashtable;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.regex.Matcher;
-
-import javax.net.ssl.KeyManager;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLPeerUnverifiedException;
-import javax.net.ssl.SSLSocket;
-import javax.net.ssl.SSLSocketFactory;
-import javax.net.ssl.X509KeyManager;
-import javax.net.ssl.X509TrustManager;
+import com.google.common.collect.ImmutableList;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.XmppDomainVerifier;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.sasl.ChannelBinding;
+import eu.siacs.conversations.crypto.sasl.ChannelBindingMechanism;
import eu.siacs.conversations.crypto.sasl.HashedToken;
import eu.siacs.conversations.crypto.sasl.SaslMechanism;
import eu.siacs.conversations.entities.Account;
@@ -106,8 +69,50 @@ import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
+
import okhttp3.HttpUrl;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.ConnectException;
+import java.net.IDN;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Matcher;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509KeyManager;
+import javax.net.ssl.X509TrustManager;
+
public class XmppConnection implements Runnable {
private static final int PACKET_IQ = 0;
@@ -161,7 +166,8 @@ public class XmppConnection implements Runnable {
private boolean quickStartInProgress = false;
private boolean isBound = false;
private Element streamFeatures;
- private String streamId = null;
+ private Element boundStreamFeatures;
+ private StreamId streamId = null;
private int stanzasReceived = 0;
private int stanzasSent = 0;
private int stanzasSentBeforeAuthentication;
@@ -184,10 +190,12 @@ public class XmppConnection implements Runnable {
private OnStatusChanged statusListener = null;
private OnBindListener bindListener = null;
private OnMessageAcknowledged acknowledgedListener = null;
- private SaslMechanism saslMechanism;
+ private LoginInfo loginInfo;
private HashedToken.Mechanism hashTokenRequest;
private HttpUrl redirectionUrl = null;
private String verifiedHostname = null;
+ private Resolver.Result currentResolverResult;
+ private Resolver.Result seeOtherHostResolverResult;
private volatile Thread mThread;
private CountDownLatch mStreamCountDownLatch;
@@ -229,10 +237,11 @@ public class XmppConnection implements Runnable {
return;
}
if (account.getStatus() != nextStatus) {
- if ((nextStatus == Account.State.OFFLINE)
- && (account.getStatus() != Account.State.CONNECTING)
- && (account.getStatus() != Account.State.ONLINE)
- && (account.getStatus() != Account.State.DISABLED)) {
+ if (nextStatus == Account.State.OFFLINE
+ && account.getStatus() != Account.State.CONNECTING
+ && account.getStatus() != Account.State.ONLINE
+ && account.getStatus() != Account.State.DISABLED
+ && account.getStatus() != Account.State.LOGGED_OUT) {
return;
}
if (nextStatus == Account.State.ONLINE) {
@@ -275,12 +284,14 @@ public class XmppConnection implements Runnable {
mXmppConnectionService.resetSendingToWaiting(account);
}
Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": connecting");
- features.encryptionEnabled = false;
+ this.loginInfo = null;
+ this.features.encryptionEnabled = false;
this.inSmacksSession = false;
this.quickStartInProgress = false;
this.isBound = false;
this.attempt++;
- this.verifiedHostname = null; // will be set if user entered hostname is being used or hostname was verified
+ this.verifiedHostname =
+ null; // will be set if user entered hostname is being used or hostname was verified
// with dnssec
try {
Socket localSocket;
@@ -327,12 +338,13 @@ public class XmppConnection implements Runnable {
}
} else {
final String domain = account.getServer();
- final List results;
+ final List results = new ArrayList<>();
final boolean hardcoded = extended && !account.getHostname().isEmpty();
if (hardcoded) {
- results = Resolver.fromHardCoded(account.getHostname(), account.getPort());
+ results.addAll(
+ Resolver.fromHardCoded(account.getHostname(), account.getPort()));
} else {
- results = Resolver.resolve(domain);
+ results.addAll(Resolver.resolve(domain));
}
if (Thread.currentThread().isInterrupted()) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Thread was interrupted");
@@ -359,7 +371,24 @@ public class XmppConnection implements Runnable {
+ storedBackupResult);
}
}
- for (Iterator iterator = results.iterator();
+ final StreamId streamId = this.streamId;
+ final Resolver.Result resumeLocation = streamId == null ? null : streamId.location;
+ if (resumeLocation != null) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": injected resume location on position 0");
+ results.add(0, resumeLocation);
+ }
+ final Resolver.Result seeOtherHost = this.seeOtherHostResolverResult;
+ if (seeOtherHost != null) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": injected see-other-host on position 0");
+ results.add(0, seeOtherHost);
+ }
+ for (final Iterator iterator = results.iterator();
iterator.hasNext(); ) {
final Resolver.Result result = iterator.next();
if (Thread.currentThread().isInterrupted()) {
@@ -373,7 +402,6 @@ public class XmppConnection implements Runnable {
features.encryptionEnabled = result.isDirectTls();
verifiedHostname =
result.isAuthenticated() ? result.getHostname().toString() : null;
- Log.d(Config.LOGTAG, "verified hostname " + verifiedHostname);
final InetSocketAddress addr;
if (result.getIp() != null) {
addr = new InetSocketAddress(result.getIp(), result.getPort());
@@ -421,6 +449,8 @@ public class XmppConnection implements Runnable {
mXmppConnectionService.databaseBackend.saveResolverResult(
domain, result);
}
+ this.currentResolverResult = result;
+ this.seeOtherHostResolverResult = null;
break; // successfully connected to server that speaks xmpp
} else {
FileBackend.close(localSocket);
@@ -497,8 +527,7 @@ public class XmppConnection implements Runnable {
tagReader.setInputStream(socket.getInputStream());
tagWriter.beginDocument();
final boolean quickStart;
- if (socket instanceof SSLSocket) {
- final SSLSocket sslSocket = (SSLSocket) socket;
+ if (socket instanceof SSLSocket sslSocket) {
SSLSockets.log(account, sslSocket);
quickStart = establishStream(SSLSockets.version(sslSocket));
} else {
@@ -508,7 +537,16 @@ public class XmppConnection implements Runnable {
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException();
}
- final boolean success = tag != null && tag.isStart("stream", Namespace.STREAMS);
+ if (tag == null) {
+ return false;
+ }
+ final boolean success = tag.isStart("stream", Namespace.STREAMS);
+ if (success) {
+ final var from = tag.getAttribute("from");
+ if (from == null || !from.equals(account.getServer())) {
+ throw new StateChangingException(Account.State.HOST_UNKNOWN);
+ }
+ }
if (success && quickStart) {
this.quickStartInProgress = true;
}
@@ -565,14 +603,18 @@ public class XmppConnection implements Runnable {
processStreamFeatures(nextTag);
} else if (nextTag.isStart("proceed", Namespace.TLS)) {
switchOverToTls();
+ } else if (nextTag.isStart("failure", Namespace.TLS)) {
+ throw new StateChangingException(Account.State.TLS_ERROR);
+ } else if (account.isOptionSet(Account.OPTION_REGISTER)
+ && nextTag.isStart("iq", Namespace.JABBER_CLIENT)) {
+ processIq(nextTag);
+ } else if (!isSecure() || this.loginInfo == null) {
+ throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
} else if (nextTag.isStart("success")) {
final Element success = tagReader.readElement(nextTag);
if (processSuccess(success)) {
break;
}
-
- } else if (nextTag.isStart("failure", Namespace.TLS)) {
- throw new StateChangingException(Account.State.TLS_ERROR);
} else if (nextTag.isStart("failure")) {
final Element failure = tagReader.readElement(nextTag);
processFailure(failure);
@@ -580,23 +622,34 @@ public class XmppConnection implements Runnable {
// two step sasl2 - we don’t support this yet
throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT);
} else if (nextTag.isStart("challenge")) {
- if (isSecure() && this.saslMechanism != null) {
- final Element challenge = tagReader.readElement(nextTag);
- processChallenge(challenge);
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": received 'challenge on an unsecure connection");
- throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT);
- }
+ final Element challenge = tagReader.readElement(nextTag);
+ processChallenge(challenge);
+ } else if (!LoginInfo.isSuccess(this.loginInfo)) {
+ throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
+ } else if (this.streamId != null
+ && nextTag.isStart("resumed", Namespace.STREAM_MANAGEMENT)) {
+ final Element resumed = tagReader.readElement(nextTag);
+ processResumed(resumed);
+ } else if (nextTag.isStart("failed", Namespace.STREAM_MANAGEMENT)) {
+ final Element failed = tagReader.readElement(nextTag);
+ processFailed(failed, true);
+ } else if (nextTag.isStart("iq", Namespace.JABBER_CLIENT)) {
+ processIq(nextTag);
+ } else if (!isBound) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": server sent unexpected"
+ + nextTag.identifier());
+ throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
+ } else if (nextTag.isStart("message", Namespace.JABBER_CLIENT)) {
+ processMessage(nextTag);
+ } else if (nextTag.isStart("presence", Namespace.JABBER_CLIENT)) {
+ processPresence(nextTag);
} else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) {
final Element enabled = tagReader.readElement(nextTag);
processEnabled(enabled);
- } else if (nextTag.isStart("resumed")) {
- final Element resumed = tagReader.readElement(nextTag);
- processResumed(resumed);
- } else if (nextTag.isStart("r")) {
+ } else if (nextTag.isStart("r", Namespace.STREAM_MANAGEMENT)) {
tagReader.readElement(nextTag);
if (Config.EXTENDED_SM_LOGGING) {
Log.d(
@@ -607,7 +660,7 @@ public class XmppConnection implements Runnable {
}
final AckPacket ack = new AckPacket(this.stanzasReceived);
tagWriter.writeStanzaAsync(ack);
- } else if (nextTag.isStart("a")) {
+ } else if (nextTag.isStart("a", Namespace.STREAM_MANAGEMENT)) {
boolean accountUiNeedsRefresh = false;
synchronized (NotificationService.CATCHUP_LOCK) {
if (mWaitingForSmCatchup.compareAndSet(true, false)) {
@@ -650,15 +703,13 @@ public class XmppConnection implements Runnable {
if (acknowledgedMessages) {
mXmppConnectionService.updateConversationUi();
}
- } else if (nextTag.isStart("failed")) {
- final Element failed = tagReader.readElement(nextTag);
- processFailed(failed, true);
- } else if (nextTag.isStart("iq")) {
- processIq(nextTag);
- } else if (nextTag.isStart("message")) {
- processMessage(nextTag);
- } else if (nextTag.isStart("presence")) {
- processPresence(nextTag);
+ } else {
+ Log.e(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": Encountered unknown stream element"
+ + nextTag.identifier());
+ throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
}
nextTag = tagReader.readTag();
}
@@ -682,8 +733,14 @@ public class XmppConnection implements Runnable {
} else {
throw new AssertionError("Missing implementation for " + version);
}
+ final LoginInfo currentLoginInfo = this.loginInfo;
+ if (currentLoginInfo == null || LoginInfo.isSuccess(currentLoginInfo)) {
+ throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
+ }
try {
- response.setContent(saslMechanism.getResponse(challenge.getContent(), sslSocketOrNull(socket)));
+ response.setContent(
+ currentLoginInfo.saslMechanism.getResponse(
+ challenge.getContent(), sslSocketOrNull(socket)));
} catch (final SaslMechanism.AuthenticationException e) {
// TODO: Send auth abort tag.
Log.e(Config.LOGTAG, e.toString());
@@ -700,8 +757,9 @@ public class XmppConnection implements Runnable {
} catch (final IllegalArgumentException e) {
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
}
- final SaslMechanism currentSaslMechanism = this.saslMechanism;
- if (currentSaslMechanism == null) {
+ final LoginInfo currentLoginInfo = this.loginInfo;
+ final SaslMechanism currentSaslMechanism = LoginInfo.mechanism(currentLoginInfo);
+ if (currentLoginInfo == null || currentSaslMechanism == null) {
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
}
final String challenge;
@@ -713,9 +771,9 @@ public class XmppConnection implements Runnable {
throw new AssertionError("Missing implementation for " + version);
}
try {
- currentSaslMechanism.getResponse(challenge, sslSocketOrNull(socket));
+ currentLoginInfo.success(challenge, sslSocketOrNull(socket));
} catch (final SaslMechanism.AuthenticationException e) {
- Log.e(Config.LOGTAG, String.valueOf(e));
+ Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": authentication failure ", e);
throw new StateChangingException(Account.State.UNAUTHORIZED);
}
Log.d(
@@ -774,8 +832,14 @@ public class XmppConnection implements Runnable {
+ ": server sent bound and resumed in SASL2 success");
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
}
- final boolean processNopStreamFeatures;
if (resumed != null && streamId != null) {
+ if (this.boundStreamFeatures != null) {
+ this.streamFeatures = this.boundStreamFeatures;
+ Log.d(
+ Config.LOGTAG,
+ "putting previous stream features back in place: "
+ + XmlHelper.printElementNames(this.boundStreamFeatures));
+ }
processResumed(resumed);
} else if (failed != null) {
processFailed(failed, false); // wait for new stream features
@@ -783,6 +847,8 @@ public class XmppConnection implements Runnable {
if (bound != null) {
clearIqCallbacks();
this.isBound = true;
+ processNopStreamFeatures();
+ this.boundStreamFeatures = this.streamFeatures;
final Element streamManagementEnabled =
bound.findChild("enabled", Namespace.STREAM_MANAGEMENT);
final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS);
@@ -792,19 +858,28 @@ public class XmppConnection implements Runnable {
processEnabled(streamManagementEnabled);
waitForDisco = true;
} else {
- //if we did not enable stream management in bind do it now
+ // if we did not enable stream management in bind do it now
waitForDisco = enableStreamManagement();
}
+ final boolean negotiatedCarbons;
if (carbonsEnabled != null) {
+ negotiatedCarbons = true;
Log.d(
Config.LOGTAG,
- account.getJid().asBareJid() + ": successfully enabled carbons");
+ account.getJid().asBareJid()
+ + ": successfully enabled carbons (via Bind 2.0)");
features.carbonsEnabled = true;
+ } else if (loginInfo.inlineBindFeatures.contains(Namespace.CARBONS)) {
+ negotiatedCarbons = true;
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": successfully enabled carbons (via Bind 2.0/implicit)");
+ features.carbonsEnabled = true;
+ } else {
+ negotiatedCarbons = false;
}
- sendPostBindInitialization(waitForDisco, carbonsEnabled != null);
- processNopStreamFeatures = true;
- } else {
- processNopStreamFeatures = false;
+ sendPostBindInitialization(waitForDisco, negotiatedCarbons);
}
final HashedToken.Mechanism tokenMechanism;
if (SaslMechanism.hashedToken(currentSaslMechanism)) {
@@ -815,10 +890,24 @@ public class XmppConnection implements Runnable {
tokenMechanism = null;
}
if (tokenMechanism != null && !Strings.isNullOrEmpty(token)) {
- this.account.setFastToken(tokenMechanism, token);
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": storing hashed token " + tokenMechanism);
+ if (ChannelBinding.priority(tokenMechanism.channelBinding)
+ >= ChannelBindingMechanism.getPriority(currentSaslMechanism)) {
+ this.account.setFastToken(tokenMechanism, token);
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": storing hashed token "
+ + tokenMechanism);
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": not accepting hashed token "
+ + tokenMechanism.name()
+ + " for log in mechanism "
+ + currentSaslMechanism.getMechanism());
+ this.account.resetFastToken();
+ }
} else if (this.hashTokenRequest != null) {
Log.w(
Config.LOGTAG,
@@ -826,10 +915,6 @@ public class XmppConnection implements Runnable {
+ ": no response to our hashed token request "
+ this.hashTokenRequest);
}
- // a successful resume will not send stream features
- if (processNopStreamFeatures) {
- processNopStreamFeatures();
- }
}
mXmppConnectionService.databaseBackend.updateAccount(account);
this.quickStartInProgress = false;
@@ -907,7 +992,7 @@ public class XmppConnection implements Runnable {
}
Log.d(Config.LOGTAG, failure.toString());
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version);
- if (SaslMechanism.hashedToken(this.saslMechanism)) {
+ if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resetting token");
account.resetFastToken();
mXmppConnectionService.databaseBackend.updateAccount(account);
@@ -933,7 +1018,7 @@ public class XmppConnection implements Runnable {
}
}
}
- if (SaslMechanism.hashedToken(this.saslMechanism)) {
+ if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
@@ -953,18 +1038,29 @@ public class XmppConnection implements Runnable {
}
private void processEnabled(final Element enabled) {
- final String streamId;
+ final String id;
if (enabled.getAttributeAsBoolean("resume")) {
- streamId = enabled.getAttribute("id");
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid().toString()
- + ": stream management enabled (resumable)");
+ id = enabled.getAttribute("id");
+ } else {
+ id = null;
+ }
+ final String locationAttribute = enabled.getAttribute("location");
+ final Resolver.Result currentResolverResult = this.currentResolverResult;
+ final Resolver.Result location;
+ if (Strings.isNullOrEmpty(locationAttribute) || currentResolverResult == null) {
+ location = null;
+ } else {
+ location = currentResolverResult.seeOtherHost(locationAttribute);
+ }
+ final StreamId streamId = id == null ? null : new StreamId(id, location);
+ if (streamId == null) {
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream management enabled");
} else {
Log.d(
Config.LOGTAG,
- account.getJid().asBareJid().toString() + ": stream management enabled");
- streamId = null;
+ account.getJid().asBareJid()
+ + ": stream management enabled. resume at: "
+ + streamId.location);
}
this.streamId = streamId;
this.stanzasReceived = 0;
@@ -1010,8 +1106,7 @@ public class XmppConnection implements Runnable {
Config.LOGTAG,
account.getJid().asBareJid() + ": resending " + failedStanzas.size() + " stanzas");
for (final AbstractAcknowledgeableStanza packet : failedStanzas) {
- if (packet instanceof MessagePacket) {
- MessagePacket message = (MessagePacket) packet;
+ if (packet instanceof MessagePacket message) {
mXmppConnectionService.markMessage(
account,
message.getTo().asBareJid(),
@@ -1074,8 +1169,7 @@ public class XmppConnection implements Runnable {
+ mStanzaQueue.keyAt(i));
}
final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
- if (stanza instanceof MessagePacket && acknowledgedListener != null) {
- final MessagePacket packet = (MessagePacket) stanza;
+ if (stanza instanceof MessagePacket packet && acknowledgedListener != null) {
final String id = packet.getId();
final Jid to = packet.getTo();
if (id != null && to != null) {
@@ -1092,20 +1186,13 @@ public class XmppConnection implements Runnable {
private @NonNull Element processPacket(final Tag currentTag, final int packetType)
throws IOException {
- final Element element;
- switch (packetType) {
- case PACKET_IQ:
- element = new IqPacket();
- break;
- case PACKET_MESSAGE:
- element = new MessagePacket();
- break;
- case PACKET_PRESENCE:
- element = new PresencePacket();
- break;
- default:
- throw new AssertionError("Should never encounter invalid type");
- }
+ final Element element =
+ switch (packetType) {
+ case PACKET_IQ -> new IqPacket();
+ case PACKET_MESSAGE -> new MessagePacket();
+ case PACKET_PRESENCE -> new PresencePacket();
+ default -> throw new AssertionError("Should never encounter invalid type");
+ };
element.setAttributes(currentTag.getAttributes());
Tag nextTag = tagReader.readTag();
if (nextTag == null) {
@@ -1159,52 +1246,74 @@ public class XmppConnection implements Runnable {
+ "'");
return;
}
- if (packet instanceof JinglePacket) {
+ if (Thread.currentThread().isInterrupted()) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + "Not processing iq. Thread was interrupted");
+ return;
+ }
+ if (packet instanceof JinglePacket jinglePacket && isBound) {
if (this.jingleListener != null) {
- this.jingleListener.onJinglePacketReceived(account, (JinglePacket) packet);
+ this.jingleListener.onJinglePacketReceived(account, jinglePacket);
}
} else {
- OnIqPacketReceived callback = null;
- synchronized (this.packetCallbacks) {
- 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)) {
- callback = packetCallbackDuple.second;
- packetCallbacks.remove(packet.getId());
- } else {
- Log.e(
- Config.LOGTAG,
- account.getJid().asBareJid().toString()
- + ": ignoring spoofed iq packet");
- }
- } else {
- if (packet.getFrom() != null
- && packet.getFrom().equals(packetCallbackDuple.first.getTo())) {
- callback = packetCallbackDuple.second;
- packetCallbacks.remove(packet.getId());
- } else {
- Log.e(
- Config.LOGTAG,
- account.getJid().asBareJid().toString()
- + ": ignoring spoofed iq packet");
- }
- }
- } else if (packet.getType() == IqPacket.TYPE.GET
- || packet.getType() == IqPacket.TYPE.SET) {
- callback = this.unregisteredIqListener;
- }
+ final OnIqPacketReceived callback = getIqPacketReceivedCallback(packet);
+ if (callback == null) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid().toString()
+ + ": no callback registered for IQ from "
+ + packet.getFrom());
+ return;
}
- if (callback != null) {
- try {
- callback.onIqPacketReceived(account, packet);
- } catch (StateChangingError error) {
- throw new StateChangingException(error.state);
+ try {
+ callback.onIqPacketReceived(account, packet);
+ } catch (final StateChangingError error) {
+ throw new StateChangingException(error.state);
+ }
+ }
+ }
+
+ private OnIqPacketReceived getIqPacketReceivedCallback(final IqPacket stanza)
+ throws StateChangingException {
+ final boolean isRequest =
+ stanza.getType() == IqPacket.TYPE.GET || stanza.getType() == IqPacket.TYPE.SET;
+ if (isRequest) {
+ if (isBound) {
+ return this.unregisteredIqListener;
+ } else {
+ throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
+ }
+ } else {
+ synchronized (this.packetCallbacks) {
+ final var pair = packetCallbacks.get(stanza.getId());
+ if (pair == null) {
+ return null;
+ }
+ if (pair.first.toServer(account)) {
+ if (stanza.fromServer(account)) {
+ packetCallbacks.remove(stanza.getId());
+ return pair.second;
+ } else {
+ Log.e(
+ Config.LOGTAG,
+ account.getJid().asBareJid().toString()
+ + ": ignoring spoofed iq packet");
+ }
+ } else {
+ if (stanza.getFrom() != null && stanza.getFrom().equals(pair.first.getTo())) {
+ packetCallbacks.remove(stanza.getId());
+ return pair.second;
+ } else {
+ Log.e(
+ Config.LOGTAG,
+ account.getJid().asBareJid().toString()
+ + ": ignoring spoofed iq packet");
+ }
}
}
}
+ return null;
}
private void processMessage(final Tag currentTag) throws IOException {
@@ -1219,11 +1328,18 @@ public class XmppConnection implements Runnable {
+ "'");
return;
}
+ if (Thread.currentThread().isInterrupted()) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + "Not processing message. Thread was interrupted");
+ return;
+ }
this.messageListener.onMessagePacketReceived(account, packet);
}
private void processPresence(final Tag currentTag) throws IOException {
- PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
+ final PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
if (!packet.valid()) {
Log.e(
Config.LOGTAG,
@@ -1234,6 +1350,13 @@ public class XmppConnection implements Runnable {
+ "'");
return;
}
+ if (Thread.currentThread().isInterrupted()) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + "Not processing presence. Thread was interrupted");
+ return;
+ }
this.presenceListener.onPresencePacketReceived(account, packet);
}
@@ -1247,8 +1370,9 @@ public class XmppConnection implements Runnable {
tagReader.readTag();
final Socket socket = this.socket;
final SSLSocket sslSocket = upgradeSocketToTls(socket);
- tagReader.setInputStream(sslSocket.getInputStream());
- tagWriter.setOutputStream(sslSocket.getOutputStream());
+ this.socket = sslSocket;
+ this.tagReader.setInputStream(sslSocket.getInputStream());
+ this.tagWriter.setOutputStream(sslSocket.getOutputStream());
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established");
final boolean quickStart;
try {
@@ -1314,7 +1438,7 @@ public class XmppConnection implements Runnable {
account.getJid().asBareJid()
+ ": quick start in progress. ignoring features: "
+ XmlHelper.printElementNames(this.streamFeatures));
- if (SaslMechanism.hashedToken(this.saslMechanism)) {
+ if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) {
return;
}
if (isFastTokenAvailable(
@@ -1363,6 +1487,8 @@ public class XmppConnection implements Runnable {
&& isSecure) {
authenticate(SaslMechanism.Version.SASL);
} else if (this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT)
+ && isSecure
+ && LoginInfo.isSuccess(loginInfo)
&& streamId != null
&& !inSmacksSession) {
if (Config.EXTENDED_SM_LOGGING) {
@@ -1372,12 +1498,14 @@ public class XmppConnection implements Runnable {
+ ": resuming after stanza #"
+ stanzasReceived);
}
- final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived);
+ final ResumePacket resume = new ResumePacket(this.streamId.id, stanzasReceived);
this.mSmCatchupMessageCounter.set(0);
this.mWaitingForSmCatchup.set(true);
this.tagWriter.writeStanzaAsync(resume);
} else if (needsBinding) {
- if (this.streamFeatures.hasChild("bind", Namespace.BIND) && isSecure) {
+ if (this.streamFeatures.hasChild("bind", Namespace.BIND)
+ && isSecure
+ && LoginInfo.isSuccess(loginInfo)) {
sendBindRequest();
} else {
Log.d(
@@ -1398,7 +1526,8 @@ public class XmppConnection implements Runnable {
private void authenticate() throws IOException {
final boolean isSecure = isSecure();
- if (isSecure && this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) {authenticate(SaslMechanism.Version.SASL_2);
+ if (isSecure && this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) {
+ authenticate(SaslMechanism.Version.SASL_2);
} else if (isSecure && this.streamFeatures.hasChild("mechanisms", Namespace.SASL)) {
authenticate(SaslMechanism.Version.SASL);
} else {
@@ -1422,11 +1551,13 @@ public class XmppConnection implements Runnable {
this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING);
final Collection channelBindings = ChannelBinding.of(cbElement);
final SaslMechanism.Factory factory = new SaslMechanism.Factory(account);
- final SaslMechanism saslMechanism = factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket));
- this.saslMechanism = validate(saslMechanism, mechanisms);
+ final SaslMechanism saslMechanism =
+ factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket));
+ this.validate(saslMechanism, mechanisms);
final boolean quickStartAvailable;
- final String firstMessage = this.saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket));
- final boolean usingFast = SaslMechanism.hashedToken(this.saslMechanism);
+ final String firstMessage =
+ saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket));
+ final boolean usingFast = SaslMechanism.hashedToken(saslMechanism);
final Element authenticate;
if (version == SaslMechanism.Version.SASL) {
authenticate = new Element("auth", Namespace.SASL);
@@ -1434,6 +1565,7 @@ public class XmppConnection implements Runnable {
authenticate.setContent(firstMessage);
}
quickStartAvailable = false;
+ this.loginInfo = new LoginInfo(saslMechanism, version, Collections.emptyList());
} else if (version == SaslMechanism.Version.SASL_2) {
final Element inline = authElement.findChild("inline", Namespace.SASL_2);
final boolean sm = inline != null && inline.hasChild("sm", Namespace.STREAM_MANAGEMENT);
@@ -1441,7 +1573,8 @@ public class XmppConnection implements Runnable {
if (usingFast) {
hashTokenRequest = null;
} else {
- final Element fast = inline == null ? null : inline.findChild("fast", Namespace.FAST);
+ final Element fast =
+ inline == null ? null : inline.findChild("fast", Namespace.FAST);
final Collection fastMechanisms = SaslMechanism.mechanisms(fast);
hashTokenRequest =
HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket));
@@ -1462,8 +1595,11 @@ public class XmppConnection implements Runnable {
return;
}
}
+ this.loginInfo = new LoginInfo(saslMechanism, version, bindFeatures);
this.hashTokenRequest = hashTokenRequest;
- authenticate = generateAuthenticationRequest(firstMessage, usingFast, hashTokenRequest, bindFeatures, sm);
+ authenticate =
+ generateAuthenticationRequest(
+ firstMessage, usingFast, hashTokenRequest, bindFeatures, sm);
} else {
throw new AssertionError("Missing implementation for " + version);
}
@@ -1478,8 +1614,8 @@ public class XmppConnection implements Runnable {
+ ": Authenticating with "
+ version
+ "/"
- + this.saslMechanism.getMechanism());
- authenticate.setAttribute("mechanism", this.saslMechanism.getMechanism());
+ + LoginInfo.mechanism(this.loginInfo).getMechanism());
+ authenticate.setAttribute("mechanism", LoginInfo.mechanism(this.loginInfo).getMechanism());
synchronized (this.mStanzaQueue) {
this.stanzasSentBeforeAuthentication = this.stanzasSent;
tagWriter.writeElement(authenticate);
@@ -1491,8 +1627,9 @@ public class XmppConnection implements Runnable {
return inline != null && inline.hasChild("fast", Namespace.FAST);
}
- @NonNull
- private SaslMechanism validate(final @Nullable SaslMechanism saslMechanism, Collection mechanisms) throws StateChangingException {
+ private void validate(
+ final @Nullable SaslMechanism saslMechanism, Collection mechanisms)
+ throws StateChangingException {
if (saslMechanism == null) {
Log.d(
Config.LOGTAG,
@@ -1502,7 +1639,7 @@ public class XmppConnection implements Runnable {
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
}
if (SaslMechanism.hashedToken(saslMechanism)) {
- return saslMechanism;
+ return;
}
final int pinnedMechanism = account.getPinnedMechanismPriority();
if (pinnedMechanism > saslMechanism.getPriority()) {
@@ -1517,11 +1654,12 @@ public class XmppConnection implements Runnable {
+ "). Possible downgrade attack?");
throw new StateChangingException(Account.State.DOWNGRADE_ATTACK);
}
- return saslMechanism;
}
- private Element generateAuthenticationRequest(final String firstMessage, final boolean usingFast) {
- return generateAuthenticationRequest(firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true);
+ private Element generateAuthenticationRequest(
+ final String firstMessage, final boolean usingFast) {
+ return generateAuthenticationRequest(
+ firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true);
}
private Element generateAuthenticationRequest(
@@ -1544,11 +1682,14 @@ public class XmppConnection implements Runnable {
.addChild("device")
.setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL));
}
- if (bind != null) {
+ // do not include bind if 'inlineStreamManagement' is missing and we have a streamId
+ // (because we would rather just do a normal SM/resume)
+ final boolean mayAttemptBind = streamId == null || inlineStreamManagement;
+ if (bind != null && mayAttemptBind) {
authenticate.addChild(generateBindRequest(bind));
}
if (inlineStreamManagement && streamId != null) {
- final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived);
+ final ResumePacket resume = new ResumePacket(this.streamId.id, stanzasReceived);
this.mSmCatchupMessageCounter.set(0);
this.mWaitingForSmCatchup.set(true);
authenticate.addChild(resume);
@@ -1711,8 +1852,10 @@ public class XmppConnection implements Runnable {
resetAttemptCount(true);
resetStreamId();
clearIqCallbacks();
- this.stanzasSent = 0;
- mStanzaQueue.clear();
+ synchronized (this.mStanzaQueue) {
+ this.stanzasSent = 0;
+ this.mStanzaQueue.clear();
+ }
this.redirectionUrl = null;
synchronized (this.disco) {
disco.clear();
@@ -1720,7 +1863,7 @@ public class XmppConnection implements Runnable {
synchronized (this.commands) {
this.commands.clear();
}
- this.saslMechanism = null;
+ this.loginInfo = null;
}
private void sendBindRequest() {
@@ -2164,6 +2307,29 @@ public class XmppConnection implements Runnable {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": policy violation. " + text);
failPendingMessages(text);
throw new StateChangingException(Account.State.POLICY_VIOLATION);
+ } else if (streamError.hasChild("see-other-host")) {
+ final String seeOtherHost = streamError.findChildContent("see-other-host");
+ final Resolver.Result currentResolverResult = this.currentResolverResult;
+ if (Strings.isNullOrEmpty(seeOtherHost) || currentResolverResult == null) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": stream error " + streamError);
+ throw new StateChangingException(Account.State.STREAM_ERROR);
+ }
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": see other host: "
+ + seeOtherHost
+ + " "
+ + currentResolverResult);
+ final Resolver.Result seeOtherResult = currentResolverResult.seeOtherHost(seeOtherHost);
+ if (seeOtherResult != null) {
+ this.seeOtherHostResolverResult = seeOtherResult;
+ throw new StateChangingException(Account.State.SEE_OTHER_HOST);
+ } else {
+ throw new StateChangingException(Account.State.STREAM_ERROR);
+ }
} else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
throw new StateChangingException(Account.State.STREAM_ERROR);
@@ -2174,8 +2340,7 @@ public class XmppConnection implements Runnable {
synchronized (this.mStanzaQueue) {
for (int i = 0; i < mStanzaQueue.size(); ++i) {
final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
- if (stanza instanceof MessagePacket) {
- final MessagePacket packet = (MessagePacket) stanza;
+ if (stanza instanceof MessagePacket packet) {
final String id = packet.getId();
final Jid to = packet.getTo();
mXmppConnectionService.markMessage(
@@ -2187,18 +2352,29 @@ public class XmppConnection implements Runnable {
private boolean establishStream(final SSLSockets.Version sslVersion)
throws IOException, InterruptedException {
- final SaslMechanism quickStartMechanism =
- SaslMechanism.ensureAvailable(account.getQuickStartMechanism(), sslVersion);
final boolean secureConnection = sslVersion != SSLSockets.Version.NONE;
+ final SaslMechanism quickStartMechanism;
+ if (secureConnection) {
+ quickStartMechanism =
+ SaslMechanism.ensureAvailable(account.getQuickStartMechanism(), sslVersion);
+ } else {
+ quickStartMechanism = null;
+ }
if (secureConnection
&& Config.QUICKSTART_ENABLED
&& quickStartMechanism != null
&& account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) {
mXmppConnectionService.restoredFromDatabaseLatch.await();
- this.saslMechanism = quickStartMechanism;
+ this.loginInfo =
+ new LoginInfo(
+ quickStartMechanism,
+ SaslMechanism.Version.SASL_2,
+ Bind2.QUICKSTART_FEATURES);
final boolean usingFast = quickStartMechanism instanceof HashedToken;
final Element authenticate =
- generateAuthenticationRequest(quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)), usingFast);
+ generateAuthenticationRequest(
+ quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)),
+ usingFast);
authenticate.setAttribute("mechanism", quickStartMechanism.getMechanism());
sendStartStream(true, false);
synchronized (this.mStanzaQueue) {
@@ -2225,7 +2401,7 @@ public class XmppConnection implements Runnable {
}
stream.setAttribute("version", "1.0");
stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE);
- stream.setAttribute("xmlns", "jabber:client");
+ stream.setAttribute("xmlns", Namespace.JABBER_CLIENT);
stream.setAttribute("xmlns:stream", Namespace.STREAMS);
tagWriter.writeTag(stream, flush);
}
@@ -2289,9 +2465,7 @@ public class XmppConnection implements Runnable {
+ " do not write stanza to unbound stream "
+ packet.toString());
}
- if (packet instanceof AbstractAcknowledgeableStanza) {
- AbstractAcknowledgeableStanza stanza = (AbstractAcknowledgeableStanza) packet;
-
+ if (packet instanceof AbstractAcknowledgeableStanza stanza) {
if (this.mStanzaQueue.size() != 0) {
int currentHighestKey = this.mStanzaQueue.keyAt(this.mStanzaQueue.size() - 1);
if (currentHighestKey != stanzasSent) {
@@ -2301,7 +2475,13 @@ public class XmppConnection implements Runnable {
++stanzasSent;
if (Config.EXTENDED_SM_LOGGING) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid()+": counting outbound "+packet.getName()+" as #" + stanzasSent);
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": counting outbound "
+ + packet.getName()
+ + " as #"
+ + stanzasSent);
}
this.mStanzaQueue.append(stanzasSent, stanza);
if (stanza instanceof MessagePacket && stanza.getId() != null && inSmacksSession) {
@@ -2422,6 +2602,7 @@ public class XmppConnection implements Runnable {
private void resetStreamId() {
this.streamId = null;
+ this.boundStreamFeatures = null;
}
private List> findDiscoItemsByFeature(final String feature) {
@@ -2480,10 +2661,15 @@ public class XmppConnection implements Runnable {
return servers.size() > 0 ? servers.get(0) : null;
}
- public int getTimeToNextAttempt() {
- final int additionalTime =
- account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0;
- final int interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300);
+ public int getTimeToNextAttempt(final boolean aggressive) {
+ final int interval;
+ if (aggressive) {
+ interval = Math.min((int) (3 * Math.pow(1.3, attempt)), 60);
+ } else {
+ final int additionalTime =
+ account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0;
+ interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300);
+ }
final int secondsSinceLast =
(int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
return interval - secondsSinceLast;
@@ -2584,6 +2770,67 @@ public class XmppConnection implements Runnable {
}
}
+ private static class LoginInfo {
+ public final SaslMechanism saslMechanism;
+ public final SaslMechanism.Version saslVersion;
+ public final List inlineBindFeatures;
+ public final AtomicBoolean success = new AtomicBoolean(false);
+
+ private LoginInfo(
+ final SaslMechanism saslMechanism,
+ final SaslMechanism.Version saslVersion,
+ final Collection inlineBindFeatures) {
+ Preconditions.checkNotNull(saslMechanism, "SASL Mechanism must not be null");
+ Preconditions.checkNotNull(saslVersion, "SASL version must not be null");
+ this.saslMechanism = saslMechanism;
+ this.saslVersion = saslVersion;
+ this.inlineBindFeatures =
+ inlineBindFeatures == null
+ ? Collections.emptyList()
+ : ImmutableList.copyOf(inlineBindFeatures);
+ }
+
+ public static SaslMechanism mechanism(final LoginInfo loginInfo) {
+ return loginInfo == null ? null : loginInfo.saslMechanism;
+ }
+
+ public void success(final String challenge, final SSLSocket sslSocket)
+ throws SaslMechanism.AuthenticationException {
+ final var response = this.saslMechanism.getResponse(challenge, sslSocket);
+ if (!Strings.isNullOrEmpty(response)) {
+ throw new SaslMechanism.AuthenticationException(
+ "processing success yielded another response");
+ }
+ if (this.success.compareAndSet(false, true)) {
+ return;
+ }
+ throw new SaslMechanism.AuthenticationException("Process 'success' twice");
+ }
+
+ public static boolean isSuccess(final LoginInfo loginInfo) {
+ return loginInfo != null && loginInfo.success.get();
+ }
+ }
+
+ private static class StreamId {
+ public final String id;
+ public final Resolver.Result location;
+
+ private StreamId(String id, Resolver.Result location) {
+ this.id = id;
+ this.location = location;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("id", id)
+ .add("location", location)
+ .toString();
+ }
+ }
+
private static class StateChangingError extends Error {
private final Account.State state;
@@ -2636,17 +2883,12 @@ public class XmppConnection implements Runnable {
&& pepPublishOptions();
}
- public boolean avatarConversion() {
- return hasDiscoFeature(account.getJid().asBareJid(), Namespace.AVATAR_CONVERSION)
- && pepPublishOptions();
- }
-
public boolean blocking() {
return hasDiscoFeature(account.getDomain(), Namespace.BLOCKING);
}
public boolean spamReporting() {
- return hasDiscoFeature(account.getDomain(), "urn:xmpp:reporting:reason:spam:0");
+ return hasDiscoFeature(account.getDomain(), Namespace.REPORTING);
}
public boolean flexibleOfflineMessageRetrieval() {
@@ -2666,7 +2908,8 @@ public class XmppConnection implements Runnable {
public boolean sm() {
return streamId != null
|| (connection.streamFeatures != null
- && connection.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT));
+ && connection.streamFeatures.hasChild(
+ "sm", Namespace.STREAM_MANAGEMENT));
}
public boolean csi() {
@@ -2787,8 +3030,8 @@ public class XmppConnection implements Runnable {
}
public boolean bookmarks2() {
- return Config
- .USE_BOOKMARKS2 /* || hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT)*/;
+ return pepPublishOptions()
+ && hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT);
}
public boolean externalServiceDiscovery() {
diff --git a/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java b/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java
index 21c957a0f..c3f847eca 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java
@@ -1,5 +1,6 @@
package eu.siacs.conversations.xmpp.bind;
+import com.google.common.base.Predicates;
import com.google.common.collect.Collections2;
import java.util.Arrays;
@@ -27,7 +28,12 @@ public class Bind2 {
if (inlineBind2Inline == null) {
return Collections.emptyList();
}
- return Collections2.transform(
- inlineBind2Inline.getChildren(), c -> c == null ? null : c.getAttribute("var"));
+ return Collections2.filter(
+ Collections2.transform(
+ Collections2.filter(
+ inlineBind2Inline.getChildren(),
+ c -> "feature".equals(c.getName())),
+ c -> c.getAttribute("var")),
+ Predicates.notNull());
}
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractContentMap.java
new file mode 100644
index 000000000..847678a05
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractContentMap.java
@@ -0,0 +1,82 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+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.JinglePacket;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public abstract class AbstractContentMap<
+ D extends GenericDescription, T extends GenericTransportInfo> {
+
+ public final Group group;
+
+ public final Map> contents;
+
+ protected AbstractContentMap(
+ final Group group, final Map> contents) {
+ this.group = group;
+ this.contents = contents;
+ }
+
+ public static class UnsupportedApplicationException extends IllegalArgumentException {
+ UnsupportedApplicationException(String message) {
+ super(message);
+ }
+ }
+
+ public static class UnsupportedTransportException extends IllegalArgumentException {
+ UnsupportedTransportException(String message) {
+ super(message);
+ }
+ }
+
+ public Set getSenders() {
+ return ImmutableSet.copyOf(Collections2.transform(contents.values(), dt -> dt.senders));
+ }
+
+ public List getNames() {
+ return ImmutableList.copyOf(contents.keySet());
+ }
+
+ JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) {
+ final JinglePacket jinglePacket = new JinglePacket(action, sessionId);
+ for (final Map.Entry> entry : this.contents.entrySet()) {
+ final DescriptionTransport descriptionTransport = entry.getValue();
+ final Content content =
+ new Content(
+ Content.Creator.INITIATOR,
+ descriptionTransport.senders,
+ entry.getKey());
+ if (descriptionTransport.description != null) {
+ content.addChild(descriptionTransport.description);
+ }
+ content.addChild(descriptionTransport.transport);
+ jinglePacket.addJingleContent(content);
+ }
+ if (this.group != null) {
+ jinglePacket.addGroup(this.group);
+ }
+ return jinglePacket;
+ }
+
+ void requireContentDescriptions() {
+ if (this.contents.size() == 0) {
+ throw new IllegalStateException("No contents available");
+ }
+ for (final Map.Entry> entry : this.contents.entrySet()) {
+ if (entry.getValue().description == null) {
+ throw new IllegalStateException(
+ String.format("%s is lacking content description", entry.getKey()));
+ }
+ }
+ }
+}
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 d719c729e..efc32f5ff 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java
@@ -1,47 +1,352 @@
package eu.siacs.conversations.xmpp.jingle;
+import android.util.Log;
+
import androidx.annotation.NonNull;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+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.Presence;
+import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
+import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
public abstract class AbstractJingleConnection {
public static final String JINGLE_MESSAGE_PROPOSE_ID_PREFIX = "jm-propose-";
public static final String JINGLE_MESSAGE_PROCEED_ID_PREFIX = "jm-proceed-";
+ protected static final List TERMINATED =
+ Arrays.asList(
+ State.ACCEPTED,
+ State.REJECTED,
+ State.REJECTED_RACED,
+ State.RETRACTED,
+ State.RETRACTED_RACED,
+ State.TERMINATED_SUCCESS,
+ State.TERMINATED_DECLINED_OR_BUSY,
+ State.TERMINATED_CONNECTIVITY_ERROR,
+ State.TERMINATED_CANCEL_OR_TIMEOUT,
+ State.TERMINATED_APPLICATION_FAILURE,
+ State.TERMINATED_SECURITY_ERROR);
+
+ 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,
+ State.TERMINATED_APPLICATION_FAILURE,
+ State.TERMINATED_SECURITY_ERROR));
+ transitionBuilder.put(
+ State.PROPOSED,
+ ImmutableList.of(
+ State.ACCEPTED,
+ State.PROCEED,
+ State.REJECTED,
+ State.RETRACTED,
+ State.TERMINATED_APPLICATION_FAILURE,
+ State.TERMINATED_SECURITY_ERROR,
+ State.TERMINATED_CONNECTIVITY_ERROR // only used when the xmpp connection
+ // rebinds
+ ));
+ transitionBuilder.put(
+ State.PROCEED,
+ ImmutableList.of(
+ State.REJECTED_RACED,
+ State.RETRACTED_RACED,
+ State.SESSION_INITIALIZED_PRE_APPROVED,
+ State.TERMINATED_SUCCESS,
+ State.TERMINATED_APPLICATION_FAILURE,
+ State.TERMINATED_SECURITY_ERROR,
+ 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_SUCCESS,
+ State.TERMINATED_DECLINED_OR_BUSY,
+ State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
+ // and IQ timeouts
+ State.TERMINATED_CANCEL_OR_TIMEOUT,
+ State.TERMINATED_APPLICATION_FAILURE,
+ State.TERMINATED_SECURITY_ERROR));
+ transitionBuilder.put(
+ State.SESSION_INITIALIZED_PRE_APPROVED,
+ ImmutableList.of(
+ State.SESSION_ACCEPTED,
+ State.TERMINATED_SUCCESS,
+ State.TERMINATED_DECLINED_OR_BUSY,
+ State.TERMINATED_CONNECTIVITY_ERROR, // at this state used for IQ errors
+ // and IQ timeouts
+ State.TERMINATED_CANCEL_OR_TIMEOUT,
+ State.TERMINATED_APPLICATION_FAILURE,
+ State.TERMINATED_SECURITY_ERROR));
+ 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,
+ State.TERMINATED_SECURITY_ERROR));
+ VALID_TRANSITIONS = transitionBuilder.build();
+ }
+
final JingleConnectionManager jingleConnectionManager;
protected final XmppConnectionService xmppConnectionService;
protected final Id id;
private final Jid initiator;
- AbstractJingleConnection(final JingleConnectionManager jingleConnectionManager, final Id id, final Jid initiator) {
+ protected State state = State.NULL;
+
+ 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);
-
public Id getId() {
return id;
}
+ boolean isInitiator() {
+ return initiator.equals(id.account.getJid());
+ }
+
+ boolean isResponder() {
+ return !initiator.equals(id.account.getJid());
+ }
+
+ public State getState() {
+ return this.state;
+ }
+
+ protected synchronized boolean isInState(State... state) {
+ return Arrays.asList(state).contains(this.state);
+ }
+
+ protected boolean transition(final State target) {
+ return transition(target, null);
+ }
+
+ protected 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);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ protected void transitionOrThrow(final State target) {
+ if (!transition(target)) {
+ throw new IllegalStateException(
+ String.format("Unable to transition from %s to %s", this.state, target));
+ }
+ }
+
+ boolean isTerminated() {
+ return TERMINATED.contains(this.state);
+ }
+
+ abstract void deliverPacket(JinglePacket jinglePacket);
+
+ protected void receiveOutOfOrderAction(
+ final JinglePacket jinglePacket, final JinglePacket.Action action) {
+ Log.d(
+ Config.LOGTAG,
+ String.format(
+ "%s: received %s even though we are in state %s",
+ id.account.getJid().asBareJid(), action, getState()));
+ if (isTerminated()) {
+ Log.d(
+ Config.LOGTAG,
+ String.format(
+ "%s: got a reason to terminate with out-of-order. but already in state %s",
+ id.account.getJid().asBareJid(), getState()));
+ respondWithOutOfOrder(jinglePacket);
+ } else {
+ terminateWithOutOfOrder(jinglePacket);
+ }
+ }
+
+ protected void terminateWithOutOfOrder(final JinglePacket jinglePacket) {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid() + ": terminating session with out-of-order");
+ terminateTransport();
+ transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE);
+ respondWithOutOfOrder(jinglePacket);
+ this.finish();
+ }
+
+ protected void finish() {
+ if (isTerminated()) {
+ this.jingleConnectionManager.finishConnectionOrThrow(this);
+ } else {
+ throw new AssertionError(
+ String.format("Unable to call finish from %s", this.state));
+ }
+ }
+
+ protected abstract void terminateTransport();
+
abstract void notifyRebound();
+ protected void sendSessionTerminate(
+ final Reason reason, final String text, final Consumer trigger) {
+ final State previous = this.state;
+ final State target = reasonToState(reason);
+ transitionOrThrow(target);
+ if (previous != State.NULL && trigger != null) {
+ trigger.accept(target);
+ }
+ final JinglePacket jinglePacket =
+ new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId);
+ jinglePacket.setReason(reason, text);
+ send(jinglePacket);
+ finish();
+ }
+
+ protected void send(final JinglePacket jinglePacket) {
+ jinglePacket.setTo(id.with);
+ xmppConnectionService.sendIqPacket(id.account, jinglePacket, this::handleIqResponse);
+ }
+
+ protected void respondOk(final JinglePacket jinglePacket) {
+ xmppConnectionService.sendIqPacket(
+ id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null);
+ }
+
+ protected void respondWithTieBreak(final JinglePacket jinglePacket) {
+ respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel");
+ }
+
+ protected void respondWithOutOfOrder(final JinglePacket jinglePacket) {
+ respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait");
+ }
+
+ protected void respondWithItemNotFound(final JinglePacket jinglePacket) {
+ respondWithJingleError(jinglePacket, null, "item-not-found", "cancel");
+ }
+
+ private void respondWithJingleError(
+ final IqPacket original,
+ String jingleCondition,
+ String condition,
+ String conditionType) {
+ jingleConnectionManager.respondWithJingleError(
+ id.account, original, jingleCondition, condition, conditionType);
+ }
+
+ private synchronized void handleIqResponse(final Account account, final IqPacket response) {
+ if (response.getType() == IqPacket.TYPE.ERROR) {
+ handleIqErrorResponse(response);
+ return;
+ }
+ if (response.getType() == IqPacket.TYPE.TIMEOUT) {
+ handleIqTimeoutResponse(response);
+ }
+ }
+
+ protected void handleIqErrorResponse(final IqPacket response) {
+ Preconditions.checkArgument(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 (isTerminated()) {
+ Log.i(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid()
+ + ": ignoring error because session was already terminated");
+ return;
+ }
+ this.terminateTransport();
+ 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;
+ }
+ transitionOrThrow(target);
+ this.finish();
+ }
+
+ protected void handleIqTimeoutResponse(final IqPacket response) {
+ Preconditions.checkArgument(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 (isTerminated()) {
+ Log.i(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid()
+ + ": ignoring error because session was already terminated");
+ return;
+ }
+ this.terminateTransport();
+ transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR);
+ this.finish();
+ }
+
+ protected boolean remoteHasFeature(final String feature) {
+ final Contact contact = id.getContact();
+ final Presence presence =
+ contact.getPresences().get(Strings.nullToEmpty(id.with.getResource()));
+ final ServiceDiscoveryResult serviceDiscoveryResult =
+ presence == null ? null : presence.getServiceDiscoveryResult();
+ final List features =
+ serviceDiscoveryResult == null ? null : serviceDiscoveryResult.getFeatures();
+ return features != null && features.contains(feature);
+ }
public static class Id implements OngoingRtpSession {
public final Account account;
@@ -73,8 +378,7 @@ public abstract class AbstractJingleConnection {
return new Id(
message.getConversation().getAccount(),
message.getCounterpart(),
- JingleConnectionManager.nextRandomId()
- );
+ JingleConnectionManager.nextRandomId());
}
public Contact getContact() {
@@ -86,9 +390,9 @@ public abstract class AbstractJingleConnection {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Id id = (Id) o;
- return Objects.equal(account.getUuid(), id.account.getUuid()) &&
- Objects.equal(with, id.with) &&
- Objects.equal(sessionId, id.sessionId);
+ return Objects.equal(account.getUuid(), id.account.getUuid())
+ && Objects.equal(with, id.with)
+ && Objects.equal(sessionId, id.sessionId);
}
@Override
@@ -122,23 +426,36 @@ public abstract class AbstractJingleConnection {
}
}
+ protected static State reasonToState(Reason reason) {
+ return switch (reason) {
+ case SUCCESS -> State.TERMINATED_SUCCESS;
+ case DECLINE, BUSY -> State.TERMINATED_DECLINED_OR_BUSY;
+ case CANCEL, TIMEOUT -> State.TERMINATED_CANCEL_OR_TIMEOUT;
+ case SECURITY_ERROR -> State.TERMINATED_SECURITY_ERROR;
+ case FAILED_APPLICATION, UNSUPPORTED_TRANSPORTS, UNSUPPORTED_APPLICATIONS -> State
+ .TERMINATED_APPLICATION_FAILURE;
+ default -> State.TERMINATED_CONNECTIVITY_ERROR;
+ };
+ }
public enum State {
- NULL, //default value; nothing has been sent or received yet
+ NULL, // default value; nothing has been sent or received yet
PROPOSED,
ACCEPTED,
PROCEED,
REJECTED,
- REJECTED_RACED, //used when we want to reject but haven’t received session init yet
+ REJECTED_RACED, // used when we want to reject but haven’t received session init yet
RETRACTED,
- RETRACTED_RACED, //used when receiving a retract after we already asked to proceed
- SESSION_INITIALIZED, //equal to 'PENDING'
+ RETRACTED_RACED, // used when receiving a retract after we already asked to proceed
+ 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)
- 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
+ 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)
+ 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_APPLICATION_FAILURE,
TERMINATED_SECURITY_ERROR
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java
index 97bf802fd..ab2dffc6d 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/ContentAddition.java
@@ -1,5 +1,7 @@
package eu.siacs.conversations.xmpp.jingle;
+import androidx.annotation.NonNull;
+
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.collect.Collections2;
@@ -8,6 +10,8 @@ import com.google.common.collect.ImmutableSet;
import java.util.Set;
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
public final class ContentAddition {
@@ -32,12 +36,13 @@ public final class ContentAddition {
Collections2.transform(
rtpContentMap.contents.entrySet(),
e -> {
- final RtpContentMap.DescriptionTransport dt = e.getValue();
+ final DescriptionTransport dt = e.getValue();
return new Summary(e.getKey(), dt.description.getMedia(), dt.senders);
}));
}
@Override
+ @NonNull
public String toString() {
return MoreObjects.toStringHelper(this)
.add("direction", direction)
@@ -77,6 +82,7 @@ public final class ContentAddition {
}
@Override
+ @NonNull
public String toString() {
return MoreObjects.toStringHelper(this)
.add("name", name)
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/DescriptionTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/DescriptionTransport.java
new file mode 100644
index 000000000..70d6c512c
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/DescriptionTransport.java
@@ -0,0 +1,19 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription;
+import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo;
+
+public class DescriptionTransport {
+
+ public final Content.Senders senders;
+ public final D description;
+ public final T transport;
+
+ public DescriptionTransport(
+ final Content.Senders senders, final D description, final T transport) {
+ this.senders = senders;
+ this.description = description;
+ this.transport = transport;
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java
index 83a2b95e4..a2a5c4032 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/DirectConnectionUtils.java
@@ -1,5 +1,7 @@
package eu.siacs.conversations.xmpp.jingle;
+import com.google.common.collect.ImmutableList;
+
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
@@ -15,13 +17,13 @@ import eu.siacs.conversations.xmpp.Jid;
public class DirectConnectionUtils {
- private static List getLocalAddresses() {
- final List addresses = new ArrayList<>();
+ public static List getLocalAddresses() {
+ final ImmutableList.Builder inetAddresses = new ImmutableList.Builder<>();
final Enumeration interfaces;
try {
interfaces = NetworkInterface.getNetworkInterfaces();
- } catch (SocketException e) {
- return addresses;
+ } catch (final SocketException e) {
+ return inetAddresses.build();
}
while (interfaces.hasMoreElements()) {
NetworkInterface networkInterface = interfaces.nextElement();
@@ -34,31 +36,15 @@ public class DirectConnectionUtils {
if (inetAddress instanceof Inet6Address) {
//let's get rid of scope
try {
- addresses.add(Inet6Address.getByAddress(inetAddress.getAddress()));
+ inetAddresses.add(Inet6Address.getByAddress(inetAddress.getAddress()));
} catch (UnknownHostException e) {
//ignored
}
} else {
- addresses.add(inetAddress);
+ inetAddresses.add(inetAddress);
}
}
}
- return addresses;
+ return inetAddresses.build();
}
-
- public static List getLocalCandidates(Jid jid) {
- SecureRandom random = new SecureRandom();
- ArrayList candidates = new ArrayList<>();
- for (InetAddress inetAddress : getLocalAddresses()) {
- final JingleCandidate candidate = new JingleCandidate(UUID.randomUUID().toString(), true);
- candidate.setHost(inetAddress.getHostAddress());
- candidate.setPort(random.nextInt(60000) + 1024);
- candidate.setType(JingleCandidate.TYPE_DIRECT);
- candidate.setJid(jid);
- candidate.setPriority(8257536 + candidates.size());
- candidates.add(candidate);
- }
- return candidates;
- }
-
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/FileTransferContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/FileTransferContentMap.java
new file mode 100644
index 000000000..c678c91cb
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/FileTransferContentMap.java
@@ -0,0 +1,219 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+
+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.FileTransferDescription;
+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.IbbTransportInfo;
+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.jingle.stanzas.SocksByteStreamsTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.transports.Transport;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class FileTransferContentMap
+ extends AbstractContentMap {
+
+ private static final List> SUPPORTED_TRANSPORTS =
+ Arrays.asList(
+ SocksByteStreamsTransportInfo.class,
+ IbbTransportInfo.class,
+ WebRTCDataChannelTransportInfo.class);
+
+ protected FileTransferContentMap(
+ final Group group, final Map>
+ contents) {
+ super(group, contents);
+ }
+
+ public static FileTransferContentMap of(final JinglePacket jinglePacket) {
+ final Map>
+ contents = of(jinglePacket.getJingleContents());
+ return new FileTransferContentMap(jinglePacket.getGroup(), contents);
+ }
+
+ public static DescriptionTransport of(
+ final Content content) {
+ final GenericDescription description = content.getDescription();
+ final GenericTransportInfo transportInfo = content.getTransport();
+ final Content.Senders senders = content.getSenders();
+ final FileTransferDescription fileTransferDescription;
+ if (description == null) {
+ fileTransferDescription = null;
+ } else if (description instanceof FileTransferDescription ftDescription) {
+ fileTransferDescription = ftDescription;
+ } else {
+ throw new UnsupportedApplicationException(
+ "Content does not contain file transfer description");
+ }
+ if (!SUPPORTED_TRANSPORTS.contains(transportInfo.getClass())) {
+ throw new UnsupportedTransportException("Content does not have supported transport");
+ }
+ return new DescriptionTransport<>(senders, fileTransferDescription, transportInfo);
+ }
+
+ private static Map>
+ of(final Map contents) {
+ return ImmutableMap.copyOf(
+ Maps.transformValues(contents, content -> content == null ? null : of(content)));
+ }
+
+ public static FileTransferContentMap of(
+ final FileTransferDescription.File file, final Transport.InitialTransportInfo initialTransportInfo) {
+ // TODO copy groups
+ final var transportInfo = initialTransportInfo.transportInfo;
+ return new FileTransferContentMap(initialTransportInfo.group,
+ Map.of(
+ initialTransportInfo.contentName,
+ new DescriptionTransport<>(
+ Content.Senders.INITIATOR,
+ FileTransferDescription.of(file),
+ transportInfo)));
+ }
+
+ public FileTransferDescription.File requireOnlyFile() {
+ if (this.contents.size() != 1) {
+ throw new IllegalStateException("Only one file at a time is supported");
+ }
+ final var dt = Iterables.getOnlyElement(this.contents.values());
+ return dt.description.getFile();
+ }
+
+ public FileTransferDescription requireOnlyFileTransferDescription() {
+ if (this.contents.size() != 1) {
+ throw new IllegalStateException("Only one file at a time is supported");
+ }
+ final var dt = Iterables.getOnlyElement(this.contents.values());
+ return dt.description;
+ }
+
+ public GenericTransportInfo requireOnlyTransportInfo() {
+ if (this.contents.size() != 1) {
+ throw new IllegalStateException(
+ "We expect exactly one content with one transport info");
+ }
+ final var dt = Iterables.getOnlyElement(this.contents.values());
+ return dt.transport;
+ }
+
+ public FileTransferContentMap withTransport(final Transport.TransportInfo transportWrapper) {
+ final var transportInfo = transportWrapper.transportInfo;
+ return new FileTransferContentMap(transportWrapper.group,
+ ImmutableMap.copyOf(
+ Maps.transformValues(
+ contents,
+ content -> {
+ if (content == null) {
+ return null;
+ }
+ return new DescriptionTransport<>(
+ content.senders, content.description, transportInfo);
+ })));
+ }
+
+ public FileTransferContentMap candidateUsed(final String streamId, final String cid) {
+ return new FileTransferContentMap(null,
+ ImmutableMap.copyOf(
+ Maps.transformValues(
+ contents,
+ content -> {
+ if (content == null) {
+ return null;
+ }
+ final var transportInfo =
+ new SocksByteStreamsTransportInfo(
+ streamId, Collections.emptyList());
+ final Element candidateUsed =
+ transportInfo.addChild(
+ "candidate-used",
+ Namespace.JINGLE_TRANSPORTS_S5B);
+ candidateUsed.setAttribute("cid", cid);
+ return new DescriptionTransport<>(
+ content.senders, null, transportInfo);
+ })));
+ }
+
+ public FileTransferContentMap candidateError(final String streamId) {
+ return new FileTransferContentMap(null,
+ ImmutableMap.copyOf(
+ Maps.transformValues(
+ contents,
+ content -> {
+ if (content == null) {
+ return null;
+ }
+ final var transportInfo =
+ new SocksByteStreamsTransportInfo(
+ streamId, Collections.emptyList());
+ transportInfo.addChild(
+ "candidate-error", Namespace.JINGLE_TRANSPORTS_S5B);
+ return new DescriptionTransport<>(
+ content.senders, null, transportInfo);
+ })));
+ }
+
+ public FileTransferContentMap proxyActivated(final String streamId, final String cid) {
+ return new FileTransferContentMap(null,
+ ImmutableMap.copyOf(
+ Maps.transformValues(
+ contents,
+ content -> {
+ if (content == null) {
+ return null;
+ }
+ final var transportInfo =
+ new SocksByteStreamsTransportInfo(
+ streamId, Collections.emptyList());
+ final Element candidateUsed =
+ transportInfo.addChild(
+ "activated", Namespace.JINGLE_TRANSPORTS_S5B);
+ candidateUsed.setAttribute("cid", cid);
+ return new DescriptionTransport<>(
+ content.senders, null, transportInfo);
+ })));
+ }
+
+ FileTransferContentMap transportInfo() {
+ return new FileTransferContentMap(this.group,
+ Maps.transformValues(
+ contents,
+ dt -> new DescriptionTransport<>(dt.senders, null, dt.transport)));
+ }
+
+ FileTransferContentMap transportInfo(
+ final String contentName, final IceUdpTransportInfo.Candidate candidate) {
+ final DescriptionTransport descriptionTransport =
+ contents.get(contentName);
+ if (descriptionTransport == null) {
+ throw new IllegalArgumentException(
+ "Unable to find transport info for content name " + contentName);
+ }
+ final WebRTCDataChannelTransportInfo transportInfo;
+ if (descriptionTransport.transport instanceof WebRTCDataChannelTransportInfo webRTCDataChannelTransportInfo) {
+ transportInfo = webRTCDataChannelTransportInfo;
+ } else {
+ throw new IllegalStateException("TransportInfo is not WebRTCDataChannel");
+ }
+ final WebRTCDataChannelTransportInfo newTransportInfo = transportInfo.cloneWrapper();
+ newTransportInfo.addCandidate(candidate);
+ return new FileTransferContentMap(
+ null,
+ ImmutableMap.of(
+ contentName,
+ new DescriptionTransport<>(
+ descriptionTransport.senders, null, newTransportInfo)));
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java
new file mode 100644
index 000000000..7b2f88457
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java
@@ -0,0 +1,98 @@
+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.primitives.Ints;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.utils.IP;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+import org.webrtc.PeerConnection;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public final class IceServers {
+
+ public static List parse(final IqPacket 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 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 (Strings.isNullOrEmpty(host) || port == null) {
+ continue;
+ }
+ if (port < 0 || port > 65535) {
+ 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.w(
+ Config.LOGTAG,
+ "skipping invalid combination of udp/tls in external services");
+ continue;
+ }
+
+ // STUN URLs do not support a query section since M110
+ final String uri;
+ if (Arrays.asList("stun", "stuns").contains(type)) {
+ uri = String.format("%s:%s:%s", type, IP.wrapIPv6(host), port);
+ } else {
+ uri =
+ String.format(
+ "%s:%s:%s?transport=%s",
+ type, IP.wrapIPv6(host), port, transport);
+ }
+
+ final PeerConnection.IceServer.Builder iceServerBuilder =
+ PeerConnection.IceServer.builder(uri);
+ iceServerBuilder.setTlsCertPolicy(
+ PeerConnection.TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK);
+ 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.w(
+ Config.LOGTAG,
+ "skipping "
+ + type
+ + "/"
+ + transport
+ + " without username and password");
+ continue;
+ }
+ final PeerConnection.IceServer iceServer =
+ iceServerBuilder.createIceServer();
+ Log.w(Config.LOGTAG, "discovered ICE Server: " + iceServer);
+ listBuilder.add(iceServer);
+ }
+ }
+ }
+ }
+ return listBuilder.build();
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java
deleted file mode 100644
index 78ffb28be..000000000
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java
+++ /dev/null
@@ -1,152 +0,0 @@
-package eu.siacs.conversations.xmpp.jingle;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import eu.siacs.conversations.xml.Element;
-import eu.siacs.conversations.xmpp.InvalidJid;
-import eu.siacs.conversations.xmpp.Jid;
-
-public class JingleCandidate {
-
- public static int TYPE_UNKNOWN;
- public static int TYPE_DIRECT = 0;
- public static int TYPE_PROXY = 1;
-
- private final boolean ours;
- private boolean usedByCounterpart = false;
- private final String cid;
- private String host;
- private int port;
- private int type;
- private Jid jid;
- private int priority;
-
- public JingleCandidate(String cid, boolean ours) {
- this.ours = ours;
- this.cid = cid;
- }
-
- public String getCid() {
- return cid;
- }
-
- public void setHost(String host) {
- this.host = host;
- }
-
- public String getHost() {
- return this.host;
- }
-
- public void setJid(final Jid jid) {
- this.jid = jid;
- }
-
- public Jid getJid() {
- return this.jid;
- }
-
- public void setPort(int port) {
- this.port = port;
- }
-
- public int getPort() {
- return this.port;
- }
-
- public void setType(int type) {
- this.type = type;
- }
-
- public void setType(String type) {
- if (type == null) {
- this.type = TYPE_UNKNOWN;
- return;
- }
- switch (type) {
- case "proxy":
- this.type = TYPE_PROXY;
- break;
- case "direct":
- this.type = TYPE_DIRECT;
- break;
- default:
- this.type = TYPE_UNKNOWN;
- break;
- }
- }
-
- public void setPriority(int i) {
- this.priority = i;
- }
-
- public int getPriority() {
- return this.priority;
- }
-
- public boolean equals(JingleCandidate other) {
- return this.getCid().equals(other.getCid());
- }
-
- public boolean equalValues(JingleCandidate other) {
- return other != null && other.getHost().equals(this.getHost()) && (other.getPort() == this.getPort());
- }
-
- public boolean isOurs() {
- return ours;
- }
-
- public int getType() {
- return this.type;
- }
-
- 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 candidates;
- }
-
- 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() {
- Element element = new Element("candidate");
- element.setAttribute("cid", this.getCid());
- element.setAttribute("host", this.getHost());
- element.setAttribute("port", Integer.toString(this.getPort()));
- if (jid != null) {
- element.setAttribute("jid", jid);
- }
- element.setAttribute("priority", Integer.toString(this.getPriority()));
- if (this.getType() == TYPE_DIRECT) {
- element.setAttribute("type", "direct");
- } else if (this.getType() == TYPE_PROXY) {
- element.setAttribute("type", "proxy");
- }
- return element;
- }
-
- public void flagAsUsedByCounterpart() {
- this.usedByCounterpart = true;
- }
-
- public boolean isUsedByCounterpart() {
- return this.usedByCounterpart;
- }
-
- public String toString() {
- return String.format("%s:%s (priority=%s,ours=%s)", getHost(), getPort(), getPriority(), isOurs());
- }
-}
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 b39673fa5..23d4f175b 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
@@ -12,20 +12,6 @@ import com.google.common.collect.Collections2;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableSet;
-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 java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
@@ -39,18 +25,33 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.XmppConnection;
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.IbbTransportInfo;
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.jingle.transports.InbandBytestreamsTransport;
+import eu.siacs.conversations.xmpp.jingle.transports.Transport;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
+import java.lang.ref.WeakReference;
+import java.security.SecureRandom;
+import java.util.Collection;
+import java.util.HashMap;
+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;
+
public class JingleConnectionManager extends AbstractConnectionManager {
static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE =
Executors.newSingleThreadScheduledExecutor();
@@ -63,8 +64,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
private final Cache terminatedSessions =
CacheBuilder.newBuilder().expireAfterWrite(24, TimeUnit.HOURS).build();
- private final HashMap primaryCandidates = new HashMap<>();
-
public JingleConnectionManager(XmppConnectionService service) {
super(service);
this.toneManager = new ToneManager(service);
@@ -73,7 +72,7 @@ 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);
+ return Base64.encodeToString(id, Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE);
}
public void deliverPacket(final Account account, final JinglePacket packet) {
@@ -92,7 +91,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
final String descriptionNamespace =
content == null ? null : content.getDescriptionNamespace();
final AbstractJingleConnection connection;
- if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) {
+ if (Namespace.JINGLE_APPS_FILE_TRANSFER.equals(descriptionNamespace)) {
connection = new JingleFileTransferConnection(this, id, from);
} else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace)
&& isUsingClearNet(account)) {
@@ -100,7 +99,8 @@ public class JingleConnectionManager extends AbstractConnectionManager {
this.terminatedSessions.asMap().containsKey(PersistableSessionId.of(id));
final boolean stranger =
isWithStrangerAndStrangerNotificationsAreOff(account, id.with);
- if (isBusy() || sessionEnded || stranger) {
+ final boolean busy = isBusy();
+ if (busy || sessionEnded || stranger) {
Log.d(
Config.LOGTAG,
id.account.getJid().asBareJid()
@@ -117,6 +117,15 @@ public class JingleConnectionManager extends AbstractConnectionManager {
sessionTermination.setTo(id.with);
sessionTermination.setReason(Reason.BUSY, null);
mXmppConnectionService.sendIqPacket(account, sessionTermination, null);
+ if (busy || stranger) {
+ writeLogMissedIncoming(
+ account,
+ id.with,
+ id.sessionId,
+ null,
+ System.currentTimeMillis(),
+ stranger);
+ }
return;
}
connection = new JingleRtpConnection(this, id, from);
@@ -158,10 +167,23 @@ public class JingleConnectionManager extends AbstractConnectionManager {
}
}
+ public boolean hasJingleRtpConnection(final Account account) {
+ for (AbstractJingleConnection connection : this.connections.values()) {
+ if (connection instanceof JingleRtpConnection rtpConnection) {
+ if (rtpConnection.isTerminated()) {
+ continue;
+ }
+ if (rtpConnection.id.account == account) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
public void notifyPhoneCallStarted() {
for (AbstractJingleConnection connection : connections.values()) {
- if (connection instanceof JingleRtpConnection) {
- final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection;
+ if (connection instanceof JingleRtpConnection rtpConnection) {
if (rtpConnection.isTerminated()) {
continue;
}
@@ -195,8 +217,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
private boolean hasMatchingRtpSession(
final Account account, final Jid with, final Set media) {
for (AbstractJingleConnection connection : this.connections.values()) {
- if (connection instanceof JingleRtpConnection) {
- final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection;
+ if (connection instanceof JingleRtpConnection rtpConnection) {
if (rtpConnection.isTerminated()) {
continue;
}
@@ -235,7 +256,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
final Element error = response.addChild("error");
error.setAttribute("type", conditionType);
error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas");
- error.addChild(jingleCondition, Namespace.JINGLE_ERRORS);
+ if (jingleCondition != null) {
+ error.addChild(jingleCondition, Namespace.JINGLE_ERRORS);
+ }
account.getXmppConnection().sendIqPacket(response, null);
}
@@ -254,8 +277,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
}
if ("accept".equals(message.getName())) {
for (AbstractJingleConnection connection : connections.values()) {
- if (connection instanceof JingleRtpConnection) {
- final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection;
+ if (connection instanceof JingleRtpConnection rtpConnection) {
final AbstractJingleConnection.Id id = connection.getId();
if (id.account == account && id.sessionId.equals(sessionId)) {
rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
@@ -266,6 +288,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return;
}
final boolean fromSelf = from.asBareJid().equals(account.getJid().asBareJid());
+ // XEP version 0.6.0 sends proceed, reject, ringing to bare jid
final boolean addressedDirectly = to != null && to.equals(account.getJid());
final AbstractJingleConnection.Id id;
if (fromSelf) {
@@ -310,6 +333,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
Config.LOGTAG,
id.account.getJid().asBareJid()
+ ": updated previous busy because call got picked up by another device");
+ mXmppConnectionService.getNotificationService().clearMissedCall(previousBusy);
return;
}
}
@@ -366,12 +390,14 @@ public class JingleConnectionManager extends AbstractConnectionManager {
this.connections.put(id, rtpConnection);
rtpConnection.setProposedMedia(ImmutableSet.copyOf(media));
rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp);
+ // TODO actually do the automatic accept?!
} else {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": our session won tie break. waiting for other party to accept. winningSession="
+ ourSessionId);
+ // TODO reject their session with ?
}
return;
}
@@ -379,7 +405,12 @@ public class JingleConnectionManager extends AbstractConnectionManager {
isWithStrangerAndStrangerNotificationsAreOff(account, id.with);
if (isBusy() || stranger) {
writeLogMissedIncoming(
- account, id.with.asBareJid(), id.sessionId, serverMsgId, timestamp);
+ account,
+ id.with.asBareJid(),
+ id.sessionId,
+ serverMsgId,
+ timestamp,
+ stranger);
if (stranger) {
Log.d(
Config.LOGTAG,
@@ -435,7 +466,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
- + ": no rtp session proposal found for "
+ + ": no rtp session ("
+ + sessionId
+ + ") proposal found for "
+ from
+ " to deliver proceed");
if (remoteMsgId == null) {
@@ -474,12 +507,19 @@ public class JingleConnectionManager extends AbstractConnectionManager {
+ " to deliver reject");
}
}
+ } else if (addressedDirectly && "ringing".equals(message.getName())) {
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + from + " started ringing");
+ updateProposedSessionDiscovered(
+ account, from, sessionId, DeviceDiscoveryState.DISCOVERED);
} else {
Log.d(
Config.LOGTAG,
- account.getJid().asBareJid()
- + ": retrieved out of order jingle message"
- + message);
+ account.getJid()
+ + ": retrieved out of order jingle message from "
+ + from
+ + message
+ + ", addressedDirectly="
+ + addressedDirectly);
}
}
@@ -514,10 +554,11 @@ public class JingleConnectionManager extends AbstractConnectionManager {
private void writeLogMissedIncoming(
final Account account,
- Jid with,
+ final Jid with,
final String sessionId,
- String serverMsgId,
- long timestamp) {
+ final String serverMsgId,
+ final long timestamp,
+ final boolean stranger) {
final Conversation conversation =
mXmppConnectionService.findOrCreateConversation(
account, with.asBareJid(), false, false);
@@ -527,7 +568,12 @@ public class JingleConnectionManager extends AbstractConnectionManager {
message.setBody(new RtpSessionStatus(false, 0).toString());
message.setServerMsgId(serverMsgId);
message.setTime(timestamp);
+ message.setCounterpart(with);
writeMessage(message);
+ if (stranger) {
+ return;
+ }
+ mXmppConnectionService.getNotificationService().pushMissedCallNow(message);
}
private void writeMessage(final Message message) {
@@ -548,13 +594,10 @@ 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, account.getJid());
- mXmppConnectionService.markMessage(message, Message.STATUS_WAITING);
- this.connections.put(id, connection);
- connection.init(message);
+ new JingleFileTransferConnection(this, message);
+ this.connections.put(connection.getId(), connection);
+ connection.sendSessionInitialize();
}
public Optional getOngoingRtpConnection(final Contact contact) {
@@ -593,15 +636,16 @@ public class JingleConnectionManager extends AbstractConnectionManager {
final AbstractJingleConnection.Id id = connection.getId();
if (this.connections.remove(id) == null) {
throw new IllegalStateException(
- String.format("Unable to finish connection with id=%s", id.toString()));
+ String.format("Unable to finish connection with id=%s", id));
}
+ // update chat UI to remove 'ongoing call' icon
+ mXmppConnectionService.updateConversationUi();
}
public boolean fireJingleRtpConnectionStateUpdates() {
boolean firedUpdates = false;
for (final AbstractJingleConnection connection : this.connections.values()) {
- if (connection instanceof JingleRtpConnection) {
- final JingleRtpConnection jingleRtpConnection = (JingleRtpConnection) connection;
+ if (connection instanceof JingleRtpConnection jingleRtpConnection) {
if (jingleRtpConnection.isTerminated()) {
continue;
}
@@ -612,73 +656,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return firedUpdates;
}
- void getPrimaryCandidate(
- final Account account,
- final boolean initiator,
- final OnPrimaryCandidateFound listener) {
- if (Config.DISABLE_PROXY_LOOKUP) {
- listener.onPrimaryCandidateFound(false, null);
- return;
- }
- if (!this.primaryCandidates.containsKey(account.getJid().asBareJid())) {
- final Jid proxy =
- account.getXmppConnection().findDiscoItemByFeature(Namespace.BYTE_STREAMS);
- if (proxy != null) {
- IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
- iq.setTo(proxy);
- iq.query(Namespace.BYTE_STREAMS);
- account.getXmppConnection()
- .sendIqPacket(
- iq,
- new OnIqPacketReceived() {
-
- @Override
- public void onIqPacketReceived(
- Account account, IqPacket packet) {
- 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) {
- try {
- JingleCandidate candidate =
- new JingleCandidate(nextRandomId(), true);
- candidate.setHost(host);
- candidate.setPort(Integer.parseInt(port));
- candidate.setType(JingleCandidate.TYPE_PROXY);
- candidate.setJid(proxy);
- candidate.setPriority(
- 655360 + (initiator ? 30 : 0));
- primaryCandidates.put(
- account.getJid().asBareJid(), candidate);
- listener.onPrimaryCandidateFound(true, candidate);
- } catch (final NumberFormatException e) {
- listener.onPrimaryCandidateFound(false, null);
- }
- } else {
- listener.onPrimaryCandidateFound(false, null);
- }
- }
- });
- } else {
- listener.onPrimaryCandidateFound(false, null);
- }
-
- } else {
- listener.onPrimaryCandidateFound(
- true, this.primaryCandidates.get(account.getJid().asBareJid()));
- }
- }
-
public void retractSessionProposal(final Account account, final Jid with) {
synchronized (this.rtpSessionProposals) {
RtpSessionProposal matchingProposal = null;
@@ -777,40 +754,53 @@ public class JingleConnectionManager extends AbstractConnectionManager {
return false;
}
- public void deliverIbbPacket(Account account, IqPacket packet) {
+ public void deliverIbbPacket(final Account account, final IqPacket packet) {
final String sid;
final Element payload;
+ final InbandBytestreamsTransport.PacketType packetType;
if (packet.hasChild("open", Namespace.IBB)) {
+ packetType = InbandBytestreamsTransport.PacketType.OPEN;
payload = packet.findChild("open", Namespace.IBB);
sid = payload.getAttribute("sid");
} else if (packet.hasChild("data", Namespace.IBB)) {
+ packetType = InbandBytestreamsTransport.PacketType.DATA;
payload = packet.findChild("data", Namespace.IBB);
sid = payload.getAttribute("sid");
} else if (packet.hasChild("close", Namespace.IBB)) {
+ packetType = InbandBytestreamsTransport.PacketType.CLOSE;
payload = packet.findChild("close", Namespace.IBB);
sid = payload.getAttribute("sid");
} else {
+ packetType = null;
payload = null;
sid = null;
}
- if (sid != null) {
- 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) {
- final JingleInBandTransport inBandTransport =
- (JingleInBandTransport) transport;
- if (inBandTransport.matches(account, sid)) {
- inBandTransport.deliverPayload(packet, payload);
+ if (sid == null) {
+ Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to deliver ibb packet. missing sid");
+ account.getXmppConnection()
+ .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
+ return;
+ }
+ for (final AbstractJingleConnection connection : this.connections.values()) {
+ if (connection instanceof JingleFileTransferConnection fileTransfer) {
+ final Transport transport = fileTransfer.getTransport();
+ if (transport instanceof InbandBytestreamsTransport inBandTransport) {
+ if (sid.equals(inBandTransport.getStreamId())) {
+ if (inBandTransport.deliverPacket(packetType, packet.getFrom(), payload)) {
+ account.getXmppConnection()
+ .sendIqPacket(
+ packet.generateResponse(IqPacket.TYPE.RESULT), null);
+ } else {
+ account.getXmppConnection()
+ .sendIqPacket(
+ packet.generateResponse(IqPacket.TYPE.ERROR), null);
}
return;
}
}
}
}
- Log.d(Config.LOGTAG, "unable to deliver ibb packet: " + packet.toString());
+ Log.d(Config.LOGTAG, account.getJid().asBareJid()+": unable to deliver ibb packet with sid="+sid);
account.getXmppConnection()
.sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null);
}
@@ -916,7 +906,8 @@ public class JingleConnectionManager extends AbstractConnectionManager {
}
}
- public void failProceed(Account account, final Jid with, final String sessionId, final String message) {
+ public void failProceed(
+ Account account, final Jid with, final String sessionId, final String message) {
final AbstractJingleConnection.Id id =
AbstractJingleConnection.Id.of(account, with, sessionId);
final AbstractJingleConnection existingJingleConnection = connections.get(id);
@@ -990,15 +981,11 @@ public class JingleConnectionManager extends AbstractConnectionManager {
FAILED;
public RtpEndUserState toEndUserState() {
- switch (this) {
- case SEARCHING:
- case SEARCHING_ACKNOWLEDGED:
- return RtpEndUserState.FINDING_DEVICE;
- case DISCOVERED:
- return RtpEndUserState.RINGING;
- default:
- return RtpEndUserState.CONNECTIVITY_ERROR;
- }
+ return switch (this) {
+ case SEARCHING, SEARCHING_ACKNOWLEDGED -> RtpEndUserState.FINDING_DEVICE;
+ case DISCOVERED -> RtpEndUserState.RINGING;
+ default -> RtpEndUserState.CONNECTIVITY_ERROR;
+ };
}
}
@@ -1008,10 +995,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
public final Set media;
private final Account account;
- 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;
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 c4ed04bd0..632a8f034 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java
@@ -1,1222 +1,306 @@
package eu.siacs.conversations.xmpp.jingle;
-import android.util.Base64;
import android.util.Log;
+import androidx.annotation.NonNull;
+
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
-import com.google.common.collect.Collections2;
-import com.google.common.collect.FluentIterable;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-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;
-import java.util.Map.Entry;
-import java.util.concurrent.ConcurrentHashMap;
+import com.google.common.hash.Hashing;
+import com.google.common.primitives.Ints;
+import com.google.common.util.concurrent.FutureCallback;
+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 eu.siacs.conversations.Config;
-import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
-import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.entities.Presence;
-import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.entities.Transferable;
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.utils.CryptoHelper;
-import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.OnIqPacketReceived;
-import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
+import eu.siacs.conversations.xmpp.XmppConnection;
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.IceUdpTransportInfo;
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.jingle.stanzas.SocksByteStreamsTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo;
+import eu.siacs.conversations.xmpp.jingle.transports.InbandBytestreamsTransport;
+import eu.siacs.conversations.xmpp.jingle.transports.SocksByteStreamsTransport;
+import eu.siacs.conversations.xmpp.jingle.transports.Transport;
+import eu.siacs.conversations.xmpp.jingle.transports.WebRTCDataChannelTransport;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
-public class JingleFileTransferConnection extends AbstractJingleConnection implements Transferable {
+import org.bouncycastle.crypto.engines.AESEngine;
+import org.bouncycastle.crypto.io.CipherInputStream;
+import org.bouncycastle.crypto.io.CipherOutputStream;
+import org.bouncycastle.crypto.modes.AEADBlockCipher;
+import org.bouncycastle.crypto.modes.GCMBlockCipher;
+import org.bouncycastle.crypto.params.AEADParameters;
+import org.bouncycastle.crypto.params.KeyParameter;
+import org.webrtc.IceCandidate;
- 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;
- private static final int JINGLE_STATUS_FAILED = 99;
- private static final int JINGLE_STATUS_OFFERED = -1;
+import java.io.Closeable;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Queue;
+import java.util.concurrent.CountDownLatch;
- private static final int MAX_IBB_BLOCK_SIZE = 8192;
+public class JingleFileTransferConnection extends AbstractJingleConnection
+ implements Transport.Callback, Transferable {
- private int ibbBlockSize = MAX_IBB_BLOCK_SIZE;
+ private final Message message;
- private int mJingleStatus = JINGLE_STATUS_OFFERED; //migrate to enum
- private int mStatus = Transferable.STATUS_UNKNOWN;
- private Message message;
- private Jid responder;
- private final List candidates = new ArrayList<>();
- private final ConcurrentHashMap connections = new ConcurrentHashMap<>();
+ private FileTransferContentMap initiatorFileTransferContentMap;
+ private FileTransferContentMap responderFileTransferContentMap;
- private String transportId;
- private FileTransferDescription description;
- private DownloadableFile file = null;
-
- private boolean proxyActivationFailed = false;
-
- private String contentName;
- private Content.Creator contentCreator;
- private Content.Senders contentSenders;
- private Class extends GenericTransportInfo> initialTransport;
- private boolean remoteSupportsOmemoJet;
-
- private int mProgress = 0;
-
- private boolean receivedCandidate = false;
- private boolean sentCandidate = false;
+ private Transport transport;
+ private TransportSecurity transportSecurity;
+ private AbstractFileTransceiver fileTransceiver;
+ private final Queue pendingIncomingIceCandidates = new LinkedList<>();
private boolean acceptedAutomatically = false;
- private boolean cancelled = false;
- private XmppAxolotlMessage mXmppAxolotlMessage;
-
- private JingleTransport transport = null;
-
- private OutputStream mFileOutputStream;
- private InputStream mFileInputStream;
-
- private final OnIqPacketReceived responseListener = (account, packet) -> {
- if (packet.getType() != IqPacket.TYPE.RESULT) {
- 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());
- }
- }
- };
- private byte[] expectedHash = new byte[0];
- private final OnFileTransmissionStatusChanged onFileTransmissionStatusChanged = new OnFileTransmissionStatusChanged() {
-
- @Override
- public void onFileTransmitted(DownloadableFile file) {
- if (responding()) {
- 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");
- }
- sendSuccess();
- xmppConnectionService.getFileBackend().updateFileParams(message);
- xmppConnectionService.databaseBackend.createMessage(message);
- xmppConnectionService.markMessage(message, Message.STATUS_RECEIVED);
- if (acceptedAutomatically) {
- message.markUnread();
- if (message.getEncryption() == Message.ENCRYPTION_PGP) {
- id.account.getPgpDecryptionService().decrypt(message, true);
- } else {
- 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) {
- id.account.getPgpDecryptionService().decrypt(message, true);
- }
- } else {
- if (description.getVersion() == FileTransferDescription.Version.FT_5) { //older Conversations will break when receiving a session-info
- sendHash();
- }
- if (message.getEncryption() == Message.ENCRYPTION_PGP) {
- id.account.getPgpDecryptionService().decrypt(message, false);
- }
- if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
- file.delete();
- }
- }
- Log.d(Config.LOGTAG, "successfully transmitted file:" + file.getAbsolutePath() + " (" + CryptoHelper.bytesToHex(file.getSha1Sum()) + ")");
- if (message.getEncryption() != Message.ENCRYPTION_PGP) {
- xmppConnectionService.getFileBackend().updateMediaScanner(file);
- }
- }
-
- @Override
- public void onFileTransferAborted() {
- JingleFileTransferConnection.this.sendSessionTerminate(Reason.CONNECTIVITY_ERROR);
- JingleFileTransferConnection.this.fail();
- }
- };
- private final OnTransportConnected onIbbTransportConnected = new OnTransportConnected() {
- @Override
- public void failed() {
- Log.d(Config.LOGTAG, "ibb open failed");
- }
-
- @Override
- public void established() {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ibb transport connected. sending file");
- mJingleStatus = JINGLE_STATUS_TRANSMITTING;
- JingleFileTransferConnection.this.transport.send(file, onFileTransmissionStatusChanged);
- }
- };
- private final OnProxyActivated onProxyActivated = new OnProxyActivated() {
-
- @Override
- public void success() {
- if (isInitiator()) {
- Log.d(Config.LOGTAG, "we were initiating. sending file");
- transport.send(file, onFileTransmissionStatusChanged);
- } else {
- transport.receive(file, onFileTransmissionStatusChanged);
- Log.d(Config.LOGTAG, "we were responding. receiving file");
- }
- }
-
- @Override
- public void failed() {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": proxy activation failed");
- proxyActivationFailed = true;
- if (isInitiator()) {
- sendFallbackToIbb();
- }
- }
- };
-
- JingleFileTransferConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) {
- super(jingleConnectionManager, id, initiator);
- }
-
- 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;
- }
- }
-
- //TODO get rid and use isInitiator() instead
- private boolean responding() {
- return responder != null && responder.equals(id.account.getJid());
- }
-
-
- InputStream getFileInputStream() {
- return this.mFileInputStream;
- }
-
- OutputStream getFileOutputStream() throws IOException {
- if (this.file == null) {
- Log.d(Config.LOGTAG, "file object was not assigned");
- return null;
- }
- 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;
- }
-
- @Override
- void deliverPacket(final JinglePacket packet) {
- final JinglePacket.Action action = packet.getAction();
- //TODO switch case
- if (action == JinglePacket.Action.SESSION_INITIATE) {
- init(packet);
- } else if (action == JinglePacket.Action.SESSION_TERMINATE) {
- final Reason reason = packet.getReason().reason;
- switch (reason) {
- case CANCEL:
- this.cancelled = true;
- this.fail();
- break;
- case SUCCESS:
- this.receiveSuccess();
- 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);
- } 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"))) {
- try {
- this.expectedHash = Base64.decode(hash.getContent(), Base64.DEFAULT);
- } catch (Exception e) {
- this.expectedHash = new byte[0];
- }
- }
- respondToIq(packet, true);
- } else if (action == JinglePacket.Action.TRANSPORT_INFO) {
- receiveTransportInfo(packet);
- } else if (action == JinglePacket.Action.TRANSPORT_REPLACE) {
- 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);
- }
- } else if (action == JinglePacket.Action.TRANSPORT_ACCEPT) {
- receiveTransportAccept(packet);
- } else {
- Log.d(Config.LOGTAG, "packet arrived in connection. action was " + packet.getAction());
- respondToIq(packet, false);
- }
- }
-
- @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) {
- response = packet.generateResponse(IqPacket.TYPE.RESULT);
- } else {
- response = packet.generateResponse(IqPacket.TYPE.ERROR);
- final Element error = response.addChild("error").setAttribute("type", "cancel");
- error.addChild("not-acceptable", "urn:ietf:params:xml:ns:xmpp-stanzas");
- }
- xmppConnectionService.sendIqPacket(id.account, response, null);
- }
-
- private void respondToIqWithOutOfOrder(final IqPacket packet) {
- final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
- 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");
- xmppConnectionService.sendIqPacket(id.account, response, null);
- }
-
- 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 -> {
- if (xmppAxolotlMessage != null) {
- init(message, xmppAxolotlMessage);
- } else {
- fail();
- }
- });
- } else {
- init(message, null);
- }
- }
-
- private void init(final Message message, final XmppAxolotlMessage xmppAxolotlMessage) {
- this.mXmppAxolotlMessage = xmppAxolotlMessage;
- this.contentCreator = Content.Creator.INITIATOR;
- this.contentSenders = Content.Senders.INITIATOR;
- this.contentName = JingleConnectionManager.nextRandomId();
+ public JingleFileTransferConnection(
+ final JingleConnectionManager jingleConnectionManager, final Message message) {
+ super(
+ jingleConnectionManager,
+ AbstractJingleConnection.Id.of(message),
+ message.getConversation().getAccount().getJid());
+ Preconditions.checkArgument(
+ message.isFileOrImage(),
+ "only file or images messages can be transported via jingle");
this.message = message;
- final List remoteFeatures = getRemoteFeatures();
- final FileTransferDescription.Version remoteVersion = getAvailableFileTransferVersion(remoteFeatures);
- 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;
- this.responder = this.id.with;
- this.transportId = JingleConnectionManager.nextRandomId();
- this.setupDescription(remoteVersion);
- if (this.initialTransport == IbbTransportInfo.class) {
- this.sendInitRequest();
- } else {
- gatherAndConnectDirectCandidates();
- this.jingleConnectionManager.getPrimaryCandidate(id.account, isInitiator(), (success, candidate) -> {
- if (success) {
- final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate);
- connections.put(candidate.getCid(), socksConnection);
- socksConnection.connect(new OnTransportConnected() {
-
- @Override
- public void failed() {
- Log.d(Config.LOGTAG, String.format("connection to our own proxy65 candidate failed (%s:%d)", candidate.getHost(), candidate.getPort()));
- sendInitRequest();
- }
-
- @Override
- public void established() {
- Log.d(Config.LOGTAG, "successfully connected to our own proxy65 candidate");
- mergeCandidate(candidate);
- sendInitRequest();
- }
- });
- mergeCandidate(candidate);
- } else {
- Log.d(Config.LOGTAG, "no proxy65 candidate of our own was found");
- sendInitRequest();
- }
- });
- }
-
+ xmppConnectionService.markMessage(message, Message.STATUS_WAITING);
}
- private void gatherAndConnectDirectCandidates() {
- final List directCandidates;
- if (Config.USE_DIRECT_JINGLE_CANDIDATES) {
- if (id.account.isOnion() || xmppConnectionService.useTorToConnect()) {
- directCandidates = Collections.emptyList();
- } else {
- directCandidates = DirectConnectionUtils.getLocalCandidates(id.account.getJid());
- }
- } else {
- directCandidates = Collections.emptyList();
- }
- for (JingleCandidate directCandidate : directCandidates) {
- final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, directCandidate);
- connections.put(directCandidate.getCid(), socksConnection);
- candidates.add(directCandidate);
- }
- }
-
- 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;
- }
- }
-
- private List getRemoteFeatures() {
- final String resource = Strings.nullToEmpty(this.id.with.getResource());
- final Presence presence = this.id.account.getRoster().getContact(id.with).getPresences().get(resource);
- final ServiceDiscoveryResult result = presence != null ? presence.getServiceDiscoveryResult() : null;
- return result == null ? Collections.emptyList() : result.getFeatures();
- }
-
- private void init(JinglePacket packet) { //should move to deliverPacket
- //TODO if not 'OFFERED' reply with out-of-order
- this.mJingleStatus = JINGLE_STATUS_INITIATED;
- final Conversation conversation = this.xmppConnectionService.findOrCreateConversation(id.account, id.with.asBareJid(), false, false);
+ public JingleFileTransferConnection(
+ final JingleConnectionManager jingleConnectionManager,
+ final Id id,
+ final Jid initiator) {
+ super(jingleConnectionManager, id, initiator);
+ 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.setErrorMessage(null);
this.message.setTransferable(this);
- this.message.setCounterpart(this.id.with);
- this.responder = this.id.account.getJid();
- final Content content = packet.getJingleContent();
- final GenericTransportInfo transportInfo = content.getTransport();
- this.contentCreator = content.getCreator();
- Content.Senders senders;
- try {
- senders = content.getSenders();
- } catch (final Exception e) {
- senders = Content.Senders.INITIATOR;
- }
- this.contentSenders = senders;
- this.contentName = content.getAttribute("name");
-
- 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();
- }
- 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();
-
- final Element fileOffer = this.description.getFileOffer();
-
- if (fileOffer != null) {
- boolean remoteIsUsingJet = false;
- Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX);
- 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, id.account.getJid().asBareJid() + ": received jingle file offer with JET");
- encrypted = security.findChild("encrypted", AxolotlService.PEP_PREFIX);
- remoteIsUsingJet = true;
- }
- }
- if (encrypted != null) {
- this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().asBareJid());
- }
- Element fileSize = fileOffer.findChild("size");
- final String path = fileOffer.findChildContent("name");
- if (path != null) {
- AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(path);
- if (VALID_IMAGE_EXTENSIONS.contains(extension.main)) {
- message.setType(Message.TYPE_IMAGE);
- xmppConnectionService.getFileBackend().setupRelativeFilePath(message, message.getUuid() + "." + extension.main);
- } else if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
- if (VALID_IMAGE_EXTENSIONS.contains(extension.secondary)) {
- message.setType(Message.TYPE_IMAGE);
- xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + "." + extension.secondary);
- } else {
- message.setType(Message.TYPE_FILE);
- xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + (extension.secondary != null ? ("." + extension.secondary) : ""));
- }
- message.setEncryption(Message.ENCRYPTION_PGP);
- } else {
- message.setType(Message.TYPE_FILE);
- xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + (extension.main != null ? ("." + extension.main) : ""));
- }
- long size = parseLong(fileSize, 0);
- message.setBody(Long.toString(size));
- conversation.add(message);
- jingleConnectionManager.updateConversationUi(true);
- this.file = this.xmppConnectionService.getFileBackend().getFile(message, false);
- if (mXmppAxolotlMessage != null) {
- XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = id.account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage, false);
- if (transportMessage != null) {
- message.setEncryption(Message.ENCRYPTION_AXOLOTL);
- this.file.setKey(transportMessage.getKey());
- this.file.setIv(transportMessage.getIv());
- message.setFingerprint(transportMessage.getFingerprint());
- } else {
- Log.d(Config.LOGTAG, "could not process KeyTransportMessage");
- }
- }
- message.resetFileParams();
- //legacy OMEMO encrypted file transfers reported the file size after encryption
- //JET reports the plain text size. however lower levels of our receiving code still
- //expect the cipher text size. so we just + 16 bytes (auth tag size) here
- this.file.setExpectedSize(size + (remoteIsUsingJet ? 16 : 0));
-
- respondToIq(packet, true);
-
- 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.with);
- this.acceptedAutomatically = true;
- this.sendAccept();
- } else {
- message.markUnread();
- Log.d(Config.LOGTAG,
- "not auto accepting new file offer with size: "
- + size
- + " allowed size:"
- + this.jingleConnectionManager
- .getAutoAcceptFileSize());
- this.xmppConnectionService.getNotificationService().push(message);
- }
- Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize());
- return;
- }
- respondToIq(packet, false);
- }
- }
-
- 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.contentSenders, this.contentName);
- 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;
- }
- 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 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()));
- }
- 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");
- 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", description.getVersion().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.addJingleChild(checksum);
- xmppConnectionService.sendIqPacket(id.account, packet, (account, response) -> {
- if (response.getType() == IqPacket.TYPE.ERROR) {
- Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignoring error response to our session-info (hash transmission)");
- }
- });
- }
-
- private 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 == S5BTransportInfo.class) {
- sendAcceptSocks();
- } else {
- sendAcceptIbb();
- }
- }
-
- private void sendAcceptSocks() {
- gatherAndConnectDirectCandidates();
- this.jingleConnectionManager.getPrimaryCandidate(this.id.account, isInitiator(), (success, candidate) -> {
- final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT);
- final Content content = new Content(contentCreator, contentSenders, contentName);
- content.setDescription(this.description);
- if (success && candidate != null && !equalCandidateExists(candidate)) {
- final JingleSocks5Transport socksConnection = new JingleSocks5Transport(this, candidate);
- connections.put(candidate.getCid(), socksConnection);
- socksConnection.connect(new OnTransportConnected() {
-
- @Override
- public void failed() {
- Log.d(Config.LOGTAG, "connection to our own proxy65 candidate failed");
- content.setTransport(new S5BTransportInfo(transportId, getOurCandidates()));
- packet.addJingleContent(content);
- sendJinglePacket(packet);
- connectNextCandidate();
- }
-
- @Override
- public void established() {
- Log.d(Config.LOGTAG, "connected to proxy65 candidate");
- mergeCandidate(candidate);
- content.setTransport(new S5BTransportInfo(transportId, getOurCandidates()));
- packet.addJingleContent(content);
- sendJinglePacket(packet);
- connectNextCandidate();
- }
- });
- } else {
- Log.d(Config.LOGTAG, "did not find a proxy65 candidate for ourselves");
- content.setTransport(new S5BTransportInfo(transportId, getOurCandidates()));
- packet.addJingleContent(content);
- sendJinglePacket(packet);
- connectNextCandidate();
- }
- });
- }
-
- private void sendAcceptIbb() {
- this.transport = new JingleInBandTransport(this, this.transportId, this.ibbBlockSize);
- final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_ACCEPT);
- final Content content = new Content(contentCreator, contentSenders, contentName);
- content.setDescription(this.description);
- content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
- packet.addJingleContent(content);
- this.transport.receive(file, onFileTransmissionStatusChanged);
- this.sendJinglePacket(packet);
- }
-
- private JinglePacket bootstrapPacket(JinglePacket.Action action) {
- final JinglePacket packet = new JinglePacket(action, this.id.sessionId);
- packet.setTo(id.with);
- return packet;
- }
-
- private void sendJinglePacket(JinglePacket packet) {
- xmppConnectionService.sendIqPacket(id.account, packet, responseListener);
- }
-
- private void sendJinglePacket(JinglePacket packet, OnIqPacketReceived callback) {
- xmppConnectionService.sendIqPacket(id.account, packet, callback);
- }
-
- private void receiveAccept(JinglePacket packet) {
- if (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, id.account.getJid().asBareJid() + ": received out of order session-accept");
- respondToIqWithOutOfOrder(packet);
- return;
- }
- this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
- xmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
- 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);
- //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 (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);
- this.transport.connect(onIbbTransportConnected);
- } else {
- respondToIq(packet, false);
- }
- }
-
- private void receiveTransportInfo(JinglePacket packet) {
- final Content content = packet.getJingleContent();
- 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 = 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);
- if (connection != null) {
- connection.setActivated(true);
- } else {
- Log.d(Config.LOGTAG, "activated connection not found");
- sendSessionTerminate(Reason.FAILED_TRANSPORT);
- this.fail();
- }
- }
- } else if (s5BTransportInfo.hasChild("proxy-error")) {
- respondToIq(packet, true);
- onProxyActivated.failed();
- } 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 (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);
- if (candidate == null) {
- Log.d(Config.LOGTAG, "could not find candidate with cid=" + cid);
- respondToIq(packet, false);
- return;
- }
- respondToIq(packet, true);
- candidate.flagAsUsedByCounterpart();
- this.receivedCandidate = true;
- if (mJingleStatus == JINGLE_STATUS_ACCEPTED && this.sentCandidate) {
- this.connect();
- } else {
- Log.d(Config.LOGTAG, "ignoring because file is already in transmission or we haven't sent our candidate yet status=" + mJingleStatus + " sentCandidate=" + sentCandidate);
- }
- } else {
- respondToIq(packet, false);
- }
- } else {
- respondToIq(packet, false);
- }
- } else {
- respondToIq(packet, true);
- }
- }
-
- private void connect() {
- final JingleSocks5Transport connection = chooseConnection();
- this.transport = connection;
- if (connection == null) {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": could not find suitable candidate");
- this.disconnectSocks5Connections();
- if (isInitiator()) {
- this.sendFallbackToIbb();
- }
- } else {
- //TODO at this point we can already close other connections to free some resources
- final JingleCandidate candidate = connection.getCandidate();
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": elected candidate " + candidate.toString());
- this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
- if (connection.needsActivation()) {
- if (connection.getCandidate().isOurs()) {
- final String sid;
- 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 {
- sid = getTransportId();
- }
- Log.d(Config.LOGTAG, "candidate "
- + connection.getCandidate().getCid()
- + " was our proxy. going to activate");
- IqPacket activation = new IqPacket(IqPacket.TYPE.SET);
- activation.setTo(connection.getCandidate().getJid());
- activation.query("http://jabber.org/protocol/bytestreams")
- .setAttribute("sid", sid);
- activation.query().addChild("activate")
- .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());
- sendProxyError();
- onProxyActivated.failed();
- } else {
- sendProxyActivated(connection.getCandidate().getCid());
- onProxyActivated.success();
- }
- });
- } else {
- Log.d(Config.LOGTAG,
- "candidate "
- + connection.getCandidate().getCid()
- + " was a proxy. waiting for other party to activate");
- }
- } else {
- if (isInitiator()) {
- Log.d(Config.LOGTAG, "we were initiating. sending file");
- connection.send(file, onFileTransmissionStatusChanged);
- } else {
- Log.d(Config.LOGTAG, "we were responding. receiving file");
- connection.receive(file, onFileTransmissionStatusChanged);
- }
- }
- }
- }
-
- private JingleSocks5Transport chooseConnection() {
- final List establishedConnections = FluentIterable.from(connections.entrySet())
- .transform(Entry::getValue)
- .filter(c -> (c != null && c.isEstablished() && (c.getCandidate().isUsedByCounterpart() || !c.getCandidate().isOurs())))
- .toSortedList((a, b) -> {
- final int compare = Integer.compare(b.getCandidate().getPriority(), a.getCandidate().getPriority());
- if (compare == 0) {
- if (isInitiator()) {
- //pick the one we sent a candidate-used for (meaning not ours)
- return a.getCandidate().isOurs() ? 1 : -1;
- } else {
- //pick the one they sent a candidate-used for (meaning ours)
- return a.getCandidate().isOurs() ? -1 : 1;
- }
- }
- return compare;
- });
- return Iterables.getFirst(establishedConnections, null);
- }
-
- private void sendSuccess() {
- sendSessionTerminate(Reason.SUCCESS);
- this.disconnectSocks5Connections();
- this.mJingleStatus = JINGLE_STATUS_FINISHED;
- this.message.setStatus(Message.STATUS_RECEIVED);
- this.message.setTransferable(null);
- this.xmppConnectionService.updateMessage(message, false);
- this.jingleConnectionManager.finishConnection(this);
- }
-
- private void sendFallbackToIbb() {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": sending fallback to ibb");
- final JinglePacket packet = this.bootstrapPacket(JinglePacket.Action.TRANSPORT_REPLACE);
- final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
- this.transportId = JingleConnectionManager.nextRandomId();
- content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
- packet.addJingleContent(content);
- this.sendJinglePacket(packet);
- }
-
-
- private void receiveFallbackToIbb(final JinglePacket packet, final IbbTransportInfo transportInfo) {
- if (isInitiator()) {
- 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, 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, id.account.getJid().asBareJid() + ": receiving fallback to ibb");
- 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 = 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, contentSenders, contentName);
- content.setTransport(new IbbTransportInfo(this.transportId, this.ibbBlockSize));
- answer.addJingleContent(content);
-
- respondToIq(packet, true);
-
- 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");
- transport.connect(onIbbTransportConnected);
- }
- });
- } else {
- this.transport.receive(file, onFileTransmissionStatusChanged);
- this.sendJinglePacket(answer);
- }
- }
-
- private void receiveTransportAccept(JinglePacket packet) {
- if (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, id.account.getJid().asBareJid() + ": received out of order transport-accept");
- respondToIqWithOutOfOrder(packet);
- return;
- }
- this.proxyActivationFailed = false; //fallback accepted; now we no longer need to accept another one;
- 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)) {
- 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
- if (isInitiator()) {
- this.transport.connect(onIbbTransportConnected);
- }
- } else {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received invalid transport-accept");
- respondToIq(packet, false);
- }
- }
-
- private void receiveSuccess() {
- if (isInitiator()) {
- this.mJingleStatus = JINGLE_STATUS_FINISHED;
- this.xmppConnectionService.markMessage(this.message, Message.STATUS_SEND_RECEIVED);
- this.disconnectSocks5Connections();
- if (this.transport instanceof JingleInBandTransport) {
- this.transport.disconnect();
- }
- this.message.setTransferable(null);
- this.jingleConnectionManager.finishConnection(this);
- } else {
- Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session-terminate/success while responding");
- }
}
@Override
- public void cancel() {
- this.cancelled = true;
- abort(Reason.CANCEL);
+ void deliverPacket(final JinglePacket jinglePacket) {
+ switch (jinglePacket.getAction()) {
+ case SESSION_ACCEPT -> receiveSessionAccept(jinglePacket);
+ case SESSION_INITIATE -> receiveSessionInitiate(jinglePacket);
+ case SESSION_INFO -> receiveSessionInfo(jinglePacket);
+ case SESSION_TERMINATE -> receiveSessionTerminate(jinglePacket);
+ case TRANSPORT_ACCEPT -> receiveTransportAccept(jinglePacket);
+ case TRANSPORT_INFO -> receiveTransportInfo(jinglePacket);
+ case TRANSPORT_REPLACE -> receiveTransportReplace(jinglePacket);
+ default -> {
+ respondOk(jinglePacket);
+ Log.d(
+ Config.LOGTAG,
+ String.format(
+ "%s: received unhandled jingle action %s",
+ id.account.getJid().asBareJid(), jinglePacket.getAction()));
+ }
+ }
}
- private void abort(final Reason reason) {
- this.disconnectSocks5Connections();
- if (this.transport instanceof JingleInBandTransport) {
- this.transport.disconnect();
- }
- sendSessionTerminate(reason);
- 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.jingleConnectionManager.updateConversationUi(true);
+ public void sendSessionInitialize() {
+ final ListenableFuture> keyTransportMessage;
+ if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
+ keyTransportMessage =
+ Futures.transform(
+ id.account
+ .getAxolotlService()
+ .prepareKeyTransportMessage(requireConversation()),
+ Optional::of,
+ MoreExecutors.directExecutor());
} else {
- this.xmppConnectionService.markMessage(this.message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : null);
- this.message.setTransferable(null);
+ keyTransportMessage = Futures.immediateFuture(Optional.empty());
+ }
+ Futures.addCallback(
+ keyTransportMessage,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final Optional xmppAxolotlMessage) {
+ sendSessionInitialize(xmppAxolotlMessage.orElse(null));
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ Log.d(Config.LOGTAG, "can not send message");
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private void sendSessionInitialize(final XmppAxolotlMessage xmppAxolotlMessage) {
+ this.transport = setupTransport();
+ this.transport.setTransportCallback(this);
+ final File file = xmppConnectionService.getFileBackend().getFile(message);
+ final var fileDescription =
+ new FileTransferDescription.File(
+ file.length(),
+ file.getName(),
+ message.getMimeType(),
+ Collections.emptyList());
+ final var transportInfoFuture = this.transport.asInitialTransportInfo();
+ Futures.addCallback(
+ transportInfoFuture,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(
+ final Transport.InitialTransportInfo initialTransportInfo) {
+ final FileTransferContentMap contentMap =
+ FileTransferContentMap.of(fileDescription, initialTransportInfo);
+ sendSessionInitialize(xmppAxolotlMessage, contentMap);
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {}
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private Conversation requireConversation() {
+ final var conversational = message.getConversation();
+ if (conversational instanceof Conversation c) {
+ return c;
+ } else {
+ throw new IllegalStateException("Message had no proper conversation attached");
}
}
- private void fail() {
- fail(null);
- }
-
- private void fail(String errorMessage) {
- this.mJingleStatus = JINGLE_STATUS_FAILED;
- this.disconnectSocks5Connections();
- if (this.transport instanceof JingleInBandTransport) {
- this.transport.disconnect();
- }
- FileBackend.close(mFileInputStream);
- FileBackend.close(mFileOutputStream);
- if (this.message != null) {
- if (responding()) {
- this.message.setTransferable(new TransferablePlaceholder(cancelled ? Transferable.STATUS_CANCELLED : Transferable.STATUS_FAILED));
- if (this.file != null) {
- file.delete();
+ private void sendSessionInitialize(
+ final XmppAxolotlMessage xmppAxolotlMessage, final FileTransferContentMap contentMap) {
+ if (transition(
+ State.SESSION_INITIALIZED,
+ () -> this.initiatorFileTransferContentMap = contentMap)) {
+ final var jinglePacket =
+ contentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId);
+ if (xmppAxolotlMessage != null) {
+ this.transportSecurity =
+ new TransportSecurity(
+ xmppAxolotlMessage.getInnerKey(), xmppAxolotlMessage.getIV());
+ final var contents = jinglePacket.getJingleContents();
+ final var rawContent =
+ contents.get(Iterables.getOnlyElement(contentMap.contents.keySet()));
+ if (rawContent != null) {
+ rawContent.setSecurity(xmppAxolotlMessage);
}
- this.jingleConnectionManager.updateConversationUi(true);
+ }
+ jinglePacket.setTo(id.with);
+ xmppConnectionService.sendIqPacket(
+ id.account,
+ jinglePacket,
+ (a, response) -> {
+ if (response.getType() == IqPacket.TYPE.RESULT) {
+ xmppConnectionService.markMessage(message, Message.STATUS_OFFERED);
+ return;
+ }
+ if (response.getType() == IqPacket.TYPE.ERROR) {
+ handleIqErrorResponse(response);
+ return;
+ }
+ if (response.getType() == IqPacket.TYPE.TIMEOUT) {
+ handleIqTimeoutResponse(response);
+ }
+ });
+ this.transport.readyToSentAdditionalCandidates();
+ }
+ }
+
+ private void receiveSessionAccept(final JinglePacket jinglePacket) {
+ Log.d(Config.LOGTAG, "receive file transfer session accept");
+ if (isResponder()) {
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT);
+ return;
+ }
+ final FileTransferContentMap contentMap;
+ try {
+ contentMap = FileTransferContentMap.of(jinglePacket);
+ contentMap.requireOnlyFileTransferDescription();
+ } catch (final RuntimeException e) {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid() + ": improperly formatted contents",
+ Throwables.getRootCause(e));
+ respondOk(jinglePacket);
+ sendSessionTerminate(Reason.of(e), e.getMessage());
+ return;
+ }
+ receiveSessionAccept(jinglePacket, contentMap);
+ }
+
+ private void receiveSessionAccept(
+ final JinglePacket jinglePacket, final FileTransferContentMap contentMap) {
+ if (transition(State.SESSION_ACCEPTED, () -> setRemoteContentMap(contentMap))) {
+ respondOk(jinglePacket);
+ final var transport = this.transport;
+ if (configureTransportWithPeerInfo(transport, contentMap)) {
+ transport.connect();
} else {
- this.xmppConnectionService.markMessage(this.message,
- Message.STATUS_SEND_FAILED,
- cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage);
- this.message.setTransferable(null);
+ Log.e(
+ Config.LOGTAG,
+ "Transport in session accept did not match our session-initialize");
+ terminateTransport();
+ sendSessionTerminate(
+ Reason.FAILED_APPLICATION,
+ "Transport in session accept did not match our session-initialize");
}
- }
- this.jingleConnectionManager.finishConnection(this);
- }
-
- private void sendSessionTerminate(Reason reason) {
- final JinglePacket packet = bootstrapPacket(JinglePacket.Action.SESSION_TERMINATE);
- packet.setReason(reason, null);
- this.sendJinglePacket(packet);
- }
-
- private void connectNextCandidate() {
- for (JingleCandidate candidate : this.candidates) {
- if ((!connections.containsKey(candidate.getCid()) && (!candidate
- .isOurs()))) {
- this.connectWithCandidate(candidate);
- return;
- }
- }
- this.sendCandidateError();
- }
-
- private void connectWithCandidate(final JingleCandidate candidate) {
- final JingleSocks5Transport socksConnection = new JingleSocks5Transport(
- this, candidate);
- connections.put(candidate.getCid(), socksConnection);
- socksConnection.connect(new OnTransportConnected() {
-
- @Override
- public void failed() {
- Log.d(Config.LOGTAG,
- "connection failed with " + candidate.getHost() + ":"
- + candidate.getPort());
- connectNextCandidate();
- }
-
- @Override
- public void established() {
- Log.d(Config.LOGTAG,
- "established connection with " + candidate.getHost()
- + ":" + candidate.getPort());
- sendCandidateUsed(candidate.getCid());
- }
- });
- }
-
- private void disconnectSocks5Connections() {
- Iterator> it = this.connections
- .entrySet().iterator();
- while (it.hasNext()) {
- Entry pairs = it.next();
- pairs.getValue().disconnect();
- it.remove();
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ id.account.getJid().asBareJid() + ": receive out of order session-accept");
+ receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT);
}
}
- private void sendProxyActivated(String cid) {
- final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
- final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
- content.setTransport(new S5BTransportInfo(this.transportId, new Element("activated").setAttribute("cid", cid)));
- packet.addJingleContent(content);
- this.sendJinglePacket(packet);
- }
-
- private void sendProxyError() {
- final JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
- final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
- content.setTransport(new S5BTransportInfo(this.transportId, new Element("proxy-error")));
- packet.addJingleContent(content);
- this.sendJinglePacket(packet);
- }
-
- private void sendCandidateUsed(final String cid) {
- JinglePacket packet = bootstrapPacket(JinglePacket.Action.TRANSPORT_INFO);
- final Content content = new Content(this.contentCreator, this.contentSenders, this.contentName);
- content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-used").setAttribute("cid", cid)));
- packet.addJingleContent(content);
- this.sentCandidate = true;
- if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
- connect();
- }
- this.sendJinglePacket(packet);
- }
-
- private void sendCandidateError() {
- 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.contentSenders, this.contentName);
- content.setTransport(new S5BTransportInfo(this.transportId, new Element("candidate-error")));
- packet.addJingleContent(content);
- this.sentCandidate = true;
- this.sendJinglePacket(packet);
- if (receivedCandidate && mJingleStatus == JINGLE_STATUS_ACCEPTED) {
- connect();
- }
- }
-
- private int getJingleStatus() {
- return this.mJingleStatus;
- }
-
- private boolean equalCandidateExists(JingleCandidate candidate) {
- for (JingleCandidate c : this.candidates) {
- if (c.equalValues(candidate)) {
- return true;
- }
- }
- return false;
- }
-
- private void mergeCandidate(JingleCandidate candidate) {
- for (JingleCandidate c : this.candidates) {
- if (c.equals(candidate)) {
- return;
- }
- }
- this.candidates.add(candidate);
- }
-
- private void mergeCandidates(List