diff --git a/.builds/debian-stable.yml b/.builds/debian-stable.yml index d9f977820..470145212 100644 --- a/.builds/debian-stable.yml +++ b/.builds/debian-stable.yml @@ -28,6 +28,6 @@ tasks: sed -ie 's/\/\/ INSERT/implementation "io.sentry:sentry-android:5.6.1"/' build.gradle - build: | cd cheogram-android - ./gradlew assembleCheogramFreeCompatDebug + ./gradlew assembleCheogramFreeDebug - assets: | - mv cheogram-android/build/outputs/apk/cheogramFreeCompat/debug/*.apk cheogram.apk + mv cheogram-android/build/outputs/apk/cheogramFree/debug/*.apk cheogram.apk diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index badbbf5d7..e25e1be7a 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -22,14 +22,10 @@ jobs: run: mkdir libs && wget -O libs/libwebrtc-m92.aar https://gultsch.de/files/libwebrtc-m92.aar - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Build Quicksy (Compat) - run: ./gradlew assembleQuicksyFreeCompatDebug - - name: Build Quicksy (System) - run: ./gradlew assembleQuicksyFreeSystemDebug - - name: Build Conversations (Compat) - run: ./gradlew assembleConversationsFreeCompatDebug - - name: Build Conversations (System) - run: ./gradlew assembleConversationsFreeSystemDebug + - name: Build Quicksy + run: ./gradlew assembleQuicksyFreeDebug + - name: Build Conversations + run: ./gradlew assembleConversationsFreeDebug - uses: actions/upload-artifact@v2 with: name: Conversations all-flavors (debug) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2bb0a938..b7ef6c6fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### Version 2.10.3 + +* Store files in location appropriate for Android 11 +* Attempt to reconnect call after network switch + ### Version 2.10.2 * Fix crash when rendering some quotes diff --git a/build.gradle b/build.gradle index 1adb59f15..6a182af6e 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.1' + classpath 'com.android.tools.build:gradle:7.1.2' } } @@ -38,14 +38,13 @@ def urlFile = { url, name -> configurations { playstoreImplementation - compatImplementation - conversationsFreeCompatImplementation - cheogramFreeCompatImplementation - conversationsPlaystoreCompatImplementation - conversationsPlaystoreSystemImplementation - quicksyPlaystoreCompatImplementation - quicksyPlaystoreSystemImplementation - quicksyFreeCompatImplementation + freeImplementation + conversationsFreeImplementation + conversationsPlaystorImplementation + conversationsPlaystoreImplementation + quicksyPlaystoreImplementation + quicksyPlaystoreImplementation + quicksyFreeImplementation quicksyImplementation } @@ -57,22 +56,19 @@ dependencies { exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' } - conversationsPlaystoreCompatImplementation("com.android.installreferrer:installreferrer:2.2") - conversationsPlaystoreSystemImplementation("com.android.installreferrer:installreferrer:2.2") - quicksyPlaystoreCompatImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1' - quicksyPlaystoreSystemImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1' + conversationsPlaystoreImplementation("com.android.installreferrer:installreferrer:2.2") + quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1' implementation 'org.sufficientlysecure:openpgp-api:10.0' implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' - implementation 'androidx.appcompat:appcompat:1.3.1' + implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'androidx.exifinterface:exifinterface:1.3.3' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'androidx.emoji:emoji:1.1.0' implementation 'com.google.android.material:material:1.4.0' - compatImplementation 'androidx.emoji:emoji-appcompat:1.1.0' - conversationsFreeCompatImplementation 'androidx.emoji:emoji-bundled:1.1.0' - cheogramFreeCompatImplementation 'androidx.emoji:emoji-bundled:1.1.0' - quicksyFreeCompatImplementation 'androidx.emoji:emoji-bundled:1.1.0' + + implementation "androidx.emoji2:emoji2:1.1.0-rc01" + freeImplementation "androidx.emoji2:emoji2-bundled:1.1.0-rc01" + implementation 'org.bouncycastle:bcmail-jdk15on:1.64' //zxing stopped supporting Java 7 so we have to stick with 3.3.3 //https://github.com/zxing/zxing/issues/1170 @@ -138,7 +134,7 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } - flavorDimensions("mode", "distribution", "emoji") + flavorDimensions("mode", "distribution") productFlavors { @@ -169,45 +165,21 @@ android { playstore { dimension "distribution" - versionNameSuffix "+p" + versionNameSuffix "+playstore" } free { dimension "distribution" - versionNameSuffix "+f" - } - system { - dimension "emoji" - versionNameSuffix "s" - } - compat { - dimension "emoji" - versionNameSuffix "c" + versionNameSuffix "+free" } } sourceSets { - quicksyFreeSystem { + quicksyFree { java { srcDir 'src/quicksyFree/java' } } - quicksyFreeCompat { - java { - srcDir 'src/freeCompat/java' - srcDir 'src/quicksyFree/java' - } - } - quicksyPlaystoreCompat { - java { - srcDir 'src/playstoreCompat/java' - srcDir 'src/quicksyPlaystore/java' - } - res { - srcDir 'src/playstoreCompat/res' - srcDir 'src/quicksyPlaystore/res' - } - } - quicksyPlaystoreSystem { + quicksyPlaystore { java { srcDir 'src/quicksyPlaystore/java' } @@ -215,39 +187,17 @@ android { srcDir 'src/quicksyPlaystore/res' } } - conversationsFreeCompat { - java { - srcDir 'src/freeCompat/java' - srcDir 'src/conversationsFree/java' - } - } - conversationsFreeSystem { + conversationsFree { java { srcDir 'src/conversationsFree/java' } } - cheogramFreeCompat { - java { - srcDir 'src/freeCompat/java' - srcDir 'src/conversationsFree/java' - } - } - cheogramFreeSystem { + cheogramFree { java { srcDir 'src/conversationsFree/java' } } - conversationsPlaystoreCompat { - java { - srcDir 'src/playstoreCompat/java' - srcDir 'src/conversationsPlaystore/java' - } - res { - srcDir 'src/playstoreCompat/res' - srcDir 'src/conversationsPlaystore/res' - } - } - conversationsPlaystoreSystem { + conversationsPlaystore { java { srcDir 'src/conversationsPlaystore/java' } @@ -262,13 +212,11 @@ android { shrinkResources true minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - versionNameSuffix "r" } debug { shrinkResources true minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - versionNameSuffix "d" } } diff --git a/src/cheogram/java/eu/siacs/conversations/services/ImportBackupService.java b/src/cheogram/java/eu/siacs/conversations/services/ImportBackupService.java index 9c6ebaafd..a1b5f9e77 100644 --- a/src/cheogram/java/eu/siacs/conversations/services/ImportBackupService.java +++ b/src/cheogram/java/eu/siacs/conversations/services/ImportBackupService.java @@ -128,16 +128,19 @@ public class ImportBackupService extends Service { final List accounts = mDatabaseBackend.getAccountJids(false); final ArrayList backupFiles = new ArrayList<>(); final Set apps = new HashSet<>(Arrays.asList("Conversations", "Quicksy", getString(R.string.app_name))); - for (String app : apps) { - final File directory = new File(FileBackend.getBackupDirectory(app)); + final List directories = new ArrayList<>(); + for (final String app : apps) { + directories.add(FileBackend.getLegacyBackupDirectory(app)); + } + directories.add(FileBackend.getBackupDirectory(this)); + for (final File directory : directories) { if (!directory.exists() || !directory.isDirectory()) { Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath()); continue; } final File[] files = directory.listFiles(); if (files == null) { - onBackupFilesLoaded.onBackupFilesLoaded(backupFiles); - return; + continue; } for (final File file : files) { if (file.isFile() && file.getName().endsWith(".ceb")) { diff --git a/src/compat/java/eu/siacs/conversations/ui/widget/EmojiWrapperEditText.java b/src/compat/java/eu/siacs/conversations/ui/widget/EmojiWrapperEditText.java deleted file mode 100644 index 01905e376..000000000 --- a/src/compat/java/eu/siacs/conversations/ui/widget/EmojiWrapperEditText.java +++ /dev/null @@ -1,18 +0,0 @@ -package eu.siacs.conversations.ui.widget; - -import android.content.Context; -import android.util.AttributeSet; - -import androidx.emoji.widget.EmojiAppCompatEditText; - -public class EmojiWrapperEditText extends EmojiAppCompatEditText { - - public EmojiWrapperEditText(Context context) { - super(context); - } - - public EmojiWrapperEditText(Context context, AttributeSet attrs) { - super(context, attrs); - } - -} \ No newline at end of file diff --git a/src/compat/java/eu/siacs/conversations/utils/EmojiWrapper.java b/src/compat/java/eu/siacs/conversations/utils/EmojiWrapper.java deleted file mode 100644 index 3b6cf71e1..000000000 --- a/src/compat/java/eu/siacs/conversations/utils/EmojiWrapper.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2017, 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 androidx.emoji.text.EmojiCompat; - -public class EmojiWrapper { - - public static CharSequence transform(CharSequence input) { - try { - if (EmojiCompat.get().getLoadState() == EmojiCompat.LOAD_STATE_SUCCEEDED) { - return EmojiCompat.get().process(input); - } else { - return input; - } - } catch (IllegalStateException e) { - return input; - } - } -} diff --git a/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java b/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java index 9c6ebaafd..a1b5f9e77 100644 --- a/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java +++ b/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java @@ -128,16 +128,19 @@ public class ImportBackupService extends Service { final List accounts = mDatabaseBackend.getAccountJids(false); final ArrayList backupFiles = new ArrayList<>(); final Set apps = new HashSet<>(Arrays.asList("Conversations", "Quicksy", getString(R.string.app_name))); - for (String app : apps) { - final File directory = new File(FileBackend.getBackupDirectory(app)); + final List directories = new ArrayList<>(); + for (final String app : apps) { + directories.add(FileBackend.getLegacyBackupDirectory(app)); + } + directories.add(FileBackend.getBackupDirectory(this)); + for (final File directory : directories) { if (!directory.exists() || !directory.isDirectory()) { Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath()); continue; } final File[] files = directory.listFiles(); if (files == null) { - onBackupFilesLoaded.onBackupFilesLoaded(backupFiles); - return; + continue; } for (final File file : files) { if (file.isFile() && file.getName().endsWith(".ceb")) { diff --git a/src/conversations/res/values-gl/strings.xml b/src/conversations/res/values-gl/strings.xml index 636f921b6..98d151721 100644 --- a/src/conversations/res/values-gl/strings.xml +++ b/src/conversations/res/values-gl/strings.xml @@ -1,15 +1,15 @@ - Escolle o teu provedor XMPP + Elixe o teu provedor XMPP Utilizar conversations.im Crear nova conta Xa posúes unha conta XMPP? Este pode ser o caso se xa estás a utilizar outro cliente XMPP ou utilizaches Conversations previamente. Se non é así podes crear unha nova conta agora mesmo.\nTruco: Algúns provedores de correo tamén proporcionan contas XMPP. XMPP é unha rede de mensaxería independente do provedor. Podes utilizar este cliente con calquera provedor XMPP da túa elección.\nMais para a tua conveniencia fixemos que fose doado crear unha conta en conversations.im¹; un provedor especialmente axeitado para utilizar con Conversations. - Convidáronte a %1$s. Guiarémoste no proceso para crear unha conta.\nAo escoller %1$s como provedor poderás comunicarte con usuarias de outros provedores cando lles deas o teu enderezo XMPP completo. - Convidáronte a %1$s. Escollemos un nome de usuaria por ti. Guiarémoste no proceso de crear unha conta.\nPoderás comunicarte con usuarias de outros provedores cando lles digas o teu enderezo XMPP completo. + Convidáronte a %1$s. Guiarémoste no proceso para crear unha conta.\nAo elexir %1$s como provedor poderás comunicarte con usuarias doutros provedores cando lles deas o teu enderezo XMPP completo. + Convidáronte a %1$s. Xa eleximos un nome de usuaria para ti. Guiarémoste no proceso de crear unha conta.\nPoderás comunicarte con usuarias doutros provedores cando lles digas o teu enderezo XMPP completo. O convite do teu servidor Código de aprovisionamento con formato non válido - Toca no botón compartir para convidar ó teu contacto a %1$s. + Toca no botón compartir para convidar ao teu contacto a %1$s. Se o contacto está preto de ti, pode escanear o código inferior para aceptar o teu convite. Únete a %1$s e conversa conmigo: %2$s Enviar convite a... diff --git a/src/free/java/eu/siacs/conversations/services/EmojiInitializationService.java b/src/free/java/eu/siacs/conversations/services/EmojiInitializationService.java new file mode 100644 index 000000000..2618d3809 --- /dev/null +++ b/src/free/java/eu/siacs/conversations/services/EmojiInitializationService.java @@ -0,0 +1,14 @@ +package eu.siacs.conversations.services; + +import android.content.Context; + +import androidx.emoji2.bundled.BundledEmojiCompatConfig; +import androidx.emoji2.text.EmojiCompat; + +public class EmojiInitializationService { + + public static void execute(final Context context) { + EmojiCompat.init(new BundledEmojiCompatConfig(context).setReplaceAll(true)); + } + +} diff --git a/src/freeCompat/java/eu/siacs/conversations/ui/service/EmojiService.java b/src/freeCompat/java/eu/siacs/conversations/ui/service/EmojiService.java deleted file mode 100644 index 1f60368bb..000000000 --- a/src/freeCompat/java/eu/siacs/conversations/ui/service/EmojiService.java +++ /dev/null @@ -1,27 +0,0 @@ -package eu.siacs.conversations.ui.service; - -import android.content.Context; -import android.os.Build; -import androidx.emoji.text.EmojiCompat; -import androidx.emoji.text.FontRequestEmojiCompatConfig; -import androidx.emoji.bundled.BundledEmojiCompatConfig; - -public class EmojiService { - - private final Context context; - - public EmojiService(Context context) { - this.context = context; - } - - public void init() { - BundledEmojiCompatConfig config = new BundledEmojiCompatConfig(context); - //On recent Androids we assume to have the latest emojis - //there are some annoying bugs with emoji compat that make it a safer choice not to use it when possible - // a) the text preview has annoying glitches when the cut of text contains emojis (the emoji will be half visible) - // b) can trigger a hardware rendering bug https://issuetracker.google.com/issues/67102093 - config.setReplaceAll(Build.VERSION.SDK_INT < Build.VERSION_CODES.O); - EmojiCompat.init(config); - } - -} \ No newline at end of file diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index ff41c07c2..f3922675d 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -50,6 +50,10 @@ android:name="android.hardware.microphone" android:required="false" /> + + + + diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java index 0ad103155..a676e5d5d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java @@ -9,6 +9,7 @@ import org.openintents.openpgp.util.OpenPgpApi; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -147,9 +148,6 @@ public class PgpDecryptionService { try { os.flush(); final String body = os.toString(); - if (body == null) { - throw new IOException("body was null"); - } message.setBody(body); message.setEncryption(Message.ENCRYPTION_DECRYPTED); final HttpConnectionManager manager = mXmppConnectionService.getHttpConnectionManager(); @@ -194,9 +192,9 @@ public class PgpDecryptionService { String originalExtension = originalFilename == null ? null : MimeUtils.extractRelevantExtension(originalFilename); if (originalExtension != null && MimeUtils.extractRelevantExtension(outputFile.getName()) == null) { Log.d(Config.LOGTAG,"detected original filename during pgp decryption"); - String mime = MimeUtils.guessMimeTypeFromExtension(originalExtension); - String path = outputFile.getName()+"."+originalExtension; - DownloadableFile fixedFile = mXmppConnectionService.getFileBackend().getFileForPath(path,mime); + final String mime = MimeUtils.guessMimeTypeFromExtension(originalExtension); + final String filename = outputFile.getName()+"."+originalExtension; + final File fixedFile = mXmppConnectionService.getFileBackend().getStorageLocation(filename,mime); if (fixedFile.getParentFile().mkdirs()) { Log.d(Config.LOGTAG,"created parent directories for "+fixedFile.getAbsolutePath()); } @@ -205,7 +203,7 @@ public class PgpDecryptionService { } if (outputFile.renameTo(fixedFile)) { Log.d(Config.LOGTAG, "renamed " + outputFile.getAbsolutePath() + " to " + fixedFile.getAbsolutePath()); - message.setRelativeFilePath(path); + message.setRelativeFilePath(fixedFile.getAbsolutePath()); } } final String url = message.getFileParams().url; diff --git a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java index c0b3512a3..072b4fd06 100644 --- a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java +++ b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java @@ -16,6 +16,10 @@ public class DownloadableFile extends File { private byte[] aeskey; private byte[] iv; + public DownloadableFile(final File parent, final String file) { + super(parent, file); + } + public DownloadableFile(String path) { super(path); } diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index 15dc6eac6..5623c0be7 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -96,11 +96,8 @@ public class HttpDownloadConnection implements Transferable { this.message.setEncryption(Message.ENCRYPTION_NONE); } final String ext = extension.getExtension(); - if (ext != null) { - message.setRelativeFilePath(String.format("%s.%s", message.getUuid(), ext)); - } else if (Strings.isNullOrEmpty(message.getRelativeFilePath())) { - message.setRelativeFilePath(message.getUuid()); - } + final String filename = Strings.isNullOrEmpty(ext) ? message.getUuid() : String.format("%s.%s", message.getUuid(), ext); + mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, filename); setupFile(); if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) { this.message.setEncryption(Message.ENCRYPTION_NONE); @@ -122,7 +119,7 @@ public class HttpDownloadConnection implements Transferable { private void setupFile() { final String reference = mUrl.fragment(); if (reference != null && AesGcmURL.IV_KEY.matcher(reference).matches()) { - this.file = new DownloadableFile(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + message.getUuid()); + this.file = new DownloadableFile(mXmppConnectionService.getCacheDir(), message.getUuid()); this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference)); Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")"); } else { @@ -326,7 +323,7 @@ public class HttpDownloadConnection implements Transferable { if (Strings.isNullOrEmpty(extension.getExtension()) && contentType != null) { final String fileExtension = MimeUtils.guessExtensionFromMimeType(contentType); if (fileExtension != null) { - message.setRelativeFilePath(String.format("%s.%s", message.getUuid(), fileExtension)); + mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), fileExtension), contentType); Log.d(Config.LOGTAG, "rewriting name after not finding extension in url but in content type"); setupFile(); } @@ -419,8 +416,9 @@ public class HttpDownloadConnection implements Transferable { Log.d(Config.LOGTAG, "content-length reported on GET (" + size + ") did not match Content-Length reported on HEAD (" + expected + ")"); } file.getParentFile().mkdirs(); + Log.d(Config.LOGTAG,"creating file: "+file.getAbsolutePath()); if (!file.exists() && !file.createNewFile()) { - throw new FileWriterException(); + throw new FileWriterException(file); } outputStream = AbstractConnectionManager.createOutputStream(file, false, false); } @@ -431,7 +429,7 @@ public class HttpDownloadConnection implements Transferable { try { outputStream.write(buffer, 0, count); } catch (IOException e) { - throw new FileWriterException(); + throw new FileWriterException(file); } updateProgress(Math.round(((double) transmitted / expected) * 100)); } diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 2cef93b00..5c23bf0fa 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.persistance; -import android.annotation.TargetApi; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; @@ -33,6 +32,7 @@ import androidx.annotation.StringRes; import androidx.core.content.FileProvider; import androidx.exifinterface.media.ExifInterface; +import com.google.common.base.Strings; import com.google.common.io.ByteStreams; import java.io.ByteArrayOutputStream; @@ -63,7 +63,7 @@ import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.AttachFileToConversationRunnable; import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.RecordingActivity; +import eu.siacs.conversations.ui.adapter.MediaAdapter; import eu.siacs.conversations.ui.util.Attachment; import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.CryptoHelper; @@ -76,7 +76,8 @@ public class FileBackend { private static final Object THUMBNAIL_LOCK = new Object(); - private static final SimpleDateFormat IMAGE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); + private static final SimpleDateFormat IMAGE_DATE_FORMAT = + new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); private static final String FILE_PROVIDER = ".files"; private static final float IGNORE_PADDING = 0.15f; @@ -86,19 +87,6 @@ public class FileBackend { this.mXmppConnectionService = service; } - private static boolean isInDirectoryThatShouldNotBeScanned(Context context, File file) { - return isInDirectoryThatShouldNotBeScanned(context, file.getAbsolutePath()); - } - - public static boolean isInDirectoryThatShouldNotBeScanned(Context context, String path) { - for (String type : new String[]{RecordingActivity.STORAGE_DIRECTORY_TYPE_NAME, "Files"}) { - if (path.startsWith(getConversationsDirectory(context, type))) { - return true; - } - } - return false; - } - public static long getFileSize(Context context, Uri uri) { try { final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); @@ -114,11 +102,14 @@ public class FileBackend { } } - public static boolean allFilesUnderSize(Context context, List attachments, long max) { - final boolean compressVideo = !AttachFileToConversationRunnable.getVideoCompression(context).equals("uncompressed"); + public static boolean allFilesUnderSize( + Context context, List attachments, long max) { + final boolean compressVideo = + !AttachFileToConversationRunnable.getVideoCompression(context) + .equals("uncompressed"); if (max <= 0) { Log.d(Config.LOGTAG, "server did not report max file size for http upload"); - return true; //exception to be compatible with HTTP Upload < v0.2 + return true; // exception to be compatible with HTTP Upload < v0.2 } for (Attachment attachment : attachments) { if (attachment.getType() != Attachment.Type.FILE) { @@ -127,41 +118,42 @@ public class FileBackend { String mime = attachment.getMime(); if (mime != null && mime.startsWith("video/") && compressVideo) { try { - Dimensions dimensions = FileBackend.getVideoDimensions(context, attachment.getUri()); + Dimensions dimensions = + FileBackend.getVideoDimensions(context, attachment.getUri()); if (dimensions.getMin() > 720) { - Log.d(Config.LOGTAG, "do not consider video file with min width larger than 720 for size check"); + Log.d( + Config.LOGTAG, + "do not consider video file with min width larger than 720 for size check"); continue; } } catch (NotAVideoFile notAVideoFile) { - //ignore and fall through + // ignore and fall through } } if (FileBackend.getFileSize(context, attachment.getUri()) > max) { - Log.d(Config.LOGTAG, "not all files are under " + max + " bytes. suggesting falling back to jingle"); + Log.d( + Config.LOGTAG, + "not all files are under " + + max + + " bytes. suggesting falling back to jingle"); return false; } } return true; } - public static String getConversationsDirectory(Context context, final String type) { - if (Config.ONLY_INTERNAL_STORAGE) { - return context.getFilesDir().getAbsolutePath() + "/" + type + "/"; - } else { - return getAppMediaDirectory(context) + context.getString(R.string.app_name) + " " + type + "/"; - } + public static File getBackupDirectory(final Context context) { + final File conversationsDownloadDirectory = + new File( + Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS), + context.getString(R.string.app_name)); + return new File(conversationsDownloadDirectory, "Backup"); } - public static String getAppMediaDirectory(Context context) { - return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + context.getString(R.string.app_name) + "/Media/"; - } - - public static String getBackupDirectory(Context context) { - return getBackupDirectory(context.getString(R.string.app_name)); - } - - public static String getBackupDirectory(String app) { - return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + app + "/Backup/"; + public static File getLegacyBackupDirectory(final String app) { + final File appDirectory = new File(Environment.getExternalStorageDirectory(), app); + return new File(appDirectory, "Backup"); } private static Bitmap rotate(final Bitmap bitmap, final int degree) { @@ -180,7 +172,8 @@ public class FileBackend { } public static boolean isPathBlacklisted(String path) { - final String androidDataPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/"; + final String androidDataPath = + Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/"; return path.startsWith(androidDataPath); } @@ -192,10 +185,6 @@ public class FileBackend { return paint; } - private static String getTakePhotoPath() { - return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + "/Camera/"; - } - public static Uri getUriForUri(Context context, Uri uri) { if ("file".equals(uri.getScheme())) { return getUriForFile(context, new File(uri.getPath())); @@ -246,7 +235,6 @@ public class FileBackend { return calcSampleSize(options, size); } - private static int calcSampleSize(BitmapFactory.Options options, int size) { int height = options.outHeight; int width = options.outWidth; @@ -256,8 +244,7 @@ public class FileBackend { int halfHeight = height / 2; int halfWidth = width / 2; - while ((halfHeight / inSampleSize) > size - && (halfWidth / inSampleSize) > size) { + while ((halfHeight / inSampleSize) > size && (halfWidth / inSampleSize) > size) { inSampleSize *= 2; } } @@ -274,7 +261,8 @@ public class FileBackend { return getVideoDimensions(mediaMetadataRetriever); } - private static Dimensions getVideoDimensionsOfFrame(MediaMetadataRetriever mediaMetadataRetriever) { + private static Dimensions getVideoDimensionsOfFrame( + MediaMetadataRetriever mediaMetadataRetriever) { Bitmap bitmap = null; try { bitmap = mediaMetadataRetriever.getFrameAtTime(); @@ -288,8 +276,10 @@ public class FileBackend { } } - private static Dimensions getVideoDimensions(MediaMetadataRetriever metadataRetriever) throws NotAVideoFile { - String hasVideo = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO); + private static Dimensions getVideoDimensions(MediaMetadataRetriever metadataRetriever) + throws NotAVideoFile { + String hasVideo = + metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO); if (hasVideo == null) { throw new NotAVideoFile(); } @@ -301,14 +291,18 @@ public class FileBackend { boolean rotated = rotation == 90 || rotation == 270; int height; try { - String h = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); + String h = + metadataRetriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); height = Integer.parseInt(h); } catch (Exception e) { height = -1; } int width; try { - String w = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); + String w = + metadataRetriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); width = Integer.parseInt(w); } catch (Exception e) { width = -1; @@ -319,7 +313,9 @@ public class FileBackend { } private static int extractRotationFromMediaRetriever(MediaMetadataRetriever metadataRetriever) { - String r = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); + String r = + metadataRetriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); try { return Integer.parseInt(r); } catch (Exception e) { @@ -357,36 +353,20 @@ public class FileBackend { } } - public static boolean weOwnFile(Context context, Uri uri) { + public static boolean weOwnFile(final Uri uri) { if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { return false; - } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - return fileIsInFilesDir(context, uri); } else { return weOwnFileLollipop(uri); } } - /** - * This is more than hacky but probably way better than doing nothing - * Further 'optimizations' might contain to get the parents of CacheDir and NoBackupDir - * and check against those as well - */ - private static boolean fileIsInFilesDir(Context context, Uri uri) { - try { - final String haystack = context.getFilesDir().getParentFile().getCanonicalPath(); - final String needle = new File(uri.getPath()).getCanonicalPath(); - return needle.startsWith(haystack); - } catch (IOException e) { - return false; - } - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private static boolean weOwnFileLollipop(Uri uri) { + private static boolean weOwnFileLollipop(final Uri uri) { try { File file = new File(uri.getPath()); - FileDescriptor fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).getFileDescriptor(); + FileDescriptor fd = + ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) + .getFileDescriptor(); StructStat st = Os.fstat(fd); return st.st_uid == android.os.Process.myUid(); } catch (FileNotFoundException e) { @@ -400,18 +380,22 @@ public class FileBackend { final String filePath = file.getAbsolutePath(); final Cursor cursor; try { - cursor = context.getContentResolver().query( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - new String[]{MediaStore.Images.Media._ID}, - MediaStore.Images.Media.DATA + "=? ", - new String[]{filePath}, null); + cursor = + context.getContentResolver() + .query( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + new String[] {MediaStore.Images.Media._ID}, + MediaStore.Images.Media.DATA + "=? ", + new String[] {filePath}, + null); } catch (SecurityException e) { return null; } if (cursor != null && cursor.moveToFirst()) { final int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID)); cursor.close(); - return Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id)); + return Uri.withAppendedPath( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id)); } else { return null; } @@ -433,15 +417,30 @@ public class FileBackend { final String mime = attachment.getMime(); if ("application/pdf".equals(mime) && Compatibility.runsTwentyOne()) { bitmap = cropCenterSquarePdf(attachment.getUri(), size); - drawOverlay(bitmap, paintOverlayBlackPdf(bitmap) ? R.drawable.open_pdf_black : R.drawable.open_pdf_white, 0.75f); + drawOverlay( + bitmap, + paintOverlayBlackPdf(bitmap) + ? R.drawable.open_pdf_black + : R.drawable.open_pdf_white, + 0.75f); } else if (mime != null && mime.startsWith("video/")) { bitmap = cropCenterSquareVideo(attachment.getUri(), size); - drawOverlay(bitmap, paintOverlayBlack(bitmap) ? R.drawable.play_video_black : R.drawable.play_video_white, 0.75f); + drawOverlay( + bitmap, + paintOverlayBlack(bitmap) + ? R.drawable.play_video_black + : R.drawable.play_video_white, + 0.75f); } else { bitmap = cropCenterSquare(attachment.getUri(), size); if (bitmap != null && "image/gif".equals(mime)) { Bitmap withGifOverlay = bitmap.copy(Bitmap.Config.ARGB_8888, true); - drawOverlay(withGifOverlay, paintOverlayBlack(withGifOverlay) ? R.drawable.play_gif_black : R.drawable.play_gif_white, 1.0f); + drawOverlay( + withGifOverlay, + paintOverlayBlack(withGifOverlay) + ? R.drawable.play_gif_black + : R.drawable.play_gif_white, + 1.0f); bitmap.recycle(); bitmap = withGifOverlay; } @@ -452,53 +451,31 @@ public class FileBackend { return bitmap; } - private void createNoMedia(File diretory) { - final File noMedia = new File(diretory, ".nomedia"); - if (!noMedia.exists()) { - try { - if (!noMedia.createNewFile()) { - Log.d(Config.LOGTAG, "created nomedia file " + noMedia.getAbsolutePath()); - } - } catch (Exception e) { - Log.d(Config.LOGTAG, "could not create nomedia file"); - } - } - } - public void updateMediaScanner(File file) { updateMediaScanner(file, null); } public void updateMediaScanner(File file, final Runnable callback) { - if (!isInDirectoryThatShouldNotBeScanned(mXmppConnectionService, file)) { - MediaScannerConnection.scanFile(mXmppConnectionService, new String[]{file.getAbsolutePath()}, null, new MediaScannerConnection.MediaScannerConnectionClient() { - @Override - public void onMediaScannerConnected() { + MediaScannerConnection.scanFile( + mXmppConnectionService, + new String[] {file.getAbsolutePath()}, + null, + new MediaScannerConnection.MediaScannerConnectionClient() { + @Override + public void onMediaScannerConnected() {} - } - - @Override - public void onScanCompleted(String path, Uri uri) { - if (callback != null && file.getAbsolutePath().equals(path)) { - callback.run(); - } else { - Log.d(Config.LOGTAG, "media scanner scanned wrong file"); - if (callback != null) { + @Override + public void onScanCompleted(String path, Uri uri) { + if (callback != null && file.getAbsolutePath().equals(path)) { callback.run(); + } else { + Log.d(Config.LOGTAG, "media scanner scanned wrong file"); + if (callback != null) { + callback.run(); + } } } - } - }); - return; - /*Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); - intent.setData(Uri.fromFile(file)); - mXmppConnectionService.sendBroadcast(intent);*/ - } else if (file.getAbsolutePath().startsWith(getAppMediaDirectory(mXmppConnectionService))) { - createNoMedia(file.getParentFile()); - } - if (callback != null) { - callback.run(); - } + }); } public boolean deleteFile(Message message) { @@ -515,25 +492,30 @@ public class FileBackend { return getFile(message, true); } - public DownloadableFile getFileForPath(String path) { - return getFileForPath(path, MimeUtils.guessMimeTypeFromExtension(MimeUtils.extractRelevantExtension(path))); + return getFileForPath( + path, + MimeUtils.guessMimeTypeFromExtension(MimeUtils.extractRelevantExtension(path))); } - public DownloadableFile getFileForPath(String path, String mime) { - final DownloadableFile file; + private DownloadableFile getFileForPath(final String path, final String mime) { if (path.startsWith("/")) { - file = new DownloadableFile(path); + return new DownloadableFile(path); } else { - if (mime != null && mime.startsWith("image/")) { - file = new DownloadableFile(getConversationsDirectory("Images") + path); - } else if (mime != null && mime.startsWith("video/")) { - file = new DownloadableFile(getConversationsDirectory("Videos") + path); - } else { - file = new DownloadableFile(getConversationsDirectory("Files") + path); - } + return getLegacyFileForFilename(path, mime); + } + } + + public DownloadableFile getLegacyFileForFilename(final String filename, final String mime) { + if (Strings.isNullOrEmpty(mime)) { + return new DownloadableFile(getLegacyStorageLocation("Files"), filename); + } else if (mime.startsWith("image/")) { + return new DownloadableFile(getLegacyStorageLocation("Images"), filename); + } else if (mime.startsWith("video/")) { + return new DownloadableFile(getLegacyStorageLocation("Videos"), filename); + } else { + return new DownloadableFile(getLegacyStorageLocation("Files"), filename); } - return file; } public boolean isInternalFile(final File file) { @@ -542,33 +524,50 @@ public class FileBackend { } public DownloadableFile getFile(Message message, boolean decrypted) { - final boolean encrypted = !decrypted - && (message.getEncryption() == Message.ENCRYPTION_PGP - || message.getEncryption() == Message.ENCRYPTION_DECRYPTED); + final boolean encrypted = + !decrypted + && (message.getEncryption() == Message.ENCRYPTION_PGP + || message.getEncryption() == Message.ENCRYPTION_DECRYPTED); String path = message.getRelativeFilePath(); if (path == null) { path = message.getUuid(); } final DownloadableFile file = getFileForPath(path, message.getMimeType()); if (encrypted) { - return new DownloadableFile(getConversationsDirectory("Files") + file.getName() + ".pgp"); + return new DownloadableFile( + mXmppConnectionService.getCacheDir(), + String.format("%s.%s", file.getName(), "pgp")); } else { return file; } } public List convertToAttachments(List relativeFilePaths) { - List attachments = new ArrayList<>(); - for (DatabaseBackend.FilePath relativeFilePath : relativeFilePaths) { - final String mime = MimeUtils.guessMimeTypeFromExtension(MimeUtils.extractRelevantExtension(relativeFilePath.path)); + final List attachments = new ArrayList<>(); + for (final DatabaseBackend.FilePath relativeFilePath : relativeFilePaths) { + final String mime = + MimeUtils.guessMimeTypeFromExtension( + MimeUtils.extractRelevantExtension(relativeFilePath.path)); final File file = getFileForPath(relativeFilePath.path, mime); attachments.add(Attachment.of(relativeFilePath.uuid, file, mime)); } return attachments; } - private String getConversationsDirectory(final String type) { - return getConversationsDirectory(mXmppConnectionService, type); + private File getLegacyStorageLocation(final String type) { + if (Config.ONLY_INTERNAL_STORAGE) { + return new File(mXmppConnectionService.getFilesDir(), type); + } else { + final File appDirectory = + new File( + Environment.getExternalStorageDirectory(), + mXmppConnectionService.getString(R.string.app_name)); + final File appMediaDirectory = new File(appDirectory, "Media"); + final String locationName = + String.format( + "%s %s", mXmppConnectionService.getString(R.string.app_name), type); + return new File(appMediaDirectory, locationName); + } } private Bitmap resize(final Bitmap originalBitmap, int size) throws IOException { @@ -586,7 +585,8 @@ public class FileBackend { scalledW = size; scalledH = Math.max((int) (h / ((double) w / size)), 1); } - final Bitmap result = Bitmap.createScaledBitmap(originalBitmap, scalledW, scalledH, true); + final Bitmap result = + Bitmap.createScaledBitmap(originalBitmap, scalledW, scalledH, true); if (!originalBitmap.isRecycled()) { originalBitmap.recycle(); } @@ -603,19 +603,26 @@ public class FileBackend { } final File file = new File(path); long size = file.length(); - if (size == 0 || size >= mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize)) { + if (size == 0 + || size + >= mXmppConnectionService + .getResources() + .getInteger(R.integer.auto_accept_filesize)) { return false; } BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; try { - final InputStream inputStream = mXmppConnectionService.getContentResolver().openInputStream(uri); + final InputStream inputStream = + mXmppConnectionService.getContentResolver().openInputStream(uri); BitmapFactory.decodeStream(inputStream, null, options); close(inputStream); if (options.outMimeType == null || options.outHeight <= 0 || options.outWidth <= 0) { return false; } - return (options.outWidth <= Config.IMAGE_SIZE && options.outHeight <= Config.IMAGE_SIZE && options.outMimeType.contains(Config.IMAGE_FORMAT.name().toLowerCase())); + return (options.outWidth <= Config.IMAGE_SIZE + && options.outHeight <= Config.IMAGE_SIZE + && options.outMimeType.contains(Config.IMAGE_FORMAT.name().toLowerCase())); } catch (FileNotFoundException e) { Log.d(Config.LOGTAG, "unable to get image dimensions", e); return false; @@ -627,7 +634,9 @@ public class FileBackend { } private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException { - Log.d(Config.LOGTAG, "copy file (" + uri.toString() + ") to private storage " + file.getAbsolutePath()); + Log.d( + Config.LOGTAG, + "copy file (" + uri.toString() + ") to private storage " + file.getAbsolutePath()); file.getParentFile().mkdirs(); try { file.createNewFile(); @@ -635,19 +644,20 @@ public class FileBackend { throw new FileCopyException(R.string.error_unable_to_create_temporary_file); } try (final OutputStream os = new FileOutputStream(file); - final InputStream is = mXmppConnectionService.getContentResolver().openInputStream(uri)) { + final InputStream is = + mXmppConnectionService.getContentResolver().openInputStream(uri)) { if (is == null) { throw new FileCopyException(R.string.error_file_not_found); } try { ByteStreams.copy(is, os); } catch (IOException e) { - throw new FileWriterException(); + throw new FileWriterException(file); } try { os.flush(); } catch (IOException e) { - throw new FileWriterException(); + throw new FileWriterException(file); } } catch (final FileNotFoundException e) { cleanup(file); @@ -664,7 +674,8 @@ public class FileBackend { } } - public void copyFileToPrivateStorage(Message message, Uri uri, String type) throws FileCopyException { + public void copyFileToPrivateStorage(Message message, Uri uri, String type) + throws FileCopyException { String mime = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type); Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage (mime=" + mime + ")"); String extension = MimeUtils.guessExtensionFromMimeType(mime); @@ -675,29 +686,22 @@ public class FileBackend { if ("ogg".equals(extension) && type != null && type.startsWith("audio/")) { extension = "oga"; } - message.setRelativeFilePath(message.getUuid() + "." + extension); + setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), extension)); copyFileToPrivateStorage(mXmppConnectionService.getFileBackend().getFile(message), uri); } - private String getExtensionFromUri(Uri uri) { - String[] projection = {MediaStore.MediaColumns.DATA}; + private String getExtensionFromUri(final Uri uri) { + final String[] projection = {MediaStore.MediaColumns.DATA}; String filename = null; - Cursor cursor; - try { - cursor = mXmppConnectionService.getContentResolver().query(uri, projection, null, null, null); - } catch (IllegalArgumentException e) { - cursor = null; - } - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - filename = cursor.getString(0); - } - } catch (Exception e) { - filename = null; - } finally { - cursor.close(); + try (final Cursor cursor = + mXmppConnectionService + .getContentResolver() + .query(uri, projection, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + filename = cursor.getString(0); } + } catch (final SecurityException | IllegalArgumentException e) { + filename = null; } if (filename == null) { final List segments = uri.getPathSegments(); @@ -705,11 +709,12 @@ public class FileBackend { filename = segments.get(segments.size() - 1); } } - int pos = filename == null ? -1 : filename.lastIndexOf('.'); + final int pos = filename == null ? -1 : filename.lastIndexOf('.'); return pos > 0 ? filename.substring(pos + 1) : null; } - private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException, ImageCompressionException { + private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) + throws FileCopyException, ImageCompressionException { final File parent = file.getParentFile(); if (parent != null && parent.mkdirs()) { Log.d(Config.LOGTAG, "created parent directory"); @@ -743,7 +748,10 @@ public class FileBackend { scaledBitmap = rotate(scaledBitmap, rotation); boolean targetSizeReached = false; int quality = Config.IMAGE_QUALITY; - final int imageMaxSize = mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize); + final int imageMaxSize = + mXmppConnectionService + .getResources() + .getInteger(R.integer.auto_accept_filesize); while (!targetSizeReached) { os = new FileOutputStream(file); Log.d(Config.LOGTAG, "compressing image with quality " + quality); @@ -788,32 +796,79 @@ public class FileBackend { } } - public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException, ImageCompressionException { - Log.d(Config.LOGTAG, "copy image (" + image.toString() + ") to private storage " + file.getAbsolutePath()); + public void copyImageToPrivateStorage(File file, Uri image) + throws FileCopyException, ImageCompressionException { + Log.d( + Config.LOGTAG, + "copy image (" + + image.toString() + + ") to private storage " + + file.getAbsolutePath()); copyImageToPrivateStorage(file, image, 0); } - public void copyImageToPrivateStorage(Message message, Uri image) throws FileCopyException, ImageCompressionException { + public void copyImageToPrivateStorage(Message message, Uri image) + throws FileCopyException, ImageCompressionException { + final String filename; switch (Config.IMAGE_FORMAT) { case JPEG: - message.setRelativeFilePath(message.getUuid() + ".jpg"); + filename = String.format("%s.%s", message.getUuid(), "jpg"); break; case PNG: - message.setRelativeFilePath(message.getUuid() + ".png"); + filename = String.format("%s.%s", message.getUuid(), "png"); break; case WEBP: - message.setRelativeFilePath(message.getUuid() + ".webp"); + filename = String.format("%s.%s", message.getUuid(), "webp"); break; + default: + throw new IllegalStateException("Unknown image format"); } + setupRelativeFilePath(message, filename); copyImageToPrivateStorage(getFile(message), image); updateFileParams(message); } + public void setupRelativeFilePath(final Message message, final String filename) { + final String extension = MimeUtils.extractRelevantExtension(filename); + final String mime = MimeUtils.guessMimeTypeFromExtension(extension); + setupRelativeFilePath(message, filename, mime); + } + + public File getStorageLocation(final String filename, final String mime) { + final File parentDirectory; + if (Strings.isNullOrEmpty(mime)) { + parentDirectory = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + } else if (mime.startsWith("image/")) { + parentDirectory = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); + } else if (mime.startsWith("video/")) { + parentDirectory = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES); + } else if (MediaAdapter.DOCUMENT_MIMES.contains(mime)) { + parentDirectory = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS); + } else { + parentDirectory = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + } + final File appDirectory = + new File(parentDirectory, mXmppConnectionService.getString(R.string.app_name)); + return new File(appDirectory, filename); + } + + public void setupRelativeFilePath( + final Message message, final String filename, final String mime) { + final File file = getStorageLocation(filename, mime); + message.setRelativeFilePath(file.getAbsolutePath()); + } + public boolean unusualBounds(final Uri image) { try { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; - final InputStream inputStream = mXmppConnectionService.getContentResolver().openInputStream(image); + final InputStream inputStream = + mXmppConnectionService.getContentResolver().openInputStream(image); BitmapFactory.decodeStream(inputStream, null, options); close(inputStream); float ratio = (float) options.outHeight / options.outWidth; @@ -833,7 +888,8 @@ public class FileBackend { } private int getRotation(final Uri image) { - try (final InputStream is = mXmppConnectionService.getContentResolver().openInputStream(image)) { + try (final InputStream is = + mXmppConnectionService.getContentResolver().openInputStream(image)) { return is == null ? 0 : getRotation(is); } catch (final Exception e) { return 0; @@ -842,7 +898,9 @@ public class FileBackend { private static int getRotation(final InputStream inputStream) throws IOException { final ExifInterface exif = new ExifInterface(inputStream); - final int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); + final int orientation = + exif.getAttributeInt( + ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_180: return 180; @@ -880,7 +938,12 @@ public class FileBackend { thumbnail = rotate(thumbnail, getRotation(file)); if (mime.equals("image/gif")) { Bitmap withGifOverlay = thumbnail.copy(Bitmap.Config.ARGB_8888, true); - drawOverlay(withGifOverlay, paintOverlayBlack(withGifOverlay) ? R.drawable.play_gif_black : R.drawable.play_gif_white, 1.0f); + drawOverlay( + withGifOverlay, + paintOverlayBlack(withGifOverlay) + ? R.drawable.play_gif_black + : R.drawable.play_gif_white, + 1.0f); thumbnail.recycle(); thumbnail = withGifOverlay; } @@ -903,27 +966,36 @@ public class FileBackend { } private void drawOverlay(Bitmap bitmap, int resource, float factor) { - Bitmap overlay = BitmapFactory.decodeResource(mXmppConnectionService.getResources(), resource); + Bitmap overlay = + BitmapFactory.decodeResource(mXmppConnectionService.getResources(), resource); Canvas canvas = new Canvas(bitmap); float targetSize = Math.min(canvas.getWidth(), canvas.getHeight()) * factor; - Log.d(Config.LOGTAG, "target size overlay: " + targetSize + " overlay bitmap size was " + overlay.getHeight()); + Log.d( + Config.LOGTAG, + "target size overlay: " + + targetSize + + " overlay bitmap size was " + + overlay.getHeight()); float left = (canvas.getWidth() - targetSize) / 2.0f; float top = (canvas.getHeight() - targetSize) / 2.0f; RectF dst = new RectF(left, top, left + targetSize - 1, top + targetSize - 1); canvas.drawBitmap(overlay, null, dst, createAntiAliasingPaint()); } - /** - * https://stackoverflow.com/a/3943023/210897 - */ + /** https://stackoverflow.com/a/3943023/210897 */ private boolean paintOverlayBlack(final Bitmap bitmap) { final int h = bitmap.getHeight(); final int w = bitmap.getWidth(); int record = 0; for (int y = Math.round(h * IGNORE_PADDING); y < h - Math.round(h * IGNORE_PADDING); ++y) { - for (int x = Math.round(w * IGNORE_PADDING); x < w - Math.round(w * IGNORE_PADDING); ++x) { + for (int x = Math.round(w * IGNORE_PADDING); + x < w - Math.round(w * IGNORE_PADDING); + ++x) { int pixel = bitmap.getPixel(x, y); - if ((Color.red(pixel) * 0.299 + Color.green(pixel) * 0.587 + Color.blue(pixel) * 0.114) > 186) { + if ((Color.red(pixel) * 0.299 + + Color.green(pixel) * 0.587 + + Color.blue(pixel) * 0.114) + > 186) { --record; } else { ++record; @@ -940,7 +1012,10 @@ public class FileBackend { for (int y = 0; y < h; ++y) { for (int x = 0; x < w; ++x) { int pixel = bitmap.getPixel(x, y); - if ((Color.red(pixel) * 0.299 + Color.green(pixel) * 0.587 + Color.blue(pixel) * 0.114) > 186) { + if ((Color.red(pixel) * 0.299 + + Color.green(pixel) * 0.587 + + Color.blue(pixel) * 0.114) + > 186) { white++; } } @@ -975,16 +1050,27 @@ public class FileBackend { frame = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); frame.eraseColor(0xff000000); } - drawOverlay(frame, paintOverlayBlack(frame) ? R.drawable.play_video_black : R.drawable.play_video_white, 0.75f); + drawOverlay( + frame, + paintOverlayBlack(frame) + ? R.drawable.play_video_black + : R.drawable.play_video_white, + 0.75f); return frame; } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private Bitmap getPdfDocumentPreview(final File file, final int size) { try { - final ParcelFileDescriptor fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); + final ParcelFileDescriptor fileDescriptor = + ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); final Bitmap rendered = renderPdfDocument(fileDescriptor, size, true); - drawOverlay(rendered, paintOverlayBlackPdf(rendered) ? R.drawable.open_pdf_black : R.drawable.open_pdf_white, 0.75f); + drawOverlay( + rendered, + paintOverlayBlackPdf(rendered) + ? R.drawable.open_pdf_black + : R.drawable.open_pdf_white, + 0.75f); return rendered; } catch (final IOException | SecurityException e) { Log.d(Config.LOGTAG, "unable to render PDF document preview", e); @@ -994,11 +1080,11 @@ public class FileBackend { } } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) private Bitmap cropCenterSquarePdf(final Uri uri, final int size) { try { - ParcelFileDescriptor fileDescriptor = mXmppConnectionService.getContentResolver().openFileDescriptor(uri, "r"); + ParcelFileDescriptor fileDescriptor = + mXmppConnectionService.getContentResolver().openFileDescriptor(uri, "r"); final Bitmap bitmap = renderPdfDocument(fileDescriptor, size, false); return cropCenterSquare(bitmap, size); } catch (Exception e) { @@ -1009,11 +1095,15 @@ public class FileBackend { } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - private Bitmap renderPdfDocument(ParcelFileDescriptor fileDescriptor, int targetSize, boolean fit) throws IOException { + private Bitmap renderPdfDocument( + ParcelFileDescriptor fileDescriptor, int targetSize, boolean fit) throws IOException { final PdfRenderer pdfRenderer = new PdfRenderer(fileDescriptor); final PdfRenderer.Page page = pdfRenderer.openPage(0); - final Dimensions dimensions = scalePdfDimensions(new Dimensions(page.getHeight(), page.getWidth()), targetSize, fit); - final Bitmap rendered = Bitmap.createBitmap(dimensions.width, dimensions.height, Bitmap.Config.ARGB_8888); + final Dimensions dimensions = + scalePdfDimensions( + new Dimensions(page.getHeight(), page.getWidth()), targetSize, fit); + final Bitmap rendered = + Bitmap.createBitmap(dimensions.width, dimensions.height, Bitmap.Config.ARGB_8888); rendered.eraseColor(0xffffffff); page.render(rendered, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); page.close(); @@ -1023,12 +1113,19 @@ public class FileBackend { } public Uri getTakePhotoUri() { - File file; + final String filename = + String.format("IMG_%s.%s", IMAGE_DATE_FORMAT.format(new Date()), "jpg"); + final File directory; if (Config.ONLY_INTERNAL_STORAGE) { - file = new File(mXmppConnectionService.getCacheDir().getAbsolutePath(), "Camera/IMG_" + IMAGE_DATE_FORMAT.format(new Date()) + ".jpg"); + directory = new File(mXmppConnectionService.getCacheDir(), "Camera"); } else { - file = new File(getTakePhotoPath() + "IMG_" + IMAGE_DATE_FORMAT.format(new Date()) + ".jpg"); + directory = + new File( + Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DCIM), + "Camera"); } + final File file = new File(directory, filename); file.getParentFile().mkdirs(); return getUriForFile(mXmppConnectionService, file); } @@ -1036,11 +1133,15 @@ public class FileBackend { public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) { final Avatar uncompressAvatar = getUncompressedAvatar(image); - if (uncompressAvatar != null && uncompressAvatar.image.length() <= Config.AVATAR_CHAR_LIMIT) { + if (uncompressAvatar != null + && uncompressAvatar.image.length() <= Config.AVATAR_CHAR_LIMIT) { return uncompressAvatar; } if (uncompressAvatar != null) { - Log.d(Config.LOGTAG, "uncompressed avatar exceeded char limit by " + (uncompressAvatar.image.length() - Config.AVATAR_CHAR_LIMIT)); + Log.d( + Config.LOGTAG, + "uncompressed avatar exceeded char limit by " + + (uncompressAvatar.image.length() - Config.AVATAR_CHAR_LIMIT)); } Bitmap bm = cropCenterSquare(image, size); @@ -1059,7 +1160,9 @@ public class FileBackend { private Avatar getUncompressedAvatar(Uri uri) { Bitmap bitmap = null; try { - bitmap = BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri)); + bitmap = + BitmapFactory.decodeStream( + mXmppConnectionService.getContentResolver().openInputStream(uri)); return getPepAvatar(bitmap, Bitmap.CompressFormat.PNG, 100); } catch (Exception e) { return null; @@ -1073,18 +1176,24 @@ public class FileBackend { private Avatar getPepAvatar(Bitmap bitmap, Bitmap.CompressFormat format, int quality) { try { ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream(); - Base64OutputStream mBase64OutputStream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT); + Base64OutputStream mBase64OutputStream = + new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT); MessageDigest digest = MessageDigest.getInstance("SHA-1"); - DigestOutputStream mDigestOutputStream = new DigestOutputStream(mBase64OutputStream, digest); + DigestOutputStream mDigestOutputStream = + new DigestOutputStream(mBase64OutputStream, digest); if (!bitmap.compress(format, quality, mDigestOutputStream)) { return null; } mDigestOutputStream.flush(); mDigestOutputStream.close(); long chars = mByteArrayOutputStream.size(); - if (format != Bitmap.CompressFormat.PNG && quality >= 50 && chars >= Config.AVATAR_CHAR_LIMIT) { + if (format != Bitmap.CompressFormat.PNG + && quality >= 50 + && chars >= Config.AVATAR_CHAR_LIMIT) { int q = quality - 2; - Log.d(Config.LOGTAG, "avatar char length was " + chars + " reducing quality to " + q); + Log.d( + Config.LOGTAG, + "avatar char length was " + chars + " reducing quality to " + q); return getPepAvatar(bitmap, format, q); } Log.d(Config.LOGTAG, "settled on char length " + chars + " with quality=" + quality); @@ -1123,7 +1232,8 @@ public class FileBackend { BitmapFactory.decodeFile(file.getAbsolutePath(), options); is = new FileInputStream(file); ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream(); - Base64OutputStream mBase64OutputStream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT); + Base64OutputStream mBase64OutputStream = + new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT); MessageDigest digest = MessageDigest.getInstance("SHA-1"); DigestOutputStream os = new DigestOutputStream(mBase64OutputStream, digest); byte[] buffer = new byte[4096]; @@ -1157,14 +1267,20 @@ public class FileBackend { file = getAvatarFile(avatar.getFilename()); avatar.size = file.length(); } else { - file = new File(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + UUID.randomUUID().toString()); + file = + new File( + mXmppConnectionService.getCacheDir().getAbsolutePath() + + "/" + + UUID.randomUUID().toString()); if (file.getParentFile().mkdirs()) { Log.d(Config.LOGTAG, "created cache directory"); } OutputStream os = null; try { if (!file.createNewFile()) { - Log.d(Config.LOGTAG, "unable to create temporary file " + file.getAbsolutePath()); + Log.d( + Config.LOGTAG, + "unable to create temporary file " + file.getAbsolutePath()); } os = new FileOutputStream(file); MessageDigest digest = MessageDigest.getInstance("SHA-1"); @@ -1182,7 +1298,9 @@ public class FileBackend { } final File avatarFile = getAvatarFile(avatar.getFilename()); if (!file.renameTo(avatarFile)) { - Log.d(Config.LOGTAG, "unable to rename " + file.getAbsolutePath() + " to " + outputFile); + Log.d( + Config.LOGTAG, + "unable to rename " + file.getAbsolutePath() + " to " + outputFile); return false; } } else { @@ -1294,7 +1412,7 @@ public class FileBackend { } return dest; } catch (SecurityException e) { - return null; //android 6.0 with revoked permissions for example + return null; // android 6.0 with revoked permissions for example } catch (FileNotFoundException e) { return null; } finally { @@ -1323,10 +1441,12 @@ public class FileBackend { return output; } - private int calcSampleSize(Uri image, int size) throws FileNotFoundException, SecurityException { + private int calcSampleSize(Uri image, int size) + throws FileNotFoundException, SecurityException { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; - final InputStream inputStream = mXmppConnectionService.getContentResolver().openInputStream(image); + final InputStream inputStream = + mXmppConnectionService.getContentResolver().openInputStream(image); BitmapFactory.decodeStream(inputStream, null, options); close(inputStream); return calcSampleSize(options, size); @@ -1340,7 +1460,9 @@ public class FileBackend { DownloadableFile file = getFile(message); final String mime = file.getMimeType(); final boolean privateMessage = message.isPrivateMessage(); - final boolean image = message.getType() == Message.TYPE_IMAGE || (mime != null && mime.startsWith("image/")); + final boolean image = + message.getType() == Message.TYPE_IMAGE + || (mime != null && mime.startsWith("image/")); final boolean video = mime != null && mime.startsWith("video/"); final boolean audio = mime != null && mime.startsWith("audio/"); final boolean pdf = "application/pdf".equals(mime); @@ -1363,22 +1485,29 @@ public class FileBackend { body.append('|').append(dimensions.width).append('|').append(dimensions.height); } } catch (NotAVideoFile notAVideoFile) { - Log.d(Config.LOGTAG, "file with mime type " + file.getMimeType() + " was not a video file"); - //fall threw + Log.d( + Config.LOGTAG, + "file with mime type " + file.getMimeType() + " was not a video file"); + // fall threw } } else if (audio) { body.append("|0|0|").append(getMediaRuntime(file)); } message.setBody(body.toString()); message.setDeleted(false); - message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : (image ? Message.TYPE_IMAGE : Message.TYPE_FILE)); + message.setType( + privateMessage + ? Message.TYPE_PRIVATE_FILE + : (image ? Message.TYPE_IMAGE : Message.TYPE_FILE)); } private int getMediaRuntime(File file) { try { MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); mediaMetadataRetriever.setDataSource(file.toString()); - return Integer.parseInt(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)); + return Integer.parseInt( + mediaMetadataRetriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_DURATION)); } catch (RuntimeException e) { return 0; } @@ -1431,12 +1560,14 @@ public class FileBackend { } private Dimensions scalePdfDimensions(Dimensions in) { - final DisplayMetrics displayMetrics = mXmppConnectionService.getResources().getDisplayMetrics(); + final DisplayMetrics displayMetrics = + mXmppConnectionService.getResources().getDisplayMetrics(); final int target = (int) (displayMetrics.density * 288); return scalePdfDimensions(in, target, true); } - private static Dimensions scalePdfDimensions(final Dimensions in, final int target, final boolean fit) { + private static Dimensions scalePdfDimensions( + final Dimensions in, final int target, final boolean fit) { final int w, h; if (fit == (in.width <= in.height)) { w = Math.max((int) (in.width / ((double) in.height / target)), 1); @@ -1491,7 +1622,6 @@ public class FileBackend { } } - public static class FileCopyException extends Exception { private final int resId; @@ -1499,8 +1629,7 @@ public class FileBackend { this.resId = resId; } - public @StringRes - int getResId() { + public @StringRes int getResId() { return resId; } } diff --git a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java b/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java index db879799d..1ddee27b5 100644 --- a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java +++ b/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java @@ -91,7 +91,7 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis private void processAsVideo() throws FileNotFoundException { Log.d(Config.LOGTAG, "processing file as video"); mXmppConnectionService.startForcingForegroundNotification(); - message.setRelativeFilePath(message.getUuid() + ".mp4"); + mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), "mp4")); final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message); if (Objects.requireNonNull(file.getParentFile()).mkdirs()) { Log.d(Config.LOGTAG, "created parent directory for video file"); diff --git a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java index 95584ae23..f89434897 100644 --- a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java +++ b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java @@ -291,7 +291,7 @@ public class ExportBackupService extends Service { secureRandom.nextBytes(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 File file = new File(FileBackend.getBackupDirectory(this), account.getJid().asBareJid().toEscapedString() + ".ceb"); files.add(file); final File directory = file.getParentFile(); if (directory != null && directory.mkdirs()) { @@ -335,7 +335,7 @@ public class ExportBackupService extends Service { } private void notifySuccess(final List files) { - final String path = FileBackend.getBackupDirectory(this); + final String path = FileBackend.getBackupDirectory(this).getAbsolutePath(); PendingIntent openFolderIntent = null; @@ -363,7 +363,7 @@ public class ExportBackupService extends Service { 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)))) + .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); diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index d35d4808c..fb716044c 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -46,7 +46,6 @@ import eu.siacs.conversations.ui.util.MyLinkify; import eu.siacs.conversations.ui.util.SoftKeyboardUtils; import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.Compatibility; -import eu.siacs.conversations.utils.EmojiWrapper; import eu.siacs.conversations.utils.StringUtils; import eu.siacs.conversations.utils.StylingHelper; import eu.siacs.conversations.utils.XmppUri; @@ -471,11 +470,11 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers String subject = mucOptions.getSubject(); final boolean hasTitle; if (printableValue(roomName)) { - this.binding.mucTitle.setText(EmojiWrapper.transform(roomName)); + this.binding.mucTitle.setText(roomName); this.binding.mucTitle.setVisibility(View.VISIBLE); hasTitle = true; } else if (!printableValue(subject)) { - this.binding.mucTitle.setText(EmojiWrapper.transform(mConversation.getName())); + this.binding.mucTitle.setText(mConversation.getName()); hasTitle = true; this.binding.mucTitle.setVisibility(View.VISIBLE); } else { @@ -486,7 +485,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers SpannableStringBuilder spannable = new SpannableStringBuilder(subject); StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor()); MyLinkify.addLinks(spannable, false); - this.binding.mucSubject.setText(EmojiWrapper.transform(spannable)); + this.binding.mucSubject.setText(spannable); this.binding.mucSubject.setTextAppearance(this, subject.length() > (hasTitle ? 128 : 196) ? R.style.TextAppearance_Conversations_Body1_Linkified : R.style.TextAppearance_Conversations_Subhead); this.binding.mucSubject.setAutoLinkMask(0); this.binding.mucSubject.setVisibility(View.VISIBLE); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 23404c77d..88b40e044 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -6,6 +6,7 @@ import android.app.Activity; import android.app.Fragment; import android.app.FragmentManager; import android.app.PendingIntent; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -1183,8 +1184,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke cancelTransmission.setVisible(true); } if (m.isFileOrImage() && !deleted && !cancelable) { - String path = m.getRelativeFilePath(); - if (path == null || !path.startsWith("/") || FileBackend.isInDirectoryThatShouldNotBeScanned(getActivity(), path)) { + final String path = m.getRelativeFilePath(); + if (path == null || !path.startsWith("/")) { deleteFile.setVisible(true); deleteFile.setTitle(activity.getString(R.string.delete_x_file, UIHelper.getFileDescriptionString(activity, m))); } @@ -1744,7 +1745,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (context == null) { return; } - if (intent.resolveActivity(context.getPackageManager()) != null) { + try { if (chooser) { startActivityForResult( Intent.createChooser(intent, getString(R.string.perform_action_with)), @@ -1752,7 +1753,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } else { startActivityForResult(intent, attachmentChoice); } - } else { + } catch (final ActivityNotFoundException e) { Toast.makeText(context, R.string.no_application_found, Toast.LENGTH_LONG).show(); } } @@ -2254,10 +2255,10 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } private List cleanUris(final List uris) { - Iterator iterator = uris.iterator(); + final Iterator iterator = uris.iterator(); while (iterator.hasNext()) { final Uri uri = iterator.next(); - if (FileBackend.weOwnFile(getActivity(), uri)) { + if (FileBackend.weOwnFile(uri)) { iterator.remove(); Toast.makeText(getActivity(), R.string.security_violation_not_attaching_file, Toast.LENGTH_SHORT).show(); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index 71afa88b8..f5e9d9f64 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -81,7 +81,6 @@ import eu.siacs.conversations.ui.util.ActivityResult; import eu.siacs.conversations.ui.util.ConversationMenuConfigurator; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; import eu.siacs.conversations.ui.util.PendingItem; -import eu.siacs.conversations.utils.EmojiWrapper; import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.utils.SignupUtils; import eu.siacs.conversations.utils.XmppUri; @@ -615,7 +614,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio if (mainFragment instanceof ConversationFragment) { final Conversation conversation = ((ConversationFragment) mainFragment).getConversation(); if (conversation != null) { - actionBar.setTitle(EmojiWrapper.transform(conversation.getName())); + actionBar.setTitle(conversation.getName()); actionBar.setDisplayHomeAsUpEnabled(true); ActionBarUtil.setActionBarOnClickListener( binding.toolbar, diff --git a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java index 2f8c98d76..15ebdb0b7 100644 --- a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java @@ -22,6 +22,7 @@ import java.util.List; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.EnterJidDialogBinding; +import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; import eu.siacs.conversations.ui.interfaces.OnBackendConnected; import eu.siacs.conversations.ui.util.DelayedHintHelper; @@ -29,234 +30,250 @@ import eu.siacs.conversations.xmpp.Jid; public class EnterJidDialog extends DialogFragment implements OnBackendConnected, TextWatcher { + private static final List SUSPICIOUS_DOMAINS = + Arrays.asList("conference", "muc", "room", "rooms", "chat"); - private static final List SUSPICIOUS_DOMAINS = Arrays.asList("conference","muc","room","rooms","chat"); + private OnEnterJidDialogPositiveListener mListener = null; - private OnEnterJidDialogPositiveListener mListener = null; + private static final String TITLE_KEY = "title"; + private static final String POSITIVE_BUTTON_KEY = "positive_button"; + private static final String PREFILLED_JID_KEY = "prefilled_jid"; + private static final String ACCOUNT_KEY = "account"; + private static final String ALLOW_EDIT_JID_KEY = "allow_edit_jid"; + private static final String ACCOUNTS_LIST_KEY = "activated_accounts_list"; + private static final String SANITY_CHECK_JID = "sanity_check_jid"; - private static final String TITLE_KEY = "title"; - private static final String POSITIVE_BUTTON_KEY = "positive_button"; - private static final String PREFILLED_JID_KEY = "prefilled_jid"; - private static final String ACCOUNT_KEY = "account"; - private static final String ALLOW_EDIT_JID_KEY = "allow_edit_jid"; - private static final String ACCOUNTS_LIST_KEY = "activated_accounts_list"; - private static final String SANITY_CHECK_JID = "sanity_check_jid"; + private KnownHostsAdapter knownHostsAdapter; + private Collection whitelistedDomains = Collections.emptyList(); - private KnownHostsAdapter knownHostsAdapter; - private Collection whitelistedDomains = Collections.emptyList(); + private EnterJidDialogBinding binding; + private AlertDialog dialog; + private boolean sanityCheckJid = false; - private EnterJidDialogBinding binding; - private AlertDialog dialog; - private boolean sanityCheckJid = false; + private boolean issuedWarning = false; + public static EnterJidDialog newInstance( + final List activatedAccounts, + final String title, + final String positiveButton, + final String prefilledJid, + final String account, + boolean allowEditJid, + final boolean sanity_check_jid) { + EnterJidDialog dialog = new EnterJidDialog(); + Bundle bundle = new Bundle(); + bundle.putString(TITLE_KEY, title); + bundle.putString(POSITIVE_BUTTON_KEY, positiveButton); + bundle.putString(PREFILLED_JID_KEY, prefilledJid); + bundle.putString(ACCOUNT_KEY, account); + bundle.putBoolean(ALLOW_EDIT_JID_KEY, allowEditJid); + bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList) activatedAccounts); + bundle.putBoolean(SANITY_CHECK_JID, sanity_check_jid); + dialog.setArguments(bundle); + return dialog; + } - private boolean issuedWarning = false; + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setRetainInstance(true); + } - public static EnterJidDialog newInstance(final List activatedAccounts, - final String title, final String positiveButton, - final String prefilledJid, final String account, - boolean allowEditJid, final boolean sanity_check_jid) { - EnterJidDialog dialog = new EnterJidDialog(); - Bundle bundle = new Bundle(); - bundle.putString(TITLE_KEY, title); - bundle.putString(POSITIVE_BUTTON_KEY, positiveButton); - bundle.putString(PREFILLED_JID_KEY, prefilledJid); - bundle.putString(ACCOUNT_KEY, account); - bundle.putBoolean(ALLOW_EDIT_JID_KEY, allowEditJid); - bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList) activatedAccounts); - bundle.putBoolean(SANITY_CHECK_JID, sanity_check_jid); - dialog.setArguments(bundle); - return dialog; - } + @Override + public void onStart() { + super.onStart(); + final Activity activity = getActivity(); + if (activity instanceof XmppActivity + && ((XmppActivity) activity).xmppConnectionService != null) { + refreshKnownHosts(); + } + } - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setRetainInstance(true); - } + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(getArguments().getString(TITLE_KEY)); + binding = + DataBindingUtil.inflate( + getActivity().getLayoutInflater(), R.layout.enter_jid_dialog, null, false); + this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.simple_list_item); + binding.jid.setAdapter(this.knownHostsAdapter); + binding.jid.addTextChangedListener(this); + String prefilledJid = getArguments().getString(PREFILLED_JID_KEY); + if (prefilledJid != null) { + binding.jid.append(prefilledJid); + if (!getArguments().getBoolean(ALLOW_EDIT_JID_KEY)) { + binding.jid.setFocusable(false); + binding.jid.setFocusableInTouchMode(false); + binding.jid.setClickable(false); + binding.jid.setCursorVisible(false); + } + } + sanityCheckJid = getArguments().getBoolean(SANITY_CHECK_JID, false); - @Override - public void onStart() { - super.onStart(); - final Activity activity = getActivity(); - if (activity instanceof XmppActivity && ((XmppActivity) activity).xmppConnectionService != null) { - refreshKnownHosts(); - } - } + DelayedHintHelper.setHint(R.string.account_settings_example_jabber_id, binding.jid); - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(getArguments().getString(TITLE_KEY)); - binding = DataBindingUtil.inflate(getActivity().getLayoutInflater(), R.layout.enter_jid_dialog, null, false); - this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.simple_list_item); - binding.jid.setAdapter(this.knownHostsAdapter); - binding.jid.addTextChangedListener(this); - String prefilledJid = getArguments().getString(PREFILLED_JID_KEY); - if (prefilledJid != null) { - binding.jid.append(prefilledJid); - if (!getArguments().getBoolean(ALLOW_EDIT_JID_KEY)) { - binding.jid.setFocusable(false); - binding.jid.setFocusableInTouchMode(false); - binding.jid.setClickable(false); - binding.jid.setCursorVisible(false); - } - } - sanityCheckJid = getArguments().getBoolean(SANITY_CHECK_JID, false); + String account = getArguments().getString(ACCOUNT_KEY); + if (account == null) { + StartConversationActivity.populateAccountSpinner( + getActivity(), + getArguments().getStringArrayList(ACCOUNTS_LIST_KEY), + binding.account); + } else { + ArrayAdapter adapter = + new ArrayAdapter<>( + getActivity(), R.layout.simple_list_item, new String[] {account}); + binding.account.setEnabled(false); + adapter.setDropDownViewResource(R.layout.simple_list_item); + binding.account.setAdapter(adapter); + } - DelayedHintHelper.setHint(R.string.account_settings_example_jabber_id, binding.jid); + builder.setView(binding.getRoot()); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(getArguments().getString(POSITIVE_BUTTON_KEY), null); + this.dialog = builder.create(); - String account = getArguments().getString(ACCOUNT_KEY); - if (account == null) { - StartConversationActivity.populateAccountSpinner(getActivity(), getArguments().getStringArrayList(ACCOUNTS_LIST_KEY), binding.account); - } else { - ArrayAdapter adapter = new ArrayAdapter<>(getActivity(), - R.layout.simple_list_item, - new String[]{account}); - binding.account.setEnabled(false); - adapter.setDropDownViewResource(R.layout.simple_list_item); - binding.account.setAdapter(adapter); - } + View.OnClickListener dialogOnClick = + v -> { + handleEnter(binding, account); + }; + binding.jid.setOnEditorActionListener( + (v, actionId, event) -> { + handleEnter(binding, account); + return true; + }); + dialog.show(); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(dialogOnClick); + return dialog; + } - builder.setView(binding.getRoot()); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(getArguments().getString(POSITIVE_BUTTON_KEY), null); - this.dialog = builder.create(); + private void handleEnter(EnterJidDialogBinding binding, String account) { + final Jid accountJid; + if (!binding.account.isEnabled() && account == null) { + return; + } + try { + if (Config.DOMAIN_LOCK != null) { + accountJid = + Jid.ofEscaped( + (String) binding.account.getSelectedItem(), + Config.DOMAIN_LOCK, + null); + } else { + accountJid = Jid.ofEscaped((String) binding.account.getSelectedItem()); + } + } catch (final IllegalArgumentException e) { + return; + } + final Jid contactJid; + try { + contactJid = Jid.ofEscaped(binding.jid.getText().toString()); + } catch (final IllegalArgumentException e) { + binding.jidLayout.setError(getActivity().getString(R.string.invalid_jid)); + return; + } - View.OnClickListener dialogOnClick = v -> { - handleEnter(binding, account); - }; + if (!issuedWarning && sanityCheckJid) { + if (contactJid.isDomainJid()) { + binding.jidLayout.setError( + getActivity().getString(R.string.this_looks_like_a_domain)); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway); + issuedWarning = true; + return; + } + if (suspiciousSubDomain(contactJid.getDomain().toEscapedString())) { + binding.jidLayout.setError( + getActivity().getString(R.string.this_looks_like_channel)); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway); + issuedWarning = true; + return; + } + } - binding.jid.setOnEditorActionListener((v, actionId, event) -> { - handleEnter(binding, account); - return true; - }); + if (mListener != null) { + try { + if (mListener.onEnterJidDialogPositive(accountJid, contactJid)) { + dialog.dismiss(); + } + } catch (JidError error) { + binding.jidLayout.setError(error.toString()); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add); + issuedWarning = false; + } + } + } - dialog.show(); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(dialogOnClick); - return dialog; - } + public void setOnEnterJidDialogPositiveListener(OnEnterJidDialogPositiveListener listener) { + this.mListener = listener; + } - private void handleEnter(EnterJidDialogBinding binding, String account) { - final Jid accountJid; - if (!binding.account.isEnabled() && account == null) { - return; - } - try { - if (Config.DOMAIN_LOCK != null) { - accountJid = Jid.ofEscaped((String) binding.account.getSelectedItem(), Config.DOMAIN_LOCK, null); - } else { - accountJid = Jid.ofEscaped((String) binding.account.getSelectedItem()); - } - } catch (final IllegalArgumentException e) { - return; - } - final Jid contactJid; - try { - contactJid = Jid.ofEscaped(binding.jid.getText().toString()); - } catch (final IllegalArgumentException e) { - binding.jidLayout.setError(getActivity().getString(R.string.invalid_jid)); - return; - } + @Override + public void onBackendConnected() { + refreshKnownHosts(); + } - if (!issuedWarning && sanityCheckJid) { - if (contactJid.isDomainJid()) { - binding.jidLayout.setError(getActivity().getString(R.string.this_looks_like_a_domain)); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway); - issuedWarning = true; - return; - } - if (suspiciousSubDomain(contactJid.getDomain().toEscapedString())) { - binding.jidLayout.setError(getActivity().getString(R.string.this_looks_like_channel)); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anway); - issuedWarning = true; - return; - } - } + private void refreshKnownHosts() { + final Activity activity = getActivity(); + if (activity instanceof XmppActivity) { + final XmppConnectionService service = ((XmppActivity) activity).xmppConnectionService; + if (service == null) { + return; + } + final Collection hosts = service.getKnownHosts(); + this.knownHostsAdapter.refresh(hosts); + this.whitelistedDomains = hosts; + } + } - if (mListener != null) { - try { - if (mListener.onEnterJidDialogPositive(accountJid, contactJid)) { - dialog.dismiss(); - } - } catch (JidError error) { - binding.jidLayout.setError(error.toString()); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add); - issuedWarning = false; - } - } - } + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - public void setOnEnterJidDialogPositiveListener(OnEnterJidDialogPositiveListener listener) { - this.mListener = listener; - } + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} - @Override - public void onBackendConnected() { - refreshKnownHosts(); - } + @Override + public void afterTextChanged(Editable s) { + if (issuedWarning) { + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add); + binding.jidLayout.setError(null); + issuedWarning = false; + } + } - private void refreshKnownHosts() { - Activity activity = getActivity(); - if (activity instanceof XmppActivity) { - Collection hosts = ((XmppActivity) activity).xmppConnectionService.getKnownHosts(); - this.knownHostsAdapter.refresh(hosts); - this.whitelistedDomains = hosts; - } - } + public interface OnEnterJidDialogPositiveListener { + boolean onEnterJidDialogPositive(Jid account, Jid contact) throws EnterJidDialog.JidError; + } - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { + public static class JidError extends Exception { + final String msg; - } + public JidError(final String msg) { + this.msg = msg; + } - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { + @NonNull + public String toString() { + return msg; + } + } - } + @Override + public void onDestroyView() { + Dialog dialog = getDialog(); + if (dialog != null && getRetainInstance()) { + dialog.setDismissMessage(null); + } + super.onDestroyView(); + } - @Override - public void afterTextChanged(Editable s) { - if (issuedWarning) { - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add); - binding.jidLayout.setError(null); - issuedWarning = false; - } - } - - public interface OnEnterJidDialogPositiveListener { - boolean onEnterJidDialogPositive(Jid account, Jid contact) throws EnterJidDialog.JidError; - } - - public static class JidError extends Exception { - final String msg; - - public JidError(final String msg) { - this.msg = msg; - } - - public String toString() { - return msg; - } - } - - @Override - public void onDestroyView() { - Dialog dialog = getDialog(); - if (dialog != null && getRetainInstance()) { - dialog.setDismissMessage(null); - } - super.onDestroyView(); - } - - private boolean suspiciousSubDomain(String domain) { - if (this.whitelistedDomains.contains(domain)) { - return false; - } - final String[] parts = domain.split("\\."); - return parts.length >= 3 && SUSPICIOUS_DOMAINS.contains(parts[0]); - } + private boolean suspiciousSubDomain(String domain) { + if (this.whitelistedDomains.contains(domain)) { + return false; + } + final String[] parts = domain.split("\\."); + return parts.length >= 3 && SUSPICIOUS_DOMAINS.contains(parts[0]); + } } diff --git a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java index 6146c4ae7..bc9972316 100644 --- a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java @@ -1,11 +1,12 @@ package eu.siacs.conversations.ui; import android.app.Activity; -import android.content.Context; import android.content.Intent; import android.media.MediaRecorder; import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.os.Environment; import android.os.FileObserver; import android.os.Handler; import android.os.SystemClock; @@ -17,25 +18,22 @@ import android.widget.Toast; import androidx.databinding.DataBindingUtil; import java.io.File; -import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; +import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityRecordingBinding; -import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.utils.ThemeHelper; import eu.siacs.conversations.utils.TimeFrameUtils; public class RecordingActivity extends Activity implements View.OnClickListener { - public static String STORAGE_DIRECTORY_TYPE_NAME = "Recordings"; - private ActivityRecordingBinding binding; private MediaRecorder mRecorder; @@ -44,13 +42,14 @@ public class RecordingActivity extends Activity implements View.OnClickListener private final CountDownLatch outputFileWrittenLatch = new CountDownLatch(1); private final Handler mHandler = new Handler(); - private final Runnable mTickExecutor = new Runnable() { - @Override - public void run() { - tick(); - mHandler.postDelayed(mTickExecutor, 100); - } - }; + private final Runnable mTickExecutor = + new Runnable() { + @Override + public void run() { + tick(); + mHandler.postDelayed(mTickExecutor, 100); + } + }; private File mOutputFile; @@ -68,7 +67,7 @@ public class RecordingActivity extends Activity implements View.OnClickListener } @Override - protected void onResume(){ + protected void onResume() { super.onResume(); SettingsUtils.applyScreenshotPreventionSetting(this); } @@ -137,56 +136,69 @@ public class RecordingActivity extends Activity implements View.OnClickListener } } if (saveFile) { - new Thread(() -> { - try { - if (!outputFileWrittenLatch.await(2, TimeUnit.SECONDS)) { - Log.d(Config.LOGTAG, "time out waiting for output file to be written"); - } - } catch (InterruptedException e) { - Log.d(Config.LOGTAG, "interrupted while waiting for output file to be written", e); - } - runOnUiThread(() -> { - setResult(Activity.RESULT_OK, new Intent().setData(Uri.fromFile(mOutputFile))); - finish(); - }); - }).start(); + new Thread( + () -> { + try { + if (!outputFileWrittenLatch.await(2, TimeUnit.SECONDS)) { + Log.d( + Config.LOGTAG, + "time out waiting for output file to be written"); + } + } catch (InterruptedException e) { + Log.d( + Config.LOGTAG, + "interrupted while waiting for output file to be written", + e); + } + runOnUiThread( + () -> { + setResult( + Activity.RESULT_OK, + new Intent() + .setData(Uri.fromFile(mOutputFile))); + finish(); + }); + }) + .start(); } } - private static File generateOutputFilename(Context context) { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); - String filename = "RECORDING_" + dateFormat.format(new Date()) + ".m4a"; - return new File(FileBackend.getConversationsDirectory(context, STORAGE_DIRECTORY_TYPE_NAME) + "/" + filename); + private File generateOutputFilename() { + final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); + final String filename = "RECORDING_" + dateFormat.format(new Date()) + ".m4a"; + final File parentDirectory; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + parentDirectory = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_RECORDINGS); + } else { + parentDirectory = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + } + final File conversationsDirectory = new File(parentDirectory, getString(R.string.app_name)); + return new File(conversationsDirectory, filename); } private void setupOutputFile() { - mOutputFile = generateOutputFilename(this); - File parentDirectory = mOutputFile.getParentFile(); - if (parentDirectory.mkdirs()) { + mOutputFile = generateOutputFilename(); + final File parentDirectory = mOutputFile.getParentFile(); + if (Objects.requireNonNull(parentDirectory).mkdirs()) { Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath()); } - File noMedia = new File(parentDirectory, ".nomedia"); - if (!noMedia.exists()) { - try { - if (noMedia.createNewFile()) { - Log.d(Config.LOGTAG, "created nomedia file in " + parentDirectory.getAbsolutePath()); - } - } catch (IOException e) { - Log.d(Config.LOGTAG, "unable to create nomedia file in " + parentDirectory.getAbsolutePath(), e); - } - } setupFileObserver(parentDirectory); } private void setupFileObserver(File directory) { - mFileObserver = new FileObserver(directory.getAbsolutePath()) { - @Override - public void onEvent(int event, String s) { - if (s != null && s.equals(mOutputFile.getName()) && event == FileObserver.CLOSE_WRITE) { - outputFileWrittenLatch.countDown(); - } - } - }; + mFileObserver = + new FileObserver(directory.getAbsolutePath()) { + @Override + public void onEvent(int event, String s) { + if (s != null + && s.equals(mOutputFile.getName()) + && event == FileObserver.CLOSE_WRITE) { + outputFileWrittenLatch.countDown(); + } + } + }; mFileObserver.startWatching(); } diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 759256c80..1b5a816e5 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -71,10 +71,9 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; -import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied; -import static java.util.Arrays.asList; - -public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate, eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged { +public class RtpSessionActivity extends XmppActivity + implements XmppConnectionService.OnJingleRtpConnectionUpdate, + eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged { public static final String EXTRA_WITH = "with"; public static final String EXTRA_SESSION_ID = "session_id"; @@ -86,33 +85,31 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private static final int CALL_DURATION_UPDATE_INTERVAL = 333; - public static final List END_CARD = Arrays.asList( - RtpEndUserState.APPLICATION_ERROR, - RtpEndUserState.SECURITY_ERROR, - RtpEndUserState.DECLINED_OR_BUSY, - RtpEndUserState.CONNECTIVITY_ERROR, - RtpEndUserState.CONNECTIVITY_LOST_ERROR, - RtpEndUserState.RETRACTED - ); - private static final List STATES_SHOWING_HELP_BUTTON = Arrays.asList( - RtpEndUserState.APPLICATION_ERROR, - RtpEndUserState.CONNECTIVITY_ERROR, - RtpEndUserState.SECURITY_ERROR - ); - private static final List STATES_SHOWING_SWITCH_TO_CHAT = Arrays.asList( - RtpEndUserState.CONNECTING, - RtpEndUserState.CONNECTED, - RtpEndUserState.RECONNECTING - ); - private static final List STATES_CONSIDERED_CONNECTED = Arrays.asList( - RtpEndUserState.CONNECTED, - RtpEndUserState.RECONNECTING - ); - private static final List STATES_SHOWING_PIP_PLACEHOLDER = Arrays.asList( - RtpEndUserState.ACCEPTING_CALL, - RtpEndUserState.CONNECTING, - RtpEndUserState.RECONNECTING - ); + public static final List END_CARD = + Arrays.asList( + RtpEndUserState.APPLICATION_ERROR, + RtpEndUserState.SECURITY_ERROR, + RtpEndUserState.DECLINED_OR_BUSY, + RtpEndUserState.CONNECTIVITY_ERROR, + RtpEndUserState.CONNECTIVITY_LOST_ERROR, + RtpEndUserState.RETRACTED); + private static final List STATES_SHOWING_HELP_BUTTON = + Arrays.asList( + RtpEndUserState.APPLICATION_ERROR, + RtpEndUserState.CONNECTIVITY_ERROR, + RtpEndUserState.SECURITY_ERROR); + private static final List STATES_SHOWING_SWITCH_TO_CHAT = + Arrays.asList( + RtpEndUserState.CONNECTING, + RtpEndUserState.CONNECTED, + RtpEndUserState.RECONNECTING); + private static final List STATES_CONSIDERED_CONNECTED = + Arrays.asList(RtpEndUserState.CONNECTED, RtpEndUserState.RECONNECTING); + private static final List STATES_SHOWING_PIP_PLACEHOLDER = + Arrays.asList( + RtpEndUserState.ACCEPTING_CALL, + RtpEndUserState.CONNECTING, + RtpEndUserState.RECONNECTING); private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session"; private static final int REQUEST_ACCEPT_CALL = 0x1111; private WeakReference rtpConnectionReference; @@ -121,13 +118,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private PowerManager.WakeLock mProximityWakeLock; private final Handler mHandler = new Handler(); - private final Runnable mTickExecutor = new Runnable() { - @Override - public void run() { - updateCallDuration(); - mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL); - } - }; + private final Runnable mTickExecutor = + new Runnable() { + @Override + public void run() { + updateCallDuration(); + mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL); + } + }; private static Set actionToMedia(final String action) { if (ACTION_MAKE_VIDEO_CALL.equals(action)) { @@ -137,21 +135,27 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } - private static void addSink(final VideoTrack videoTrack, final SurfaceViewRenderer surfaceViewRenderer) { + private static void addSink( + final VideoTrack videoTrack, final SurfaceViewRenderer surfaceViewRenderer) { try { videoTrack.addSink(surfaceViewRenderer); } catch (final IllegalStateException e) { - Log.e(Config.LOGTAG, "possible race condition on trying to display video track. ignoring", e); + Log.e( + Config.LOGTAG, + "possible race condition on trying to display video track. ignoring", + e); } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON - | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD - | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED - | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); + getWindow() + .addFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session); setSupportActionBar(binding.toolbar); @@ -194,7 +198,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe return STATES_SHOWING_HELP_BUTTON.contains(requireRtpConnection().getEndUserState()); } catch (IllegalStateException e) { final Intent intent = getIntent(); - final String state = intent != null ? intent.getStringExtra(EXTRA_LAST_REPORTED_STATE) : null; + final String state = + intent != null ? intent.getStringExtra(EXTRA_LAST_REPORTED_STATE) : null; if (state != null) { return STATES_SHOWING_HELP_BUTTON.contains(RtpEndUserState.valueOf(state)); } else { @@ -204,8 +209,10 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private boolean isSwitchToConversationVisible() { - final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; - return connection != null && STATES_SHOWING_SWITCH_TO_CHAT.contains(connection.getEndUserState()); + final JingleRtpConnection connection = + this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + return connection != null + && STATES_SHOWING_SWITCH_TO_CHAT.contains(connection.getEndUserState()); } private boolean isAudioOnlyConversation() { @@ -217,7 +224,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void switchToConversation() { final Contact contact = getWith(); - final Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true); + final Conversation conversation = + xmppConnectionService.findOrCreateConversation( + contact.getAccount(), contact.getJid(), false, true); switchToConversation(conversation); } @@ -250,7 +259,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe try { startActivity(intent); } catch (final ActivityNotFoundException e) { - Toast.makeText(this, R.string.no_application_found_to_open_link, Toast.LENGTH_LONG).show(); + Toast.makeText(this, R.string.no_application_found_to_open_link, Toast.LENGTH_LONG) + .show(); } } @@ -273,10 +283,15 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final Account account = extractAccount(intent); final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH)); final String state = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE); - if (!Intent.ACTION_VIEW.equals(action) || state == null || !END_CARD.contains(RtpEndUserState.valueOf(state))) { - resetIntent(account, with, RtpEndUserState.RETRACTED, actionToMedia(intent.getAction())); + if (!Intent.ACTION_VIEW.equals(action) + || state == null + || !END_CARD.contains(RtpEndUserState.valueOf(state))) { + resetIntent( + account, with, RtpEndUserState.RETRACTED, actionToMedia(intent.getAction())); } - xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid()); + xmppConnectionService + .getJingleConnectionManager() + .retractSessionProposal(account, with.asBareJid()); } private void rejectCall(View view) { @@ -291,7 +306,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void requestPermissionsAndAcceptCall() { final List permissions; if (getMedia().contains(Media.VIDEO)) { - permissions = ImmutableList.of(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO); + permissions = + ImmutableList.of(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO); } else { permissions = ImmutableList.of(Manifest.permission.RECORD_AUDIO); } @@ -302,7 +318,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void checkRecorderAndAcceptCall() { - checkMicrophoneAvailability(); + checkMicrophoneAvailabilityAsync(); try { requireRtpConnection().acceptCall(); } catch (final IllegalStateException e) { @@ -310,18 +326,22 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } + private void checkMicrophoneAvailabilityAsync() { + new Thread(this::checkMicrophoneAvailability).start(); + } + private void checkMicrophoneAvailability() { - new Thread(() -> { - final long start = SystemClock.elapsedRealtime(); - final boolean isMicrophoneAvailable = AppRTCAudioManager.isMicrophoneAvailable(); - final long stop = SystemClock.elapsedRealtime(); - Log.d(Config.LOGTAG, "checking microphone availability took " + (stop - start) + "ms"); - if (isMicrophoneAvailable) { - return; - } - runOnUiThread(() -> Toast.makeText(this, R.string.microphone_unavailable, Toast.LENGTH_LONG).show()); + final long start = SystemClock.elapsedRealtime(); + final boolean isMicrophoneAvailable = AppRTCAudioManager.isMicrophoneAvailable(); + final long stop = SystemClock.elapsedRealtime(); + Log.d(Config.LOGTAG, "checking microphone availability took " + (stop - start) + "ms"); + if (isMicrophoneAvailable) { + return; } - ).start(); + runOnUiThread( + () -> + Toast.makeText(this, R.string.microphone_unavailable, Toast.LENGTH_LONG) + .show()); } private void putScreenInCallMode() { @@ -331,9 +351,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void putScreenInCallMode(final Set media) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); if (!media.contains(Media.VIDEO)) { - final JingleRtpConnection rtpConnection = rtpConnectionReference != null ? rtpConnectionReference.get() : null; - final AppRTCAudioManager audioManager = rtpConnection == null ? null : rtpConnection.getAudioManager(); - if (audioManager == null || audioManager.getSelectedAudioDevice() == AppRTCAudioManager.AudioDevice.EARPIECE) { + final JingleRtpConnection rtpConnection = + rtpConnectionReference != null ? rtpConnectionReference.get() : null; + final AppRTCAudioManager audioManager = + rtpConnection == null ? null : rtpConnection.getAudioManager(); + if (audioManager == null + || audioManager.getSelectedAudioDevice() + == AppRTCAudioManager.AudioDevice.EARPIECE) { acquireProximityWakeLock(); } } @@ -346,30 +370,31 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe Log.e(Config.LOGTAG, "power manager not available"); return; } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - if (this.mProximityWakeLock == null) { - this.mProximityWakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, PROXIMITY_WAKE_LOCK_TAG); - } - if (!this.mProximityWakeLock.isHeld()) { - Log.d(Config.LOGTAG, "acquiring proximity wake lock"); - this.mProximityWakeLock.acquire(); - } + if (isFinishing()) { + Log.e(Config.LOGTAG, "do not acquire wakelock. activity is finishing"); + return; + } + if (this.mProximityWakeLock == null) { + this.mProximityWakeLock = + powerManager.newWakeLock( + PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, PROXIMITY_WAKE_LOCK_TAG); + } + if (!this.mProximityWakeLock.isHeld()) { + Log.d(Config.LOGTAG, "acquiring proximity wake lock"); + this.mProximityWakeLock.acquire(); } } private void releaseProximityWakeLock() { if (this.mProximityWakeLock != null && mProximityWakeLock.isHeld()) { Log.d(Config.LOGTAG, "releasing proximity wake lock"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - this.mProximityWakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY); - } else { - this.mProximityWakeLock.release(); - } + this.mProximityWakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY); this.mProximityWakeLock = null; } } - private void putProximityWakeLockInProperState(final AppRTCAudioManager.AudioDevice audioDevice) { + private void putProximityWakeLockInProperState( + final AppRTCAudioManager.AudioDevice audioDevice) { if (audioDevice == AppRTCAudioManager.AudioDevice.EARPIECE) { acquireProximityWakeLock(); } else { @@ -378,9 +403,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } @Override - protected void refreshUiReal() { - - } + protected void refreshUiReal() {} @Override public void onNewIntent(final Intent intent) { @@ -388,7 +411,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe super.onNewIntent(intent); setIntent(intent); if (xmppConnectionService == null) { - Log.d(Config.LOGTAG, "RtpSessionActivity: background service wasn't bound in onNewIntent()"); + Log.d( + Config.LOGTAG, + "RtpSessionActivity: background service wasn't bound in onNewIntent()"); return; } final Account account = extractAccount(intent); @@ -407,8 +432,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) { proposeJingleRtpSession(account, with, actionToMedia(action)); - binding.with.setText(account.getRoster().getContact(with).getDisplayName()); - binding.withJid.setText(with.asBareJid()); + setWith(account.getRoster().getContact(with)); } else { throw new IllegalStateException("received onNewIntent without sessionId"); } @@ -432,25 +456,29 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) { proposeJingleRtpSession(account, with, actionToMedia(action)); - binding.with.setText(account.getRoster().getContact(with).getDisplayName()); - binding.withJid.setText(with.asBareJid()); + setWith(account.getRoster().getContact(with)); } else if (Intent.ACTION_VIEW.equals(action)) { final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE); - final RtpEndUserState state = extraLastState == null ? null : RtpEndUserState.valueOf(extraLastState); + final RtpEndUserState state = + extraLastState == null ? null : RtpEndUserState.valueOf(extraLastState); if (state != null) { Log.d(Config.LOGTAG, "restored last state from intent extra"); updateButtonConfiguration(state); updateVerifiedShield(false); updateStateDisplay(state); - updateProfilePicture(state); + updateIncomingCallScreen(state); invalidateOptionsMenu(); } - binding.with.setText(account.getRoster().getContact(with).getDisplayName()); - binding.withJid.setText(with.asBareJid()); - if (xmppConnectionService.getJingleConnectionManager().fireJingleRtpConnectionStateUpdates()) { + setWith(account.getRoster().getContact(with)); + if (xmppConnectionService + .getJingleConnectionManager() + .fireJingleRtpConnectionStateUpdates()) { return; } - if (END_CARD.contains(state) || xmppConnectionService.getJingleConnectionManager().hasMatchingProposal(account, with)) { + if (END_CARD.contains(state) + || xmppConnectionService + .getJingleConnectionManager() + .hasMatchingProposal(account, with)) { return; } Log.d(Config.LOGTAG, "restored state (" + state + ") was not an end card. finishing"); @@ -458,12 +486,27 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } - private void proposeJingleRtpSession(final Account account, final Jid with, final Set media) { - checkMicrophoneAvailability(); + private void setWith() { + setWith(getWith()); + } + + private void setWith(final Contact contact) { + binding.with.setText(contact.getDisplayName()); + binding.withJid.setText(contact.getJid().asBareJid().toEscapedString()); + } + + private void proposeJingleRtpSession( + final Account account, final Jid with, final Set media) { + checkMicrophoneAvailabilityAsync(); if (with.isBareJid()) { - xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with, media); + xmppConnectionService + .getJingleConnectionManager() + .proposeJingleRtpSession(account, with, media); } else { - final String sessionId = xmppConnectionService.getJingleConnectionManager().initializeRtpSession(account, with, media); + final String sessionId = + xmppConnectionService + .getJingleConnectionManager() + .initializeRtpSession(account, with, media); initializeActivityWithRunningRtpSession(account, with, sessionId); resetIntent(account, with, sessionId); } @@ -471,7 +514,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (PermissionUtils.allGranted(grantResults)) { if (requestCode == REQUEST_ACCEPT_CALL) { @@ -487,7 +531,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } else { throw new IllegalStateException("Invalid permission result request"); } - Toast.makeText(this, getString(res, getString(R.string.app_name)), Toast.LENGTH_SHORT).show(); + Toast.makeText(this, getString(res, getString(R.string.app_name)), Toast.LENGTH_SHORT) + .show(); } } @@ -505,7 +550,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe binding.remoteVideo.setOnAspectRatioChanged(null); binding.localVideo.release(); final WeakReference weakReference = this.rtpConnectionReference; - final JingleRtpConnection jingleRtpConnection = weakReference == null ? null : weakReference.get(); + final JingleRtpConnection jingleRtpConnection = + weakReference == null ? null : weakReference.get(); if (jingleRtpConnection != null) { releaseVideoTracks(jingleRtpConnection); } @@ -542,15 +588,18 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe if (switchToPictureInPicture()) { return; } - //TODO apparently this method is not getting called on Android 10 when using the task switcher + // TODO apparently this method is not getting called on Android 10 when using the task + // switcher if (emptyReference(rtpConnectionReference) && xmppConnectionService != null) { retractSessionProposal(); } } private boolean isConnected() { - final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; - return connection != null && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState()); + final JingleRtpConnection connection = + this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + return connection != null + && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState()); } private boolean switchToPictureInPicture() { @@ -568,14 +617,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe try { final Rational rational = this.binding.remoteVideo.getAspectRatio(); final Rational clippedRational = Rationals.clip(rational); - Log.d(Config.LOGTAG, "suggested rational " + rational + ". clipped to " + clippedRational); + Log.d( + Config.LOGTAG, + "suggested rational " + rational + ". clipped to " + clippedRational); enterPictureInPictureMode( - new PictureInPictureParams.Builder() - .setAspectRatio(clippedRational) - .build() - ); + new PictureInPictureParams.Builder().setAspectRatio(clippedRational).build()); } catch (final IllegalStateException e) { - //this sometimes happens on Samsung phones (possibly when Knox is enabled) + // this sometimes happens on Samsung phones (possibly when Knox is enabled) Log.w(Config.LOGTAG, "unable to enter picture in picture mode", e); } } @@ -584,10 +632,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe public void onAspectRatioChanged(final Rational rational) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPictureInPicture()) { final Rational clippedRational = Rationals.clip(rational); - Log.d(Config.LOGTAG, "suggested rational after aspect ratio change " + rational + ". clipped to " + clippedRational); - setPictureInPictureParams(new PictureInPictureParams.Builder() - .setAspectRatio(clippedRational) - .build()); + Log.d( + Config.LOGTAG, + "suggested rational after aspect ratio change " + + rational + + ". clipped to " + + clippedRational); + setPictureInPictureParams( + new PictureInPictureParams.Builder().setAspectRatio(clippedRational).build()); } } @@ -602,24 +654,31 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private boolean shouldBePictureInPicture() { try { final JingleRtpConnection rtpConnection = requireRtpConnection(); - return rtpConnection.getMedia().contains(Media.VIDEO) && Arrays.asList( - RtpEndUserState.ACCEPTING_CALL, - RtpEndUserState.CONNECTING, - RtpEndUserState.CONNECTED - ).contains(rtpConnection.getEndUserState()); + return rtpConnection.getMedia().contains(Media.VIDEO) + && Arrays.asList( + RtpEndUserState.ACCEPTING_CALL, + RtpEndUserState.CONNECTING, + RtpEndUserState.CONNECTED) + .contains(rtpConnection.getEndUserState()); } catch (final IllegalStateException e) { return false; } } - private boolean initializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) { - final WeakReference reference = xmppConnectionService.getJingleConnectionManager() - .findJingleRtpConnection(account, with, sessionId); + private boolean initializeActivityWithRunningRtpSession( + final Account account, Jid with, String sessionId) { + final WeakReference reference = + xmppConnectionService + .getJingleConnectionManager() + .findJingleRtpConnection(account, with, sessionId); if (reference == null || reference.get() == null) { - final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession = xmppConnectionService - .getJingleConnectionManager().getTerminalSessionState(with, sessionId); + final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession = + xmppConnectionService + .getJingleConnectionManager() + .getTerminalSessionState(with, sessionId); if (terminatedRtpSession == null) { - throw new IllegalStateException("failed to initialize activity with running rtp session. session not found"); + throw new IllegalStateException( + "failed to initialize activity with running rtp session. session not found"); } initializeWithTerminatedSessionState(account, with, terminatedRtpSession); return true; @@ -628,7 +687,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final RtpEndUserState currentState = requireRtpConnection().getEndUserState(); final boolean verified = requireRtpConnection().isVerified(); if (currentState == RtpEndUserState.ENDED) { - reference.get().throwStateTransitionException(); finish(); return true; } @@ -636,21 +694,24 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe if (currentState == RtpEndUserState.INCOMING_CALL) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } - if (JingleRtpConnection.STATES_SHOWING_ONGOING_CALL.contains(requireRtpConnection().getState())) { + if (JingleRtpConnection.STATES_SHOWING_ONGOING_CALL.contains( + requireRtpConnection().getState())) { putScreenInCallMode(); } - binding.with.setText(getWith().getDisplayName()); - binding.withJid.setText(with.asBareJid()); + setWith(); updateVideoViews(currentState); updateStateDisplay(currentState, media); updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(currentState)); updateButtonConfiguration(currentState, media); - updateProfilePicture(currentState); + updateIncomingCallScreen(currentState); invalidateOptionsMenu(); return false; } - private void initializeWithTerminatedSessionState(final Account account, final Jid with, final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession) { + private void initializeWithTerminatedSessionState( + final Account account, + final Jid with, + final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession) { Log.d(Config.LOGTAG, "initializeWithTerminatedSessionState()"); if (terminatedRtpSession.state == RtpEndUserState.ENDED) { finish(); @@ -660,15 +721,15 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe resetIntent(account, with, terminatedRtpSession.state, terminatedRtpSession.media); updateButtonConfiguration(state); updateStateDisplay(state); - updateProfilePicture(state); + updateIncomingCallScreen(state); updateCallDuration(); updateVerifiedShield(false); invalidateOptionsMenu(); - binding.with.setText(account.getRoster().getContact(with).getDisplayName()); - binding.withJid.setText(with.asBareJid()); + setWith(account.getRoster().getContact(with)); } - private void reInitializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) { + private void reInitializeActivityWithRunningRtpSession( + final Account account, Jid with, String sessionId) { runOnUiThread(() -> initializeActivityWithRunningRtpSession(account, with, sessionId)); resetIntent(account, with, sessionId); } @@ -686,7 +747,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe try { surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null); } catch (final IllegalStateException e) { - //Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized"); + // Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized"); } surfaceViewRenderer.setEnableHardwareScaler(true); } @@ -745,9 +806,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe setTitle(R.string.rtp_state_security_error); break; case ENDED: - throw new IllegalStateException("Activity should have called finishAndReleaseWakeLock();"); + throw new IllegalStateException( + "Activity should have called finishAndReleaseWakeLock();"); default: - throw new IllegalStateException(String.format("State %s has not been handled in UI", state)); + throw new IllegalStateException( + String.format("State %s has not been handled in UI", state)); } } @@ -759,24 +822,33 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.binding.verified.setVisibility(verified ? View.VISIBLE : View.GONE); } - private void updateProfilePicture(final RtpEndUserState state) { - updateProfilePicture(state, null); + private void updateIncomingCallScreen(final RtpEndUserState state) { + updateIncomingCallScreen(state, null); } - private void updateProfilePicture(final RtpEndUserState state, final Contact contact) { + private void updateIncomingCallScreen(final RtpEndUserState state, final Contact contact) { if (state == RtpEndUserState.INCOMING_CALL || state == RtpEndUserState.ACCEPTING_CALL) { final boolean show = getResources().getBoolean(R.bool.show_avatar_incoming_call); if (show) { binding.contactPhoto.setVisibility(View.VISIBLE); if (contact == null) { - AvatarWorkerTask.loadAvatar(getWith(), binding.contactPhoto, R.dimen.publish_avatar_size); + AvatarWorkerTask.loadAvatar( + getWith(), binding.contactPhoto, R.dimen.publish_avatar_size); } else { - AvatarWorkerTask.loadAvatar(contact, binding.contactPhoto, R.dimen.publish_avatar_size); + AvatarWorkerTask.loadAvatar( + contact, binding.contactPhoto, R.dimen.publish_avatar_size); } } else { binding.contactPhoto.setVisibility(View.GONE); } + final Account account = contact == null ? getWith().getAccount() : contact.getAccount(); + binding.usingAccount.setVisibility(View.VISIBLE); + binding.usingAccount.setText( + getString( + R.string.using_account, + account.getJid().asBareJid().toEscapedString())); } else { + binding.usingAccount.setVisibility(View.GONE); binding.contactPhoto.setVisibility(View.GONE); } } @@ -816,12 +888,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.binding.acceptCall.setImageResource(R.drawable.ic_voicemail_white_24dp); this.binding.acceptCall.setVisibility(View.VISIBLE); } else if (asList( - RtpEndUserState.CONNECTIVITY_ERROR, - RtpEndUserState.CONNECTIVITY_LOST_ERROR, - RtpEndUserState.APPLICATION_ERROR, - RtpEndUserState.RETRACTED, - RtpEndUserState.SECURITY_ERROR - ).contains(state)) { + RtpEndUserState.CONNECTIVITY_ERROR, + RtpEndUserState.CONNECTIVITY_LOST_ERROR, + RtpEndUserState.APPLICATION_ERROR, + RtpEndUserState.RETRACTED, + RtpEndUserState.SECURITY_ERROR) + .contains(state)) { this.binding.rejectCall.setContentDescription(getString(R.string.exit)); this.binding.rejectCall.setOnClickListener(this::exit); this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp); @@ -851,26 +923,29 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void updateInCallButtonConfiguration() { - updateInCallButtonConfiguration(requireRtpConnection().getEndUserState(), requireRtpConnection().getMedia()); + updateInCallButtonConfiguration( + requireRtpConnection().getEndUserState(), requireRtpConnection().getMedia()); } @SuppressLint("RestrictedApi") - private void updateInCallButtonConfiguration(final RtpEndUserState state, final Set media) { + private void updateInCallButtonConfiguration( + final RtpEndUserState state, final Set media) { if (STATES_CONSIDERED_CONNECTED.contains(state) && !isPictureInPicture()) { Preconditions.checkArgument(media.size() > 0, "Media must not be empty"); if (media.contains(Media.VIDEO)) { final JingleRtpConnection rtpConnection = requireRtpConnection(); - updateInCallButtonConfigurationVideo(rtpConnection.isVideoEnabled(), rtpConnection.isCameraSwitchable()); + updateInCallButtonConfigurationVideo( + rtpConnection.isVideoEnabled(), rtpConnection.isCameraSwitchable()); } else { final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager(); updateInCallButtonConfigurationSpeaker( audioManager.getSelectedAudioDevice(), - audioManager.getAudioDevices().size() - ); + audioManager.getAudioDevices().size()); this.binding.inCallActionFarRight.setVisibility(View.GONE); } if (media.contains(Media.AUDIO)) { - updateInCallButtonConfigurationMicrophone(requireRtpConnection().isMicrophoneEnabled()); + updateInCallButtonConfigurationMicrophone( + requireRtpConnection().isMicrophoneEnabled()); } else { this.binding.inCallActionLeft.setVisibility(View.GONE); } @@ -882,10 +957,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } @SuppressLint("RestrictedApi") - private void updateInCallButtonConfigurationSpeaker(final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices) { + private void updateInCallButtonConfigurationSpeaker( + final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices) { switch (selectedAudioDevice) { case EARPIECE: - this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_off_black_24dp); + this.binding.inCallActionRight.setImageResource( + R.drawable.ic_volume_off_black_24dp); if (numberOfChoices >= 2) { this.binding.inCallActionRight.setOnClickListener(this::switchToSpeaker); } else { @@ -908,7 +985,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } break; case BLUETOOTH: - this.binding.inCallActionRight.setImageResource(R.drawable.ic_bluetooth_audio_black_24dp); + this.binding.inCallActionRight.setImageResource( + R.drawable.ic_bluetooth_audio_black_24dp); this.binding.inCallActionRight.setOnClickListener(null); this.binding.inCallActionRight.setClickable(false); break; @@ -917,10 +995,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } @SuppressLint("RestrictedApi") - private void updateInCallButtonConfigurationVideo(final boolean videoEnabled, final boolean isCameraSwitchable) { + private void updateInCallButtonConfigurationVideo( + final boolean videoEnabled, final boolean isCameraSwitchable) { this.binding.inCallActionRight.setVisibility(View.VISIBLE); if (isCameraSwitchable) { - this.binding.inCallActionFarRight.setImageResource(R.drawable.ic_flip_camera_android_black_24dp); + this.binding.inCallActionFarRight.setImageResource( + R.drawable.ic_flip_camera_android_black_24dp); this.binding.inCallActionFarRight.setVisibility(View.VISIBLE); this.binding.inCallActionFarRight.setOnClickListener(this::switchCamera); } else { @@ -936,18 +1016,28 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void switchCamera(final View view) { - Futures.addCallback(requireRtpConnection().switchCamera(), new FutureCallback() { - @Override - public void onSuccess(@NullableDecl Boolean isFrontCamera) { - binding.localVideo.setMirror(isFrontCamera); - } + Futures.addCallback( + requireRtpConnection().switchCamera(), + new FutureCallback() { + @Override + public void onSuccess(@NullableDecl Boolean isFrontCamera) { + binding.localVideo.setMirror(isFrontCamera); + } - @Override - public void onFailure(@NonNull final Throwable throwable) { - Log.d(Config.LOGTAG, "could not switch camera", Throwables.getRootCause(throwable)); - Toast.makeText(RtpSessionActivity.this, R.string.could_not_switch_camera, Toast.LENGTH_LONG).show(); - } - }, MainThreadExecutor.getInstance()); + @Override + public void onFailure(@NonNull final Throwable throwable) { + Log.d( + Config.LOGTAG, + "could not switch camera", + Throwables.getRootCause(throwable)); + Toast.makeText( + RtpSessionActivity.this, + R.string.could_not_switch_camera, + Toast.LENGTH_LONG) + .show(); + } + }, + MainThreadExecutor.getInstance()); } private void enableVideo(View view) { @@ -963,7 +1053,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private void disableVideo(View view) { requireRtpConnection().setVideoEnabled(false); updateInCallButtonConfigurationVideo(false, requireRtpConnection().isCameraSwitchable()); - } @SuppressLint("RestrictedApi") @@ -979,7 +1068,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void updateCallDuration() { - final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + final JingleRtpConnection connection = + this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; if (connection == null || connection.getMedia().contains(Media.VIDEO)) { this.binding.duration.setVisibility(View.GONE); return; @@ -987,7 +1077,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe if (connection.zeroDuration()) { this.binding.duration.setVisibility(View.GONE); } else { - this.binding.duration.setText(TimeFrameUtils.formatElapsedTime(connection.getCallDuration(), false)); + this.binding.duration.setText( + TimeFrameUtils.formatElapsedTime(connection.getCallDuration(), false)); this.binding.duration.setVisibility(View.VISIBLE); } } @@ -1003,9 +1094,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe binding.appBarLayout.setVisibility(View.GONE); binding.pipPlaceholder.setVisibility(View.VISIBLE); if (Arrays.asList( - RtpEndUserState.APPLICATION_ERROR, - RtpEndUserState.CONNECTIVITY_ERROR, - RtpEndUserState.SECURITY_ERROR) + RtpEndUserState.APPLICATION_ERROR, + RtpEndUserState.CONNECTIVITY_ERROR, + RtpEndUserState.SECURITY_ERROR) .contains(state)) { binding.pipWarning.setVisibility(View.VISIBLE); binding.pipWaiting.setVisibility(View.GONE); @@ -1033,7 +1124,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final Optional localVideoTrack = getLocalVideoTrack(); if (localVideoTrack.isPresent() && !isPictureInPicture()) { ensureSurfaceViewRendererIsSetup(binding.localVideo); - //paint local view over remote view + // paint local view over remote view binding.localVideo.setZOrderMediaOverlay(true); binding.localVideo.setMirror(requireRtpConnection().isFrontCamera()); addSink(localVideoTrack.get(), binding.localVideo); @@ -1046,8 +1137,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe addSink(remoteVideoTrack.get(), binding.remoteVideo); binding.remoteVideo.setScalingType( RendererCommon.ScalingType.SCALE_ASPECT_FILL, - RendererCommon.ScalingType.SCALE_ASPECT_FIT - ); + RendererCommon.ScalingType.SCALE_ASPECT_FIT); if (state == RtpEndUserState.CONNECTED) { binding.appBarLayout.setVisibility(View.GONE); getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); @@ -1070,7 +1160,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private Optional getLocalVideoTrack() { - final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + final JingleRtpConnection connection = + this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; if (connection == null) { return Optional.absent(); } @@ -1078,7 +1169,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private Optional getRemoteVideoTrack() { - final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + final JingleRtpConnection connection = + this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; if (connection == null) { return Optional.absent(); } @@ -1100,12 +1192,16 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private void switchToEarpiece(View view) { - requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE); + requireRtpConnection() + .getAudioManager() + .setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE); acquireProximityWakeLock(); } private void switchToSpeaker(View view) { - requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE); + requireRtpConnection() + .getAudioManager() + .setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE); releaseProximityWakeLock(); } @@ -1129,12 +1225,15 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final Intent intent = getIntent(); final Account account = extractAccount(intent); final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH)); - final Conversation conversation = xmppConnectionService.findOrCreateConversation(account, with, false, true); + final Conversation conversation = + xmppConnectionService.findOrCreateConversation(account, with, false, true); final Intent launchIntent = new Intent(this, ConversationsActivity.class); launchIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION); launchIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid()); launchIntent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP); - launchIntent.putExtra(ConversationsActivity.EXTRA_POST_INIT_ACTION, ConversationsActivity.POST_ACTION_RECORD_VOICE); + launchIntent.putExtra( + ConversationsActivity.EXTRA_POST_INIT_ACTION, + ConversationsActivity.POST_ACTION_RECORD_VOICE); startActivity(launchIntent); finish(); } @@ -1146,7 +1245,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } private JingleRtpConnection requireRtpConnection() { - final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; + final JingleRtpConnection connection = + this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; if (connection == null) { throw new IllegalStateException("No RTP connection found"); } @@ -1154,12 +1254,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } @Override - public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) { + public void onJingleRtpConnectionUpdate( + Account account, Jid with, final String sessionId, RtpEndUserState state) { Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")"); if (END_CARD.contains(state)) { Log.d(Config.LOGTAG, "end card reached"); releaseProximityWakeLock(); - runOnUiThread(() -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)); + runOnUiThread( + () -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)); } if (with.isBareJid()) { updateRtpSessionProposalState(account, with, state); @@ -1170,7 +1272,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe Log.d(Config.LOGTAG, "not reinitializing session"); return; } - //this happens when going from proposed session to actual session + // this happens when going from proposed session to actual session reInitializeActivityWithRunningRtpSession(account, with, sessionId); return; } @@ -1183,14 +1285,16 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe finish(); return; } - runOnUiThread(() -> { - updateStateDisplay(state, media); - updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(state)); - updateButtonConfiguration(state, media); - updateVideoViews(state); - updateProfilePicture(state, contact); - invalidateOptionsMenu(); - }); + runOnUiThread( + () -> { + updateStateDisplay(state, media); + updateVerifiedShield( + verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(state)); + updateButtonConfiguration(state, media); + updateVideoViews(state); + updateIncomingCallScreen(state, contact); + invalidateOptionsMenu(); + }); if (END_CARD.contains(state)) { final JingleRtpConnection rtpConnection = requireRtpConnection(); resetIntent(account, with, state, rtpConnection.getMedia()); @@ -1203,8 +1307,15 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } @Override - public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { - Log.d(Config.LOGTAG, "onAudioDeviceChanged in activity: selected:" + selectedAudioDevice + ", available:" + availableAudioDevices); + public void onAudioDeviceChanged( + AppRTCAudioManager.AudioDevice selectedAudioDevice, + Set availableAudioDevices) { + Log.d( + Config.LOGTAG, + "onAudioDeviceChanged in activity: selected:" + + selectedAudioDevice + + ", available:" + + availableAudioDevices); try { if (getMedia().contains(Media.VIDEO)) { Log.d(Config.LOGTAG, "nothing to do; in video mode"); @@ -1215,10 +1326,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager(); updateInCallButtonConfigurationSpeaker( audioManager.getSelectedAudioDevice(), - audioManager.getAudioDevices().size() - ); + audioManager.getAudioDevices().size()); } else if (END_CARD.contains(endUserState)) { - Log.d(Config.LOGTAG, "onAudioDeviceChanged() nothing to do because end card has been reached"); + Log.d( + Config.LOGTAG, + "onAudioDeviceChanged() nothing to do because end card has been reached"); } else { putProximityWakeLockInProperState(selectedAudioDevice); } @@ -1233,20 +1345,23 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe outState.putBoolean("dialpad_visible", binding.dialpad.getVisibility() == View.VISIBLE); } - private void updateRtpSessionProposalState(final Account account, final Jid with, final RtpEndUserState state) { + private void updateRtpSessionProposalState( + final Account account, final Jid with, final RtpEndUserState state) { final Intent currentIntent = getIntent(); - final String withExtra = currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH); + final String withExtra = + currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH); if (withExtra == null) { return; } if (Jid.ofEscaped(withExtra).asBareJid().equals(with)) { - runOnUiThread(() -> { - updateVerifiedShield(false); - updateStateDisplay(state); - updateButtonConfiguration(state); - updateProfilePicture(state); - invalidateOptionsMenu(); - }); + runOnUiThread( + () -> { + updateVerifiedShield(false); + updateStateDisplay(state); + updateButtonConfiguration(state); + updateIncomingCallScreen(state); + invalidateOptionsMenu(); + }); resetIntent(account, with, state, actionToMedia(currentIntent.getAction())); } } @@ -1257,16 +1372,22 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe setIntent(intent); } - private void resetIntent(final Account account, Jid with, final RtpEndUserState state, final Set media) { + private void resetIntent( + final Account account, Jid with, final RtpEndUserState state, final Set media) { final Intent intent = new Intent(Intent.ACTION_VIEW); intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString()); - if (account.getRoster().getContact(with).getPresences().anySupport(Namespace.JINGLE_MESSAGE)) { + if (account.getRoster() + .getContact(with) + .getPresences() + .anySupport(Namespace.JINGLE_MESSAGE)) { intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString()); } else { intent.putExtra(EXTRA_WITH, with.toEscapedString()); } intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString()); - intent.putExtra(EXTRA_LAST_ACTION, media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL); + intent.putExtra( + EXTRA_LAST_ACTION, + media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL); setIntent(intent); } diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index 7f4e59d1a..7073b881d 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -224,7 +224,7 @@ public class SettingsActivity extends XmppActivity implements final Preference createBackupPreference = mSettingsFragment.findPreference("create_backup"); if (createBackupPreference != null) { - createBackupPreference.setSummary(getString(R.string.pref_create_backup_summary, FileBackend.getBackupDirectory(this))); + createBackupPreference.setSummary(getString(R.string.pref_create_backup_summary, FileBackend.getBackupDirectory(this).getAbsolutePath())); createBackupPreference.setOnPreferenceClickListener(preference -> { if (hasStoragePermission(REQUEST_CREATE_BACKUP)) { createBackup(); diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 4b5382b44..4ca49fa50 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -71,10 +71,10 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.BarcodeProvider; +import eu.siacs.conversations.services.EmojiInitializationService; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; -import eu.siacs.conversations.ui.service.EmojiService; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; import eu.siacs.conversations.ui.util.PresenceSelector; import eu.siacs.conversations.ui.util.SoftKeyboardUtils; @@ -408,7 +408,7 @@ public abstract class XmppActivity extends ActionBarActivity { super.onCreate(savedInstanceState); metrics = getResources().getDisplayMetrics(); ExceptionHelper.init(getApplicationContext()); - new EmojiService(this).init(); + EmojiInitializationService.execute(this); this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); this.mTheme = findTheme(); setTheme(this.mTheme); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java index 049703597..662120d84 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -23,7 +23,6 @@ import eu.siacs.conversations.ui.ConversationFragment; import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.ui.util.AvatarWorkerTask; import eu.siacs.conversations.ui.util.StyledAttributes; -import eu.siacs.conversations.utils.EmojiWrapper; import eu.siacs.conversations.utils.IrregularUnicodeDetector; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; @@ -57,7 +56,7 @@ public class ConversationAdapter extends RecyclerView.Adapter preview = UIHelper.getMessagePreview(activity, message, viewHolder.binding.conversationLastmsg.getCurrentTextColor()); if (showPreviewText) { - viewHolder.binding.conversationLastmsg.setText(EmojiWrapper.transform(UIHelper.shorten(preview.first))); + viewHolder.binding.conversationLastmsg.setText(UIHelper.shorten(preview.first)); } else { viewHolder.binding.conversationLastmsgImg.setContentDescription(preview.first); } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java index 7665710b0..92da1a34d 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -22,7 +22,6 @@ import eu.siacs.conversations.ui.SettingsActivity; import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.ui.util.AvatarWorkerTask; import eu.siacs.conversations.ui.util.StyledAttributes; -import eu.siacs.conversations.utils.EmojiWrapper; import eu.siacs.conversations.utils.IrregularUnicodeDetector; import eu.siacs.conversations.xmpp.Jid; @@ -85,7 +84,7 @@ public class ListItemAdapter extends ArrayAdapter { } else { viewHolder.jid.setVisibility(View.GONE); } - viewHolder.name.setText(EmojiWrapper.transform(item.getDisplayName())); + viewHolder.name.setText(item.getDisplayName()); AvatarWorkerTask.loadAvatar(item, viewHolder.avatar, R.dimen.avatar); return view; } 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 21473dafd..2733e7b8b 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java @@ -34,7 +34,7 @@ import eu.siacs.conversations.ui.util.ViewUtil; public class MediaAdapter extends RecyclerView.Adapter { - private static final List DOCUMENT_MIMES = Arrays.asList( + public static final List DOCUMENT_MIMES = Arrays.asList( "application/pdf", "application/vnd.oasis.opendocument.text", "application/msword", diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 486ed2325..8e14654a6 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -67,7 +67,6 @@ import eu.siacs.conversations.ui.util.QuoteHelper; import eu.siacs.conversations.ui.util.ViewUtil; import eu.siacs.conversations.ui.widget.ClickableMovementMethod; import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.EmojiWrapper; import eu.siacs.conversations.utils.Emoticons; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.MessageUtils; @@ -339,7 +338,7 @@ public class MessageAdapter extends ArrayAdapter { Spannable span = new SpannableString(body); float size = Emoticons.isEmoji(body) ? 3.0f : 2.0f; span.setSpan(new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - viewHolder.messageBody.setText(EmojiWrapper.transform(span)); + viewHolder.messageBody.setText(span); } private void applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground) { @@ -510,7 +509,7 @@ public class MessageAdapter extends ArrayAdapter { } } viewHolder.messageBody.setAutoLinkMask(0); - viewHolder.messageBody.setText(EmojiWrapper.transform(body)); + viewHolder.messageBody.setText(body); viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance()); } else { viewHolder.messageBody.setText(""); diff --git a/src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java index 013927c4e..1cc630ad3 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java @@ -37,9 +37,10 @@ public class ViewUtil { view(context, file, mime); } - public static void view(Context context, File file, String mime) { - Intent openIntent = new Intent(Intent.ACTION_VIEW); - Uri uri; + private static void view(Context context, File file, String mime) { + Log.d(Config.LOGTAG,"viewing "+file.getAbsolutePath()+" "+mime); + final Intent openIntent = new Intent(Intent.ACTION_VIEW); + final Uri uri; try { uri = FileBackend.getUriForFile(context, file); } catch (SecurityException e) { @@ -49,14 +50,9 @@ public class ViewUtil { } openIntent.setDataAndType(uri, mime); openIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - PackageManager manager = context.getPackageManager(); - List info = manager.queryIntentActivities(openIntent, 0); - if (info.size() == 0) { - openIntent.setDataAndType(uri, "*/*"); - } try { context.startActivity(openIntent); - } catch (ActivityNotFoundException e) { + } catch (final ActivityNotFoundException e) { Toast.makeText(context, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show(); } } diff --git a/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java b/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java index eba833c9b..e890e5984 100644 --- a/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java +++ b/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java @@ -15,6 +15,7 @@ import android.view.KeyEvent; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; +import androidx.appcompat.widget.AppCompatEditText; import androidx.core.view.inputmethod.EditorInfoCompat; import androidx.core.view.inputmethod.InputConnectionCompat; import androidx.core.view.inputmethod.InputContentInfoCompat; @@ -26,7 +27,7 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.ui.util.QuoteHelper; -public class EditMessage extends EmojiWrapperEditText { +public class EditMessage extends AppCompatEditText { private static final InputFilter SPAN_FILTER = (source, start, end, dest, dstart, dend) -> source instanceof Spanned ? source.toString() : source; private final ExecutorService executor = Executors.newSingleThreadExecutor(); diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index 4ee826c3c..a92d48825 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -36,7 +36,7 @@ public final class CryptoHelper { public static final Pattern UUID_PATTERN = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"); final public static byte[] ONE = new byte[]{0, 0, 0, 1}; private static final char[] CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz123456789+-/#$!?".toCharArray(); - private static final int PW_LENGTH = 10; + private static final int PW_LENGTH = 12; private static final char[] VOWELS = "aeiou".toCharArray(); private static final char[] CONSONANTS = "bcfghjklmnpqrstvwxyz".toCharArray(); private final static char[] hexArray = "0123456789abcdef".toCharArray(); diff --git a/src/main/java/eu/siacs/conversations/utils/FileWriterException.java b/src/main/java/eu/siacs/conversations/utils/FileWriterException.java index f406f4197..7e41edfdc 100644 --- a/src/main/java/eu/siacs/conversations/utils/FileWriterException.java +++ b/src/main/java/eu/siacs/conversations/utils/FileWriterException.java @@ -1,4 +1,14 @@ package eu.siacs.conversations.utils; +import java.io.File; + public class FileWriterException extends Exception { + + public FileWriterException(File file) { + super(String.format("Could not write to %s", file.getAbsolutePath())); + } + + FileWriterException() { + + } } diff --git a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java index c0a6f4cfd..90f27f65f 100644 --- a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java @@ -568,11 +568,15 @@ public final class MimeUtils { } private static String getDisplayName(final Context context, final Uri uri) { - try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) { + try (final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { - return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); + final int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + if (index == -1) { + return null; + } + return cursor.getString(index); } - } catch (Exception e) { + } catch (final Exception e) { return null; } return null; 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 90f06fe26..d719c729e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.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.base.Preconditions; @@ -110,6 +112,7 @@ public abstract class AbstractJingleConnection { } @Override + @NonNull public String toString() { return MoreObjects.toStringHelper(this) .add("account", account.getJid()) 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 806108c85..49a3726a5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -52,14 +52,16 @@ import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; public class JingleConnectionManager extends AbstractConnectionManager { - static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor(); + static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = + Executors.newSingleThreadScheduledExecutor(); final ToneManager toneManager; - private final HashMap rtpSessionProposals = new HashMap<>(); - private final ConcurrentHashMap connections = new ConcurrentHashMap<>(); + private final HashMap rtpSessionProposals = + new HashMap<>(); + private final ConcurrentHashMap + connections = new ConcurrentHashMap<>(); - private final Cache terminatedSessions = CacheBuilder.newBuilder() - .expireAfterWrite(24, TimeUnit.HOURS) - .build(); + private final Cache terminatedSessions = + CacheBuilder.newBuilder().expireAfterWrite(24, TimeUnit.HOURS).build(); private final HashMap primaryCandidates = new HashMap<>(); @@ -87,17 +89,31 @@ public class JingleConnectionManager extends AbstractConnectionManager { } else if (packet.getAction() == JinglePacket.Action.SESSION_INITIATE) { final Jid from = packet.getFrom(); final Content content = packet.getJingleContent(); - final String descriptionNamespace = content == null ? null : content.getDescriptionNamespace(); + final String descriptionNamespace = + content == null ? null : content.getDescriptionNamespace(); final AbstractJingleConnection connection; if (FileTransferDescription.NAMESPACES.contains(descriptionNamespace)) { connection = new JingleFileTransferConnection(this, id, from); - } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace) && isUsingClearNet(account)) { - final boolean sessionEnded = this.terminatedSessions.asMap().containsKey(PersistableSessionId.of(id)); - final boolean stranger = isWithStrangerAndStrangerNotificationsAreOff(account, id.with); + } else if (Namespace.JINGLE_APPS_RTP.equals(descriptionNamespace) + && isUsingClearNet(account)) { + final boolean sessionEnded = + this.terminatedSessions.asMap().containsKey(PersistableSessionId.of(id)); + final boolean stranger = + isWithStrangerAndStrangerNotificationsAreOff(account, id.with); if (isBusy() != null || sessionEnded || stranger) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": rejected session with " + id.with + " because busy. sessionEnded=" + sessionEnded + ", stranger=" + stranger); - mXmppConnectionService.sendIqPacket(account, packet.generateResponse(IqPacket.TYPE.RESULT), null); - final JinglePacket sessionTermination = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": rejected session with " + + id.with + + " because busy. sessionEnded=" + + sessionEnded + + ", stranger=" + + stranger); + mXmppConnectionService.sendIqPacket( + account, packet.generateResponse(IqPacket.TYPE.RESULT), null); + final JinglePacket sessionTermination = + new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); sessionTermination.setTo(id.with); sessionTermination.setReason(Reason.BUSY, null); mXmppConnectionService.sendIqPacket(account, sessionTermination, null); @@ -105,7 +121,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { } connection = new JingleRtpConnection(this, id, from); } else { - respondWithJingleError(account, packet, "unsupported-info", "feature-not-implemented", "cancel"); + respondWithJingleError( + account, packet, "unsupported-info", "feature-not-implemented", "cancel"); return; } connections.put(id, connection); @@ -153,14 +170,17 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } - private Optional findMatchingSessionProposal(final Account account, final Jid with, final Set media) { + private Optional findMatchingSessionProposal( + final Account account, final Jid with, final Set media) { synchronized (this.rtpSessionProposals) { - for (Map.Entry entry : this.rtpSessionProposals.entrySet()) { + for (Map.Entry entry : + this.rtpSessionProposals.entrySet()) { final RtpSessionProposal proposal = entry.getKey(); final DeviceDiscoveryState state = entry.getValue(); - final boolean openProposal = state == DeviceDiscoveryState.DISCOVERED - || state == DeviceDiscoveryState.SEARCHING - || state == DeviceDiscoveryState.SEARCHING_ACKNOWLEDGED; + final boolean openProposal = + state == DeviceDiscoveryState.DISCOVERED + || state == DeviceDiscoveryState.SEARCHING + || state == DeviceDiscoveryState.SEARCHING_ACKNOWLEDGED; if (openProposal && proposal.account == account && proposal.with.equals(with.asBareJid()) @@ -190,7 +210,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { } private boolean isWithStrangerAndStrangerNotificationsAreOff(final Account account, Jid with) { - final boolean notifyForStrangers = mXmppConnectionService.getNotificationService().notificationsFromStrangers(); + final boolean notifyForStrangers = + mXmppConnectionService.getNotificationService().notificationsFromStrangers(); if (notifyForStrangers) { return false; } @@ -198,11 +219,17 @@ public class JingleConnectionManager extends AbstractConnectionManager { return !contact.showInContactList(); } - ScheduledFuture schedule(final Runnable runnable, final long delay, final TimeUnit timeUnit) { + ScheduledFuture schedule( + final Runnable runnable, final long delay, final TimeUnit timeUnit) { return SCHEDULED_EXECUTOR_SERVICE.schedule(runnable, delay, timeUnit); } - void respondWithJingleError(final Account account, final IqPacket original, String jingleCondition, String condition, String conditionType) { + void respondWithJingleError( + final Account account, + final IqPacket original, + String jingleCondition, + String condition, + String conditionType) { final IqPacket response = original.generateResponse(IqPacket.TYPE.ERROR); final Element error = response.addChild("error"); error.setAttribute("type", conditionType); @@ -211,7 +238,14 @@ public class JingleConnectionManager extends AbstractConnectionManager { account.getXmppConnection().sendIqPacket(response, null); } - public void deliverMessage(final Account account, final Jid to, final Jid from, final Element message, String remoteMsgId, String serverMsgId, long timestamp) { + public void deliverMessage( + final Account account, + final Jid to, + final Jid from, + final Element message, + String remoteMsgId, + String serverMsgId, + long timestamp) { Preconditions.checkArgument(Namespace.JINGLE_MESSAGE.equals(message.getNamespace())); final String sessionId = message.getAttribute("id"); if (sessionId == null) { @@ -245,16 +279,24 @@ public class JingleConnectionManager extends AbstractConnectionManager { final AbstractJingleConnection existingJingleConnection = connections.get(id); if (existingJingleConnection != null) { if (existingJingleConnection instanceof JingleRtpConnection) { - ((JingleRtpConnection) existingJingleConnection).deliveryMessage(from, message, serverMsgId, timestamp); + ((JingleRtpConnection) existingJingleConnection) + .deliveryMessage(from, message, serverMsgId, timestamp); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + existingJingleConnection.getClass().getName() + " does not support jingle messages"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": " + + existingJingleConnection.getClass().getName() + + " does not support jingle messages"); } return; } if (fromSelf) { if ("proceed".equals(message.getName())) { - final Conversation c = mXmppConnectionService.findOrCreateConversation(account, id.with, false, false); + final Conversation c = + mXmppConnectionService.findOrCreateConversation( + account, id.with, false, false); final Message previousBusy = c.findRtpSession(sessionId, Message.STATUS_RECEIVED); if (previousBusy != null) { previousBusy.setBody(new RtpSessionStatus(true, 0).toString()); @@ -263,45 +305,72 @@ public class JingleConnectionManager extends AbstractConnectionManager { } previousBusy.setTime(timestamp); mXmppConnectionService.updateMessage(previousBusy, true); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": updated previous busy because call got picked up by another device"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": updated previous busy because call got picked up by another device"); return; } } - //TODO handle reject for cases where we don’t have carbon copies (normally reject is to be sent to own bare jid as well) - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignore jingle message from self"); + // TODO handle reject for cases where we don’t have carbon copies (normally reject is to + // be sent to own bare jid as well) + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": ignore jingle message from self"); return; } if ("propose".equals(message.getName())) { final Propose propose = Propose.upgrade(message); final List descriptions = propose.getDescriptions(); - final Collection rtpDescriptions = Collections2.transform( - Collections2.filter(descriptions, d -> d instanceof RtpDescription), - input -> (RtpDescription) input - ); - if (rtpDescriptions.size() > 0 && rtpDescriptions.size() == descriptions.size() && isUsingClearNet(account)) { - final Collection media = Collections2.transform(rtpDescriptions, RtpDescription::getMedia); + final Collection rtpDescriptions = + Collections2.transform( + Collections2.filter(descriptions, d -> d instanceof RtpDescription), + input -> (RtpDescription) input); + if (rtpDescriptions.size() > 0 + && rtpDescriptions.size() == descriptions.size() + && isUsingClearNet(account)) { + final Collection media = + Collections2.transform(rtpDescriptions, RtpDescription::getMedia); if (media.contains(Media.UNKNOWN)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": encountered unknown media in session proposal. " + propose); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": encountered unknown media in session proposal. " + + propose); return; } - final Optional matchingSessionProposal = findMatchingSessionProposal(account, id.with, ImmutableSet.copyOf(media)); + final Optional matchingSessionProposal = + findMatchingSessionProposal(account, id.with, ImmutableSet.copyOf(media)); if (matchingSessionProposal.isPresent()) { final String ourSessionId = matchingSessionProposal.get().sessionId; final String theirSessionId = id.sessionId; if (ComparisonChain.start() - .compare(ourSessionId, theirSessionId) - .compare(account.getJid().toEscapedString(), id.with.toEscapedString()) - .result() > 0) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": our session lost tie break. automatically accepting their session. winning Session=" + theirSessionId); - //TODO a retract for this reason should probably include some indication of tie break + .compare(ourSessionId, theirSessionId) + .compare( + account.getJid().toEscapedString(), + id.with.toEscapedString()) + .result() + > 0) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": our session lost tie break. automatically accepting their session. winning Session=" + + theirSessionId); + // TODO a retract for this reason should probably include some indication of + // tie break retractSessionProposal(matchingSessionProposal.get()); - final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, from); + final JingleRtpConnection rtpConnection = + new JingleRtpConnection(this, id, from); this.connections.put(id, rtpConnection); rtpConnection.setProposedMedia(ImmutableSet.copyOf(media)); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": our session won tie break. waiting for other party to accept. winningSession=" + ourSessionId); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": our session won tie break. waiting for other party to accept. winningSession=" + + ourSessionId); } return; } @@ -309,38 +378,63 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (isBusy() != null || stranger) { writeLogMissedIncoming(account, id.with.asBareJid(), id.sessionId, serverMsgId, timestamp); if (stranger) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring call proposal from stranger " + id.with); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring call proposal from stranger " + + id.with); return; } final int activeDevices = account.activeDevicesWithRtpCapability(); Log.d(Config.LOGTAG, "active devices with rtp capability: " + activeDevices); if (activeDevices == 0) { - final MessagePacket reject = mXmppConnectionService.getMessageGenerator().sessionReject(from, sessionId); + final MessagePacket reject = + mXmppConnectionService + .getMessageGenerator() + .sessionReject(from, sessionId); mXmppConnectionService.sendMessagePacket(account, reject); } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring proposal because busy on this device but there are other devices"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring proposal because busy on this device but there are other devices"); } } else { - final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, from); + final JingleRtpConnection rtpConnection = + new JingleRtpConnection(this, id, from); this.connections.put(id, rtpConnection); rtpConnection.setProposedMedia(ImmutableSet.copyOf(media)); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); } } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to react to proposed session with " + rtpDescriptions.size() + " rtp descriptions of " + descriptions.size() + " total descriptions"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": unable to react to proposed session with " + + rtpDescriptions.size() + + " rtp descriptions of " + + descriptions.size() + + " total descriptions"); } } else if (addressedDirectly && "proceed".equals(message.getName())) { synchronized (rtpSessionProposals) { - final RtpSessionProposal proposal = getRtpSessionProposal(account, from.asBareJid(), sessionId); + final RtpSessionProposal proposal = + getRtpSessionProposal(account, from.asBareJid(), sessionId); if (proposal != null) { rtpSessionProposals.remove(proposal); - final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, account.getJid()); + final JingleRtpConnection rtpConnection = + new JingleRtpConnection(this, id, account.getJid()); rtpConnection.setProposedMedia(proposal.media); this.connections.put(id, rtpConnection); rtpConnection.transitionOrThrow(AbstractJingleConnection.State.PROPOSED); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver proceed"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": no rtp session proposal found for " + + from + + " to deliver proceed"); if (remoteMsgId == null) { return; } @@ -356,63 +450,77 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } } else if (addressedDirectly && "reject".equals(message.getName())) { - final RtpSessionProposal proposal = getRtpSessionProposal(account, from.asBareJid(), sessionId); + final RtpSessionProposal proposal = + getRtpSessionProposal(account, from.asBareJid(), sessionId); synchronized (rtpSessionProposals) { if (proposal != null && rtpSessionProposals.remove(proposal) != null) { - writeLogMissedOutgoing(account, proposal.with, proposal.sessionId, serverMsgId, timestamp); + writeLogMissedOutgoing( + account, proposal.with, proposal.sessionId, serverMsgId, timestamp); toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY, proposal.media); - mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, proposal.with, proposal.sessionId, RtpEndUserState.DECLINED_OR_BUSY); + mXmppConnectionService.notifyJingleRtpConnectionUpdate( + account, + proposal.with, + proposal.sessionId, + RtpEndUserState.DECLINED_OR_BUSY); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver reject"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": no rtp session proposal found for " + + from + + " to deliver reject"); } } } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved out of order jingle message" + message); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": retrieved out of order jingle message" + + message); } - } - private RtpSessionProposal getRtpSessionProposal(final Account account, Jid from, String sessionId) { + private RtpSessionProposal getRtpSessionProposal( + final Account account, Jid from, String sessionId) { for (RtpSessionProposal rtpSessionProposal : rtpSessionProposals.keySet()) { - if (rtpSessionProposal.sessionId.equals(sessionId) && rtpSessionProposal.with.equals(from) && rtpSessionProposal.account.getJid().equals(account.getJid())) { + if (rtpSessionProposal.sessionId.equals(sessionId) + && rtpSessionProposal.with.equals(from) + && rtpSessionProposal.account.getJid().equals(account.getJid())) { return rtpSessionProposal; } } return null; } - private void writeLogMissedOutgoing(final Account account, Jid with, final String sessionId, String serverMsgId, long timestamp) { - final Conversation conversation = mXmppConnectionService.findOrCreateConversation( - account, - with.asBareJid(), - false, - false - ); - final Message message = new Message( - conversation, - Message.STATUS_SEND, - Message.TYPE_RTP_SESSION, - sessionId - ); + private void writeLogMissedOutgoing( + final Account account, + Jid with, + final String sessionId, + String serverMsgId, + long timestamp) { + final Conversation conversation = + mXmppConnectionService.findOrCreateConversation( + account, with.asBareJid(), false, false); + final Message message = + new Message(conversation, Message.STATUS_SEND, Message.TYPE_RTP_SESSION, sessionId); message.setBody(new RtpSessionStatus(false, 0).toString()); message.setServerMsgId(serverMsgId); message.setTime(timestamp); writeMessage(message); } - private void writeLogMissedIncoming(final Account account, Jid with, final String sessionId, String serverMsgId, long timestamp) { - final Conversation conversation = mXmppConnectionService.findOrCreateConversation( - account, - with.asBareJid(), - false, - false - ); - final Message message = new Message( - conversation, - Message.STATUS_RECEIVED, - Message.TYPE_RTP_SESSION, - sessionId - ); + private void writeLogMissedIncoming( + final Account account, + Jid with, + final String sessionId, + String serverMsgId, + long timestamp) { + final Conversation conversation = + mXmppConnectionService.findOrCreateConversation( + account, with.asBareJid(), false, false); + final Message message = + new Message( + conversation, Message.STATUS_RECEIVED, Message.TYPE_RTP_SESSION, sessionId); message.setBody(new RtpSessionStatus(false, 0).toString()); message.setServerMsgId(serverMsgId); message.setTime(timestamp); @@ -431,34 +539,41 @@ public class JingleConnectionManager extends AbstractConnectionManager { } public void startJingleFileTransfer(final Message message) { - Preconditions.checkArgument(message.isFileOrImage(), "Message is not of type file or image"); + Preconditions.checkArgument( + message.isFileOrImage(), "Message is not of type file or image"); final Transferable old = message.getTransferable(); 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()); + final JingleFileTransferConnection connection = + new JingleFileTransferConnection(this, id, account.getJid()); mXmppConnectionService.markMessage(message, Message.STATUS_WAITING); this.connections.put(id, connection); connection.init(message); } public Optional getOngoingRtpConnection(final Contact contact) { - for (final Map.Entry entry : this.connections.entrySet()) { + for (final Map.Entry entry : + this.connections.entrySet()) { if (entry.getValue() instanceof JingleRtpConnection) { final AbstractJingleConnection.Id id = entry.getKey(); - if (id.account == contact.getAccount() && id.with.asBareJid().equals(contact.getJid().asBareJid())) { + if (id.account == contact.getAccount() + && id.with.asBareJid().equals(contact.getJid().asBareJid())) { return Optional.of(id); } } } synchronized (this.rtpSessionProposals) { - for (Map.Entry entry : this.rtpSessionProposals.entrySet()) { + for (Map.Entry entry : + this.rtpSessionProposals.entrySet()) { RtpSessionProposal proposal = entry.getKey(); - if (proposal.account == contact.getAccount() && contact.getJid().asBareJid().equals(proposal.with)) { + if (proposal.account == contact.getAccount() + && contact.getJid().asBareJid().equals(proposal.with)) { final DeviceDiscoveryState preexistingState = entry.getValue(); - if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) { + if (preexistingState != null + && preexistingState != DeviceDiscoveryState.FAILED) { return Optional.of(proposal); } } @@ -474,7 +589,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { void finishConnectionOrThrow(final AbstractJingleConnection connection) { final AbstractJingleConnection.Id id = connection.getId(); if (this.connections.remove(id) == null) { - throw new IllegalStateException(String.format("Unable to finish connection with id=%s", id.toString())); + throw new IllegalStateException( + String.format("Unable to finish connection with id=%s", id.toString())); } } @@ -493,49 +609,70 @@ public class JingleConnectionManager extends AbstractConnectionManager { return firedUpdates; } - void getPrimaryCandidate(final Account account, final boolean initiator, final OnPrimaryCandidateFound listener) { + void getPrimaryCandidate( + final Account account, + final boolean initiator, + final OnPrimaryCandidateFound listener) { if (Config.DISABLE_PROXY_LOOKUP) { listener.onPrimaryCandidateFound(false, null); return; } if (!this.primaryCandidates.containsKey(account.getJid().asBareJid())) { - final Jid proxy = account.getXmppConnection().findDiscoItemByFeature(Namespace.BYTE_STREAMS); + 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() { + 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); - } - } - }); + @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())); + listener.onPrimaryCandidateFound( + true, this.primaryCandidates.get(account.getJid().asBareJid())); } } @@ -557,16 +694,28 @@ public class JingleConnectionManager extends AbstractConnectionManager { private void retractSessionProposal(RtpSessionProposal rtpSessionProposal) { final Account account = rtpSessionProposal.account; toneManager.transition(RtpEndUserState.ENDED, rtpSessionProposal.media); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retracting rtp session proposal with " + rtpSessionProposal.with); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": retracting rtp session proposal with " + + rtpSessionProposal.with); this.rtpSessionProposals.remove(rtpSessionProposal); - final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal); - writeLogMissedOutgoing(account, rtpSessionProposal.with, rtpSessionProposal.sessionId, null, System.currentTimeMillis()); + final MessagePacket messagePacket = + mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal); + writeLogMissedOutgoing( + account, + rtpSessionProposal.with, + rtpSessionProposal.sessionId, + null, + System.currentTimeMillis()); mXmppConnectionService.sendMessagePacket(account, messagePacket); } - public String initializeRtpSession(final Account account, final Jid with, final Set media) { + public String initializeRtpSession( + final Account account, final Jid with, final Set media) { final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with); - final JingleRtpConnection rtpConnection = new JingleRtpConnection(this, id, account.getJid()); + final JingleRtpConnection rtpConnection = + new JingleRtpConnection(this, id, account.getJid()); rtpConnection.setProposedMedia(media); this.connections.put(id, rtpConnection); rtpConnection.sendSessionInitiate(); @@ -575,11 +724,13 @@ public class JingleConnectionManager extends AbstractConnectionManager { public String proposeJingleRtpSession(final Account account, final Jid with, final Set media) { synchronized (this.rtpSessionProposals) { - for (Map.Entry entry : this.rtpSessionProposals.entrySet()) { + for (Map.Entry entry : + this.rtpSessionProposals.entrySet()) { RtpSessionProposal proposal = entry.getKey(); if (proposal.account == account && with.asBareJid().equals(proposal.with)) { final DeviceDiscoveryState preexistingState = entry.getValue(); - if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) { + if (preexistingState != null + && preexistingState != DeviceDiscoveryState.FAILED) { final RtpEndUserState endUserState = preexistingState.toEndUserState(); toneManager.transition(endUserState, media); mXmppConnectionService.notifyJingleRtpConnectionUpdate( @@ -601,15 +752,13 @@ public class JingleConnectionManager extends AbstractConnectionManager { } throw new IllegalStateException("There is already a running RTP session: " + busyCode); } - final RtpSessionProposal proposal = RtpSessionProposal.of(account, with.asBareJid(), media); + final RtpSessionProposal proposal = + RtpSessionProposal.of(account, with.asBareJid(), media); this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING); mXmppConnectionService.notifyJingleRtpConnectionUpdate( - account, - proposal.with, - proposal.sessionId, - RtpEndUserState.FINDING_DEVICE - ); - final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); + account, proposal.with, proposal.sessionId, RtpEndUserState.FINDING_DEVICE); + final MessagePacket messagePacket = + mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); mXmppConnectionService.sendMessagePacket(account, messagePacket); return proposal.sessionId; } @@ -617,7 +766,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { public boolean hasMatchingProposal(final Account account, final Jid with) { synchronized (this.rtpSessionProposals) { - for (Map.Entry entry : this.rtpSessionProposals.entrySet()) { + for (Map.Entry entry : + this.rtpSessionProposals.entrySet()) { final RtpSessionProposal proposal = entry.getKey(); if (proposal.account == account && with.asBareJid().equals(proposal.with)) { return true; @@ -646,10 +796,12 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (sid != null) { for (final AbstractJingleConnection connection : this.connections.values()) { if (connection instanceof JingleFileTransferConnection) { - final JingleFileTransferConnection fileTransfer = (JingleFileTransferConnection) connection; + final JingleFileTransferConnection fileTransfer = + (JingleFileTransferConnection) connection; final JingleTransport transport = fileTransfer.getTransport(); if (transport instanceof JingleInBandTransport) { - final JingleInBandTransport inBandTransport = (JingleInBandTransport) transport; + final JingleInBandTransport inBandTransport = + (JingleInBandTransport) transport; if (inBandTransport.matches(account, sid)) { inBandTransport.deliverPayload(packet, payload); } @@ -659,7 +811,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } Log.d(Config.LOGTAG, "unable to deliver ibb packet: " + packet.toString()); - account.getXmppConnection().sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null); + account.getXmppConnection() + .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null); } public void notifyRebound(final Account account) { @@ -672,8 +825,10 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } - public WeakReference findJingleRtpConnection(Account account, Jid with, String sessionId) { - final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with, sessionId); + public WeakReference findJingleRtpConnection( + Account account, Jid with, String sessionId) { + final AbstractJingleConnection.Id id = + AbstractJingleConnection.Id.of(account, with, sessionId); final AbstractJingleConnection connection = connections.get(id); if (connection instanceof JingleRtpConnection) { return new WeakReference<>((JingleRtpConnection) connection); @@ -683,34 +838,53 @@ public class JingleConnectionManager extends AbstractConnectionManager { private void resendSessionProposals(final Account account) { synchronized (this.rtpSessionProposals) { - for (final Map.Entry entry : this.rtpSessionProposals.entrySet()) { + for (final Map.Entry entry : + this.rtpSessionProposals.entrySet()) { final RtpSessionProposal proposal = entry.getKey(); - if (entry.getValue() == DeviceDiscoveryState.SEARCHING && proposal.account == account) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resending session proposal to " + proposal.with); - final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); + if (entry.getValue() == DeviceDiscoveryState.SEARCHING + && proposal.account == account) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": resending session proposal to " + + proposal.with); + final MessagePacket messagePacket = + mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); mXmppConnectionService.sendMessagePacket(account, messagePacket); } } } } - public void updateProposedSessionDiscovered(Account account, Jid from, String sessionId, final DeviceDiscoveryState target) { + public void updateProposedSessionDiscovered( + Account account, Jid from, String sessionId, final DeviceDiscoveryState target) { synchronized (this.rtpSessionProposals) { - final RtpSessionProposal sessionProposal = getRtpSessionProposal(account, from.asBareJid(), sessionId); - final DeviceDiscoveryState currentState = sessionProposal == null ? null : rtpSessionProposals.get(sessionProposal); + final RtpSessionProposal sessionProposal = + getRtpSessionProposal(account, from.asBareJid(), sessionId); + final DeviceDiscoveryState currentState = + sessionProposal == null ? null : rtpSessionProposals.get(sessionProposal); if (currentState == null) { Log.d(Config.LOGTAG, "unable to find session proposal for session id " + sessionId); return; } if (currentState == DeviceDiscoveryState.DISCOVERED) { - Log.d(Config.LOGTAG, "session proposal already at discovered. not going to fall back"); + Log.d( + Config.LOGTAG, + "session proposal already at discovered. not going to fall back"); return; } this.rtpSessionProposals.put(sessionProposal, target); final RtpEndUserState endUserState = target.toEndUserState(); toneManager.transition(endUserState, sessionProposal.media); - mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, sessionProposal.with, sessionProposal.sessionId, endUserState); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": flagging session " + sessionId + " as " + target); + mXmppConnectionService.notifyJingleRtpConnectionUpdate( + account, sessionProposal.with, sessionProposal.sessionId, endUserState); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": flagging session " + + sessionId + + " as " + + target); } } @@ -735,7 +909,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { } public void failProceed(Account account, final Jid with, String sessionId) { - final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with, sessionId); + final AbstractJingleConnection.Id id = + AbstractJingleConnection.Id.of(account, with, sessionId); final AbstractJingleConnection existingJingleConnection = connections.get(id); if (existingJingleConnection instanceof JingleRtpConnection) { ((JingleRtpConnection) existingJingleConnection).deliverFailedProceed(); @@ -746,13 +921,17 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (connections.containsValue(connection)) { return; } - final IllegalStateException e = new IllegalStateException("JingleConnection has not been registered with connection manager"); + final IllegalStateException e = + new IllegalStateException( + "JingleConnection has not been registered with connection manager"); Log.e(Config.LOGTAG, "ensureConnectionIsRegistered() failed. Going to throw", e); throw e; } - void setTerminalSessionState(AbstractJingleConnection.Id id, final RtpEndUserState state, final Set media) { - this.terminatedSessions.put(PersistableSessionId.of(id), new TerminatedRtpSession(state, media)); + void setTerminalSessionState( + AbstractJingleConnection.Id id, final RtpEndUserState state, final Set media) { + this.terminatedSessions.put( + PersistableSessionId.of(id), new TerminatedRtpSession(state, media)); } public TerminatedRtpSession getTerminalSessionState(final Jid with, final String sessionId) { @@ -777,8 +956,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PersistableSessionId that = (PersistableSessionId) o; - return Objects.equal(with, that.with) && - Objects.equal(sessionId, that.sessionId); + return Objects.equal(with, that.with) && Objects.equal(sessionId, that.sessionId); } @Override @@ -798,7 +976,10 @@ public class JingleConnectionManager extends AbstractConnectionManager { } public enum DeviceDiscoveryState { - SEARCHING, SEARCHING_ACKNOWLEDGED, DISCOVERED, FAILED; + SEARCHING, + SEARCHING_ACKNOWLEDGED, + DISCOVERED, + FAILED; public RtpEndUserState toEndUserState() { switch (this) { @@ -839,9 +1020,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; RtpSessionProposal proposal = (RtpSessionProposal) o; - return Objects.equal(account.getJid(), proposal.account.getJid()) && - Objects.equal(with, proposal.with) && - Objects.equal(sessionId, proposal.sessionId); + return Objects.equal(account.getJid(), proposal.account.getJid()) + && Objects.equal(with, proposal.with) + && Objects.equal(sessionId, proposal.sessionId); } @Override 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 40edf0b2c..43aaa54b5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -492,19 +492,19 @@ public class JingleFileTransferConnection extends AbstractJingleConnection imple AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(path); if (VALID_IMAGE_EXTENSIONS.contains(extension.main)) { message.setType(Message.TYPE_IMAGE); - message.setRelativeFilePath(message.getUuid() + "." + extension.main); + 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); - message.setRelativeFilePath(message.getUuid() + "." + extension.secondary); + xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + "." + extension.secondary); } else { message.setType(Message.TYPE_FILE); - message.setRelativeFilePath(message.getUuid() + (extension.secondary != null ? ("." + extension.secondary) : "")); + xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + (extension.secondary != null ? ("." + extension.secondary) : "")); } message.setEncryption(Message.ENCRYPTION_PGP); } else { message.setType(Message.TYPE_FILE); - message.setRelativeFilePath(message.getUuid() + (extension.main != null ? ("." + extension.main) : "")); + xmppConnectionService.getFileBackend().setupRelativeFilePath(message,message.getUuid() + (extension.main != null ? ("." + extension.main) : "")); } long size = parseLong(fileSize, 0); message.setBody(Long.toString(size)); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 4d97ed23f..c93e323e5 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -62,91 +62,103 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; -public class JingleRtpConnection extends AbstractJingleConnection implements WebRTCWrapper.EventCallback { +public class JingleRtpConnection extends AbstractJingleConnection + implements WebRTCWrapper.EventCallback { - public static final List STATES_SHOWING_ONGOING_CALL = Arrays.asList( - State.PROCEED, - State.SESSION_INITIALIZED_PRE_APPROVED, - State.SESSION_ACCEPTED - ); + public static final List STATES_SHOWING_ONGOING_CALL = + Arrays.asList( + State.PROCEED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED); private static final long BUSY_TIME_OUT = 30; - private 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 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( + 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, - State.SESSION_INITIALIZED, - State.TERMINATED_APPLICATION_FAILURE, - State.TERMINATED_SECURITY_ERROR - )); - transitionBuilder.put(State.PROPOSED, ImmutableList.of( - State.ACCEPTED, + 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, - 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, + 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, - 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( + 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, - 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 - )); + 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(); } private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this); - private final Queue> pendingIceCandidates = new LinkedList<>(); + private final Queue> + pendingIceCandidates = new LinkedList<>(); private final OmemoVerification omemoVerification = new OmemoVerification(); private final Message message; private State state = State.NULL; - private StateTransitionException stateTransitionException; private Set proposedMedia; private RtpContentMap initiatorRtpContentMap; private RtpContentMap responderRtpContentMap; @@ -157,18 +169,16 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { super(jingleConnectionManager, id, initiator); - final Conversation conversation = jingleConnectionManager.getXmppConnectionService().findOrCreateConversation( - id.account, - id.with.asBareJid(), - false, - false - ); - this.message = new Message( - conversation, - isInitiator() ? Message.STATUS_SEND : Message.STATUS_RECEIVED, - Message.TYPE_RTP_SESSION, - id.sessionId - ); + final Conversation conversation = + jingleConnectionManager + .getXmppConnectionService() + .findOrCreateConversation(id.account, id.with.asBareJid(), false, false); + this.message = + new Message( + conversation, + isInitiator() ? Message.STATUS_SEND : Message.STATUS_RECEIVED, + Message.TYPE_RTP_SESSION, + id.sessionId); } private static State reasonToState(Reason reason) { @@ -209,7 +219,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web break; default: respondOk(jinglePacket); - Log.d(Config.LOGTAG, String.format("%s: received unhandled jingle action %s", id.account.getJid().asBareJid(), jinglePacket.getAction())); + Log.d( + Config.LOGTAG, + String.format( + "%s: received unhandled jingle action %s", + id.account.getJid().asBareJid(), jinglePacket.getAction())); break; } } @@ -223,8 +237,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (!isInitiator() && isInState(State.PROPOSED, State.SESSION_INITIALIZED)) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); } - if (isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { - //we might have already changed resources (full jid) at this point; so this might not even reach the other party + if (isInState( + State.SESSION_INITIALIZED, + State.SESSION_INITIALIZED_PRE_APPROVED, + State.SESSION_ACCEPTED)) { + // we might have already changed resources (full jid) at this point; so this might not + // even reach the other party sendSessionTerminate(Reason.CONNECTIVITY_ERROR); } else { transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR); @@ -240,9 +258,21 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web respondOk(jinglePacket); final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason(); final State previous = this.state; - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session terminate reason=" + wrapper.reason + "(" + Strings.nullToEmpty(wrapper.text) + ") while in state " + previous); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received session terminate reason=" + + wrapper.reason + + "(" + + Strings.nullToEmpty(wrapper.text) + + ") while in state " + + previous); if (TERMINATED.contains(previous)) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring session terminate because already in " + previous); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring session terminate because already in " + + previous); return; } webRTCWrapper.close(); @@ -256,13 +286,23 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void receiveTransportInfo(final JinglePacket jinglePacket) { - //Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to INITIALIZED only after transport-info has been received - if (isInState(State.NULL, State.PROCEED, State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { + // Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to + // INITIALIZED only after transport-info has been received + if (isInState( + State.NULL, + State.PROCEED, + State.SESSION_INITIALIZED, + State.SESSION_INITIALIZED_PRE_APPROVED, + State.SESSION_ACCEPTED)) { final RtpContentMap contentMap; try { contentMap = RtpContentMap.of(jinglePacket); } catch (final IllegalArgumentException | NullPointerException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents; ignoring", e); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": improperly formatted contents; ignoring", + e); respondOk(jinglePacket); return; } @@ -270,18 +310,27 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } else { if (isTerminated()) { respondOk(jinglePacket); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring out-of-order transport info; we where already terminated"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring out-of-order transport info; we where already terminated"); } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received transport info while in state=" + this.state); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received transport info while in state=" + + this.state); terminateWithOutOfOrder(jinglePacket); } } } - private void receiveTransportInfo(final JinglePacket jinglePacket, final RtpContentMap contentMap) { - final Set> candidates = contentMap.contents.entrySet(); + private void receiveTransportInfo( + final JinglePacket jinglePacket, final RtpContentMap contentMap) { + final Set> candidates = + contentMap.contents.entrySet(); if (this.state == State.SESSION_ACCEPTED) { - //zero candidates + modified credentials are an ICE restart offer + // zero candidates + modified credentials are an ICE restart offer if (checkForIceRestart(jinglePacket, contentMap)) { return; } @@ -289,7 +338,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { processCandidates(candidates); } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnection was not initialized when processing transport info. this usually indicates a race condition that can be ignored"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": PeerConnection was not initialized when processing transport info. this usually indicates a race condition that can be ignored"); } } else { respondOk(jinglePacket); @@ -297,21 +349,23 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) { + private boolean checkForIceRestart( + final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) { final RtpContentMap existing = getRemoteContentMap(); - final IceUdpTransportInfo.Credentials existingCredentials; + final Set existingCredentials; final IceUdpTransportInfo.Credentials newCredentials; try { existingCredentials = existing.getCredentials(); - newCredentials = rtpContentMap.getCredentials(); + newCredentials = rtpContentMap.getDistinctCredentials(); } catch (final IllegalStateException e) { Log.d(Config.LOGTAG, "unable to gather credentials for comparison", e); return false; } - if (existingCredentials.equals(newCredentials)) { + if (existingCredentials.contains(newCredentials)) { return false; } - //TODO an alternative approach is to check if we already got an iq result to our ICE-restart + // TODO an alternative approach is to check if we already got an iq result to our + // ICE-restart // and if that's the case we are seeing an answer. // This might be more spec compliant but also more error prone potentially final boolean isOffer = rtpContentMap.emptyCandidates(); @@ -319,10 +373,17 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { if (isOffer) { Log.d(Config.LOGTAG, "received offer to restart ICE " + newCredentials); - restartContentMap = existing.modifiedCredentials(newCredentials, IceUdpTransportInfo.Setup.ACTPASS); + restartContentMap = + existing.modifiedCredentials( + newCredentials, IceUdpTransportInfo.Setup.ACTPASS); } else { final IceUdpTransportInfo.Setup setup = getPeerDtlsSetup(); - Log.d(Config.LOGTAG, "received confirmation of ICE restart" + newCredentials + " peer_setup=" + setup); + Log.d( + Config.LOGTAG, + "received confirmation of ICE restart" + + newCredentials + + " peer_setup=" + + setup); // DTLS setup attribute needs to be rewritten to reflect current peer state // https://groups.google.com/g/discuss-webrtc/c/DfpIMwvUfeM restartContentMap = existing.modifiedCredentials(newCredentials, setup); @@ -338,7 +399,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web respondOk(jinglePacket); final Throwable rootCause = Throwables.getRootCause(exception); if (rootCause instanceof WebRTCWrapper.PeerConnectionNotInitialized) { - //If this happens a termination is already in progress + // If this happens a termination is already in progress Log.d(Config.LOGTAG, "ignoring PeerConnectionNotInitialized on ICE restart"); return true; } @@ -364,13 +425,22 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.peerDtlsSetup = setup; } - private boolean applyIceRestart(final JinglePacket jinglePacket, final RtpContentMap restartContentMap, final boolean isOffer) throws ExecutionException, InterruptedException { + private boolean applyIceRestart( + final JinglePacket jinglePacket, + final RtpContentMap restartContentMap, + final boolean isOffer) + throws ExecutionException, InterruptedException { final SessionDescription sessionDescription = SessionDescription.of(restartContentMap); - final org.webrtc.SessionDescription.Type type = isOffer ? org.webrtc.SessionDescription.Type.OFFER : org.webrtc.SessionDescription.Type.ANSWER; - org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription(type, sessionDescription.toString()); + final org.webrtc.SessionDescription.Type type = + isOffer + ? org.webrtc.SessionDescription.Type.OFFER + : org.webrtc.SessionDescription.Type.ANSWER; + org.webrtc.SessionDescription sdp = + new org.webrtc.SessionDescription(type, sessionDescription.toString()); if (isOffer && webRTCWrapper.getSignalingState() != PeerConnection.SignalingState.STABLE) { if (isInitiator()) { - //We ignore the offer and respond with tie-break. This will clause the responder not to apply the content map + // We ignore the offer and respond with tie-break. This will clause the responder + // not to apply the content map return false; } } @@ -380,7 +450,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web webRTCWrapper.setIsReadyToReceiveIceCandidates(false); final SessionDescription localSessionDescription = setLocalSessionDescription(); setLocalContentMap(RtpContentMap.of(localSessionDescription)); - //We need to respond OK before sending any candidates + // We need to respond OK before sending any candidates respondOk(jinglePacket); webRTCWrapper.setIsReadyToReceiveIceCandidates(true); } else { @@ -389,32 +459,40 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return true; } - private void processCandidates(final Set> contents) { + private void processCandidates( + final Set> contents) { for (final Map.Entry content : contents) { processCandidate(content); } } - private void processCandidate(final Map.Entry content) { + private void processCandidate( + final Map.Entry content) { final RtpContentMap rtpContentMap = getRemoteContentMap(); final List indices = toIdentificationTags(rtpContentMap); - final String sdpMid = content.getKey(); //aka content name + final String sdpMid = content.getKey(); // aka content name final IceUdpTransportInfo transport = content.getValue().transport; final IceUdpTransportInfo.Credentials credentials = transport.getCredentials(); - //TODO check that credentials remained the same + // TODO check that credentials remained the same for (final IceUdpTransportInfo.Candidate candidate : transport.getCandidates()) { final String sdp; try { sdp = candidate.toSdpAttribute(credentials.ufrag); } catch (final IllegalArgumentException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage()); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring invalid ICE candidate " + + e.getMessage()); continue; } final int mLineIndex = indices.indexOf(sdpMid); if (mLineIndex < 0) { - Log.w(Config.LOGTAG, "mLineIndex not found for " + sdpMid + ". available indices " + indices); + Log.w( + Config.LOGTAG, + "mLineIndex not found for " + sdpMid + ". available indices " + indices); } final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp); Log.d(Config.LOGTAG, "received candidate: " + iceCandidate); @@ -428,14 +506,21 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private List toIdentificationTags(final RtpContentMap rtpContentMap) { final Group originalGroup = rtpContentMap.group; - final List identificationTags = originalGroup == null ? rtpContentMap.getNames() : originalGroup.getIdentificationTags(); + final List identificationTags = + originalGroup == null + ? rtpContentMap.getNames() + : originalGroup.getIdentificationTags(); if (identificationTags.size() == 0) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices"); } return identificationTags; } - private ListenableFuture receiveRtpContentMap(final JinglePacket jinglePacket, final boolean expectVerification) { + private ListenableFuture receiveRtpContentMap( + final JinglePacket jinglePacket, final boolean expectVerification) { final RtpContentMap receivedContentMap; try { receivedContentMap = RtpContentMap.of(jinglePacket); @@ -443,17 +528,26 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return Futures.immediateFailedFuture(e); } if (receivedContentMap instanceof OmemoVerifiedRtpContentMap) { - final ListenableFuture> future = id.account.getAxolotlService().decrypt((OmemoVerifiedRtpContentMap) receivedContentMap, id.with); - return Futures.transform(future, omemoVerifiedPayload -> { - //TODO test if an exception here triggers a correct abort - omemoVerification.setOrEnsureEqual(omemoVerifiedPayload); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received verifiable DTLS fingerprint via " + omemoVerification); - return omemoVerifiedPayload.getPayload(); - }, MoreExecutors.directExecutor()); + final ListenableFuture> future = + id.account + .getAxolotlService() + .decrypt((OmemoVerifiedRtpContentMap) receivedContentMap, id.with); + return Futures.transform( + future, + omemoVerifiedPayload -> { + // TODO test if an exception here triggers a correct abort + omemoVerification.setOrEnsureEqual(omemoVerifiedPayload); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received verifiable DTLS fingerprint via " + + omemoVerification); + return omemoVerifiedPayload.getPayload(); + }, + MoreExecutors.directExecutor()); } else if (Config.REQUIRE_RTP_VERIFICATION || expectVerification) { return Futures.immediateFailedFuture( - new SecurityException("DTLS fingerprint was unexpectedly not verifiable") - ); + new SecurityException("DTLS fingerprint was unexpectedly not verifiable")); } else { return Futures.immediateFuture(receivedContentMap); } @@ -461,13 +555,17 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void receiveSessionInitiate(final JinglePacket jinglePacket) { if (isInitiator()) { - Log.d(Config.LOGTAG, String.format("%s: received session-initiate even though we were initiating", id.account.getJid().asBareJid())); + Log.d( + Config.LOGTAG, + String.format( + "%s: received session-initiate even though we were initiating", + id.account.getJid().asBareJid())); 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() - )); + 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); @@ -475,43 +573,51 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return; } final ListenableFuture future = receiveRtpContentMap(jinglePacket, false); - Futures.addCallback(future, new FutureCallback() { - @Override - public void onSuccess(@Nullable RtpContentMap rtpContentMap) { - receiveSessionInitiate(jinglePacket, rtpContentMap); - } + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(@Nullable RtpContentMap rtpContentMap) { + receiveSessionInitiate(jinglePacket, rtpContentMap); + } - @Override - public void onFailure(@NonNull final Throwable throwable) { - respondOk(jinglePacket); - sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage()); - } - }, MoreExecutors.directExecutor()); + @Override + public void onFailure(@NonNull final Throwable throwable) { + respondOk(jinglePacket); + sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage()); + } + }, + MoreExecutors.directExecutor()); } - private void receiveSessionInitiate(final JinglePacket jinglePacket, final RtpContentMap contentMap) { + private void receiveSessionInitiate( + final JinglePacket jinglePacket, final RtpContentMap contentMap) { try { contentMap.requireContentDescriptions(); contentMap.requireDTLSFingerprint(true); } catch (final RuntimeException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e)); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": improperly formatted contents", + Throwables.getRootCause(e)); respondOk(jinglePacket); sendSessionTerminate(Reason.of(e), e.getMessage()); return; } - Log.d(Config.LOGTAG, "processing session-init with " + contentMap.contents.size() + " contents"); + Log.d( + Config.LOGTAG, + "processing session-init with " + contentMap.contents.size() + " contents"); final State target; if (this.state == State.PROCEED) { Preconditions.checkState( proposedMedia != null && proposedMedia.size() > 0, - "proposed media must be set when processing pre-approved session-initiate" - ); + "proposed media must be set when processing pre-approved session-initiate"); if (!this.proposedMedia.equals(contentMap.getMedia())) { - sendSessionTerminate(Reason.SECURITY_ERROR, String.format( - "Your session proposal (Jingle Message Initiation) included media %s but your session-initiate was %s", - this.proposedMedia, - contentMap.getMedia() - )); + sendSessionTerminate( + Reason.SECURITY_ERROR, + String.format( + "Your session proposal (Jingle Message Initiation) included media %s but your session-initiate was %s", + this.proposedMedia, contentMap.getMedia())); return; } target = State.SESSION_INITIALIZED_PRE_APPROVED; @@ -522,67 +628,100 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web respondOk(jinglePacket); pendingIceCandidates.addAll(contentMap.contents.entrySet()); if (target == State.SESSION_INITIALIZED_PRE_APPROVED) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": automatically accepting session-initiate"); sendSessionAccept(); } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received not pre-approved session-initiate. start ringing"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received not pre-approved session-initiate. start ringing"); startRinging(); } } else { - Log.d(Config.LOGTAG, String.format("%s: received session-initiate while in state %s", id.account.getJid().asBareJid(), state)); + Log.d( + Config.LOGTAG, + String.format( + "%s: received session-initiate while in state %s", + id.account.getJid().asBareJid(), state)); terminateWithOutOfOrder(jinglePacket); } } private void receiveSessionAccept(final JinglePacket jinglePacket) { if (!isInitiator()) { - Log.d(Config.LOGTAG, String.format("%s: received session-accept even though we were responding", id.account.getJid().asBareJid())); + Log.d( + Config.LOGTAG, + String.format( + "%s: received session-accept even though we were responding", + id.account.getJid().asBareJid())); terminateWithOutOfOrder(jinglePacket); return; } - final ListenableFuture future = receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint()); - Futures.addCallback(future, new FutureCallback() { - @Override - public void onSuccess(@Nullable RtpContentMap rtpContentMap) { - receiveSessionAccept(jinglePacket, rtpContentMap); - } + final ListenableFuture future = + receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint()); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(@Nullable RtpContentMap rtpContentMap) { + receiveSessionAccept(jinglePacket, rtpContentMap); + } - @Override - public void onFailure(@NonNull final Throwable throwable) { - respondOk(jinglePacket); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents in session-accept", throwable); - webRTCWrapper.close(); - sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage()); - } - }, MoreExecutors.directExecutor()); + @Override + public void onFailure(@NonNull final Throwable throwable) { + respondOk(jinglePacket); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": improperly formatted contents in session-accept", + throwable); + webRTCWrapper.close(); + sendSessionTerminate(Reason.ofThrowable(throwable), throwable.getMessage()); + } + }, + MoreExecutors.directExecutor()); } - private void receiveSessionAccept(final JinglePacket jinglePacket, final RtpContentMap contentMap) { + private void receiveSessionAccept( + final JinglePacket jinglePacket, final RtpContentMap contentMap) { try { contentMap.requireContentDescriptions(); contentMap.requireDTLSFingerprint(); } catch (final RuntimeException e) { respondOk(jinglePacket); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents in session-accept", e); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": improperly formatted contents in session-accept", + e); webRTCWrapper.close(); sendSessionTerminate(Reason.of(e), e.getMessage()); return; } final Set initiatorMedia = this.initiatorRtpContentMap.getMedia(); if (!initiatorMedia.equals(contentMap.getMedia())) { - sendSessionTerminate(Reason.SECURITY_ERROR, String.format( - "Your session-included included media %s but our session-initiate was %s", - this.proposedMedia, - contentMap.getMedia() - )); + sendSessionTerminate( + Reason.SECURITY_ERROR, + String.format( + "Your session-included included media %s but our session-initiate was %s", + this.proposedMedia, contentMap.getMedia())); return; } - Log.d(Config.LOGTAG, "processing session-accept with " + contentMap.contents.size() + " contents"); + Log.d( + Config.LOGTAG, + "processing session-accept with " + contentMap.contents.size() + " contents"); if (transition(State.SESSION_ACCEPTED)) { respondOk(jinglePacket); receiveSessionAccept(contentMap); } else { - Log.d(Config.LOGTAG, String.format("%s: received session-accept while in state %s", id.account.getJid().asBareJid(), state)); + Log.d( + Config.LOGTAG, + String.format( + "%s: received session-accept while in state %s", + id.account.getJid().asBareJid(), state)); respondOk(jinglePacket); } } @@ -594,21 +733,29 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { sessionDescription = SessionDescription.of(contentMap); } catch (final IllegalArgumentException | NullPointerException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable convert offer from session-accept to SDP", e); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": unable convert offer from session-accept to SDP", + e); webRTCWrapper.close(); sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); return; } - final org.webrtc.SessionDescription answer = new org.webrtc.SessionDescription( - org.webrtc.SessionDescription.Type.ANSWER, - sessionDescription.toString() - ); + final org.webrtc.SessionDescription answer = + new org.webrtc.SessionDescription( + org.webrtc.SessionDescription.Type.ANSWER, sessionDescription.toString()); try { this.webRTCWrapper.setRemoteDescription(answer).get(); } catch (final Exception e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", Throwables.getRootCause(e)); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": unable to set remote description after receiving session-accept", + Throwables.getRootCause(e)); webRTCWrapper.close(); - sendSessionTerminate(Reason.FAILED_APPLICATION, Throwables.getRootCause(e).getMessage()); + sendSessionTerminate( + Reason.FAILED_APPLICATION, Throwables.getRootCause(e).getMessage()); return; } processCandidates(contentMap.contents.entrySet()); @@ -623,7 +770,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { offer = SessionDescription.of(rtpContentMap); } catch (final IllegalArgumentException | NullPointerException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable convert offer from session-initiate to SDP", e); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": unable convert offer from session-initiate to SDP", + e); webRTCWrapper.close(); sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); return; @@ -635,9 +786,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web discoverIceServers(iceServers -> sendSessionAccept(media, offer, iceServers)); } - private synchronized void sendSessionAccept(final Set media, final SessionDescription offer, final List iceServers) { + private synchronized void sendSessionAccept( + final Set media, + final SessionDescription offer, + final List iceServers) { if (isTerminated()) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": ICE servers got discovered when session was already terminated. nothing to do."); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ICE servers got discovered when session was already terminated. nothing to do."); return; } try { @@ -648,14 +805,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web sendSessionTerminate(Reason.FAILED_APPLICATION); return; } - final org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription( - org.webrtc.SessionDescription.Type.OFFER, - offer.toString() - ); + final org.webrtc.SessionDescription sdp = + new org.webrtc.SessionDescription( + org.webrtc.SessionDescription.Type.OFFER, offer.toString()); try { this.webRTCWrapper.setRemoteDescription(sdp).get(); addIceCandidatesFromBlackLog(); - org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.setLocalDescription().get(); + org.webrtc.SessionDescription webRTCSessionDescription = + this.webRTCWrapper.setLocalDescription().get(); prepareSessionAccept(webRTCSessionDescription); } catch (final Exception e) { failureToAcceptSession(e); @@ -676,18 +833,24 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web Map.Entry foo; while ((foo = this.pendingIceCandidates.poll()) != null) { processCandidate(foo); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added candidate from back log"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": added candidate from back log"); } } - private void prepareSessionAccept(final org.webrtc.SessionDescription webRTCSessionDescription) { - final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); + private void prepareSessionAccept( + final org.webrtc.SessionDescription webRTCSessionDescription) { + final SessionDescription sessionDescription = + SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); this.responderRtpContentMap = respondingRtpContentMap; storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip()); webRTCWrapper.setIsReadyToReceiveIceCandidates(true); - final ListenableFuture outgoingContentMapFuture = prepareOutgoingContentMap(respondingRtpContentMap); - Futures.addCallback(outgoingContentMapFuture, + final ListenableFuture outgoingContentMapFuture = + prepareOutgoingContentMap(respondingRtpContentMap); + Futures.addCallback( + outgoingContentMapFuture, new FutureCallback() { @Override public void onSuccess(final RtpContentMap outgoingContentMap) { @@ -699,35 +862,56 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web failureToAcceptSession(throwable); } }, - MoreExecutors.directExecutor() - ); + MoreExecutors.directExecutor()); } private void sendSessionAccept(final RtpContentMap rtpContentMap) { if (isTerminated()) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session accept was too slow. already terminated. nothing to do."); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": preparing session accept was too slow. already terminated. nothing to do."); return; } transitionOrThrow(State.SESSION_ACCEPTED); - final JinglePacket sessionAccept = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId); + final JinglePacket sessionAccept = + rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId); send(sessionAccept); } - private ListenableFuture prepareOutgoingContentMap(final RtpContentMap rtpContentMap) { + private ListenableFuture prepareOutgoingContentMap( + final RtpContentMap rtpContentMap) { if (this.omemoVerification.hasDeviceId()) { - ListenableFuture> verifiedPayloadFuture = id.account.getAxolotlService() - .encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId()); - return Futures.transform(verifiedPayloadFuture, verifiedPayload -> { - omemoVerification.setOrEnsureEqual(verifiedPayload); - return verifiedPayload.getPayload(); - }, MoreExecutors.directExecutor()); + ListenableFuture> + verifiedPayloadFuture = + id.account + .getAxolotlService() + .encrypt( + rtpContentMap, + id.with, + omemoVerification.getDeviceId()); + return Futures.transform( + verifiedPayloadFuture, + verifiedPayload -> { + omemoVerification.setOrEnsureEqual(verifiedPayload); + return verifiedPayload.getPayload(); + }, + MoreExecutors.directExecutor()); } else { return Futures.immediateFuture(rtpContentMap); } } - synchronized void deliveryMessage(final Jid from, final Element message, final String serverMessageId, final long timestamp) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": delivered message to JingleRtpConnection " + message); + synchronized void deliveryMessage( + final Jid from, + final Element message, + final String serverMessageId, + final long timestamp) { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": delivered message to JingleRtpConnection " + + message); switch (message.getName()) { case "propose": receivePropose(from, Propose.upgrade(message), serverMessageId, timestamp); @@ -750,47 +934,73 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } void deliverFailedProceed() { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": receive message error for proceed message"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": receive message error for proceed message"); if (transition(State.TERMINATED_CONNECTIVITY_ERROR)) { webRTCWrapper.close(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": transitioned into connectivity error"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": transitioned into connectivity error"); this.finish(); } } private void receiveAccept(final Jid from, final String serverMsgId, final long timestamp) { - final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); + final boolean originatedFromMyself = + from.asBareJid().equals(id.account.getJid().asBareJid()); if (originatedFromMyself) { if (transition(State.ACCEPTED)) { if (serverMsgId != null) { this.message.setServerMsgId(serverMsgId); } this.message.setTime(timestamp); - this.message.setCarbon(true); //indicate that call was accepted on other device + this.message.setCarbon(true); // indicate that call was accepted on other device this.writeLogMessageSuccess(0); - this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); + this.xmppConnectionService + .getNotificationService() + .cancelIncomingCallNotification(); this.finish(); } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to transition to accept because already in state=" + this.state); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": unable to transition to accept because already in state=" + + this.state); } } else { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring 'accept' from " + from); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": ignoring 'accept' from " + from); } } private void receiveReject(final Jid from, final String serverMsgId, final long timestamp) { - final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); - //reject from another one of my clients + final boolean originatedFromMyself = + from.asBareJid().equals(id.account.getJid().asBareJid()); + // reject from another one of my clients if (originatedFromMyself) { receiveRejectFromMyself(serverMsgId, timestamp); } else if (isInitiator()) { if (from.equals(id.with)) { receiveRejectFromResponder(); } else { - Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring reject from " + from + " for session with " + id.with); + Log.d( + Config.LOGTAG, + id.account.getJid() + + ": ignoring reject from " + + from + + " for session with " + + id.with); } } else { - Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring reject from " + from + " for session with " + id.with); + Log.d( + Config.LOGTAG, + id.account.getJid() + + ": ignoring reject from " + + from + + " for session with " + + id.with); } } @@ -802,54 +1012,94 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.message.setServerMsgId(serverMsgId); } this.message.setTime(timestamp); - this.message.setCarbon(true); //indicate that call was rejected on other device + this.message.setCarbon(true); // indicate that call was rejected on other device writeLogMessageMissed(); } else { - Log.d(Config.LOGTAG, "not able to transition into REJECTED because already in " + this.state); + Log.d( + Config.LOGTAG, + "not able to transition into REJECTED because already in " + this.state); } } private void receiveRejectFromResponder() { if (isInState(State.PROCEED)) { - Log.d(Config.LOGTAG, id.account.getJid() + ": received reject while still in proceed. callee reconsidered"); + Log.d( + Config.LOGTAG, + id.account.getJid() + + ": received reject while still in proceed. callee reconsidered"); closeTransitionLogFinish(State.REJECTED_RACED); return; } if (isInState(State.SESSION_INITIALIZED_PRE_APPROVED)) { - Log.d(Config.LOGTAG, id.account.getJid() + ": received reject while in SESSION_INITIATED_PRE_APPROVED. callee reconsidered before receiving session-init"); + Log.d( + Config.LOGTAG, + id.account.getJid() + + ": received reject while in SESSION_INITIATED_PRE_APPROVED. callee reconsidered before receiving session-init"); closeTransitionLogFinish(State.TERMINATED_DECLINED_OR_BUSY); return; } - Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring reject from responder because already in state " + this.state); + Log.d( + Config.LOGTAG, + id.account.getJid() + + ": ignoring reject from responder because already in state " + + this.state); } - private void receivePropose(final Jid from, final Propose propose, final String serverMsgId, final long timestamp) { - final boolean originatedFromMyself = from.asBareJid().equals(id.account.getJid().asBareJid()); + private void receivePropose( + final Jid from, final Propose propose, final String serverMsgId, final long timestamp) { + final boolean originatedFromMyself = + from.asBareJid().equals(id.account.getJid().asBareJid()); if (originatedFromMyself) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": saw proposal from myself. ignoring"); - } else if (transition(State.PROPOSED, () -> { - final Collection descriptions = Collections2.transform( - Collections2.filter(propose.getDescriptions(), d -> d instanceof RtpDescription), - input -> (RtpDescription) input - ); - final Collection media = Collections2.transform(descriptions, RtpDescription::getMedia); - Preconditions.checkState(!media.contains(Media.UNKNOWN), "RTP descriptions contain unknown media"); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received session proposal from " + from + " for " + media); - this.proposedMedia = Sets.newHashSet(media); - })) { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": saw proposal from myself. ignoring"); + } else if (transition( + State.PROPOSED, + () -> { + final Collection descriptions = + Collections2.transform( + Collections2.filter( + propose.getDescriptions(), + d -> d instanceof RtpDescription), + input -> (RtpDescription) input); + final Collection media = + Collections2.transform(descriptions, RtpDescription::getMedia); + Preconditions.checkState( + !media.contains(Media.UNKNOWN), + "RTP descriptions contain unknown media"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received session proposal from " + + from + + " for " + + media); + this.proposedMedia = Sets.newHashSet(media); + })) { if (serverMsgId != null) { this.message.setServerMsgId(serverMsgId); } this.message.setTime(timestamp); startRinging(); } else { - Log.d(Config.LOGTAG, id.account.getJid() + ": ignoring session proposal because already in " + state); + Log.d( + Config.LOGTAG, + id.account.getJid() + + ": ignoring session proposal because already in " + + state); } } private void startRinging() { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received call from " + id.with + ". start ringing"); - ringingTimeoutFuture = jingleConnectionManager.schedule(this::ringingTimeout, BUSY_TIME_OUT, TimeUnit.SECONDS); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received call from " + + id.with + + ". start ringing"); + ringingTimeoutFuture = + jingleConnectionManager.schedule( + this::ringingTimeout, BUSY_TIME_OUT, TimeUnit.SECONDS); xmppConnectionService.getNotificationService().startRinging(id, getMedia()); } @@ -875,8 +1125,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - private void receiveProceed(final Jid from, final Proceed proceed, final String serverMsgId, final long timestamp) { - final Set media = Preconditions.checkNotNull(this.proposedMedia, "Proposed media has to be set before handling proceed"); + private void receiveProceed( + final Jid from, final Proceed proceed, final String serverMsgId, final long timestamp) { + final Set media = + Preconditions.checkNotNull( + this.proposedMedia, "Proposed media has to be set before handling proceed"); Preconditions.checkState(media.size() > 0, "Proposed media should not be empty"); if (from.equals(id.with)) { if (isInitiator()) { @@ -890,35 +1143,65 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.omemoVerification.setDeviceId(remoteDeviceId); } else { if (remoteDeviceId != null) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": remote party signaled support for OMEMO verification but we have OMEMO disabled"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": remote party signaled support for OMEMO verification but we have OMEMO disabled"); } this.omemoVerification.setDeviceId(null); } this.sendSessionInitiate(media, State.SESSION_INITIALIZED_PRE_APPROVED); } else { - Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because already in %s", id.account.getJid().asBareJid(), this.state)); + Log.d( + Config.LOGTAG, + String.format( + "%s: ignoring proceed because already in %s", + id.account.getJid().asBareJid(), this.state)); } } else { - Log.d(Config.LOGTAG, String.format("%s: ignoring proceed because we were not initializing", id.account.getJid().asBareJid())); + Log.d( + Config.LOGTAG, + String.format( + "%s: ignoring proceed because we were not initializing", + id.account.getJid().asBareJid())); } } else if (from.asBareJid().equals(id.account.getJid().asBareJid())) { if (transition(State.ACCEPTED)) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": moved session with " + id.with + " into state accepted after received carbon copied procced"); - this.xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": moved session with " + + id.with + + " into state accepted after received carbon copied procced"); + this.xmppConnectionService + .getNotificationService() + .cancelIncomingCallNotification(); this.finish(); } } else { - Log.d(Config.LOGTAG, String.format("%s: ignoring proceed from %s. was expected from %s", id.account.getJid().asBareJid(), from, id.with)); + Log.d( + Config.LOGTAG, + String.format( + "%s: ignoring proceed from %s. was expected from %s", + id.account.getJid().asBareJid(), from, id.with)); } } private void receiveRetract(final Jid from, final String serverMsgId, final long timestamp) { if (from.equals(id.with)) { - final State target = this.state == State.PROCEED ? State.RETRACTED_RACED : State.RETRACTED; + final State target = + this.state == State.PROCEED ? State.RETRACTED_RACED : State.RETRACTED; if (transition(target)) { xmppConnectionService.getNotificationService().cancelIncomingCallNotification(); xmppConnectionService.getNotificationService().pushMissedCallNow(message); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": session with " + id.with + " has been retracted (serverMsgId=" + serverMsgId + ")"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": session with " + + id.with + + " has been retracted (serverMsgId=" + + serverMsgId + + ")"); if (serverMsgId != null) { this.message.setServerMsgId(serverMsgId); } @@ -932,8 +1215,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web Log.d(Config.LOGTAG, "ignoring retract because already in " + this.state); } } else { - //TODO parse retract from self - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received retract from " + from + ". expected retract from" + id.with + ". ignoring"); + // TODO parse retract from self + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received retract from " + + from + + ". expected retract from" + + id.with + + ". ignoring"); } } @@ -946,9 +1236,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web discoverIceServers(iceServers -> sendSessionInitiate(media, targetState, iceServers)); } - private synchronized void sendSessionInitiate(final Set media, final State targetState, final List iceServers) { + private synchronized void sendSessionInitiate( + final Set media, + final State targetState, + final List iceServers) { if (isTerminated()) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": ICE servers got discovered when session was already terminated. nothing to do."); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ICE servers got discovered when session was already terminated. nothing to do."); return; } try { @@ -960,10 +1256,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return; } try { - org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.setLocalDescription().get(); + org.webrtc.SessionDescription webRTCSessionDescription = + this.webRTCWrapper.setLocalDescription().get(); prepareSessionInitiate(webRTCSessionDescription, targetState); } catch (final Exception e) { - //TODO sending the error text is worthwhile as well. Especially for FailureToSet exceptions + // TODO sending the error text is worthwhile as well. Especially for FailureToSet + // exceptions failureToInitiateSession(e, targetState); } } @@ -972,7 +1270,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (isTerminated()) { return; } - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", Throwables.getRootCause(throwable)); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": unable to sendSessionInitiate", + Throwables.getRootCause(throwable)); webRTCWrapper.close(); final Reason reason = Reason.ofThrowable(throwable); if (isInState(targetState)) { @@ -983,49 +1284,71 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void sendRetract(final Reason reason) { - //TODO embed reason into retract + // TODO embed reason into retract sendJingleMessage("retract", id.with.asBareJid()); transitionOrThrow(reasonToState(reason)); this.finish(); } - private void prepareSessionInitiate(final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) { - final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); + private void prepareSessionInitiate( + final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) { + final SessionDescription sessionDescription = + SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); this.initiatorRtpContentMap = rtpContentMap; this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true); - final ListenableFuture outgoingContentMapFuture = encryptSessionInitiate(rtpContentMap); - Futures.addCallback(outgoingContentMapFuture, new FutureCallback() { - @Override - public void onSuccess(final RtpContentMap outgoingContentMap) { - sendSessionInitiate(outgoingContentMap, targetState); - } + final ListenableFuture outgoingContentMapFuture = + encryptSessionInitiate(rtpContentMap); + Futures.addCallback( + outgoingContentMapFuture, + new FutureCallback() { + @Override + public void onSuccess(final RtpContentMap outgoingContentMap) { + sendSessionInitiate(outgoingContentMap, targetState); + } - @Override - public void onFailure(@NonNull final Throwable throwable) { - failureToInitiateSession(throwable, targetState); - } - }, MoreExecutors.directExecutor()); + @Override + public void onFailure(@NonNull final Throwable throwable) { + failureToInitiateSession(throwable, targetState); + } + }, + MoreExecutors.directExecutor()); } private void sendSessionInitiate(final RtpContentMap rtpContentMap, final State targetState) { if (isTerminated()) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session was too slow. already terminated. nothing to do."); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": preparing session was too slow. already terminated. nothing to do."); return; } this.transitionOrThrow(targetState); - final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); + final JinglePacket sessionInitiate = + rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); send(sessionInitiate); } - private ListenableFuture encryptSessionInitiate(final RtpContentMap rtpContentMap) { + private ListenableFuture encryptSessionInitiate( + final RtpContentMap rtpContentMap) { if (this.omemoVerification.hasDeviceId()) { - final ListenableFuture> verifiedPayloadFuture = id.account.getAxolotlService() - .encrypt(rtpContentMap, id.with, omemoVerification.getDeviceId()); - final ListenableFuture future = Futures.transform(verifiedPayloadFuture, verifiedPayload -> { - omemoVerification.setSessionFingerprint(verifiedPayload.getFingerprint()); - return verifiedPayload.getPayload(); - }, MoreExecutors.directExecutor()); + final ListenableFuture> + verifiedPayloadFuture = + id.account + .getAxolotlService() + .encrypt( + rtpContentMap, + id.with, + omemoVerification.getDeviceId()); + final ListenableFuture future = + Futures.transform( + verifiedPayloadFuture, + verifiedPayload -> { + omemoVerification.setSessionFingerprint( + verifiedPayload.getFingerprint()); + return verifiedPayload.getPayload(); + }, + MoreExecutors.directExecutor()); if (Config.REQUIRE_RTP_VERIFICATION) { return future; } @@ -1033,11 +1356,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web future, CryptoFailedException.class, e -> { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to use OMEMO DTLS verification on outgoing session initiate. falling back", e); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": unable to use OMEMO DTLS verification on outgoing session initiate. falling back", + e); return rtpContentMap; }, - MoreExecutors.directExecutor() - ); + MoreExecutors.directExecutor()); } else { return Futures.immediateFuture(rtpContentMap); } @@ -1054,23 +1380,31 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (previous != State.NULL) { writeLogMessage(target); } - final JinglePacket jinglePacket = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); + final JinglePacket jinglePacket = + new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); jinglePacket.setReason(reason, text); Log.d(Config.LOGTAG, jinglePacket.toString()); send(jinglePacket); finish(); } - private void sendTransportInfo(final String contentName, IceUdpTransportInfo.Candidate candidate) { + private void sendTransportInfo( + final String contentName, IceUdpTransportInfo.Candidate candidate) { final RtpContentMap transportInfo; try { - final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; + final RtpContentMap rtpContentMap = + isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; transportInfo = rtpContentMap.transportInfo(contentName, candidate); } catch (final Exception e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to prepare transport-info from candidate for content=" + contentName); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": unable to prepare transport-info from candidate for content=" + + contentName); return; } - final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); + final JinglePacket jinglePacket = + transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); send(jinglePacket); } @@ -1092,19 +1426,28 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private 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); + 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"); + Log.i( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring error because session was already terminated"); return; } this.webRTCWrapper.close(); final State target; if (Arrays.asList( - "service-unavailable", - "recipient-unavailable", - "remote-server-not-found", - "remote-server-timeout" - ).contains(errorCondition)) { + "service-unavailable", + "recipient-unavailable", + "remote-server-not-found", + "remote-server-timeout") + .contains(errorCondition)) { target = State.TERMINATED_CONNECTIVITY_ERROR; } else { target = State.TERMINATED_APPLICATION_FAILURE; @@ -1115,9 +1458,17 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private 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"); + 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"); + Log.i( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": ignoring error because session was already terminated"); return; } this.webRTCWrapper.close(); @@ -1126,7 +1477,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void terminateWithOutOfOrder(final JinglePacket jinglePacket) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": terminating session with out-of-order"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": terminating session with out-of-order"); this.webRTCWrapper.close(); transitionOrThrow(State.TERMINATED_APPLICATION_FAILURE); respondWithOutOfOrder(jinglePacket); @@ -1141,19 +1494,18 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait"); } - void respondWithJingleError(final IqPacket original, String jingleCondition, String condition, String conditionType) { - jingleConnectionManager.respondWithJingleError(id.account, original, jingleCondition, condition, conditionType); + void respondWithJingleError( + final IqPacket original, + String jingleCondition, + String condition, + String conditionType) { + jingleConnectionManager.respondWithJingleError( + id.account, original, jingleCondition, condition, conditionType); } private void respondOk(final JinglePacket jinglePacket) { - xmppConnectionService.sendIqPacket(id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null); - } - - public void throwStateTransitionException() { - final StateTransitionException exception = this.stateTransitionException; - if (exception != null) { - throw new IllegalStateException(String.format("Transition to %s did not call finish", exception.state), exception); - } + xmppConnectionService.sendIqPacket( + id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null); } public RtpEndUserState getEndUserState() { @@ -1200,23 +1552,25 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return RtpEndUserState.RETRACTED; } case TERMINATED_CONNECTIVITY_ERROR: - return zeroDuration() ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR; + return zeroDuration() + ? RtpEndUserState.CONNECTIVITY_ERROR + : RtpEndUserState.CONNECTIVITY_LOST_ERROR; case TERMINATED_APPLICATION_FAILURE: return RtpEndUserState.APPLICATION_ERROR; case TERMINATED_SECURITY_ERROR: return RtpEndUserState.SECURITY_ERROR; } - throw new IllegalStateException(String.format("%s has no equivalent EndUserState", this.state)); + throw new IllegalStateException( + String.format("%s has no equivalent EndUserState", this.state)); } - private RtpEndUserState getPeerConnectionStateAsEndUserState() { final PeerConnection.PeerConnectionState state; try { state = webRTCWrapper.getState(); } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) { - //We usually close the WebRTCWrapper *before* transitioning so we might still - //be in SESSION_ACCEPTED even though the peerConnection has been torn down + // We usually close the WebRTCWrapper *before* transitioning so we might still + // be in SESSION_ACCEPTED even though the peerConnection has been torn down return RtpEndUserState.ENDING_CALL; } switch (state) { @@ -1228,7 +1582,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web case CLOSED: return RtpEndUserState.ENDING_CALL; default: - return zeroDuration() ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.RECONNECTING; + return zeroDuration() + ? RtpEndUserState.CONNECTIVITY_ERROR + : RtpEndUserState.RECONNECTING; } } @@ -1237,35 +1593,32 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (current == State.NULL) { if (isInitiator()) { return Preconditions.checkNotNull( - this.proposedMedia, - "RTP connection has not been initialized properly" - ); + this.proposedMedia, "RTP connection has not been initialized properly"); } throw new IllegalStateException("RTP connection has not been initialized yet"); } if (Arrays.asList(State.PROPOSED, State.PROCEED).contains(current)) { return Preconditions.checkNotNull( - this.proposedMedia, - "RTP connection has not been initialized properly" - ); + this.proposedMedia, "RTP connection has not been initialized properly"); } final RtpContentMap initiatorContentMap = initiatorRtpContentMap; if (initiatorContentMap != null) { return initiatorContentMap.getMedia(); } else if (isTerminated()) { - return Collections.emptySet(); //we might fail before we ever got a chance to set media + return Collections.emptySet(); // we might fail before we ever got a chance to set media } else { - return Preconditions.checkNotNull(this.proposedMedia, "RTP connection has not been initialized properly"); + return Preconditions.checkNotNull( + this.proposedMedia, "RTP connection has not been initialized properly"); } } - public boolean isVerified() { final String fingerprint = this.omemoVerification.getFingerprint(); if (fingerprint == null) { return false; } - final FingerprintStatus status = id.account.getAxolotlService().getFingerprintTrust(fingerprint); + final FingerprintStatus status = + id.account.getAxolotlService().getFingerprintTrust(fingerprint); return status != null && status.isVerified(); } @@ -1280,18 +1633,23 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web acceptCallFromSessionInitialized(); break; case ACCEPTED: - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": the call has already been accepted with another client. UI was just lagging behind"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": the call has already been accepted with another client. UI was just lagging behind"); break; case PROCEED: case SESSION_ACCEPTED: - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": the call has already been accepted. user probably double tapped the UI"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": the call has already been accepted. user probably double tapped the UI"); break; default: throw new IllegalStateException("Can not accept call from " + this.state); } } - public void notifyPhoneCall() { Log.d(Config.LOGTAG, "a phone call has just been started. killing jingle rtp connections"); if (Arrays.asList(State.PROPOSED, State.SESSION_INITIALIZED).contains(this.state)) { @@ -1303,7 +1661,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web public synchronized void rejectCall() { if (isTerminated()) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": received rejectCall() when session has already been terminated. nothing to do"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received rejectCall() when session has already been terminated. nothing to do"); return; } switch (this.state) { @@ -1320,7 +1681,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web public synchronized void endCall() { if (isTerminated()) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": received endCall() when session has already been terminated. nothing to do"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": received endCall() when session has already been terminated. nothing to do"); return; } if (isInState(State.PROPOSED) && !isInitiator()) { @@ -1335,7 +1699,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } return; } - if (isInitiator() && isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED)) { + if (isInitiator() + && isInState(State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED)) { this.webRTCWrapper.close(); sendSessionTerminate(Reason.CANCEL); return; @@ -1349,11 +1714,17 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web sendSessionTerminate(Reason.SUCCESS); return; } - if (isInState(State.TERMINATED_APPLICATION_FAILURE, State.TERMINATED_CONNECTIVITY_ERROR, State.TERMINATED_DECLINED_OR_BUSY)) { - Log.d(Config.LOGTAG, "ignoring request to end call because already in state " + this.state); + if (isInState( + State.TERMINATED_APPLICATION_FAILURE, + State.TERMINATED_CONNECTIVITY_ERROR, + State.TERMINATED_DECLINED_OR_BUSY)) { + Log.d( + Config.LOGTAG, + "ignoring request to end call because already in state " + this.state); return; } - throw new IllegalStateException("called 'endCall' while in state " + this.state + ". isInitiator=" + isInitiator()); + throw new IllegalStateException( + "called 'endCall' while in state " + this.state + ". isInitiator=" + isInitiator()); } private void retractFromProceed() { @@ -1369,7 +1740,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web finish(); } - private void setupWebRTC(final Set media, final List iceServers) throws WebRTCWrapper.InitializationException { + private void setupWebRTC( + final Set media, final List iceServers) + throws WebRTCWrapper.InitializationException { this.jingleConnectionManager.ensureConnectionIsRegistered(this); final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference; if (media.contains(Media.VIDEO)) { @@ -1413,14 +1786,18 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void sendJingleMessage(final String action, final Jid to) { final MessagePacket messagePacket = new MessagePacket(); - messagePacket.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those + messagePacket.setType(MessagePacket.TYPE_CHAT); // we want to carbon copy those messagePacket.setTo(to); - final Element intent = messagePacket.addChild(action, Namespace.JINGLE_MESSAGE).setAttribute("id", id.sessionId); + final Element intent = + messagePacket + .addChild(action, Namespace.JINGLE_MESSAGE) + .setAttribute("id", id.sessionId); if ("proceed".equals(action)) { messagePacket.setId(JINGLE_MESSAGE_PROCEED_ID_PREFIX + id.sessionId); if (isOmemoEnabled()) { final int deviceId = id.account.getAxolotlService().getOwnDeviceId(); - final Element device = intent.addChild("device", Namespace.OMEMO_DTLS_SRTP_VERIFICATION); + final Element device = + intent.addChild("device", Namespace.OMEMO_DTLS_SRTP_VERIFICATION); device.setAttribute("id", deviceId); } } @@ -1431,7 +1808,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private boolean isOmemoEnabled() { final Conversational conversational = message.getConversation(); if (conversational instanceof Conversation) { - return ((Conversation) conversational).getNextEncryption() == Message.ENCRYPTION_AXOLOTL; + return ((Conversation) conversational).getNextEncryption() + == Message.ENCRYPTION_AXOLOTL; } return false; } @@ -1453,7 +1831,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final Collection validTransitions = VALID_TRANSITIONS.get(this.state); if (validTransitions != null && validTransitions.contains(target)) { this.state = target; - this.stateTransitionException = new StateTransitionException(target); if (runnable != null) { runnable.run(); } @@ -1468,26 +1845,38 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web void transitionOrThrow(final State target) { if (!transition(target)) { - throw new IllegalStateException(String.format("Unable to transition from %s to %s", this.state, target)); + throw new IllegalStateException( + String.format("Unable to transition from %s to %s", this.state, target)); } } @Override public void onIceCandidate(final IceCandidate iceCandidate) { - final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; - final String ufrag = rtpContentMap.getCredentials().ufrag; - final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, ufrag); - if (candidate == null) { - Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate.toString()); + final RtpContentMap rtpContentMap = + isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; + final IceUdpTransportInfo.Credentials credentials; + try { + credentials = rtpContentMap.getCredentials(iceCandidate.sdpMid); + } catch (final IllegalArgumentException e) { + Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate, e); return; } - Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate.toString()); + final String uFrag = credentials.ufrag; + final IceUdpTransportInfo.Candidate candidate = + IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, uFrag); + if (candidate == null) { + Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate); + return; + } + Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate); sendTransportInfo(iceCandidate.sdpMid, candidate); } @Override public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); this.stateHistory.add(newState); if (newState == PeerConnection.PeerConnectionState.CONNECTED) { this.sessionDuration.start(); @@ -1497,12 +1886,17 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web updateOngoingCallNotification(); } - final boolean neverConnected = !this.stateHistory.contains(PeerConnection.PeerConnectionState.CONNECTED); + final boolean neverConnected = + !this.stateHistory.contains(PeerConnection.PeerConnectionState.CONNECTED); if (newState == PeerConnection.PeerConnectionState.FAILED) { if (neverConnected) { if (isTerminated()) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": not sending session-terminate after connectivity error because session is already in state " + + this.state); return; } webRTCWrapper.execute(this::closeWebRTCSessionAfterFailedConnection); @@ -1520,7 +1914,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void initiateIceRestart() { - //TODO discover new TURN/STUN credentials + // TODO discover new TURN/STUN credentials this.stateHistory.clear(); this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false); final SessionDescription sessionDescription; @@ -1534,28 +1928,32 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); final RtpContentMap transportInfo = rtpContentMap.transportInfo(); - final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); + final JinglePacket jinglePacket = + transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); Log.d(Config.LOGTAG, "initiating ice restart: " + jinglePacket); jinglePacket.setTo(id.with); - xmppConnectionService.sendIqPacket(id.account, jinglePacket, (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, "received success to our ice restart"); - setLocalContentMap(rtpContentMap); - webRTCWrapper.setIsReadyToReceiveIceCandidates(true); - return; - } - if (response.getType() == IqPacket.TYPE.ERROR) { - final Element error = response.findChild("error"); - if (error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS)) { - Log.d(Config.LOGTAG, "received tie-break as result of ice restart"); - return; - } - handleIqErrorResponse(response); - } - if (response.getType() == IqPacket.TYPE.TIMEOUT) { - handleIqTimeoutResponse(response); - } - }); + xmppConnectionService.sendIqPacket( + id.account, + jinglePacket, + (account, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, "received success to our ice restart"); + setLocalContentMap(rtpContentMap); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); + return; + } + if (response.getType() == IqPacket.TYPE.ERROR) { + final Element error = response.findChild("error"); + if (error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS)) { + Log.d(Config.LOGTAG, "received tie-break as result of ice restart"); + return; + } + handleIqErrorResponse(response); + } + if (response.getType() == IqPacket.TYPE.TIMEOUT) { + handleIqTimeoutResponse(response); + } + }); } private void setLocalContentMap(final RtpContentMap rtpContentMap) { @@ -1574,8 +1972,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - private SessionDescription setLocalSessionDescription() throws ExecutionException, InterruptedException { - final org.webrtc.SessionDescription sessionDescription = this.webRTCWrapper.setLocalDescription().get(); + private SessionDescription setLocalSessionDescription() + throws ExecutionException, InterruptedException { + final org.webrtc.SessionDescription sessionDescription = + this.webRTCWrapper.setLocalDescription().get(); return SessionDescription.parse(sessionDescription.description); } @@ -1583,7 +1983,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.webRTCWrapper.close(); synchronized (this) { if (isTerminated()) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": no need to send session-terminate after failed connection. Other party already did"); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": no need to send session-terminate after failed connection. Other party already did"); return; } sendSessionTerminate(Reason.CONNECTIVITY_ERROR); @@ -1631,14 +2034,18 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } @Override - public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { - xmppConnectionService.notifyJingleRtpConnectionUpdate(selectedAudioDevice, availableAudioDevices); + public void onAudioDeviceChanged( + AppRTCAudioManager.AudioDevice selectedAudioDevice, + Set availableAudioDevices) { + xmppConnectionService.notifyJingleRtpConnectionUpdate( + selectedAudioDevice, availableAudioDevices); } private void updateEndUserState() { final RtpEndUserState endUserState = getEndUserState(); jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, getMedia()); - xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, endUserState); + xmppConnectionService.notifyJingleRtpConnectionUpdate( + id.account, id.with, id.sessionId, endUserState); } private void updateOngoingCallNotification() { @@ -1646,7 +2053,8 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (STATES_SHOWING_ONGOING_CALL.contains(state)) { final boolean reconnecting; if (state == State.SESSION_ACCEPTED) { - reconnecting = getPeerConnectionStateAsEndUserState() == RtpEndUserState.RECONNECTING; + reconnecting = + getPeerConnectionStateAsEndUserState() == RtpEndUserState.RECONNECTING; } else { reconnecting = false; } @@ -1661,58 +2069,102 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final IqPacket request = new IqPacket(IqPacket.TYPE.GET); request.setTo(id.account.getDomain()); request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); - xmppConnectionService.sendIqPacket(id.account, request, (account, response) -> { - ImmutableList.Builder listBuilder = new ImmutableList.Builder<>(); - if (response.getType() == IqPacket.TYPE.RESULT) { - final Element services = response.findChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); - final List children = services == null ? Collections.emptyList() : services.getChildren(); - for (final Element child : children) { - if ("service".equals(child.getName())) { - final String type = child.getAttribute("type"); - final String host = child.getAttribute("host"); - final String 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.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": skipping invalid combination of udp/tls in external services"); - continue; + xmppConnectionService.sendIqPacket( + id.account, + request, + (account, response) -> { + ImmutableList.Builder listBuilder = + new ImmutableList.Builder<>(); + if (response.getType() == IqPacket.TYPE.RESULT) { + final Element services = + response.findChild( + "services", Namespace.EXTERNAL_SERVICE_DISCOVERY); + final List children = + services == null + ? Collections.emptyList() + : services.getChildren(); + for (final Element child : children) { + if ("service".equals(child.getName())) { + final String type = child.getAttribute("type"); + final String host = child.getAttribute("host"); + final String 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.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": skipping invalid combination of udp/tls in external services"); + continue; + } + final PeerConnection.IceServer.Builder iceServerBuilder = + PeerConnection.IceServer.builder( + String.format( + "%s:%s:%s?transport=%s", + type, + IP.wrapIPv6(host), + port, + transport)); + 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.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": skipping " + + type + + "/" + + transport + + " without username and password"); + continue; + } + final PeerConnection.IceServer iceServer = + iceServerBuilder.createIceServer(); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": discovered ICE Server: " + + iceServer); + listBuilder.add(iceServer); + } } - final PeerConnection.IceServer.Builder iceServerBuilder = PeerConnection.IceServer - .builder(String.format("%s:%s:%s?transport=%s", type, IP.wrapIPv6(host), port, transport)); - 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.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": skipping " + type + "/" + transport + " without username and password"); - continue; - } - final PeerConnection.IceServer iceServer = iceServerBuilder.createIceServer(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": discovered ICE Server: " + iceServer); - listBuilder.add(iceServer); } } - } - } - final List iceServers = listBuilder.build(); - if (iceServers.size() == 0) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no ICE server found " + response); - } - onIceServersDiscovered.onIceServersDiscovered(iceServers); - }); + final List iceServers = listBuilder.build(); + if (iceServers.size() == 0) { + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": no ICE server found " + + response); + } + onIceServersDiscovered.onIceServersDiscovered(iceServers); + }); } else { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": has no external service discovery"); + Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": has no external service discovery"); onIceServersDiscovered.onIceServersDiscovered(Collections.emptyList()); } } @@ -1724,13 +2176,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.jingleConnectionManager.setTerminalSessionState(id, getEndUserState(), getMedia()); this.jingleConnectionManager.finishConnectionOrThrow(this); } else { - throw new IllegalStateException(String.format("Unable to call finish from %s", this.state)); + throw new IllegalStateException( + String.format("Unable to call finish from %s", this.state)); } } private void writeLogMessage(final State state) { final long duration = getCallDuration(); - if (state == State.TERMINATED_SUCCESS || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) { + if (state == State.TERMINATED_SUCCESS + || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) { writeLogMessageSuccess(duration); } else { writeLogMessageMissed(); @@ -1784,18 +2238,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web public void fireStateUpdate() { final RtpEndUserState endUserState = getEndUserState(); - xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, endUserState); + xmppConnectionService.notifyJingleRtpConnectionUpdate( + id.account, id.with, id.sessionId, endUserState); } private interface OnIceServersDiscovered { void onIceServersDiscovered(List iceServers); } - - private static class StateTransitionException extends Exception { - private final State state; - - private StateTransitionException(final State state) { - this.state = state; - } - } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index 21684a165..e95a7e36d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -16,7 +16,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableDecl; import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; @@ -39,7 +38,8 @@ public class RtpContentMap { } public static RtpContentMap of(final JinglePacket jinglePacket) { - final Map contents = DescriptionTransport.of(jinglePacket.getJingleContents()); + final Map contents = + DescriptionTransport.of(jinglePacket.getJingleContents()); if (isOmemoVerified(contents)) { return new OmemoVerifiedRtpContentMap(jinglePacket.getGroup(), contents); } else { @@ -62,22 +62,30 @@ public class RtpContentMap { } public static RtpContentMap of(final SessionDescription sessionDescription) { - final ImmutableMap.Builder contentMapBuilder = new ImmutableMap.Builder<>(); + final ImmutableMap.Builder contentMapBuilder = + new ImmutableMap.Builder<>(); for (SessionDescription.Media media : sessionDescription.media) { final String id = Iterables.getFirst(media.attributes.get("mid"), null); Preconditions.checkNotNull(id, "media has no mid"); contentMapBuilder.put(id, DescriptionTransport.of(sessionDescription, media)); } - final String groupAttribute = Iterables.getFirst(sessionDescription.attributes.get("group"), null); + final String groupAttribute = + Iterables.getFirst(sessionDescription.attributes.get("group"), null); final Group group = groupAttribute == null ? null : Group.ofSdpString(groupAttribute); return new RtpContentMap(group, contentMapBuilder.build()); } public Set getMedia() { - return Sets.newHashSet(Collections2.transform(contents.values(), input -> { - final RtpDescription rtpDescription = input == null ? null : input.description; - return rtpDescription == null ? Media.UNKNOWN : input.description.getMedia(); - })); + return Sets.newHashSet( + Collections2.transform( + contents.values(), + input -> { + final RtpDescription rtpDescription = + input == null ? null : input.description; + return rtpDescription == null + ? Media.UNKNOWN + : input.description.getMedia(); + })); } public List getNames() { @@ -90,7 +98,8 @@ public class RtpContentMap { } for (Map.Entry entry : this.contents.entrySet()) { if (entry.getValue().description == null) { - throw new IllegalStateException(String.format("%s is lacking content description", entry.getKey())); + throw new IllegalStateException( + String.format("%s is lacking content description", entry.getKey())); } } } @@ -106,15 +115,24 @@ public class RtpContentMap { for (Map.Entry entry : this.contents.entrySet()) { final IceUdpTransportInfo transport = entry.getValue().transport; final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); - if (fingerprint == null || Strings.isNullOrEmpty(fingerprint.getContent()) || Strings.isNullOrEmpty(fingerprint.getHash())) { - throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s", entry.getKey())); + if (fingerprint == null + || Strings.isNullOrEmpty(fingerprint.getContent()) + || Strings.isNullOrEmpty(fingerprint.getHash())) { + throw new SecurityException( + String.format( + "Use of DTLS-SRTP (XEP-0320) is required for content %s", + entry.getKey())); } final IceUdpTransportInfo.Setup setup = fingerprint.getSetup(); if (setup == null) { - throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute", entry.getKey())); + throw new SecurityException( + String.format( + "Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute", + entry.getKey())); } if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) { - throw new SecurityException("Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)"); + throw new SecurityException( + "Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)"); } } } @@ -135,41 +153,66 @@ public class RtpContentMap { return jinglePacket; } - RtpContentMap transportInfo(final String contentName, final IceUdpTransportInfo.Candidate candidate) { + RtpContentMap transportInfo( + final String contentName, final IceUdpTransportInfo.Candidate candidate) { final RtpContentMap.DescriptionTransport descriptionTransport = contents.get(contentName); - final IceUdpTransportInfo transportInfo = descriptionTransport == null ? null : descriptionTransport.transport; + final IceUdpTransportInfo transportInfo = + descriptionTransport == null ? null : descriptionTransport.transport; if (transportInfo == null) { - throw new IllegalArgumentException("Unable to find transport info for content name " + contentName); + throw new IllegalArgumentException( + "Unable to find transport info for content name " + contentName); } final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper(); newTransportInfo.addChild(candidate); - return new RtpContentMap(null, ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo))); + return new RtpContentMap( + null, + ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo))); } RtpContentMap transportInfo() { return new RtpContentMap( null, - Maps.transformValues(contents, dt -> new DescriptionTransport(null, dt.transport.cloneWrapper())) - ); + Maps.transformValues( + contents, + dt -> new DescriptionTransport(null, dt.transport.cloneWrapper()))); } - public IceUdpTransportInfo.Credentials getCredentials() { - final Set allCredentials = ImmutableSet.copyOf(Collections2.transform( - contents.values(), - dt -> dt.transport.getCredentials() - )); - final IceUdpTransportInfo.Credentials credentials = Iterables.getFirst(allCredentials, null); + public IceUdpTransportInfo.Credentials getDistinctCredentials() { + final Set allCredentials = getCredentials(); + final IceUdpTransportInfo.Credentials credentials = + Iterables.getFirst(allCredentials, null); if (allCredentials.size() == 1 && credentials != null) { return credentials; } throw new IllegalStateException("Content map does not have distinct credentials"); } + public Set getCredentials() { + final Set credentials = + ImmutableSet.copyOf( + Collections2.transform( + contents.values(), dt -> dt.transport.getCredentials())); + if (credentials.isEmpty()) { + throw new IllegalStateException("Content map does not have any credentials"); + } + return credentials; + } + + public IceUdpTransportInfo.Credentials getCredentials(final String contentName) { + final DescriptionTransport descriptionTransport = this.contents.get(contentName); + if (descriptionTransport == null) { + throw new IllegalArgumentException( + String.format( + "Unable to find transport info for content name %s", contentName)); + } + return descriptionTransport.transport.getCredentials(); + } + public IceUdpTransportInfo.Setup getDtlsSetup() { - final Set setups = ImmutableSet.copyOf(Collections2.transform( - contents.values(), - dt -> dt.transport.getFingerprint().getSetup() - )); + final Set setups = + ImmutableSet.copyOf( + Collections2.transform( + contents.values(), dt -> dt.transport.getFingerprint().getSetup())); final IceUdpTransportInfo.Setup setup = Iterables.getFirst(setups, null); if (setups.size() == 1 && setup != null) { return setup; @@ -185,13 +228,18 @@ public class RtpContentMap { return count == 0; } - public RtpContentMap modifiedCredentials(IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) { - final ImmutableMap.Builder contentMapBuilder = new ImmutableMap.Builder<>(); + public RtpContentMap modifiedCredentials( + IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) { + final ImmutableMap.Builder contentMapBuilder = + new ImmutableMap.Builder<>(); for (final Map.Entry content : contents.entrySet()) { final RtpDescription rtpDescription = content.getValue().description; IceUdpTransportInfo transportInfo = content.getValue().transport; - final IceUdpTransportInfo modifiedTransportInfo = transportInfo.modifyCredentials(credentials, setup); - contentMapBuilder.put(content.getKey(), new DescriptionTransport(rtpDescription, modifiedTransportInfo)); + final IceUdpTransportInfo modifiedTransportInfo = + transportInfo.modifyCredentials(credentials, setup); + contentMapBuilder.put( + content.getKey(), + new DescriptionTransport(rtpDescription, modifiedTransportInfo)); } return new RtpContentMap(this.group, contentMapBuilder.build()); } @@ -200,7 +248,8 @@ public class RtpContentMap { public final RtpDescription description; public final IceUdpTransportInfo transport; - public DescriptionTransport(final RtpDescription description, final IceUdpTransportInfo transport) { + public DescriptionTransport( + final RtpDescription description, final IceUdpTransportInfo transport) { this.description = description; this.transport = transport; } @@ -215,33 +264,38 @@ public class RtpContentMap { } else if (description instanceof RtpDescription) { rtpDescription = (RtpDescription) description; } else { - throw new UnsupportedApplicationException("Content does not contain rtp description"); + throw new UnsupportedApplicationException( + "Content does not contain rtp description"); } if (transportInfo instanceof IceUdpTransportInfo) { iceUdpTransportInfo = (IceUdpTransportInfo) transportInfo; } else { - throw new UnsupportedTransportException("Content does not contain ICE-UDP transport"); + throw new UnsupportedTransportException( + "Content does not contain ICE-UDP transport"); } return new DescriptionTransport( - rtpDescription, - OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo) - ); + rtpDescription, OmemoVerifiedIceUdpTransportInfo.upgrade(iceUdpTransportInfo)); } - public static DescriptionTransport of(final SessionDescription sessionDescription, final SessionDescription.Media media) { + public static DescriptionTransport of( + final SessionDescription sessionDescription, final SessionDescription.Media media) { final RtpDescription rtpDescription = RtpDescription.of(sessionDescription, media); - final IceUdpTransportInfo transportInfo = IceUdpTransportInfo.of(sessionDescription, media); + final IceUdpTransportInfo transportInfo = + IceUdpTransportInfo.of(sessionDescription, media); return new DescriptionTransport(rtpDescription, transportInfo); } public static Map of(final Map contents) { - return ImmutableMap.copyOf(Maps.transformValues(contents, new Function() { - @NullableDecl - @Override - public DescriptionTransport apply(@NullableDecl Content content) { - return content == null ? null : of(content); - } - })); + return ImmutableMap.copyOf( + Maps.transformValues( + contents, + new Function() { + @NullableDecl + @Override + public DescriptionTransport apply(@NullableDecl Content content) { + return content == null ? null : of(content); + } + })); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index 45260cafb..ee8d12b70 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; +import androidx.annotation.NonNull; + import com.google.common.base.Joiner; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; @@ -123,6 +125,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo { } @Override + @NonNull public String toString() { return MoreObjects.toStringHelper(this) .add("ufrag", ufrag) diff --git a/src/main/res/layout/activity_muc_details.xml b/src/main/res/layout/activity_muc_details.xml index d19bf93e9..f2b4b00d7 100644 --- a/src/main/res/layout/activity_muc_details.xml +++ b/src/main/res/layout/activity_muc_details.xml @@ -106,7 +106,7 @@ app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error" app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint"> - - - + tools:text="jcapulet@example.com" /> @@ -256,5 +254,15 @@ app:tint="?attr/icon_tint" /> + + diff --git a/src/main/res/layout/create_conference_dialog.xml b/src/main/res/layout/create_conference_dialog.xml index 2296699a5..3883a0f5e 100644 --- a/src/main/res/layout/create_conference_dialog.xml +++ b/src/main/res/layout/create_conference_dialog.xml @@ -28,7 +28,7 @@ app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error" app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint"> - - - - + diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml index ee4687dfa..d4603cc3c 100644 --- a/src/main/res/values-bg/strings.xml +++ b/src/main/res/values-bg/strings.xml @@ -965,5 +965,4 @@ Създаването на резервно копие е стартирано. Ще получите известие, когато приключи. Видеото не може да бъде включено. Обикновен текстов документ - - + diff --git a/src/main/res/values-da-rDK/strings.xml b/src/main/res/values-da-rDK/strings.xml index 8c06e3f50..22f4bce2d 100644 --- a/src/main/res/values-da-rDK/strings.xml +++ b/src/main/res/values-da-rDK/strings.xml @@ -968,5 +968,4 @@ Sikkerhedskopieringen er startet. Du får en notifikation, når den er afsluttet. Kunne ikke aktivere video. Ren tekstdokument - - + diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 787025a67..7d8f45319 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -968,5 +968,4 @@ Die Sicherung wurde gestartet. Du bekommst eine Benachrichtigung, sobald sie fertig ist. Video kann nicht aktiviert werden. Textdokument - - + diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 535209db8..c958b30b2 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -968,5 +968,4 @@ La copia de seguridad ha empezado. Recibirás una notificación cuando se haya completado. No se ha podido habilitar el vídeo. Documento de texto plano - - + diff --git a/src/main/res/values-fi/strings.xml b/src/main/res/values-fi/strings.xml index 3fa9ad89f..9a03e5808 100644 --- a/src/main/res/values-fi/strings.xml +++ b/src/main/res/values-fi/strings.xml @@ -914,5 +914,4 @@ Varmuuskopion teko aloitettu. Saat ilmoituksen kun se on valmis. Videon käyttöönotto epäonnistui Perustekstiasiakirja - - + diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index fb320cfaa..be14eb100 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -479,7 +479,7 @@ Axustes ampliados de conexión Mostar axustes de servidor e porto cando se configura unha conta xmpp.exemplo.com - Conéctate con certificado + Accede con certificado Non se puido procesar o certificado Gardando axustes Axustes de gardado no servidor @@ -968,5 +968,4 @@ Comezou a creación da copia de apoio. Recibirás unha notificación cando remate. Non se puido activar o vídeo. Documento de texto plano - - + diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 3a760a44e..07a619dff 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -968,5 +968,4 @@ Il backup è iniziato. Riceverai una notifica una volta completato. Impossibile attivare il video. Documento di testo - - + diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 9963796ae..f94eb51e4 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -950,5 +950,4 @@ バックアップを開始しました。 バックアップが完了すると通知が届きます。 映像を有効化できません。 プレーンテキスト文書 - - + diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 7325816ed..2cba52246 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -431,7 +431,7 @@ Oferowanie %s Ukryj niedostępnych %s pisze... - %s przestał(a) pisać + %s już nie pisze %s piszą... %s przestali pisać Powiadomienia pisania @@ -995,5 +995,4 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Tworzenie kopii zapasowej się rozpoczęło. Dostaniesz powiadomienie kiedy się zakończy. Nie można włączyć wideo. Dokument zwykłego tekstu - - + diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 5e15e47c1..9efea8ca8 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -968,5 +968,4 @@ O backup foi iniciado. Você receberá uma notificação assim que ele for concluído. Não foi possível habilitar o vídeo. Documento em texto puro - - + diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index d3ddbe6ca..93b11c506 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -981,5 +981,4 @@ Se creează copia de siguranță. Veți primi o notificare când acesta este completă. Nu s-a putut activa camera video. Document text - - + diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 2c2f413d0..d9d728049 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -993,5 +993,4 @@ Резервное копирование было начато. Вы получите уведомление, как только оно будет завершено. Невозможно включить видео. Текстовые данные - - + diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index 138586237..d73433c36 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -537,6 +537,7 @@ Säkerhetsfel: Ogiltig filåtkomst! Ingen applikation hittades för att dela URI Dela URI med... + Acceptera och gå vidare Din fullständiga XMPP-adress kommer att vara: %s Skapa konto Använd min egen leverantör @@ -662,7 +663,9 @@ Igår Bekräfta värdnamn med DNSSEC Certifikatet innehåller ej en XMPP-adress + delvis Spela in video + Kopiera till urklipp Meddelande kopierat till urklipp Meddelande Godkänn okänt certifikat? diff --git a/src/main/res/values-tr-rTR/strings.xml b/src/main/res/values-tr-rTR/strings.xml index 72094ced9..f3e135b6b 100644 --- a/src/main/res/values-tr-rTR/strings.xml +++ b/src/main/res/values-tr-rTR/strings.xml @@ -968,5 +968,4 @@ Yedekleme başlatıldı. Tamamlandığı zaman bir bildirim alacaksınız. Video etkinleştirilemedi Düz metin dosyası - - + diff --git a/src/main/res/values-vi/strings.xml b/src/main/res/values-vi/strings.xml index 9fef998b0..76eb33c37 100644 --- a/src/main/res/values-vi/strings.xml +++ b/src/main/res/values-vi/strings.xml @@ -955,5 +955,4 @@ Việc sao lưu đã được bắt đầu. Bạn sẽ nhận một thông báo khi việc đó đã hoàn tất. Không thể bật video. Tài liệu văn bản thuần - - + diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index b3dcf959d..6323cea4d 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -955,5 +955,4 @@ 已启动备份。一旦完成,你会收到通知。 无法启用视频 纯文本文档 - - + diff --git a/src/main/res/values/about.xml b/src/main/res/values/about.xml index 5953d543d..dd5d32a15 100644 --- a/src/main/res/values/about.xml +++ b/src/main/res/values/about.xml @@ -31,7 +31,7 @@ Conversations • the very last word in instant messaging. - \n\nCopyright © 2014-2021 Daniel Gultsch + \n\nCopyright © 2014-2022 Daniel Gultsch \n\nThis program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or diff --git a/src/playstore/java/eu/siacs/conversations/services/EmojiInitializationService.java b/src/playstore/java/eu/siacs/conversations/services/EmojiInitializationService.java new file mode 100644 index 000000000..b5a57d374 --- /dev/null +++ b/src/playstore/java/eu/siacs/conversations/services/EmojiInitializationService.java @@ -0,0 +1,10 @@ +package eu.siacs.conversations.services; + +import android.content.Context; + +public class EmojiInitializationService { + + public static void execute(final Context context) { + + } +} diff --git a/src/playstoreCompat/java/eu/siacs/conversations/ui/service/EmojiService.java b/src/playstoreCompat/java/eu/siacs/conversations/ui/service/EmojiService.java deleted file mode 100644 index 5ed8c100a..000000000 --- a/src/playstoreCompat/java/eu/siacs/conversations/ui/service/EmojiService.java +++ /dev/null @@ -1,55 +0,0 @@ -package eu.siacs.conversations.ui.service; - -import android.content.Context; -import android.os.Build; -import android.util.Log; - -import androidx.core.provider.FontRequest; -import androidx.emoji.text.EmojiCompat; -import androidx.emoji.text.FontRequestEmojiCompatConfig; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; - -public class EmojiService { - - - private final EmojiCompat.InitCallback initCallback = new EmojiCompat.InitCallback() { - @Override - public void onInitialized() { - super.onInitialized(); - Log.d(Config.LOGTAG, "EmojiService succeeded in loading fonts"); - - } - - @Override - public void onFailed(Throwable throwable) { - super.onFailed(throwable); - Log.d(Config.LOGTAG, "EmojiService failed to load fonts", throwable); - } - }; - - private final Context context; - - public EmojiService(Context context) { - this.context = context; - } - - public void init() { - final FontRequest fontRequest = new FontRequest( - "com.google.android.gms.fonts", - "com.google.android.gms", - "Noto Color Emoji Compat", - R.array.font_certs); - FontRequestEmojiCompatConfig fontRequestEmojiCompatConfig = new FontRequestEmojiCompatConfig(context, fontRequest); - fontRequestEmojiCompatConfig.registerInitCallback(initCallback); - //On recent Androids we assume to have the latest emojis - //there are some annoying bugs with emoji compat that make it a safer choice not to use it when possible - // a) when using the ondemand emoji font (play store) flags don’t work - // b) the text preview has annoying glitches when the cut of text contains emojis (the emoji will be half visible) - // c) can trigger a hardware rendering bug https://issuetracker.google.com/issues/67102093 - fontRequestEmojiCompatConfig.setReplaceAll(Build.VERSION.SDK_INT < Build.VERSION_CODES.O); - EmojiCompat.init(fontRequestEmojiCompatConfig); - } - -} \ No newline at end of file diff --git a/src/playstoreCompat/res/values/font_certs.xml b/src/playstoreCompat/res/values/font_certs.xml deleted file mode 100644 index cc0ad3b3c..000000000 --- a/src/playstoreCompat/res/values/font_certs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK - - \ No newline at end of file diff --git a/src/system/java/eu/siacs/conversations/ui/service/EmojiService.java b/src/system/java/eu/siacs/conversations/ui/service/EmojiService.java deleted file mode 100644 index 6ca66fd62..000000000 --- a/src/system/java/eu/siacs/conversations/ui/service/EmojiService.java +++ /dev/null @@ -1,14 +0,0 @@ -package eu.siacs.conversations.ui.service; - -import android.content.Context; - -public class EmojiService { - - public EmojiService(Context context) { - //nop - } - - public void init() { - //nop - } -} \ No newline at end of file diff --git a/src/system/java/eu/siacs/conversations/ui/widget/EmojiWrapperEditText.java b/src/system/java/eu/siacs/conversations/ui/widget/EmojiWrapperEditText.java deleted file mode 100644 index 58e1ab318..000000000 --- a/src/system/java/eu/siacs/conversations/ui/widget/EmojiWrapperEditText.java +++ /dev/null @@ -1,16 +0,0 @@ -package eu.siacs.conversations.ui.widget; - -import android.content.Context; -import androidx.appcompat.widget.AppCompatEditText; -import android.util.AttributeSet; - -public class EmojiWrapperEditText extends AppCompatEditText { - - public EmojiWrapperEditText(Context context) { - super(context); - } - - public EmojiWrapperEditText(Context context, AttributeSet attrs) { - super(context, attrs); - } -} \ No newline at end of file diff --git a/src/system/java/eu/siacs/conversations/utils/EmojiWrapper.java b/src/system/java/eu/siacs/conversations/utils/EmojiWrapper.java deleted file mode 100644 index 3b6cf71e1..000000000 --- a/src/system/java/eu/siacs/conversations/utils/EmojiWrapper.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2017, 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 androidx.emoji.text.EmojiCompat; - -public class EmojiWrapper { - - public static CharSequence transform(CharSequence input) { - try { - if (EmojiCompat.get().getLoadState() == EmojiCompat.LOAD_STATE_SUCCEEDED) { - return EmojiCompat.get().process(input); - } else { - return input; - } - } catch (IllegalStateException e) { - return input; - } - } -}