diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 000000000..affaf7110 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,7 @@ +steps: + build: + image: codeberg.org/freeyourgadget/android-fdroid-tools:latest + commands: + - ./gradlew clean + - ./gradlew assembleConversationsFreeDebug + - ./gradlew assembleQuicksyFreeDebug diff --git a/CHANGELOG.md b/CHANGELOG.md index 2baf9330a..0e9eb28bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,50 @@ # Changelog +### Version 2.13.0 + +* Easier access to 'Show QR code' +* Support PEP Native Bookmarks +* Add support for SDP Offer / Answer Model (Used by SIP gateways) +* Raise target API to Android 14 + +### Version 2.12.12 + +* Support Private DNS (DNS over TLS) +* Support themed launcher icon +* Fix rare permission issue when sharing files on Android 11+ + +### Version 2.12.11 + +* Bump libwebrtc dependency to M117 and bump libvpx +* Go back to AAC for voice messages +* Support per app language settings + +### Version 2.12.10 + +* support per conversation notification settings +* use opus for voice messages on Android 10 + +### Version 2.12.9 + +* Introduce new backup file format + +### Version 2.12.8 + +* Disable opening backup files (.ceb) from file manager + +### Version 2.12.7 + +* Remove channel discovery feature from Google Play version + +### Version 2.12.6 + +* Fix 'q' falsely being recognized as cyrillic + +### Version 2.12.5 + +* Bump Target SDK to 33 again +* Fix issues on servers supporting SASL2 w/o inline Stream Management + ### Version 2.12.4 * Revert Target SDK bump (back to 32) to fix various issues on Android 13 diff --git a/README.md b/README.md index 7484a02ad..a0cfed0b8 100644 --- a/README.md +++ b/README.md @@ -78,11 +78,7 @@ Conversations is entirely open source and licensed under GPLv3. So if you are a software developer you can check out the sources from GitHub and use Gradle to build your apk file. -The more convenient way — which not only gives you automatic updates but also -supports the further development of Conversations — is to buy the App in the -Google [Play Store](https://play.google.com/store/apps/details?id=eu.siacs.conversations&referrer=utm_source%3Dcodeberg). - -Buying the App from the Play Store will also give you access to our [beta test](#beta). +Conversations is available on [Google Play](https://play.google.com/store/apps/details?id=eu.siacs.conversations) and on [F-Droid](https://f-droid.org/en/packages/eu.siacs.conversations/). #### I don't have a Google Account but I would still like to make a donation @@ -99,11 +95,11 @@ Learn more about [conversations.im Jabber/XMPP domain hosting](https://account.c ##### Running your own If you already have a server somewhere and are willing and able to put the necessary work in you can run your own XMPP server. -As of 2019 we recommend you use [ejabberd](https://ejabberd.im). The default configuration file already enables everything you need to pass the [Conversations Compliance Suite](https://compliance.conversations.im). Make sure your Linux distribution ships a fairly recent version. +As of 2023 XMPP has reached a level of maturity where all major XMPP servers ([ejabberd](https://ejabberd.im), [Prosody](https://prosody.im), [Openfire](https://www.igniterealtime.org/projects/openfire/), [Tigase](https://tigase.net/xmpp-server/)) should work well with Conversations. -With a little bit of effort [Prosody](https://prosody.im) can be configured to support all necessary extensions as well. However you will have to rely on so called [Community Modules](https://modules.prosody.im/) of varying quality. Prosody can be interesting to people who like to modify their server and create / prototype own modules. +Interoperability with Prosody and ejabberd is tested fairly regularly just because of their market share but we occasionally test with other servers too and fix issues as soon as we are being made aware of them. -Performance wise - for small deployments - both ejabberd and Prosody should be fine. +The default configurations are usually fine but you might want to use the [Conversations Compliance Suite](https://compliance.conversations.im) after install just to be sure. #### Where can I set up a custom hostname / port Conversations will automatically look up the SRV records for your domain name @@ -350,16 +346,6 @@ this.) Read more about the concept on https://gultsch.de/trust.html -#### What happened to OTR support? -OTR was removed because it was highly unreliable. It didn’t work with multiple devices and was never really specified to work with XMPP. The codebase was a mess (There was an HTML parser in there for crying out loud to deal with the garbage some OTR clients would send.) Verification was implemented in a non-blocking way. It would tell you if the current session was using an unknown fingerprint but it didn’t actively stopped you from sending messages until you have confirmed the new fingerprint. (Like Conversations would do now with BTBV after verification or when BTBV is turned off.) Considering the previous points there was little to no desire from my point to fix this potential security issue or clean up the code base. Another reason for the removal was that people would use it *accidentally* even to communicate between two Conversations clients because they read somewhere that OTR is good. - -### What clients do I use on other platforms -There are XMPP Clients available for all major platforms. -#### Windows / Linux -For your desktop computer we recommend that you use [Gajim](https://gajim.org). You need to install the `OMEMO` plugin to get the best compatibility with Conversations. Plugins can be installed from within the app, from your distribution, or from flatpak if you installed it from there. -#### iOS -Unfortunately we don‘t have a recommendation for iPhones right now. There are three clients available [Siskin](https://siskin.im/), [ChatSecure](https://chatsecure.org/) and [Monal](https://monal.im/). Each with their own pros and cons. - ### Development diff --git a/build.gradle b/build.gradle index 5eba51800..236168cd6 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { maven { url "https://www.jitpack.io" } } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.2.0-rc03' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21" } } @@ -19,7 +19,7 @@ repositories { google() mavenCentral() jcenter() - maven { url "https://www.jitpack.io" } + maven { url='https://jitpack.io'} } configurations { @@ -35,25 +35,27 @@ configurations { } dependencies { + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' + implementation 'androidx.viewpager:viewpager:1.0.0' - playstoreImplementation('com.google.firebase:firebase-messaging:23.1.2') { + playstoreImplementation('com.google.firebase:firebase-messaging:23.3.1') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' } 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 'com.github.open-keychain.open-keychain:openpgp-api:v5.7.1' + implementation("com.github.CanHub:Android-Image-Cropper:2.0.0") implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.exifinterface:exifinterface:1.3.6' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'com.google.android.material:material:1.9.0' + implementation 'com.google.android.material:material:1.10.0' - implementation "androidx.emoji2:emoji2:1.2.0" - freeImplementation "androidx.emoji2:emoji2-bundled:1.2.0" + implementation "androidx.emoji2:emoji2:1.4.0" + freeImplementation "androidx.emoji2:emoji2-bundled:1.4.0" implementation 'org.bouncycastle:bcmail-jdk15on:1.64' //zxing stopped supporting Java 7 so we have to stick with 3.3.3 @@ -65,9 +67,13 @@ dependencies { implementation 'com.makeramen:roundedimageview:2.3.0' implementation "com.wefika:flowlayout:0.4.1" //noinspection GradleDependency - implementation 'com.otaliastudios:transcoder:0.9.1' + implementation('com.github.natario1:Transcoder:v0.9.1') { + exclude group: 'com.otaliastudios.opengl', module: 'egloo' + } + implementation 'com.github.natario1:Egloo:v0.4.0' implementation 'org.jxmpp:jxmpp-jid:1.0.3' + implementation 'org.jxmpp:jxmpp-stringprep-libidn:1.0.3' implementation 'org.osmdroid:osmdroid-android:6.1.11' implementation 'org.hsluv:hsluv:0.2' implementation 'org.conscrypt:conscrypt-android:2.5.2' @@ -76,11 +82,11 @@ dependencies { implementation "com.squareup.retrofit2:retrofit:2.9.0" implementation "com.squareup.retrofit2:converter-gson:2.9.0" - implementation "com.squareup.okhttp3:okhttp:4.10.0" + implementation "com.squareup.okhttp3:okhttp:4.11.0" - implementation 'com.google.guava:guava:31.1-android' - implementation 'io.michaelrocks:libphonenumber-android:8.12.49' - implementation 'im.conversations.webrtc:webrtc-android:104.0.0' + implementation 'com.google.guava:guava:32.1.3-android' + implementation 'io.michaelrocks:libphonenumber-android:8.13.17' + implementation 'im.conversations.webrtc:webrtc-android:119.0.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "androidx.recyclerview:recyclerview:1.2.1" @@ -93,6 +99,7 @@ dependencies { implementation 'com.github.kizitonwose.colorpreference:support:1.1.0' implementation 'com.caverock:androidsvg-aar:1.4' implementation 'com.github.singpolyma:Better-Link-Movement-Method:4df081e1e4' + implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' } ext { @@ -102,13 +109,13 @@ ext { android { namespace 'eu.siacs.conversations' - compileSdkVersion 33 + compileSdk 34 defaultConfig { minSdkVersion 24 - targetSdkVersion 33 - versionCode 42059 - versionName "2.12.5" + targetSdkVersion 34 + versionCode 42081 + versionName "2.13.0" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations.narayana" resValue "string", "applicationId", applicationId @@ -121,6 +128,9 @@ android { abi { universalApk true enable true + reset() + //noinspection ChromeOsAbiSupport + include project.ext.abiCodes.keySet() as String[] } } @@ -133,11 +143,13 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + coreLibraryDesugaringEnabled true + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } - flavorDimensions("mode", "distribution") + flavorDimensions += "mode" + flavorDimensions += "distribution" productFlavors { @@ -243,10 +255,13 @@ android { } } lint { - disable 'MissingTranslation', 'InvalidPackage', 'AppCompatResource', 'ExtraTranslation' + disable 'MissingTranslation', 'InvalidPackage', 'AppCompatResource' + } + buildFeatures { + buildConfig true } - android.applicationVariants.all { variant -> + android.applicationVariants.configureEach { variant -> variant.outputs.each { output -> def baseAbiVersionCode = project.ext.abiCodes.get(output.getFilter(com.android.build.OutputFile.ABI)) if (baseAbiVersionCode != null) { diff --git a/conversations.doap b/conversations.doap index 7f5e654b2..4d492f9b3 100644 --- a/conversations.doap +++ b/conversations.doap @@ -91,13 +91,6 @@ 1.1 - - - - complete - 1.1 - - @@ -215,13 +208,6 @@ 2.0.1 - - - - complete - 2.0.1 - - @@ -389,7 +375,7 @@ complete - 0.2 + 0.3.1 @@ -399,6 +385,20 @@ 0.3.0 + + + + complete + 0.4.0 + + + + + + complete + 0.4.0 + + @@ -438,6 +438,13 @@ 0.2.1 + + + + complete + 1.1.3 + + @@ -453,6 +460,13 @@ 0.2.0 + + + + complete + 0.4.0 + + diff --git a/docs/user/migrating_to_new_device.md b/docs/user/migrating_to_new_device.md index 401a15386..e7d50a1ce 100644 --- a/docs/user/migrating_to_new_device.md +++ b/docs/user/migrating_to_new_device.md @@ -22,12 +22,11 @@ This tutorial explains how you can transfer your Conversations data from an old ## 3. Import the backup (new device) 1. Install Conversations on your new device. 2. Open Conversations for the first time. -3. Tap on "Use other server" -4. Tap on the three dot menu in the upper right corner and tap on "Import backup" -5. If your backup files are not listed, tap on the cloud symbol in the upper right corner to choose the files from the where you saved them. -6. Enter your account password to decrypt the backup. -7. Remember to activate your account (head back to "manage accounts", see step 1.2). -8. Check if chats work. +3. Tap on the three dot menu in the upper right corner and tap on "Import backup" +4. If your backup files are not listed, tap on the cloud symbol in the upper right corner to choose the files from where you saved them. +5. Enter your account password to decrypt the backup. +6. Remember to activate your account (head back to "manage accounts", see step 1.2). +7. Check if chats work. Once confirmed that the new device is running fine you can just uninstall the app from the old device. diff --git a/fastlane/metadata/android/de-DE/changelogs/42059.txt b/fastlane/metadata/android/de-DE/changelogs/42059.txt new file mode 100644 index 000000000..39abfa314 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42059.txt @@ -0,0 +1,2 @@ +* Ziel-SDK wieder auf 33 erhöht +* Behebt Probleme auf Servern, die SASL2 ohne Inline Stream Management unterstützen diff --git a/fastlane/metadata/android/de-DE/changelogs/42060.txt b/fastlane/metadata/android/de-DE/changelogs/42060.txt new file mode 100644 index 000000000..535628636 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42060.txt @@ -0,0 +1 @@ +* Fehlerhafte Erkennung von 'q' als kyrillisch behoben diff --git a/fastlane/metadata/android/de-DE/changelogs/42061.txt b/fastlane/metadata/android/de-DE/changelogs/42061.txt new file mode 100644 index 000000000..9673c61d8 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42061.txt @@ -0,0 +1 @@ +* Channelsuchfunktion aus der Google Play-Version entfernt diff --git a/fastlane/metadata/android/de-DE/changelogs/42062.txt b/fastlane/metadata/android/de-DE/changelogs/42062.txt new file mode 100644 index 000000000..7b9eff49c --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42062.txt @@ -0,0 +1 @@ +* Öffnen von Sicherungsdateien (.ceb) im Dateimanager deaktiviert diff --git a/fastlane/metadata/android/de-DE/changelogs/42065.txt b/fastlane/metadata/android/de-DE/changelogs/42065.txt new file mode 100644 index 000000000..79d8f5738 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42065.txt @@ -0,0 +1 @@ +* Einführung eines neuen Formats für Sicherungsdateien diff --git a/fastlane/metadata/android/de-DE/changelogs/42068.txt b/fastlane/metadata/android/de-DE/changelogs/42068.txt new file mode 100644 index 000000000..8204a5fe5 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42068.txt @@ -0,0 +1,2 @@ +* Unterstützung der Benachrichtigungseinstellungen pro Unterhaltung +* Verwendung von Opus für Sprachnachrichten unter Android 10 diff --git a/fastlane/metadata/android/de-DE/changelogs/42072.txt b/fastlane/metadata/android/de-DE/changelogs/42072.txt new file mode 100644 index 000000000..bc6504828 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42072.txt @@ -0,0 +1,3 @@ +* Änderung der libwebrtc-Abhängigkeit auf M117 und Änderung von libvpx +* Rückkehr zu AAC für Sprachnachrichten +* Unterstützung von Spracheinstellungen innerhalb einer App diff --git a/fastlane/metadata/android/de-DE/changelogs/42074.txt b/fastlane/metadata/android/de-DE/changelogs/42074.txt new file mode 100644 index 000000000..ad343db66 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/42074.txt @@ -0,0 +1,3 @@ +* Unterstützung von Private DNS (DNS über TLS) +* Unterstützung für designbasiertes Startsymbol +* Behebt ein seltenes Berechtigungsproblem beim Teilen von Dateien unter Android 11+ diff --git a/fastlane/metadata/android/de-DE/changelogs/4207704.txt b/fastlane/metadata/android/de-DE/changelogs/4207704.txt new file mode 100644 index 000000000..61579ecd3 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4207704.txt @@ -0,0 +1,3 @@ +* Unterstützung von Private DNS (DNS über TLS) +* Unterstützung von designbezogenem Startsymbol +* Behebt ein seltenes Berechtigungsproblem beim Teilen von Dateien unter Android 11+ diff --git a/fastlane/metadata/android/en-US/changelogs/42059.txt b/fastlane/metadata/android/en-US/changelogs/42059.txt new file mode 100644 index 000000000..042b86fcb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42059.txt @@ -0,0 +1,2 @@ +* Bump Target SDK to 33 again +* Fix issues on servers supporting SASL2 w/o inline Stream Management diff --git a/fastlane/metadata/android/en-US/changelogs/42060.txt b/fastlane/metadata/android/en-US/changelogs/42060.txt new file mode 100644 index 000000000..65c918e49 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42060.txt @@ -0,0 +1 @@ +* Fix 'q' falsely being recognized as cyrillic diff --git a/fastlane/metadata/android/en-US/changelogs/42061.txt b/fastlane/metadata/android/en-US/changelogs/42061.txt new file mode 100644 index 000000000..0475d110d --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42061.txt @@ -0,0 +1 @@ +* Remove channel discovery feature from Google Play version diff --git a/fastlane/metadata/android/en-US/changelogs/42062.txt b/fastlane/metadata/android/en-US/changelogs/42062.txt new file mode 100644 index 000000000..833c320e1 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42062.txt @@ -0,0 +1 @@ +* Disable opening backup files (.ceb) from file manager diff --git a/fastlane/metadata/android/en-US/changelogs/42065.txt b/fastlane/metadata/android/en-US/changelogs/42065.txt new file mode 100644 index 000000000..9b314f571 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42065.txt @@ -0,0 +1 @@ +* Introduce new backup file format diff --git a/fastlane/metadata/android/en-US/changelogs/42068.txt b/fastlane/metadata/android/en-US/changelogs/42068.txt new file mode 100644 index 000000000..1ddfe9ea5 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42068.txt @@ -0,0 +1,2 @@ +* support per conversation notification settings +* use opus for voice messages on Android 10 diff --git a/fastlane/metadata/android/en-US/changelogs/42072.txt b/fastlane/metadata/android/en-US/changelogs/42072.txt new file mode 100644 index 000000000..6a84b9489 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42072.txt @@ -0,0 +1,3 @@ +* Bump libwebrtc dependency to M117 and bump libvpx +* Go back to AAC for voice messages +* Support per app language settings diff --git a/fastlane/metadata/android/en-US/changelogs/4207704.txt b/fastlane/metadata/android/en-US/changelogs/4207704.txt new file mode 100644 index 000000000..6b19df3eb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4207704.txt @@ -0,0 +1,3 @@ +* Support Private DNS (DNS over TLS) +* Support themed launcher icon +* Fix rare permission issue when sharing files on Android 11+ diff --git a/fastlane/metadata/android/en-US/changelogs/4208104.txt b/fastlane/metadata/android/en-US/changelogs/4208104.txt new file mode 100644 index 000000000..a945cbcb3 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4208104.txt @@ -0,0 +1,4 @@ +* Easier access to 'Show QR code' +* Support PEP Native Bookmarks +* Add support for SDP Offer / Answer Model (Used by SIP gateways) +* Raise target API to Android 14 diff --git a/fastlane/metadata/android/es-ES/changelogs/349.txt b/fastlane/metadata/android/es-ES/changelogs/349.txt new file mode 100644 index 000000000..8f84c2432 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/349.txt @@ -0,0 +1,4 @@ +* Introducir configuración experta para realizar el descubrimiento de canales en el servidor local en lugar de search.jabber.network +* Habilitar las marcas de verificación de entrega por defecto y eliminar la configuración +* Habilitar «Enviar botón indica estado» por defecto y eliminar la configuración +* Mover los ajustes de copia de seguridad y servicio en primer plano a la pantalla principal diff --git a/fastlane/metadata/android/es-ES/changelogs/351.txt b/fastlane/metadata/android/es-ES/changelogs/351.txt new file mode 100644 index 000000000..a89b01aee --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/351.txt @@ -0,0 +1,3 @@ +* Corrección de la transferencia de archivos Jingle IBB +* Corrección de correcciones repetidas que llenaban la base de datos. +* Transición a Last Message Correction v1.1 diff --git a/fastlane/metadata/android/es-ES/changelogs/353.txt b/fastlane/metadata/android/es-ES/changelogs/353.txt new file mode 100644 index 000000000..e0f55a6b1 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/353.txt @@ -0,0 +1,4 @@ +* Permitir a los usuarios establecer su propio apodo +* reanudar la descarga de archivos encriptados OMEMO +* Los canales ahora usan '#' como símbolo en el avatar +* Quicksy utiliza «siempre» como cifrado OMEMO por defecto (oculta el icono del candado) diff --git a/fastlane/metadata/android/es-ES/changelogs/360.txt b/fastlane/metadata/android/es-ES/changelogs/360.txt new file mode 100644 index 000000000..169ae4b3e --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/360.txt @@ -0,0 +1 @@ +* Soporte para los parámetros URI de XMPP «?register» y «?register;preauth» diff --git a/fastlane/metadata/android/es-ES/changelogs/362.txt b/fastlane/metadata/android/es-ES/changelogs/362.txt new file mode 100644 index 000000000..bd88ea0f4 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/362.txt @@ -0,0 +1 @@ +* Soporte para el cambio automático de tema en Android 10 diff --git a/fastlane/metadata/android/es-ES/changelogs/364.txt b/fastlane/metadata/android/es-ES/changelogs/364.txt new file mode 100644 index 000000000..cc2be8a4d --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/364.txt @@ -0,0 +1,2 @@ +* Proporcionar vista previa de PDF en Android 5+ +* Utilizar IVs de 12 bytes para OMEMO diff --git a/fastlane/metadata/android/es-ES/changelogs/367.txt b/fastlane/metadata/android/es-ES/changelogs/367.txt new file mode 100644 index 000000000..e95c61e6b --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/367.txt @@ -0,0 +1,2 @@ +* Corregir la selección de avatar en algunos dispositivos Android 10 +* Corregir la transferencia de archivos más grandes diff --git a/fastlane/metadata/android/es-ES/changelogs/379.txt b/fastlane/metadata/android/es-ES/changelogs/379.txt new file mode 100644 index 000000000..74870d83f --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/379.txt @@ -0,0 +1 @@ +* Llamadas de audio/vídeo (requiere soporte de servidor en forma de servidores STUN y TURN detectables mediante XEP-0215) diff --git a/fastlane/metadata/android/es-ES/changelogs/381.txt b/fastlane/metadata/android/es-ES/changelogs/381.txt new file mode 100644 index 000000000..44512a40d --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/381.txt @@ -0,0 +1,2 @@ +* Respuesta audible (marcación, llamada iniciada, llamada finalizada) para llamadas de voz. +* Solucionado el problema de reintento de videollamada fallida diff --git a/fastlane/metadata/android/es-ES/changelogs/382.txt b/fastlane/metadata/android/es-ES/changelogs/382.txt new file mode 100644 index 000000000..8421f8e55 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/382.txt @@ -0,0 +1,2 @@ +* Añadir botón para cambiar de cámara durante la videollamada +* Corregidas las llamadas de voz en tablets diff --git a/fastlane/metadata/android/es-ES/changelogs/383.txt b/fastlane/metadata/android/es-ES/changelogs/383.txt new file mode 100644 index 000000000..189cd9c75 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/383.txt @@ -0,0 +1,3 @@ +* Mover el icono de llamada a la izquierda para mantener otros iconos de la barra de herramientas en un lugar coherente. +* Mostrar la duración de la llamada durante las llamadas de audio +* Desempate en las llamadas A/V (dos personas que se llaman al mismo tiempo) diff --git a/fastlane/metadata/android/es-ES/changelogs/387.txt b/fastlane/metadata/android/es-ES/changelogs/387.txt new file mode 100644 index 000000000..28af6206b --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/387.txt @@ -0,0 +1,2 @@ +* Reestructuración de la interfaz de inicio de sesión con certificado +* Añadir la posibilidad de anclar chats en la parte superior (añadir a favoritos) diff --git a/fastlane/metadata/android/es-ES/changelogs/388.txt b/fastlane/metadata/android/es-ES/changelogs/388.txt new file mode 100644 index 000000000..cd381e62a --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/388.txt @@ -0,0 +1,3 @@ +* Reducir el eco durante las llamadas en algunos dispositivos +* Arreglar el inicio de sesión cuando las contraseñas contienen caracteres especiales +* Reproducir tonos de marcado y ocupado en el altavoz durante las videollamadas diff --git a/fastlane/metadata/android/es-ES/changelogs/390.txt b/fastlane/metadata/android/es-ES/changelogs/390.txt new file mode 100644 index 000000000..7f1ba8d5e --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/390.txt @@ -0,0 +1 @@ +* Ofrecer la grabación de un mensaje de voz cuando la persona que llama está ocupada diff --git a/fastlane/metadata/android/es-ES/changelogs/393.txt b/fastlane/metadata/android/es-ES/changelogs/393.txt new file mode 100644 index 000000000..e6f08c87a --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/393.txt @@ -0,0 +1,3 @@ +* Mostrar botón de ayuda si falla la llamada A/V +* Corregidos algunos fallos molestos +* Corregidas conexiones Jingle (transferencia de archivos + llamadas) con JIDs sin ocultar diff --git a/fastlane/metadata/android/es-ES/changelogs/394.txt b/fastlane/metadata/android/es-ES/changelogs/394.txt new file mode 100644 index 000000000..72b50cdfb --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/394.txt @@ -0,0 +1,2 @@ +* Se ha corregido el problema de las notificaciones que no aparecían en determinadas circunstancias. +* Se han solucionado problemas de compatibilidad y bloqueos relacionados con las llamadas A/V diff --git a/fastlane/metadata/android/es-ES/changelogs/395.txt b/fastlane/metadata/android/es-ES/changelogs/395.txt new file mode 100644 index 000000000..7fd679913 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/395.txt @@ -0,0 +1,3 @@ +* Añadir 'Volver al chat' a la pantalla de llamada de audio +* Mejorar los atajos del teclado +* corrección de errores diff --git a/fastlane/metadata/android/es-ES/changelogs/397.txt b/fastlane/metadata/android/es-ES/changelogs/397.txt new file mode 100644 index 000000000..5bf18c91d --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/397.txt @@ -0,0 +1,3 @@ +* Gestión de archivos GPX +* Mejorar el rendimiento de la restauración de las copias de seguridad +* Corrección de errores diff --git a/fastlane/metadata/android/es-ES/changelogs/398.txt b/fastlane/metadata/android/es-ES/changelogs/398.txt new file mode 100644 index 000000000..b665a46bd --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/398.txt @@ -0,0 +1,4 @@ +* Buscar conversaciones individuales +* Notificar al usuario si falla la entrega del mensaje +* Recordar los nombres de usuario (nicks) de los usuarios de Quicksy en los reinicios +* Añadir el botón para iniciar Orbot (Tor) desde la notificación si es necesario diff --git a/fastlane/metadata/android/es-ES/changelogs/401.txt b/fastlane/metadata/android/es-ES/changelogs/401.txt new file mode 100644 index 000000000..f0818cbe9 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/401.txt @@ -0,0 +1,2 @@ +* búsqueda fija en Android <= 5 +* optimizar el consumo de la memoria diff --git a/fastlane/metadata/android/es-ES/changelogs/402.txt b/fastlane/metadata/android/es-ES/changelogs/402.txt new file mode 100644 index 000000000..079ced197 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/402.txt @@ -0,0 +1,3 @@ +* Ofrece la generación de invitaciones fáciles en los servidores compatibles +* Mostrar GIFs enviados desde Movim +* Almacenar avatares en caché diff --git a/fastlane/metadata/android/es-ES/changelogs/403.txt b/fastlane/metadata/android/es-ES/changelogs/403.txt new file mode 100644 index 000000000..3a4c6b698 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/403.txt @@ -0,0 +1,3 @@ +* Corregidos problemas de conectividad cuando diferentes cuentas utilizaban diferentes mecanismos SCRAM. +* Añadir soporte para SCRAM-SHA-512 +* Permitir la transferencia de archivos P2P (Jingle) con autocontacto diff --git a/fastlane/metadata/android/es-ES/changelogs/404.txt b/fastlane/metadata/android/es-ES/changelogs/404.txt new file mode 100644 index 000000000..c594e57cd --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/404.txt @@ -0,0 +1 @@ +* Pequeñas mejoras de estabilidad en las llamadas A/V diff --git a/fastlane/metadata/android/es-ES/changelogs/405.txt b/fastlane/metadata/android/es-ES/changelogs/405.txt new file mode 100644 index 000000000..a341b6579 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/405.txt @@ -0,0 +1 @@ +* Quicksy: Recibir automáticamente SMS de verificación diff --git a/fastlane/metadata/android/es-ES/changelogs/407.txt b/fastlane/metadata/android/es-ES/changelogs/407.txt new file mode 100644 index 000000000..3958fd6ef --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/407.txt @@ -0,0 +1,3 @@ +* Mostrar botón de llamada para contactos desconectados si previamente anunciaron soporte +* El botón Atrás ya no finaliza la llamada cuando está conectada. +* Corrección de errores diff --git a/fastlane/metadata/android/es-ES/changelogs/42000.txt b/fastlane/metadata/android/es-ES/changelogs/42000.txt new file mode 100644 index 000000000..8ad2c5472 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42000.txt @@ -0,0 +1,4 @@ +* Posibilidad de seleccionar el tono de la llamada entrante +* Corrección de la identificación de claves OpenPGP para OpenKeychain 5.6+. +* Verificación correcta de los certificados TLS punycode +* Mejora de la estabilidad del establecimiento de sesiones RTP (llamadas) diff --git a/fastlane/metadata/android/es-ES/changelogs/42006.txt b/fastlane/metadata/android/es-ES/changelogs/42006.txt new file mode 100644 index 000000000..eb1ea92f4 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42006.txt @@ -0,0 +1,2 @@ +* Verificar llamadas A/V con sesiones OMEMO preexistentes +* Mejorar la compatibilidad con implementaciones WebRTC no libwebrtc diff --git a/fastlane/metadata/android/es-ES/changelogs/42010.txt b/fastlane/metadata/android/es-ES/changelogs/42010.txt new file mode 100644 index 000000000..41e1148ba --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42010.txt @@ -0,0 +1,2 @@ +* Varias correcciones de errores en torno a la compatibilidad con Tor +* Mejora de la compatibilidad de llamadas con Dino diff --git a/fastlane/metadata/android/es-ES/changelogs/42012.txt b/fastlane/metadata/android/es-ES/changelogs/42012.txt new file mode 100644 index 000000000..814a09be7 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42012.txt @@ -0,0 +1 @@ +* Corrección de la carga/descarga HTTP para usuarios que no confían en las CA del sistema diff --git a/fastlane/metadata/android/es-ES/changelogs/42013.txt b/fastlane/metadata/android/es-ES/changelogs/42013.txt new file mode 100644 index 000000000..e31ca2cab --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42013.txt @@ -0,0 +1 @@ +* Solucionados los problemas de 'No Conectividad' en Android 7.1 diff --git a/fastlane/metadata/android/es-ES/changelogs/42014.txt b/fastlane/metadata/android/es-ES/changelogs/42014.txt new file mode 100644 index 000000000..a06654b64 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42014.txt @@ -0,0 +1,2 @@ +* Verificar siempre el nombre del dominio. No sobrescribir al usuario +* Soporta pre autenticación de roster diff --git a/fastlane/metadata/android/es-ES/changelogs/42015.txt b/fastlane/metadata/android/es-ES/changelogs/42015.txt new file mode 100644 index 000000000..ad8b35003 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42015.txt @@ -0,0 +1 @@ +* pequeñas mejoras en A/V diff --git a/fastlane/metadata/android/es-ES/changelogs/42018.txt b/fastlane/metadata/android/es-ES/changelogs/42018.txt new file mode 100644 index 000000000..25ec35a9b --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42018.txt @@ -0,0 +1,3 @@ +* Mostrar barras negras cuando el vídeo remoto no coincide con la relación del aspecto de la pantalla. +* Mejorar el rendimiento de la búsqueda +* Añadir configuración para evitar capturas de pantalla diff --git a/fastlane/metadata/android/es-ES/changelogs/42022.txt b/fastlane/metadata/android/es-ES/changelogs/42022.txt new file mode 100644 index 000000000..db5c79bc7 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42022.txt @@ -0,0 +1,2 @@ +* Corrección de un problema que impedía comprimir algunos vídeos. +* Corrección de un fallo poco frecuente al abrir una notificación diff --git a/fastlane/metadata/android/es-ES/changelogs/42023.txt b/fastlane/metadata/android/es-ES/changelogs/42023.txt new file mode 100644 index 000000000..0a1dd4ce3 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42023.txt @@ -0,0 +1,2 @@ +* Corrección de fallos en la representación de algunas citas +* Corrección del fallo en la pantalla de bienvenida diff --git a/fastlane/metadata/android/es-ES/changelogs/42037.txt b/fastlane/metadata/android/es-ES/changelogs/42037.txt new file mode 100644 index 000000000..b10095829 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42037.txt @@ -0,0 +1,11 @@ +Versión 2.10.9 +* Pedir permisos Bluetooth al hacer llamadas A/V (Puede rechazar esto si no utiliza auriculares Bluetooth). +* Corrección de error al llamar a Movim +* Corregir avatar incorrecto que se muestra para los chats de grupo +* Preguntar siempre por las optimizaciones de batería +* Establecer sólo local bandera en 'x cuentas conectadas' notificaciones +* Corrección de la interacción con Google Maps Share Location Plugin +* Eliminar nota a pie de página con respecto a la cuota del servidor +* Almacenar archivos en la ubicación adecuada para Android 11 +* Intento de reconectar llamada tras cambio de red +* Mostrar el JID de la persona que llama y el JID de la cuenta en la pantalla de llamada entrante diff --git a/fastlane/metadata/android/es-ES/changelogs/42038.txt b/fastlane/metadata/android/es-ES/changelogs/42038.txt new file mode 100644 index 000000000..0864d1d84 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42038.txt @@ -0,0 +1,2 @@ +* Corrección de errores menores +* Restaurar la capacidad de llamar a través de JMP y otros servicios (versión Playstore) diff --git a/fastlane/metadata/android/es-ES/changelogs/42041.txt b/fastlane/metadata/android/es-ES/changelogs/42041.txt new file mode 100644 index 000000000..03c4e761a --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42041.txt @@ -0,0 +1,5 @@ +* Implementación del perfil SASL extensible, Bind 2.0 y Fast para reconexiones más rápidas. +* Implementación de Channel Binding +* Añadir la posibilidad de cambiar de llamada de audio a videollamada +* Añadir la posibilidad de eliminar el propio avatar +* Notificación de llamadas perdidas diff --git a/fastlane/metadata/android/es-ES/changelogs/42042.txt b/fastlane/metadata/android/es-ES/changelogs/42042.txt new file mode 100644 index 000000000..288d6502a --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42042.txt @@ -0,0 +1,2 @@ +* Corrección del bucle de reenvío en servidores que sólo admiten sm:2 +* Mostrar "Cambiar a vídeo" sólo si la otra parte admite vídeo diff --git a/fastlane/metadata/android/es-ES/changelogs/42043.txt b/fastlane/metadata/android/es-ES/changelogs/42043.txt new file mode 100644 index 000000000..131ea2911 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42043.txt @@ -0,0 +1 @@ +* Corregida una regresión en la transferencia de archivos P2P diff --git a/fastlane/metadata/android/es-ES/changelogs/42044.txt b/fastlane/metadata/android/es-ES/changelogs/42044.txt new file mode 100644 index 000000000..1b98bb2aa --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42044.txt @@ -0,0 +1,3 @@ +* Corrección del reenvío de mensajes al utilizar SASL2 +* Corregir vídeo negro entre algunos dispositivos +* Arreglar fallo en contraseñas vacías diff --git a/fastlane/metadata/android/es-ES/changelogs/42046.txt b/fastlane/metadata/android/es-ES/changelogs/42046.txt new file mode 100644 index 000000000..f1576f520 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42046.txt @@ -0,0 +1 @@ +* Integrar el Distribuidor UnifiedPush para facilitar los mensajes push a otras aplicaciones habilitadas para UnifiedPush como Tusky y Fedilab diff --git a/fastlane/metadata/android/es-ES/changelogs/42047.txt b/fastlane/metadata/android/es-ES/changelogs/42047.txt new file mode 100644 index 000000000..8f3909b76 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42047.txt @@ -0,0 +1 @@ +* Corrección de fallos en el distribuidor de UnifiedPush diff --git a/fastlane/metadata/android/es-ES/changelogs/42050.txt b/fastlane/metadata/android/es-ES/changelogs/42050.txt new file mode 100644 index 000000000..fff8b62cb --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42050.txt @@ -0,0 +1 @@ +* Aumentar el radio de las esquinas en las fotos de perfil diff --git a/fastlane/metadata/android/es-ES/changelogs/42059.txt b/fastlane/metadata/android/es-ES/changelogs/42059.txt new file mode 100644 index 000000000..3a6ce0d00 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42059.txt @@ -0,0 +1,2 @@ +* Actualizar Target SDK a 33 de nuevo +* Corrección de problemas en servidores que soportan SASL2 sin Stream Management en línea diff --git a/fastlane/metadata/android/es-ES/changelogs/42060.txt b/fastlane/metadata/android/es-ES/changelogs/42060.txt new file mode 100644 index 000000000..624c64e92 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42060.txt @@ -0,0 +1 @@ +* Arreglar 'q' falsamente siendo reconocido como cirílico diff --git a/fastlane/metadata/android/es-ES/changelogs/42061.txt b/fastlane/metadata/android/es-ES/changelogs/42061.txt new file mode 100644 index 000000000..da7a92ab9 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42061.txt @@ -0,0 +1 @@ +* Eliminar la función de descubrimiento de canales de la versión de Google Play diff --git a/fastlane/metadata/android/es-ES/changelogs/42062.txt b/fastlane/metadata/android/es-ES/changelogs/42062.txt new file mode 100644 index 000000000..c1729884a --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42062.txt @@ -0,0 +1 @@ +* Desactivar la apertura de archivos de copia de seguridad (.ceb) desde el gestor de archivos diff --git a/fastlane/metadata/android/es-ES/changelogs/42065.txt b/fastlane/metadata/android/es-ES/changelogs/42065.txt new file mode 100644 index 000000000..eceb051e1 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42065.txt @@ -0,0 +1 @@ +* Introducir un nuevo formato de archivo de copia de seguridad diff --git a/fastlane/metadata/android/es-ES/changelogs/42068.txt b/fastlane/metadata/android/es-ES/changelogs/42068.txt new file mode 100644 index 000000000..b651cc178 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42068.txt @@ -0,0 +1,2 @@ +* soporte para los ajustes de la notificación de la conversación +* usar opus para mensajes de voz en Android 10 diff --git a/fastlane/metadata/android/es-ES/changelogs/42072.txt b/fastlane/metadata/android/es-ES/changelogs/42072.txt new file mode 100644 index 000000000..a4b7e971d --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/42072.txt @@ -0,0 +1,3 @@ +* Aumenta la dependencia de libwebrtc a M117 y aumenta la de libvpx. +* Volver a AAC para mensajes de voz +* Soporta ajustes de idioma por aplicación diff --git a/fastlane/metadata/android/es-ES/changelogs/4207704.txt b/fastlane/metadata/android/es-ES/changelogs/4207704.txt new file mode 100644 index 000000000..9396b343d --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4207704.txt @@ -0,0 +1,3 @@ +* Soporta DNS Privado (DNS sobre TLS) +* Icono temático del lanzador +* Corrección de un problema de permisos poco frecuente al compartir archivos en Android 11+ diff --git a/fastlane/metadata/android/es-ES/changelogs/4208104.txt b/fastlane/metadata/android/es-ES/changelogs/4208104.txt new file mode 100644 index 000000000..dde4e4f05 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4208104.txt @@ -0,0 +1,4 @@ +* Acceso más fácil a "Mostrar el código QR +* Soporte para marcadores nativos PEP +* Añadir soporte para SDP Oferta / Respuesta Modelo (Utilizado por pasarelas SIP) +* Aumento de la API de destino a Android 14 diff --git a/fastlane/metadata/android/gl-ES/changelogs/349.txt b/fastlane/metadata/android/gl-ES/changelogs/349.txt new file mode 100644 index 000000000..ce9204ef3 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/349.txt @@ -0,0 +1,4 @@ +* Introdución do axuste de experta para realizar o descubrimento de canle no servidor local e non buscar en search.jabber.network +* Activadas as marcas de comprobación de entrega por defecto e eliminación do axuste +* Activar por defecto 'O botón enviar indica estado' e eliminar o axuste +* Mover os axustes Copia de Apoio e Servizo en primeiro plano á pantalla principal diff --git a/fastlane/metadata/android/gl-ES/changelogs/351.txt b/fastlane/metadata/android/gl-ES/changelogs/351.txt new file mode 100644 index 000000000..8fabff2f1 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/351.txt @@ -0,0 +1,3 @@ +* fixes for Jingle IBB file transfer +* fixes for repeated corrections filling up the database +* switched to Last Message Correction v1.1 diff --git a/fastlane/metadata/android/gl-ES/changelogs/353.txt b/fastlane/metadata/android/gl-ES/changelogs/353.txt new file mode 100644 index 000000000..388f9ae91 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/353.txt @@ -0,0 +1,4 @@ +* permitir que as usuarias elixan o seu propio alcume +* retomar a descarga de ficheiros cifrados con OMEMO +* agora as Canles usan '#' como símbolo no avatar +* Quicksy establece 'sempre' para a cifraxe OMEMO por defecto (agocha a icona do cadeado) diff --git a/fastlane/metadata/android/gl-ES/changelogs/360.txt b/fastlane/metadata/android/gl-ES/changelogs/360.txt new file mode 100644 index 000000000..f31c6d51c --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/360.txt @@ -0,0 +1 @@ +* Soporte para os parámetros ?register e ?register;preauth da uri XMPP diff --git a/fastlane/metadata/android/gl-ES/changelogs/362.txt b/fastlane/metadata/android/gl-ES/changelogs/362.txt new file mode 100644 index 000000000..e357ba94c --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/362.txt @@ -0,0 +1 @@ +* Soporte para o cambio automático de decorado en Android 10 diff --git a/fastlane/metadata/android/gl-ES/changelogs/364.txt b/fastlane/metadata/android/gl-ES/changelogs/364.txt new file mode 100644 index 000000000..338355cbf --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/364.txt @@ -0,0 +1,2 @@ +* Proporciona vista previa dos PDF en Android 5+ +* Usa 12 byte IVs con OMEMO diff --git a/fastlane/metadata/android/gl-ES/changelogs/367.txt b/fastlane/metadata/android/gl-ES/changelogs/367.txt new file mode 100644 index 000000000..9b96da762 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/367.txt @@ -0,0 +1,2 @@ +* Arranxo da selección do avatar en dispositivos Android 10 +* Arranxo da transferencia de ficheiros moi grandes diff --git a/fastlane/metadata/android/gl-ES/changelogs/379.txt b/fastlane/metadata/android/gl-ES/changelogs/379.txt new file mode 100644 index 000000000..495c4da0c --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/379.txt @@ -0,0 +1 @@ +* Chamadas de Audio/Video (Require soporte no servidor para que os servidores STUN e TURN sexan accesibles vía XEP-0215) diff --git a/fastlane/metadata/android/gl-ES/changelogs/381.txt b/fastlane/metadata/android/gl-ES/changelogs/381.txt new file mode 100644 index 000000000..a2df5e828 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/381.txt @@ -0,0 +1,2 @@ +* Audible feedback (dialing, call started, call ended) for voice calls. +* Fixed issue with retrying failed video call diff --git a/fastlane/metadata/android/gl-ES/changelogs/382.txt b/fastlane/metadata/android/gl-ES/changelogs/382.txt new file mode 100644 index 000000000..64e23e14d --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/382.txt @@ -0,0 +1,2 @@ +* Add button to switch camera during video call +* Fixed voice calls on tablets diff --git a/fastlane/metadata/android/gl-ES/changelogs/383.txt b/fastlane/metadata/android/gl-ES/changelogs/383.txt new file mode 100644 index 000000000..19c9a0116 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/383.txt @@ -0,0 +1,3 @@ +* Move call icon to the left in order to keep other toolbar icons in a consistent place +* Show call duration during audio calls +* Tie breaking for A/V calls (the same two people calling each other at the same time) diff --git a/fastlane/metadata/android/gl-ES/changelogs/387.txt b/fastlane/metadata/android/gl-ES/changelogs/387.txt new file mode 100644 index 000000000..2710be0a5 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/387.txt @@ -0,0 +1,2 @@ +* Rework Login with certificate UI +* Add ability to pin chats on top (add to favorites) diff --git a/fastlane/metadata/android/gl-ES/changelogs/388.txt b/fastlane/metadata/android/gl-ES/changelogs/388.txt new file mode 100644 index 000000000..6a4909652 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/388.txt @@ -0,0 +1,3 @@ +* Reduce echo during calls on some devices +* Fix login when passwords contains special characters +* Play dial and busy tones on speaker during video calls diff --git a/fastlane/metadata/android/gl-ES/changelogs/390.txt b/fastlane/metadata/android/gl-ES/changelogs/390.txt new file mode 100644 index 000000000..56ed78885 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/390.txt @@ -0,0 +1 @@ +* Offer to record voice message when callee is busy diff --git a/fastlane/metadata/android/gl-ES/changelogs/393.txt b/fastlane/metadata/android/gl-ES/changelogs/393.txt new file mode 100644 index 000000000..82250ee87 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/393.txt @@ -0,0 +1,3 @@ +* Show help button if A/V call fails +* Fixed some annoying crashes +* Fixed Jingle connections (file transfer + calls) with bare JIDs diff --git a/fastlane/metadata/android/gl-ES/changelogs/394.txt b/fastlane/metadata/android/gl-ES/changelogs/394.txt new file mode 100644 index 000000000..b04adbd56 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/394.txt @@ -0,0 +1,2 @@ +* Fixed notifications not showing up under certain conditions +* Fixed compatibility issues and crashes related to A/V calls diff --git a/fastlane/metadata/android/gl-ES/changelogs/395.txt b/fastlane/metadata/android/gl-ES/changelogs/395.txt new file mode 100644 index 000000000..76a654338 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/395.txt @@ -0,0 +1,3 @@ +* add 'Return to chat' to audio call screen +* Improve keyboard shortcuts +* bug fixes diff --git a/fastlane/metadata/android/gl-ES/changelogs/397.txt b/fastlane/metadata/android/gl-ES/changelogs/397.txt new file mode 100644 index 000000000..207b36708 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/397.txt @@ -0,0 +1,3 @@ +* Handle GPX files +* Improve performance for backup restore +* bug fixes diff --git a/fastlane/metadata/android/gl-ES/changelogs/398.txt b/fastlane/metadata/android/gl-ES/changelogs/398.txt new file mode 100644 index 000000000..95280ea88 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/398.txt @@ -0,0 +1,4 @@ +* Search individual conversations +* Notify user if message delivery fails +* Remember display names (nicks) from Quicksy users across restarts +* Add button to start Orbot (Tor) from notification if necessary diff --git a/fastlane/metadata/android/gl-ES/changelogs/401.txt b/fastlane/metadata/android/gl-ES/changelogs/401.txt new file mode 100644 index 000000000..907063eb6 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/401.txt @@ -0,0 +1,2 @@ +* fixed search on Android <= 5 +* optimize memory consumption diff --git a/fastlane/metadata/android/gl-ES/changelogs/402.txt b/fastlane/metadata/android/gl-ES/changelogs/402.txt new file mode 100644 index 000000000..53f461756 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/402.txt @@ -0,0 +1,3 @@ +* Offer Easy Invite generation on supporting servers +* Display GIFs send from Movim +* store avatars in cache diff --git a/fastlane/metadata/android/gl-ES/changelogs/403.txt b/fastlane/metadata/android/gl-ES/changelogs/403.txt new file mode 100644 index 000000000..99d62ca48 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/403.txt @@ -0,0 +1,3 @@ +* Fixed connectivity issues when different accounts used different SCRAM mechanisms +* Add support for SCRAM-SHA-512 +* Allow P2P (Jingle) file transfer with self contact diff --git a/fastlane/metadata/android/gl-ES/changelogs/404.txt b/fastlane/metadata/android/gl-ES/changelogs/404.txt new file mode 100644 index 000000000..d4f2e7b6d --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/404.txt @@ -0,0 +1 @@ +* minor stability improvements for A/V calls diff --git a/fastlane/metadata/android/gl-ES/changelogs/405.txt b/fastlane/metadata/android/gl-ES/changelogs/405.txt new file mode 100644 index 000000000..e858b6cd1 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/405.txt @@ -0,0 +1 @@ +* Quicksy: Automatically receive verification SMS diff --git a/fastlane/metadata/android/gl-ES/changelogs/407.txt b/fastlane/metadata/android/gl-ES/changelogs/407.txt new file mode 100644 index 000000000..e746bc7d7 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/407.txt @@ -0,0 +1,3 @@ +* Show call button for offline contacts if they previously announced support +* Back button no longer ends call when call is connected +* bug fixes diff --git a/fastlane/metadata/android/gl-ES/changelogs/42000.txt b/fastlane/metadata/android/gl-ES/changelogs/42000.txt new file mode 100644 index 000000000..1ecfe204d --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42000.txt @@ -0,0 +1,4 @@ +* Ability to select incoming call ringtone +* Fix OpenPGP key id discovery for OpenKeychain 5.6+ +* Properly verify punycode TLS certificates +* Improve stability of RTP session establishment (calling) diff --git a/fastlane/metadata/android/gl-ES/changelogs/42006.txt b/fastlane/metadata/android/gl-ES/changelogs/42006.txt new file mode 100644 index 000000000..91e2b904f --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42006.txt @@ -0,0 +1,2 @@ +* Verify A/V calls with preexisting OMEMO sessions +* Improve compatibility with non libwebrtc WebRTC implementations diff --git a/fastlane/metadata/android/gl-ES/changelogs/42010.txt b/fastlane/metadata/android/gl-ES/changelogs/42010.txt new file mode 100644 index 000000000..3a1c234c1 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42010.txt @@ -0,0 +1,2 @@ +* Various bug fixes around Tor support +* Improve call compatibility with Dino diff --git a/fastlane/metadata/android/gl-ES/changelogs/42012.txt b/fastlane/metadata/android/gl-ES/changelogs/42012.txt new file mode 100644 index 000000000..967fae964 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42012.txt @@ -0,0 +1 @@ +* fix HTTP up/download for users that don’t trust system CAs diff --git a/fastlane/metadata/android/gl-ES/changelogs/42013.txt b/fastlane/metadata/android/gl-ES/changelogs/42013.txt new file mode 100644 index 000000000..8749f0a0f --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42013.txt @@ -0,0 +1 @@ +* Fixed 'No Connectivity' issues on Android 7.1 diff --git a/fastlane/metadata/android/gl-ES/changelogs/42014.txt b/fastlane/metadata/android/gl-ES/changelogs/42014.txt new file mode 100644 index 000000000..8ae96511e --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42014.txt @@ -0,0 +1,2 @@ +* Always verify domain name. No user overwrite +* Support roster pre authentication diff --git a/fastlane/metadata/android/gl-ES/changelogs/42015.txt b/fastlane/metadata/android/gl-ES/changelogs/42015.txt new file mode 100644 index 000000000..1980efb2a --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42015.txt @@ -0,0 +1 @@ +* minor A/V improvements diff --git a/fastlane/metadata/android/gl-ES/changelogs/42018.txt b/fastlane/metadata/android/gl-ES/changelogs/42018.txt new file mode 100644 index 000000000..8f4d66caa --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42018.txt @@ -0,0 +1,3 @@ +* Show black bars when remote video does not match aspect ratio of screen +* Improve search performance +* Add setting to prevent screenshots diff --git a/fastlane/metadata/android/gl-ES/changelogs/42022.txt b/fastlane/metadata/android/gl-ES/changelogs/42022.txt new file mode 100644 index 000000000..eaaa190fa --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42022.txt @@ -0,0 +1,2 @@ +* Fix issue with some videos not being compressed +* Fix rare crash when opening notification diff --git a/fastlane/metadata/android/gl-ES/changelogs/42023.txt b/fastlane/metadata/android/gl-ES/changelogs/42023.txt new file mode 100644 index 000000000..ed3c25380 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42023.txt @@ -0,0 +1,2 @@ +* Fix crash when rendering some quotes +* Fix crash in welcome screen diff --git a/fastlane/metadata/android/gl-ES/changelogs/42037.txt b/fastlane/metadata/android/gl-ES/changelogs/42037.txt new file mode 100644 index 000000000..375905aa8 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42037.txt @@ -0,0 +1,11 @@ +Version 2.10.9 +* Ask for Bluetooth permissions when making A/V calls (You can reject this if you don’t use Bluetooth headsets) +* Fix bug when calling Movim +* Fix wrong avatar being shown for group chats +* Always ask for battery optimizations opt-out +* Set local only flag on 'x connected accounts' notifications +* Fix interaction with Google Maps Share Location Plugin +* Remove footnote with regards to server fee +* Store files in location appropriate for Android 11 +* Attempt to reconnect call after network switch +* Show caller JID and account JID in incoming call screen diff --git a/fastlane/metadata/android/gl-ES/changelogs/42038.txt b/fastlane/metadata/android/gl-ES/changelogs/42038.txt new file mode 100644 index 000000000..71373032f --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42038.txt @@ -0,0 +1,2 @@ +* Arranxos menores +* Restablecida a posibilidade de chamar vía JMP e outros servizos (versión Playstore) diff --git a/fastlane/metadata/android/gl-ES/changelogs/42041.txt b/fastlane/metadata/android/gl-ES/changelogs/42041.txt new file mode 100644 index 000000000..37079b4f7 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42041.txt @@ -0,0 +1,5 @@ +* Implementamos Extensible SASL Profile, Bind 2.0 e Fast para reconectar máis rápidamente +* Implementamos Channel Binding +* Engadimos a posibilidade de pasar de chamada de audio a chamada de vídeo +* Podes eliminar o teu propio avatar +* Engadida notificación de chamada perdida diff --git a/fastlane/metadata/android/gl-ES/changelogs/42042.txt b/fastlane/metadata/android/gl-ES/changelogs/42042.txt new file mode 100644 index 000000000..520578662 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42042.txt @@ -0,0 +1,2 @@ +* Arranxo do reenvío contínuo en servidores que só teñen soporte sm:2 +* Mostrar 'Cambiar a vídeo' só se a outra parte tamén soporta chamada de vídeo diff --git a/fastlane/metadata/android/gl-ES/changelogs/42043.txt b/fastlane/metadata/android/gl-ES/changelogs/42043.txt new file mode 100644 index 000000000..91937ba45 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42043.txt @@ -0,0 +1 @@ +* Arranxo da regresión na transferencia de ficheiros con P2P diff --git a/fastlane/metadata/android/gl-ES/changelogs/42044.txt b/fastlane/metadata/android/gl-ES/changelogs/42044.txt new file mode 100644 index 000000000..b6df29794 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42044.txt @@ -0,0 +1,3 @@ +* Arranxo do reenvío usando SASL2 +* Arranxo dos vídeos en negro nalgúns dispositivos +* Arranxo do fallo ao usar un contrasinal baleiro diff --git a/fastlane/metadata/android/gl-ES/changelogs/42046.txt b/fastlane/metadata/android/gl-ES/changelogs/42046.txt new file mode 100644 index 000000000..ec1c1ca76 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42046.txt @@ -0,0 +1 @@ +* Integrar UnifiedPush Distributor para facilitar a entrega de mensaxes push a outras apps con UnifiedPush activado como Tusky e Fedilab diff --git a/fastlane/metadata/android/gl-ES/changelogs/42047.txt b/fastlane/metadata/android/gl-ES/changelogs/42047.txt new file mode 100644 index 000000000..359f0e958 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42047.txt @@ -0,0 +1 @@ +* Arranxar o fallo en UnifiedPush Distributor diff --git a/fastlane/metadata/android/gl-ES/changelogs/42050.txt b/fastlane/metadata/android/gl-ES/changelogs/42050.txt new file mode 100644 index 000000000..40a774b21 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42050.txt @@ -0,0 +1 @@ +* Aumentar o radio dos cantos nas imaxes de perfil diff --git a/fastlane/metadata/android/gl-ES/changelogs/42059.txt b/fastlane/metadata/android/gl-ES/changelogs/42059.txt new file mode 100644 index 000000000..44356c7e7 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42059.txt @@ -0,0 +1,2 @@ +* Establecer o Target SDK ao 33 de novo +* Arranxar problemas cos servidores con soporte SASL2 sen Stream Management en liña diff --git a/fastlane/metadata/android/gl-ES/changelogs/42060.txt b/fastlane/metadata/android/gl-ES/changelogs/42060.txt new file mode 100644 index 000000000..b79b6c0a7 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42060.txt @@ -0,0 +1 @@ +* Arranxa o problema de considerar o 'q' como cirílico diff --git a/fastlane/metadata/android/gl-ES/changelogs/42061.txt b/fastlane/metadata/android/gl-ES/changelogs/42061.txt new file mode 100644 index 000000000..3173f43f4 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42061.txt @@ -0,0 +1 @@ +* Retira, da versión Google Play, a ferramenta de descubrimento de canles diff --git a/fastlane/metadata/android/gl-ES/changelogs/42062.txt b/fastlane/metadata/android/gl-ES/changelogs/42062.txt new file mode 100644 index 000000000..4d9358f7d --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42062.txt @@ -0,0 +1 @@ +* Desactiva a apertura de ficheiros de copia de apoio (.ceb) desde o xestor de ficheiros diff --git a/fastlane/metadata/android/gl-ES/changelogs/42065.txt b/fastlane/metadata/android/gl-ES/changelogs/42065.txt new file mode 100644 index 000000000..ce952a317 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42065.txt @@ -0,0 +1 @@ +* Presenta o novo formato para as copias de apoio diff --git a/fastlane/metadata/android/gl-ES/changelogs/42068.txt b/fastlane/metadata/android/gl-ES/changelogs/42068.txt new file mode 100644 index 000000000..49d898d51 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42068.txt @@ -0,0 +1,2 @@ +* soporte para os axustes das notificacións por conversa +* usar opus para as mensaxes de voz en Android 10 diff --git a/fastlane/metadata/android/gl-ES/changelogs/42072.txt b/fastlane/metadata/android/gl-ES/changelogs/42072.txt new file mode 100644 index 000000000..534c20ed1 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42072.txt @@ -0,0 +1,3 @@ +*Subir a dependencia libwebrtc a M117 e tamén libvpx +* Volver a AAC para as mensaxes de voz +* Soporte para indicar na app os axustes do idioma diff --git a/fastlane/metadata/android/gl-ES/changelogs/42074.txt b/fastlane/metadata/android/gl-ES/changelogs/42074.txt new file mode 100644 index 000000000..6729e251d --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/42074.txt @@ -0,0 +1,3 @@ +* Soport para DNS Privado (DNS sobre TLS) +* Soporte para personalizar a icona de inicio +* Arranxamos un raro problema de permisos ao compartir ficheiros en Android 11+ diff --git a/fastlane/metadata/android/gl-ES/changelogs/4207704.txt b/fastlane/metadata/android/gl-ES/changelogs/4207704.txt new file mode 100644 index 000000000..f09552438 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4207704.txt @@ -0,0 +1,3 @@ +* Soporte para Private DNS (DNS sobre TLS) +* Soporte para decorar a icona no iniciador +* Arranxo dun problema pouco común de permisos ao compartir ficheiros en Android 11+ diff --git a/fastlane/metadata/android/gl-ES/changelogs/4208104.txt b/fastlane/metadata/android/gl-ES/changelogs/4208104.txt new file mode 100644 index 000000000..f4532b679 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4208104.txt @@ -0,0 +1,4 @@ +* Acceso mais rápido á 'Mostrar código QR' +* Soporte para a PEP Marcadores Nativos +* Engadido soporte para SDP Offer / Answer Model (usado por pasarelas SIP) +* Establecida a API de Android 14 como obxectivo diff --git a/fastlane/metadata/android/it-IT/changelogs/349.txt b/fastlane/metadata/android/it-IT/changelogs/349.txt new file mode 100644 index 000000000..170eda951 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/349.txt @@ -0,0 +1,4 @@ +* Introdotta l'impostazione per esperti per eseguire la ricerca dei canali sul server locale invece che su search.jabber.network +* Attivati i segni di spunta per la consegna in modo predefinito e rimossa l'impostazione +* Attivato "Il pulsante di invio indica lo stato" in modo predefinito e rimossa l'impostazione +* Spostate le impostazioni del servizio di backup e di primo piano nella schermata principale diff --git a/fastlane/metadata/android/it-IT/changelogs/351.txt b/fastlane/metadata/android/it-IT/changelogs/351.txt new file mode 100644 index 000000000..3c3b94459 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/351.txt @@ -0,0 +1,3 @@ +* Correzioni per il trasferimento di file Jingle IBB +* Risolte le correzioni ripetute che riempivano il database +* Transizione a Last Message Correction v1.1 diff --git a/fastlane/metadata/android/it-IT/changelogs/353.txt b/fastlane/metadata/android/it-IT/changelogs/353.txt new file mode 100644 index 000000000..dc3a5160d --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/353.txt @@ -0,0 +1,4 @@ +* Consente agli utenti di impostare il proprio nick name +* Riprende il download dei file criptati OMEMO +* I canali ora usano '#' come simbolo nell'avatar. +* Quicksy imposta "sempre" come crittografia OMEMO in modo predefinito (nasconde l'icona del lucchetto) diff --git a/fastlane/metadata/android/it-IT/changelogs/360.txt b/fastlane/metadata/android/it-IT/changelogs/360.txt new file mode 100644 index 000000000..82fbedd2d --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/360.txt @@ -0,0 +1 @@ +* Supporto per i parametri uri XMPP ?register e ?register;preauth diff --git a/fastlane/metadata/android/it-IT/changelogs/362.txt b/fastlane/metadata/android/it-IT/changelogs/362.txt new file mode 100644 index 000000000..c276d02c2 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/362.txt @@ -0,0 +1 @@ +* Supporto del cambio automatico dei temi su Android 10 diff --git a/fastlane/metadata/android/it-IT/changelogs/364.txt b/fastlane/metadata/android/it-IT/changelogs/364.txt new file mode 100644 index 000000000..7b461a350 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/364.txt @@ -0,0 +1,2 @@ +* Fornisce l'anteprima PDF su Android 5+ +* Utilizzo di IVs a 12 byte per OMEMO diff --git a/fastlane/metadata/android/it-IT/changelogs/367.txt b/fastlane/metadata/android/it-IT/changelogs/367.txt new file mode 100644 index 000000000..89239a58f --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/367.txt @@ -0,0 +1,2 @@ +* Corretta la selezione dell'avatar su alcuni dispositivi Android 10 +* Corretto il trasferimento di file più grandi diff --git a/fastlane/metadata/android/it-IT/changelogs/379.txt b/fastlane/metadata/android/it-IT/changelogs/379.txt new file mode 100644 index 000000000..fe27f8ec6 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/379.txt @@ -0,0 +1 @@ +* Chiamate audio/video (richiede il supporto di server sotto forma di server STUN e TURN rilevabili tramite XEP-0215) diff --git a/fastlane/metadata/android/it-IT/changelogs/381.txt b/fastlane/metadata/android/it-IT/changelogs/381.txt new file mode 100644 index 000000000..39bd79c54 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/381.txt @@ -0,0 +1,2 @@ +* Feedback acustico (composizione, inizio e fine chiamata) per le chiamate vocali. +* Risolto un problema con la ripetizione di una videochiamata fallita diff --git a/fastlane/metadata/android/it-IT/changelogs/382.txt b/fastlane/metadata/android/it-IT/changelogs/382.txt new file mode 100644 index 000000000..8a8bd6863 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/382.txt @@ -0,0 +1,2 @@ +* Aggiunto un pulsante per cambiare telecamera durante la videochiamata +* Corrette le chiamate vocali sui tablet diff --git a/fastlane/metadata/android/it-IT/changelogs/383.txt b/fastlane/metadata/android/it-IT/changelogs/383.txt new file mode 100644 index 000000000..32d16941b --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/383.txt @@ -0,0 +1,3 @@ +* Spostata l'icona della chiamata a sinistra per mantenere le altre icone della barra degli strumenti in una posizione coerente +* Mostra la durata della chiamata durante le chiamate audio +* Interruzione della parità per le chiamate A/V (due persone che si chiamano contemporaneamente) diff --git a/fastlane/metadata/android/it-IT/changelogs/387.txt b/fastlane/metadata/android/it-IT/changelogs/387.txt new file mode 100644 index 000000000..e78cea033 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/387.txt @@ -0,0 +1,2 @@ +* Ristrutturata l'interfaccia utente per l'accesso con certificato +* Aggiunta la possibilità di fissare le chat in alto (aggiungi ai preferiti) diff --git a/fastlane/metadata/android/it-IT/changelogs/388.txt b/fastlane/metadata/android/it-IT/changelogs/388.txt new file mode 100644 index 000000000..20b0c18fa --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/388.txt @@ -0,0 +1,3 @@ +* Riduzione dell'eco durante le chiamate su alcuni dispositivi +* Corretto l'accesso quando le password contengono caratteri speciali +* Riproduzione dei toni di chiamata e di occupato sull'altoparlante durante le videochiamate diff --git a/fastlane/metadata/android/it-IT/changelogs/390.txt b/fastlane/metadata/android/it-IT/changelogs/390.txt new file mode 100644 index 000000000..35b00a97d --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/390.txt @@ -0,0 +1 @@ +* Offerta di registrazione del messaggio vocale quando il chiamante è occupato diff --git a/fastlane/metadata/android/it-IT/changelogs/393.txt b/fastlane/metadata/android/it-IT/changelogs/393.txt new file mode 100644 index 000000000..3238e8910 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/393.txt @@ -0,0 +1,3 @@ +* Mostra il pulsante di aiuto se la chiamata A/V fallisce +* Risolti alcuni fastidiosi arresti anomali +* Corrette le connessioni Jingle (trasferimento di file + chiamate) con JID nudi diff --git a/fastlane/metadata/android/it-IT/changelogs/394.txt b/fastlane/metadata/android/it-IT/changelogs/394.txt new file mode 100644 index 000000000..d94866c6d --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/394.txt @@ -0,0 +1,2 @@ +* Corrette le notifiche che non vengono visualizzate in determinate condizioni +* Corretti i problemi di compatibilità e gli arresti anomali relativi alle chiamate A/V diff --git a/fastlane/metadata/android/it-IT/changelogs/395.txt b/fastlane/metadata/android/it-IT/changelogs/395.txt new file mode 100644 index 000000000..264021fd3 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/395.txt @@ -0,0 +1,3 @@ +* Aggiunta la funzione "Torna alla chat" alla schermata delle chiamate audio +* Migliorate le scorciatoie da tastiera +* Correzioni di errori diff --git a/fastlane/metadata/android/it-IT/changelogs/397.txt b/fastlane/metadata/android/it-IT/changelogs/397.txt new file mode 100644 index 000000000..295266e01 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/397.txt @@ -0,0 +1,3 @@ +* Gestisce i file GPX +* Migliorate le prestazioni per il ripristino dei backup +* Correzioni di errori diff --git a/fastlane/metadata/android/it-IT/changelogs/398.txt b/fastlane/metadata/android/it-IT/changelogs/398.txt new file mode 100644 index 000000000..5efa2686e --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/398.txt @@ -0,0 +1,4 @@ +* Ricerca di conversazioni individuali +* Notifica all'utente se la consegna del messaggio fallisce +* Ricorda i nomi visualizzati (nick) degli utenti di Quicksy durante i vari riavvii +* Aggiunto un pulsante per avviare Orbot (Tor) dalla notifica, se necessario diff --git a/fastlane/metadata/android/it-IT/changelogs/401.txt b/fastlane/metadata/android/it-IT/changelogs/401.txt new file mode 100644 index 000000000..fbcf70c2a --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/401.txt @@ -0,0 +1,2 @@ +* Corretta la ricerca su Android <= 5 +* Ottimizzato il consumo di memoria diff --git a/fastlane/metadata/android/it-IT/changelogs/402.txt b/fastlane/metadata/android/it-IT/changelogs/402.txt new file mode 100644 index 000000000..e4a1a9fa0 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/402.txt @@ -0,0 +1,3 @@ +* Offre la generazione di inviti facili sui server di supporto +* Visualizzazione delle GIF inviate da Movim +* Memorizzazione degli avatar nella cache diff --git a/fastlane/metadata/android/it-IT/changelogs/403.txt b/fastlane/metadata/android/it-IT/changelogs/403.txt new file mode 100644 index 000000000..c0ee371d8 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/403.txt @@ -0,0 +1,3 @@ +* Corretti i problemi di connettività quando profili diversi usavano meccanismi SCRAM diversi +* Aggiunto il supporto per SCRAM-SHA-512 +* Consente il trasferimento di file P2P (Jingle) con l'auto contatto diff --git a/fastlane/metadata/android/it-IT/changelogs/404.txt b/fastlane/metadata/android/it-IT/changelogs/404.txt new file mode 100644 index 000000000..6346ddb12 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/404.txt @@ -0,0 +1 @@ +* Miglioramenti di stabilità minori per le chiamate A/V diff --git a/fastlane/metadata/android/it-IT/changelogs/405.txt b/fastlane/metadata/android/it-IT/changelogs/405.txt new file mode 100644 index 000000000..7325ea2a9 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/405.txt @@ -0,0 +1 @@ +* Quicksy: ricevi automaticamente SMS di verifica diff --git a/fastlane/metadata/android/it-IT/changelogs/407.txt b/fastlane/metadata/android/it-IT/changelogs/407.txt new file mode 100644 index 000000000..2ba4dd4fd --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/407.txt @@ -0,0 +1,3 @@ +* Mostra il pulsante di chiamata per i contatti offline se hanno precedentemente annunciato il supporto +* Il pulsante Indietro non termina più la chiamata quando questa è connessa +* Correzioni di errori diff --git a/fastlane/metadata/android/it-IT/changelogs/42000.txt b/fastlane/metadata/android/it-IT/changelogs/42000.txt new file mode 100644 index 000000000..5697c2ed2 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42000.txt @@ -0,0 +1,4 @@ +* Possibilità di selezionare la suoneria delle chiamate in arrivo +* Correzione del rilevamento dell'id della chiave OpenPGP per OpenKeychain 5.6+ +* Verifica corretta dei certificati TLS con codice punycode +* Miglioramento della stabilità della creazione di sessioni RTP (chiamate) diff --git a/fastlane/metadata/android/it-IT/changelogs/42006.txt b/fastlane/metadata/android/it-IT/changelogs/42006.txt new file mode 100644 index 000000000..dda32ef6a --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42006.txt @@ -0,0 +1,2 @@ +* Verifica delle chiamate A/V con le sessioni OMEMO preesistenti +* Miglioramento della compatibilità con le implementazioni WebRTC non libwebrtc diff --git a/fastlane/metadata/android/it-IT/changelogs/42010.txt b/fastlane/metadata/android/it-IT/changelogs/42010.txt new file mode 100644 index 000000000..2140a3a44 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42010.txt @@ -0,0 +1,2 @@ +* Correzione di vari errori relativi al supporto di Tor +* Migliorata la compatibilità delle chiamate con Dino diff --git a/fastlane/metadata/android/it-IT/changelogs/42012.txt b/fastlane/metadata/android/it-IT/changelogs/42012.txt new file mode 100644 index 000000000..9da7d48df --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42012.txt @@ -0,0 +1 @@ +* Corretto l'up/download HTTP per gli utenti che non si fidano delle CA di sistema diff --git a/fastlane/metadata/android/it-IT/changelogs/42013.txt b/fastlane/metadata/android/it-IT/changelogs/42013.txt new file mode 100644 index 000000000..f58811956 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42013.txt @@ -0,0 +1 @@ +* Risolti i problemi di "assenza di connettività" su Android 7.1 diff --git a/fastlane/metadata/android/it-IT/changelogs/42014.txt b/fastlane/metadata/android/it-IT/changelogs/42014.txt new file mode 100644 index 000000000..95c2355ff --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42014.txt @@ -0,0 +1,2 @@ +* Verifica sempre il nome del dominio. Nessuna sovrascrittura dell'utente +* Supporto della pre-autenticazione del roster diff --git a/fastlane/metadata/android/it-IT/changelogs/42015.txt b/fastlane/metadata/android/it-IT/changelogs/42015.txt new file mode 100644 index 000000000..b296f82ca --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42015.txt @@ -0,0 +1 @@ +* Miglioramenti A/V minori diff --git a/fastlane/metadata/android/it-IT/changelogs/42018.txt b/fastlane/metadata/android/it-IT/changelogs/42018.txt new file mode 100644 index 000000000..a6eac7a8f --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42018.txt @@ -0,0 +1,3 @@ +* Mostra barre nere quando il video remoto non corrisponde alle proporzioni dello schermo +* Migliorate le prestazioni della ricerca +* Aggiunta un'impostazione per impedire gli screenshot diff --git a/fastlane/metadata/android/it-IT/changelogs/42022.txt b/fastlane/metadata/android/it-IT/changelogs/42022.txt new file mode 100644 index 000000000..1ff223875 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42022.txt @@ -0,0 +1,2 @@ +* Corretto il problema di alcuni video che non vengono compressi +* Corretti rari arresti anomali all'apertura delle notifiche diff --git a/fastlane/metadata/android/it-IT/changelogs/42023.txt b/fastlane/metadata/android/it-IT/changelogs/42023.txt new file mode 100644 index 000000000..a75ef6f39 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42023.txt @@ -0,0 +1,2 @@ +* Corretto l'arresto anomalo durante il rendering di alcune citazioni +* Corretto l'arresto anomalo nella schermata di benvenuto diff --git a/fastlane/metadata/android/it-IT/changelogs/42037.txt b/fastlane/metadata/android/it-IT/changelogs/42037.txt new file mode 100644 index 000000000..9b2cf0e25 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42037.txt @@ -0,0 +1,11 @@ +2.10.9 +* Permessi Bluetooth per chiamate A/V (puoi rifiutare) +* Correto bug chiamando Movim +* Corretti avatar nelle chat di gruppo +* Chiedi sempre opt-out di ottimizzazione batteria +* Flag solo locale su notifiche "x profili connessi" +* Corretta interazione con plugin di condivisione posizione Google Maps +* Rimossa nota del costo del server +* Archivia i file nel posto giusto su Android 11 +* Ricollega chiamata dopo il cambio di rete +* Mostra JID chiamante e JID profilo nelle chiamate in entrata diff --git a/fastlane/metadata/android/it-IT/changelogs/42038.txt b/fastlane/metadata/android/it-IT/changelogs/42038.txt new file mode 100644 index 000000000..04af5aca6 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42038.txt @@ -0,0 +1,2 @@ +* Correzione di errori minori +* Ripristino della possibilità di richiamare tramite JMP e altri servizi (versione Playstore) diff --git a/fastlane/metadata/android/it-IT/changelogs/42041.txt b/fastlane/metadata/android/it-IT/changelogs/42041.txt new file mode 100644 index 000000000..2331de300 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42041.txt @@ -0,0 +1,5 @@ +* Implementato il profilo SASL estensibile, Bind 2.0 e Fast per riconnettersi più velocemente +* Implementato il Channel Binding +* Aggiunta la possibilità di passare da una chiamata audio a una videochiamata +* Aggiunta la possibilità di cancellare il proprio avatar +* Aggiunta la notifica per le chiamate perse diff --git a/fastlane/metadata/android/it-IT/changelogs/42042.txt b/fastlane/metadata/android/it-IT/changelogs/42042.txt new file mode 100644 index 000000000..3bc5bb2b7 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42042.txt @@ -0,0 +1,2 @@ +* Corretto un loop di reinvio sui server che supportano solo sm:2 +* Mostra 'Passa al video' solo se l'altra parte supporta il video diff --git a/fastlane/metadata/android/it-IT/changelogs/42043.txt b/fastlane/metadata/android/it-IT/changelogs/42043.txt new file mode 100644 index 000000000..95b94c630 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42043.txt @@ -0,0 +1 @@ +* Corretta regressione nel trasferimento di file P2P diff --git a/fastlane/metadata/android/it-IT/changelogs/42044.txt b/fastlane/metadata/android/it-IT/changelogs/42044.txt new file mode 100644 index 000000000..8354cbf1e --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42044.txt @@ -0,0 +1,3 @@ +* Corretto il reinvio dei messaggi quando si usa SASL2 +* Corretto il video nero tra alcuni dispositivi +* Corretto l'arresto anomalo delle password vuote diff --git a/fastlane/metadata/android/it-IT/changelogs/42046.txt b/fastlane/metadata/android/it-IT/changelogs/42046.txt new file mode 100644 index 000000000..e8a8d3d2e --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42046.txt @@ -0,0 +1 @@ +* Integra il distributore UnifiedPush per facilitare i messaggi push ad altre app abilitate a UnifiedPush come Tusky e Fedilab diff --git a/fastlane/metadata/android/it-IT/changelogs/42047.txt b/fastlane/metadata/android/it-IT/changelogs/42047.txt new file mode 100644 index 000000000..abc0c9e6c --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42047.txt @@ -0,0 +1 @@ +* Corretto l'arresto anomalo del distributore UnifiedPush diff --git a/fastlane/metadata/android/it-IT/changelogs/42050.txt b/fastlane/metadata/android/it-IT/changelogs/42050.txt new file mode 100644 index 000000000..071ad6d75 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42050.txt @@ -0,0 +1 @@ +* Aumenta il raggio degli angoli nelle immagini del profilo diff --git a/fastlane/metadata/android/it-IT/changelogs/42059.txt b/fastlane/metadata/android/it-IT/changelogs/42059.txt new file mode 100644 index 000000000..405597cd0 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42059.txt @@ -0,0 +1,2 @@ +* Aggiornato l'SDK di destinazione di nuovo alla versione 33 +* Corretti problemi sui server che supportano SASL2 senza gestione inline dei flussi diff --git a/fastlane/metadata/android/it-IT/changelogs/42060.txt b/fastlane/metadata/android/it-IT/changelogs/42060.txt new file mode 100644 index 000000000..14a8a6a1f --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42060.txt @@ -0,0 +1 @@ +* Corretta la 'q' che viene erroneamente riconosciuta come cirillico diff --git a/fastlane/metadata/android/it-IT/changelogs/42061.txt b/fastlane/metadata/android/it-IT/changelogs/42061.txt new file mode 100644 index 000000000..00af956c5 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42061.txt @@ -0,0 +1 @@ +* Rimossa la funzione di scoperta dei canali dalla versione di Google Play diff --git a/fastlane/metadata/android/it-IT/changelogs/42062.txt b/fastlane/metadata/android/it-IT/changelogs/42062.txt new file mode 100644 index 000000000..c2d024121 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42062.txt @@ -0,0 +1 @@ +* Disattiva l'apertura dei file di backup (.ceb) dal file manager diff --git a/fastlane/metadata/android/it-IT/changelogs/42065.txt b/fastlane/metadata/android/it-IT/changelogs/42065.txt new file mode 100644 index 000000000..ae3fca640 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42065.txt @@ -0,0 +1 @@ +* Introdotto un nuovo formato di file di backup diff --git a/fastlane/metadata/android/it-IT/changelogs/42068.txt b/fastlane/metadata/android/it-IT/changelogs/42068.txt new file mode 100644 index 000000000..801dbb015 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42068.txt @@ -0,0 +1,2 @@ +* supporta impostazioni di notifica per singola conversazione +* usa opus per i messaggi vocali su Android 10 diff --git a/fastlane/metadata/android/it-IT/changelogs/42072.txt b/fastlane/metadata/android/it-IT/changelogs/42072.txt new file mode 100644 index 000000000..c497edc32 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42072.txt @@ -0,0 +1,3 @@ +* Aggiornata la dipendenza libwebrtc a M117 e libvpx +* Ritorno a AAC per i messaggi vocali +* Supporta impostazioni di lingua per app diff --git a/fastlane/metadata/android/it-IT/changelogs/42074.txt b/fastlane/metadata/android/it-IT/changelogs/42074.txt new file mode 100644 index 000000000..675715118 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/42074.txt @@ -0,0 +1,3 @@ +* Supporto per DNS Privato (DNS over TLS) +* Supporto per icona del launcher a tema +* Risolto un raro problema di autorizzazione durante la condivisione di file su Android 11+ diff --git a/fastlane/metadata/android/it-IT/changelogs/4207704.txt b/fastlane/metadata/android/it-IT/changelogs/4207704.txt new file mode 100644 index 000000000..675715118 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4207704.txt @@ -0,0 +1,3 @@ +* Supporto per DNS Privato (DNS over TLS) +* Supporto per icona del launcher a tema +* Risolto un raro problema di autorizzazione durante la condivisione di file su Android 11+ diff --git a/fastlane/metadata/android/it-IT/changelogs/4208104.txt b/fastlane/metadata/android/it-IT/changelogs/4208104.txt new file mode 100644 index 000000000..00be75ede --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4208104.txt @@ -0,0 +1,4 @@ +* Accesso più facile a 'Mostra codice QR' +* Supporto per PEP Native Bookmarks +* Aggiunto supporto per il modello Offerta / Risposta SDP (usato dai gateway SIP) +* Aumentata l'API di destinazione ad Android 14 diff --git a/fastlane/metadata/android/ro/changelogs/349.txt b/fastlane/metadata/android/ro/changelogs/349.txt new file mode 100644 index 000000000..0f1ea5501 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/349.txt @@ -0,0 +1,4 @@ +* Introducerea setărilor pentru experți pentru a efectua descoperirea canalelor pe serverul local în loc de search.jabber.network +* Activarea marcajelor de verificare a livrării în mod implicit și eliminarea setării +* Activarea "Butonul de trimitere indică starea" în mod implicit și eliminarea setării +*Mutarea setărilor Serviciului de rezervă și ale Serviciului de prim-plan în ecranul principal diff --git a/fastlane/metadata/android/ro/changelogs/351.txt b/fastlane/metadata/android/ro/changelogs/351.txt new file mode 100644 index 000000000..629d86109 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/351.txt @@ -0,0 +1,3 @@ +* reparații pentru transferul de fișiere Jingle IBB +* reparații pentru corecțiile repetate care umplu baza de date +* schimbarea la Corectarea Ultimului Mesaj v1.1 diff --git a/fastlane/metadata/android/ro/changelogs/353.txt b/fastlane/metadata/android/ro/changelogs/353.txt new file mode 100644 index 000000000..1c788dab6 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/353.txt @@ -0,0 +1,4 @@ +* utilizatorii pot să își seteze propria poreclă +* continuarea descărcării de fișiere criptate OMEMO +* Canalele folosesc '#' ca simbol în avatar +* Quicksy folosește 'mereu' ca și criptare implicită OMEMO (ascunde iconița lacăt) diff --git a/fastlane/metadata/android/ro/changelogs/360.txt b/fastlane/metadata/android/ro/changelogs/360.txt new file mode 100644 index 000000000..7949e0d14 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/360.txt @@ -0,0 +1 @@ +* Suport pentru parametrii uri ?register și ?register;preauth XMPP diff --git a/fastlane/metadata/android/ro/changelogs/362.txt b/fastlane/metadata/android/ro/changelogs/362.txt new file mode 100644 index 000000000..46a802c73 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/362.txt @@ -0,0 +1 @@ +* Suport pentru comutarea automată a temei pe Android 10 diff --git a/fastlane/metadata/android/ro/changelogs/364.txt b/fastlane/metadata/android/ro/changelogs/364.txt new file mode 100644 index 000000000..95ea86302 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/364.txt @@ -0,0 +1,2 @@ +* Furnizarea de previzualizări PDF pe Android 5+ +* Folosirea IV-urilor de 12 biți pentru OMEMO diff --git a/fastlane/metadata/android/ro/changelogs/367.txt b/fastlane/metadata/android/ro/changelogs/367.txt new file mode 100644 index 000000000..9575f811d --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/367.txt @@ -0,0 +1,2 @@ +* Repararea selecției de avatar pe unele dispozitive ce rulează Android 10 +* Repararea transferului de fișiere pentru fișiere mari diff --git a/fastlane/metadata/android/ro/changelogs/379.txt b/fastlane/metadata/android/ro/changelogs/379.txt new file mode 100644 index 000000000..7bbc1c965 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/379.txt @@ -0,0 +1 @@ +* Apeluri Audio/Video (Necesită suport pe server în formă de servere STUN și TURN descoperibile prin XEP-0125) diff --git a/fastlane/metadata/android/ro/changelogs/381.txt b/fastlane/metadata/android/ro/changelogs/381.txt new file mode 100644 index 000000000..9d8e76ba3 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/381.txt @@ -0,0 +1,2 @@ +* Feedback auditoriu (apelare, apel început, apel terminat) pentru apeluri vocale +* Problemă rezolvată cu reîncercarea apelului video eșuat diff --git a/fastlane/metadata/android/ro/changelogs/382.txt b/fastlane/metadata/android/ro/changelogs/382.txt new file mode 100644 index 000000000..f9eb35219 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/382.txt @@ -0,0 +1,2 @@ +* Adăugarea butonului pentru a schimba camera în timpul apelului video +* Repararea apelurilor voce pe tablete diff --git a/fastlane/metadata/android/ro/changelogs/383.txt b/fastlane/metadata/android/ro/changelogs/383.txt new file mode 100644 index 000000000..ee79d607e --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/383.txt @@ -0,0 +1,3 @@ +* Mutarea iconiței de apel către stânga pentru a ține celelalte iconițe din bara de instrumente într-un loc consistent +* Afișarea durației apelurilor în timpul apelurilor audio +* Ruperea egalității pentru apeluri audio/video (aceleași două persoane care se sună între ele în același timp) diff --git a/fastlane/metadata/android/ro/changelogs/387.txt b/fastlane/metadata/android/ro/changelogs/387.txt new file mode 100644 index 000000000..4faeea6b1 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/387.txt @@ -0,0 +1,2 @@ +* Refacerea logării cu UI pentru certificate +* Adăugarea abilității de a fixa conversații sus (adăugarea la favorite) diff --git a/fastlane/metadata/android/ro/changelogs/388.txt b/fastlane/metadata/android/ro/changelogs/388.txt new file mode 100644 index 000000000..0fbd0d92e --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/388.txt @@ -0,0 +1,3 @@ +* Reducerea ecoului în timpul apelurilor pe unele dispozitive +* Repararea logării când parolele conțin caractere speciale +* Redarea tonurilor de apel și ocupat pe difuzor în timpul apelurilor video diff --git a/fastlane/metadata/android/ro/changelogs/390.txt b/fastlane/metadata/android/ro/changelogs/390.txt new file mode 100644 index 000000000..8ae2da5cb --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/390.txt @@ -0,0 +1 @@ +* Oferirea de a înregistra mesaj vocal când persoana apelată este ocupată diff --git a/fastlane/metadata/android/ro/changelogs/393.txt b/fastlane/metadata/android/ro/changelogs/393.txt new file mode 100644 index 000000000..712b8e420 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/393.txt @@ -0,0 +1,3 @@ +* Afișarea butonului de ajutor dacă apelul audio/video eșuează +* Repararea unor crash-uri enervante +* Repararea conexiunilor Jingle (transfer fișiere + apeluri) cu JID-uri goale diff --git a/fastlane/metadata/android/uk/changelogs/349.txt b/fastlane/metadata/android/uk/changelogs/349.txt new file mode 100644 index 000000000..03f0b8764 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/349.txt @@ -0,0 +1,4 @@ +* Додано Експертні налаштування для пошуку каналів на локальному сервері замість search.jabber.network +* Позначки про доставку увімкнено за замовчуванням, а налаштування видалено +* «Кнопка надсилання показує стан» увімкнено за замовчуванням, а налаштування видалено +* Налаштування резервного копіювання і процесу на передньому плані перенесено на основний екран diff --git a/fastlane/metadata/android/uk/changelogs/351.txt b/fastlane/metadata/android/uk/changelogs/351.txt new file mode 100644 index 000000000..4b92092b9 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/351.txt @@ -0,0 +1,3 @@ +* Виправлення обміну файлами Jingle IBB +* Повторювані виправлення правопису більше не заповнюють базу даних +* Перехід на Last Message Correction v1.1 diff --git a/fastlane/metadata/android/uk/changelogs/353.txt b/fastlane/metadata/android/uk/changelogs/353.txt new file mode 100644 index 000000000..e0ab4b1da --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/353.txt @@ -0,0 +1,4 @@ +* Користувачі можуть встановлювати своє прізвисько (нікнейм) +* Відновлювати завантаження файлів, зашифрованих OMEMO +* Канали тепер позначаються символом «#» на піктограмі +* Quicksy за замовчуванням використовує «завжди» для шифрування OMEMO (приховує значок замка) diff --git a/fastlane/metadata/android/uk/changelogs/360.txt b/fastlane/metadata/android/uk/changelogs/360.txt new file mode 100644 index 000000000..6685cdb3a --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/360.txt @@ -0,0 +1 @@ +* Підтримка параметрів XMPP URI ?register та ?register;preauth diff --git a/fastlane/metadata/android/uk/changelogs/362.txt b/fastlane/metadata/android/uk/changelogs/362.txt new file mode 100644 index 000000000..742bbaab6 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/362.txt @@ -0,0 +1 @@ +* Підтримка автоматичного перемикання теми на Android 10 diff --git a/fastlane/metadata/android/uk/changelogs/364.txt b/fastlane/metadata/android/uk/changelogs/364.txt new file mode 100644 index 000000000..acf1518ac --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/364.txt @@ -0,0 +1,2 @@ +* Попередній перегляд PDF на Android 5 і новіших +* Використання 12-байтових IV для OMEMO diff --git a/fastlane/metadata/android/uk/changelogs/367.txt b/fastlane/metadata/android/uk/changelogs/367.txt new file mode 100644 index 000000000..4e697be13 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/367.txt @@ -0,0 +1,2 @@ +* Виправлено вибір піктограми користувача на деяких пристроях з Android 10 +* Виправлення обміну файлами для великих файлів diff --git a/fastlane/metadata/android/uk/changelogs/379.txt b/fastlane/metadata/android/uk/changelogs/379.txt new file mode 100644 index 000000000..0143a133a --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/379.txt @@ -0,0 +1 @@ +* Голосові та відеовиклики (необхідна підтримка сервера у вигляді серверів STUN і TURN, доступних для виявлення через XEP-0215) diff --git a/fastlane/metadata/android/uk/changelogs/381.txt b/fastlane/metadata/android/uk/changelogs/381.txt new file mode 100644 index 000000000..c59e5669c --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/381.txt @@ -0,0 +1,2 @@ +* Зворотний зв'язок (звуки «набір номера», «початок дзвінка», «завершення дзвінка») для голосових викликів +* Виправлено проблему з повторною спробою невдалого відеовиклику diff --git a/fastlane/metadata/android/uk/changelogs/382.txt b/fastlane/metadata/android/uk/changelogs/382.txt new file mode 100644 index 000000000..49a02dfbb --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/382.txt @@ -0,0 +1,2 @@ +* Додано кнопку перемикання камери під час відеовиклику +* Виправлення для голосових дзвінків на планшетах diff --git a/fastlane/metadata/android/uk/changelogs/383.txt b/fastlane/metadata/android/uk/changelogs/383.txt new file mode 100644 index 000000000..66d879d4b --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/383.txt @@ -0,0 +1,3 @@ +* Значок дзвінка переміщено ліворуч, щоб інші значки панелі інструментів залишалися на відповідних місцях +* Показувати тривалість розмови під час голосових викликів +* Визначення переваги в голосових та відеовикликах (двоє людей телефонують один одному одночасно) diff --git a/fastlane/metadata/android/uk/changelogs/387.txt b/fastlane/metadata/android/uk/changelogs/387.txt new file mode 100644 index 000000000..a26a7642f --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/387.txt @@ -0,0 +1,2 @@ +* Перероблено інтерфейс входу з сертифікатом +* Додано можливість закріплювати чати (додати до вибраного) diff --git a/fastlane/metadata/android/uk/changelogs/388.txt b/fastlane/metadata/android/uk/changelogs/388.txt new file mode 100644 index 000000000..6e5f7899a --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/388.txt @@ -0,0 +1,3 @@ +* Зменшено відлуння під час викликів на деяких пристроях +* Виправлено вхід з паролями, що містять спеціальні символи +* Сигнали набору номера та зайнятості відтворюються через динамік під час відеовикликів diff --git a/fastlane/metadata/android/uk/changelogs/390.txt b/fastlane/metadata/android/uk/changelogs/390.txt new file mode 100644 index 000000000..f12a56fda --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/390.txt @@ -0,0 +1 @@ +* Можливість записати голосове повідомлення, коли абонент зайнятий diff --git a/fastlane/metadata/android/uk/changelogs/393.txt b/fastlane/metadata/android/uk/changelogs/393.txt new file mode 100644 index 000000000..c4bd20e66 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/393.txt @@ -0,0 +1,3 @@ +* Показувати кнопку «Довідка» у випадку невдалого голосового чи відеовиклику +* Виправлено деякі неприємні збої +* Виправлено з'єднання Jingle (обмін файлами + дзвінки) з JID'ами без ресурсу diff --git a/fastlane/metadata/android/uk/changelogs/394.txt b/fastlane/metadata/android/uk/changelogs/394.txt new file mode 100644 index 000000000..374c95fec --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/394.txt @@ -0,0 +1,2 @@ +* Виправлено сповіщення, які не з'являлися за певних умов +* Виправлення проблем сумісності та збоїв, пов’язаних з голосовими та відеовикликами diff --git a/fastlane/metadata/android/uk/changelogs/395.txt b/fastlane/metadata/android/uk/changelogs/395.txt new file mode 100644 index 000000000..890b5c473 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/395.txt @@ -0,0 +1,3 @@ +* Додано «Повернутися до чату» на екрані звукового виклику +* Удосконалено комбінації клавіш +* Виправлення помилок diff --git a/fastlane/metadata/android/uk/changelogs/397.txt b/fastlane/metadata/android/uk/changelogs/397.txt new file mode 100644 index 000000000..0c9af0508 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/397.txt @@ -0,0 +1,3 @@ +* Обробляти файли GPX +* Покращення продуктивності при відновленні резервної копії +* Виправлення помилок diff --git a/fastlane/metadata/android/uk/changelogs/398.txt b/fastlane/metadata/android/uk/changelogs/398.txt new file mode 100644 index 000000000..837e85eac --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/398.txt @@ -0,0 +1,4 @@ +* Пошук в окремих розмовах +* Сповіщення про невдале надсилання повідомлень +* Імена (нікнейми) користувачів Quicksy зберігаються після перезапуску застосунку +* Додано кнопку для запуску Orbot (Tor) із сповіщення, якщо це необхідно diff --git a/fastlane/metadata/android/uk/changelogs/401.txt b/fastlane/metadata/android/uk/changelogs/401.txt new file mode 100644 index 000000000..dbce88932 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/401.txt @@ -0,0 +1,2 @@ +* Виправлено пошук на версіях Android до 5-ї включно +* Оптимізація використання пам'яті diff --git a/fastlane/metadata/android/uk/changelogs/402.txt b/fastlane/metadata/android/uk/changelogs/402.txt new file mode 100644 index 000000000..1f2ec0fbd --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/402.txt @@ -0,0 +1,3 @@ +* Просте створення запрошень на серверах з підтримкою запрошень +* Перегляд файлів GIF, отриманих з Movim +* Піктограми користувачів зберігаються у кеші diff --git a/fastlane/metadata/android/uk/changelogs/403.txt b/fastlane/metadata/android/uk/changelogs/403.txt new file mode 100644 index 000000000..75ec2df34 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/403.txt @@ -0,0 +1,3 @@ +* Виправлено проблеми з підключенням, коли різні облікові записи використовували різні механізми SCRAM +* Додано підтримку SCRAM-SHA-512 +* Дозволено обмін файлами P2P (Jingle) із власним контактом diff --git a/fastlane/metadata/android/uk/changelogs/404.txt b/fastlane/metadata/android/uk/changelogs/404.txt new file mode 100644 index 000000000..130dce1c6 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/404.txt @@ -0,0 +1 @@ +* Незначні покращення стабільності для голосових та відеовикликів diff --git a/fastlane/metadata/android/uk/changelogs/405.txt b/fastlane/metadata/android/uk/changelogs/405.txt new file mode 100644 index 000000000..215a7e262 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/405.txt @@ -0,0 +1 @@ +* Quicksy: Автоматично отримувати SMS підтвердження diff --git a/fastlane/metadata/android/uk/changelogs/407.txt b/fastlane/metadata/android/uk/changelogs/407.txt new file mode 100644 index 000000000..abc18bf99 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/407.txt @@ -0,0 +1,3 @@ +* Показувати кнопку виклику для контактів поза мережею, якщо вони раніше оголосили про підтримку дзвінків +* Кнопка «Назад» більше не завершує дзвінок під час виклику +* Виправлення помилок diff --git a/fastlane/metadata/android/uk/changelogs/42000.txt b/fastlane/metadata/android/uk/changelogs/42000.txt new file mode 100644 index 000000000..7657ede43 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42000.txt @@ -0,0 +1,4 @@ +* Можливість вибирати мелодію для вхідних викликів +* Виправлено виявлення ідентифікатора ключа OpenPGP для OpenKeychain 5.6+ +* Коректна перевірка сертифікатів punycode TLS +* Покращення стабільності встановлення сесії RTP (дзвінки) diff --git a/fastlane/metadata/android/uk/changelogs/42006.txt b/fastlane/metadata/android/uk/changelogs/42006.txt new file mode 100644 index 000000000..6077b0312 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42006.txt @@ -0,0 +1,2 @@ +* Перевіряти голосові та відеовиклики за допомогою вже існуючих сесій OMEMO +* Покращено сумісність із реалізаціями WebRTC без libwebrtc diff --git a/fastlane/metadata/android/uk/changelogs/42010.txt b/fastlane/metadata/android/uk/changelogs/42010.txt new file mode 100644 index 000000000..0c25752b4 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42010.txt @@ -0,0 +1,2 @@ +* Виправлення різноманітних помилок у підтримці Tor +* Покращення сумісності дзвінків із Dino diff --git a/fastlane/metadata/android/uk/changelogs/42012.txt b/fastlane/metadata/android/uk/changelogs/42012.txt new file mode 100644 index 000000000..654b6af7f --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42012.txt @@ -0,0 +1 @@ +* Виправлено передачу/завантаження через HTTP для користувачів, які не довіряють системним ЦС diff --git a/fastlane/metadata/android/uk/changelogs/42013.txt b/fastlane/metadata/android/uk/changelogs/42013.txt new file mode 100644 index 000000000..f1a182eb3 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42013.txt @@ -0,0 +1 @@ +* Виправлено проблеми з повідомленням про відсутність з'єднання на Android 7.1 diff --git a/fastlane/metadata/android/uk/changelogs/42014.txt b/fastlane/metadata/android/uk/changelogs/42014.txt new file mode 100644 index 000000000..39b9354ce --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42014.txt @@ -0,0 +1,2 @@ +* Завжди перевіряти ім'я домену. Без перезапису користувачем +* Підтримка попередньої автентифікації списку контактів diff --git a/fastlane/metadata/android/uk/changelogs/42015.txt b/fastlane/metadata/android/uk/changelogs/42015.txt new file mode 100644 index 000000000..e80184657 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42015.txt @@ -0,0 +1 @@ +* Незначні покращення голосових та відеовикликів diff --git a/fastlane/metadata/android/uk/changelogs/42018.txt b/fastlane/metadata/android/uk/changelogs/42018.txt new file mode 100644 index 000000000..01ffffc78 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42018.txt @@ -0,0 +1,3 @@ +* Показувати чорні смуги, коли віддалене відео не відповідає пропорціям екрана +* Покращення ефективності пошуку +* Додано налаштування для заборони знімків екрана diff --git a/fastlane/metadata/android/uk/changelogs/42022.txt b/fastlane/metadata/android/uk/changelogs/42022.txt new file mode 100644 index 000000000..4a5bfc7bb --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42022.txt @@ -0,0 +1,2 @@ +* Виправлено проблему коли деякі відео не стискалися +* Виправлено рідкісний збій під час відкриття сповіщення diff --git a/fastlane/metadata/android/uk/changelogs/42023.txt b/fastlane/metadata/android/uk/changelogs/42023.txt new file mode 100644 index 000000000..e85b7c0ad --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42023.txt @@ -0,0 +1,2 @@ +* Виправлено збій при відтворенні деяких лапок +* Виправлено збій на екрані привітання diff --git a/fastlane/metadata/android/uk/changelogs/42037.txt b/fastlane/metadata/android/uk/changelogs/42037.txt new file mode 100644 index 000000000..723563a95 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42037.txt @@ -0,0 +1,11 @@ +Версія 2.10.9 +* Запитувати дозволи Bluetooth для голосових та відеовикликів (можна відхилити, якщо не використовуєте гарнітуру Bluetooth) +* Виправлено помилку під час виклику Movim +* Виправлено відображення неправильної піктограми для групових чатів +* Завжди запитувати про вимкнення оптимізації батареї +* Установлено прапорець «лише локально» для сповіщень «x облікових записів у мережі» +* Виправлено взаємодію з плагіном Google Maps Share Location +* Видалено примітку щодо плати за сервер +* Зберігати файли в місці, яке підходить для Android 11 +* Пробувати повторно підключити виклик після перемикання мережі +* Показувати JID абонента та JID облікового запису на екрані вхідного виклику diff --git a/fastlane/metadata/android/uk/changelogs/42038.txt b/fastlane/metadata/android/uk/changelogs/42038.txt new file mode 100644 index 000000000..df23d2664 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42038.txt @@ -0,0 +1,2 @@ +* Незначні виправлення помилок +* Відновлено можливість викликів через JMP та інші служби (версія Playstore) diff --git a/fastlane/metadata/android/uk/changelogs/42041.txt b/fastlane/metadata/android/uk/changelogs/42041.txt new file mode 100644 index 000000000..f0c963522 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42041.txt @@ -0,0 +1,5 @@ +* Реалізація Extensible SASL Profile, Bind 2.0 і Fast для швидшого повторного з'єднання +* Реалізація Channel Binding +* Додано можливість перемикатися з голосового на відеовиклик +* Додано можливість видаляти свою піктограму користувача +* Додано сповіщення про пропущені виклики diff --git a/fastlane/metadata/android/uk/changelogs/42042.txt b/fastlane/metadata/android/uk/changelogs/42042.txt new file mode 100644 index 000000000..771ad5b10 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42042.txt @@ -0,0 +1,2 @@ +* Виправлено циклічне повторне надсилання на сервери, які підтримують лише sm:2 +* Показувати «Перемкнути на відео» тільки якщо інша сторона підтримує відео diff --git a/fastlane/metadata/android/uk/changelogs/42043.txt b/fastlane/metadata/android/uk/changelogs/42043.txt new file mode 100644 index 000000000..a92ebf0fc --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42043.txt @@ -0,0 +1 @@ +* Виправлено регресивну помилку в обміні файлами P2P diff --git a/fastlane/metadata/android/uk/changelogs/42044.txt b/fastlane/metadata/android/uk/changelogs/42044.txt new file mode 100644 index 000000000..8facdb3bf --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42044.txt @@ -0,0 +1,3 @@ +* Виправлено повторне надсилання повідомлень при використанні SASL2 +* Виправлення чорного відео між деякими пристроями +* Виправлено збій з порожніми паролями diff --git a/fastlane/metadata/android/uk/changelogs/42046.txt b/fastlane/metadata/android/uk/changelogs/42046.txt new file mode 100644 index 000000000..36f6ac725 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42046.txt @@ -0,0 +1 @@ +* Інтегрований дистриб'ютор UnifiedPush для надсилання push-повідомлень іншим застосункам, які підтримують UnifiedPush, як-от Tusky і Fedilab diff --git a/fastlane/metadata/android/uk/changelogs/42047.txt b/fastlane/metadata/android/uk/changelogs/42047.txt new file mode 100644 index 000000000..5cf6316f1 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42047.txt @@ -0,0 +1 @@ +* Виправлено збій у дистриб'юторі UnifiedPush diff --git a/fastlane/metadata/android/uk/changelogs/42050.txt b/fastlane/metadata/android/uk/changelogs/42050.txt new file mode 100644 index 000000000..cc1ed9ce3 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42050.txt @@ -0,0 +1 @@ +* Збільшено радіус заокруглення кутів зображення профілю diff --git a/fastlane/metadata/android/uk/changelogs/42059.txt b/fastlane/metadata/android/uk/changelogs/42059.txt new file mode 100644 index 000000000..06af124bb --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42059.txt @@ -0,0 +1,2 @@ +* Цільовий SDK знову підвищено до 33 +* Виправлення проблем із серверами, які підтримують SASL2 без вбудованого керування потоком diff --git a/fastlane/metadata/android/uk/changelogs/42060.txt b/fastlane/metadata/android/uk/changelogs/42060.txt new file mode 100644 index 000000000..20fde53a4 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42060.txt @@ -0,0 +1 @@ +* Виправлено помилкове розпізнавання літери «q» як кириличної diff --git a/fastlane/metadata/android/uk/changelogs/42061.txt b/fastlane/metadata/android/uk/changelogs/42061.txt new file mode 100644 index 000000000..2b4441c6e --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42061.txt @@ -0,0 +1 @@ +* Видалено функцію пошуку каналів із версії Google Play diff --git a/fastlane/metadata/android/uk/changelogs/42062.txt b/fastlane/metadata/android/uk/changelogs/42062.txt new file mode 100644 index 000000000..f5768cc61 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42062.txt @@ -0,0 +1 @@ +* Вимкнено відкривання файлів резервних копій (.ceb) із файлового менеджера diff --git a/fastlane/metadata/android/uk/changelogs/42065.txt b/fastlane/metadata/android/uk/changelogs/42065.txt new file mode 100644 index 000000000..4a8122be7 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42065.txt @@ -0,0 +1 @@ +* Запроваджено новий формат файлу резервної копії diff --git a/fastlane/metadata/android/uk/changelogs/42068.txt b/fastlane/metadata/android/uk/changelogs/42068.txt new file mode 100644 index 000000000..eabdf5366 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42068.txt @@ -0,0 +1,2 @@ +* Підтримка налаштування сповіщень окремо для кожної розмови +* Використання Opus для голосових повідомлень на Android 10 diff --git a/fastlane/metadata/android/uk/changelogs/42072.txt b/fastlane/metadata/android/uk/changelogs/42072.txt new file mode 100644 index 000000000..b8ff7f7c7 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/42072.txt @@ -0,0 +1,3 @@ +* Підвищено залежність libwebrtc до M117 і оновлено libvpx +* Повернення до AAC для голосових повідомлень +* Підтримка своїх налаштувань мови в додатку diff --git a/fastlane/metadata/android/uk/changelogs/4207704.txt b/fastlane/metadata/android/uk/changelogs/4207704.txt new file mode 100644 index 000000000..db6654491 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4207704.txt @@ -0,0 +1,3 @@ +* Підтримка приватної DNS (DNS over TLS) +* Підтримка тематичного значка додатка в лаунчері +* Виправлено рідкісну проблему з дозволами під час обміну файлами на Android 11 і новіших diff --git a/fastlane/metadata/android/uk/changelogs/4208104.txt b/fastlane/metadata/android/uk/changelogs/4208104.txt new file mode 100644 index 000000000..9235074b1 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4208104.txt @@ -0,0 +1,4 @@ +* Простіший доступ до «Показати QR-код» +* Підтримка закладок PEP Native Bookmarks +* Додано підтримку моделі SDP пропозиція/відповідь (Використовується шлюзами SIP) +* Підвищено цільовий API до Android 14 diff --git a/fastlane/metadata/android/zh-CN/changelogs/349.txt b/fastlane/metadata/android/zh-CN/changelogs/349.txt new file mode 100644 index 000000000..689259e2b --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/349.txt @@ -0,0 +1,4 @@ +* 引入专家设置在本地服务器上执行频道发现而不是 search.jabber.network +* 默认启用传递复选标记并移除设置 +* 默认启用“发送按钮指示状态”并移除设置 +* 将备份和前台服务设置移至主屏幕 diff --git a/fastlane/metadata/android/zh-CN/changelogs/351.txt b/fastlane/metadata/android/zh-CN/changelogs/351.txt new file mode 100644 index 000000000..c3ff6bc46 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/351.txt @@ -0,0 +1,3 @@ +* 修复了 Jingle IBB 文件传输问题 +* 修复了重复更正填满数据库的问题 +* 切换到最后消息更正 v1.1 diff --git a/fastlane/metadata/android/zh-CN/changelogs/353.txt b/fastlane/metadata/android/zh-CN/changelogs/353.txt new file mode 100644 index 000000000..75d7425d1 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/353.txt @@ -0,0 +1,4 @@ +* 让用户设置自己的昵称 +* 恢复 OMEMO 加密文件的下载 +* 频道现在使用“#”作为头像中的符号 +* Quicksy 使用“始终”作为 OMEMO 加密默认值(隐藏锁定图标) diff --git a/fastlane/metadata/android/zh-CN/changelogs/360.txt b/fastlane/metadata/android/zh-CN/changelogs/360.txt new file mode 100644 index 000000000..fc2e905de --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/360.txt @@ -0,0 +1 @@ +* 支持 ?register 和 ?register;preauth XMPP uri 参数 diff --git a/fastlane/metadata/android/zh-CN/changelogs/362.txt b/fastlane/metadata/android/zh-CN/changelogs/362.txt new file mode 100644 index 000000000..27249c94f --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/362.txt @@ -0,0 +1 @@ +* 支持在 Android 10 上自动切换主题 diff --git a/fastlane/metadata/android/zh-CN/changelogs/364.txt b/fastlane/metadata/android/zh-CN/changelogs/364.txt new file mode 100644 index 000000000..2b4d64fcb --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/364.txt @@ -0,0 +1,2 @@ +* 在 Android 5 以上版本上提供 PDF 预览 +* 为 OMEMO 使用 12 byte IVs diff --git a/fastlane/metadata/android/zh-CN/changelogs/367.txt b/fastlane/metadata/android/zh-CN/changelogs/367.txt new file mode 100644 index 000000000..59ac67d50 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/367.txt @@ -0,0 +1,2 @@ +* 修复部分 Android 10 设备上的头像选择问题 +* 修复较大文件的文件传输 diff --git a/fastlane/metadata/android/zh-CN/changelogs/379.txt b/fastlane/metadata/android/zh-CN/changelogs/379.txt new file mode 100644 index 000000000..9bd6e9e93 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/379.txt @@ -0,0 +1 @@ +* 音频/视频通话(需要通过 XEP-0215 发现的 STUN 和 TURN 服务器形式的服务器支持) diff --git a/fastlane/metadata/android/zh-CN/changelogs/381.txt b/fastlane/metadata/android/zh-CN/changelogs/381.txt new file mode 100644 index 000000000..b8db10afd --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/381.txt @@ -0,0 +1,2 @@ +* 语音通话的声音反馈(拨号、通话开始、通话结束)。 +* 修复了重试失败视频通话的问题 diff --git a/fastlane/metadata/android/zh-CN/changelogs/382.txt b/fastlane/metadata/android/zh-CN/changelogs/382.txt new file mode 100644 index 000000000..334e75d43 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/382.txt @@ -0,0 +1,2 @@ +* 添加视频通话时切换摄像头的按钮 +* 修复了平板电脑上的语音通话问题 diff --git a/fastlane/metadata/android/zh-CN/changelogs/383.txt b/fastlane/metadata/android/zh-CN/changelogs/383.txt new file mode 100644 index 000000000..31ee7aa89 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/383.txt @@ -0,0 +1,3 @@ +* 将通话图标移至左侧,以保持其他工具栏图标在一致的位置 +* 音频通话时显示通话时长 +* 音频/视频通话打破僵局(两个人同时打电话给对方) diff --git a/fastlane/metadata/android/zh-CN/changelogs/387.txt b/fastlane/metadata/android/zh-CN/changelogs/387.txt new file mode 100644 index 000000000..e8c9f29a3 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/387.txt @@ -0,0 +1,2 @@ +* 重新设计使用证书登录的用户界面 +* 添加将聊天固定在顶部的功能(添加到收藏夹) diff --git a/fastlane/metadata/android/zh-CN/changelogs/388.txt b/fastlane/metadata/android/zh-CN/changelogs/388.txt new file mode 100644 index 000000000..8b8536bbe --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/388.txt @@ -0,0 +1,3 @@ +* 在某些设备上通话时减少回声 +* 修复密码包含特殊字符时的登录问题 +* 视频通话期间扬声器上播放拨号音和忙音 diff --git a/fastlane/metadata/android/zh-CN/changelogs/390.txt b/fastlane/metadata/android/zh-CN/changelogs/390.txt new file mode 100644 index 000000000..ee78f954d --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/390.txt @@ -0,0 +1 @@ +* 在接听者忙时提供录制语音消息服务 diff --git a/fastlane/metadata/android/zh-CN/changelogs/393.txt b/fastlane/metadata/android/zh-CN/changelogs/393.txt new file mode 100644 index 000000000..1d903fe6c --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/393.txt @@ -0,0 +1,3 @@ +* 如果音频/视频通话失败则显示帮助按钮 +* 修复了一些恼人的崩溃问题 +* 修复了带有纯 JID 的 Jingle 连接(文件传输 + 通话) diff --git a/fastlane/metadata/android/zh-CN/changelogs/394.txt b/fastlane/metadata/android/zh-CN/changelogs/394.txt new file mode 100644 index 000000000..df4f52a34 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/394.txt @@ -0,0 +1,2 @@ +* 修复了某些情况下不显示通知的问题 +* 修复了与音频/视频通话相关的兼容性问题和崩溃 diff --git a/fastlane/metadata/android/zh-CN/changelogs/395.txt b/fastlane/metadata/android/zh-CN/changelogs/395.txt new file mode 100644 index 000000000..010d78c4d --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/395.txt @@ -0,0 +1,3 @@ +* 在音频通话屏幕中添加“返回聊天” +* 改进键盘快捷键 +* bug 修复 diff --git a/fastlane/metadata/android/zh-CN/changelogs/397.txt b/fastlane/metadata/android/zh-CN/changelogs/397.txt new file mode 100644 index 000000000..96c3815c3 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/397.txt @@ -0,0 +1,3 @@ +* 处理 GPX 文件 +* 提高备份恢复性能 +* bug 修复 diff --git a/fastlane/metadata/android/zh-CN/changelogs/398.txt b/fastlane/metadata/android/zh-CN/changelogs/398.txt new file mode 100644 index 000000000..3985836d3 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/398.txt @@ -0,0 +1,4 @@ +* 搜索个人对话 +* 消息传递失败时通知用户 +* 重启时记住 Quicksy 用户的显示名称(昵称) +* 如有必要,添加按钮以从通知中启动 Orbot(Tor) diff --git a/fastlane/metadata/android/zh-CN/changelogs/401.txt b/fastlane/metadata/android/zh-CN/changelogs/401.txt new file mode 100644 index 000000000..df75923de --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/401.txt @@ -0,0 +1,2 @@ +* 修复了 Android <= 5 上的搜索 +* 优化内存消耗 diff --git a/fastlane/metadata/android/zh-CN/changelogs/402.txt b/fastlane/metadata/android/zh-CN/changelogs/402.txt new file mode 100644 index 000000000..7bcabf3b9 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/402.txt @@ -0,0 +1,3 @@ +* 在支持的服务器上提供简易邀请生成功能 +* 显示从 Movim 发送的 GIF +* 在缓存中存储头像 diff --git a/fastlane/metadata/android/zh-CN/changelogs/403.txt b/fastlane/metadata/android/zh-CN/changelogs/403.txt new file mode 100644 index 000000000..52a13575a --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/403.txt @@ -0,0 +1,3 @@ +* 修复了不同账号使用不同 SCRAM 机制时的连接问题 +* 添加对 SCRAM-SHA-512 的支持 +* 允许通过自联系进行 P2P(Jingle)文件传输 diff --git a/fastlane/metadata/android/zh-CN/changelogs/404.txt b/fastlane/metadata/android/zh-CN/changelogs/404.txt new file mode 100644 index 000000000..4619eb7cc --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/404.txt @@ -0,0 +1 @@ +* 对音频/视频通话的稳定性略有改善 diff --git a/fastlane/metadata/android/zh-CN/changelogs/405.txt b/fastlane/metadata/android/zh-CN/changelogs/405.txt new file mode 100644 index 000000000..a0fa53f8b --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/405.txt @@ -0,0 +1 @@ +* Quicksy:自动接收验证短信 diff --git a/fastlane/metadata/android/zh-CN/changelogs/407.txt b/fastlane/metadata/android/zh-CN/changelogs/407.txt new file mode 100644 index 000000000..33e0919ad --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/407.txt @@ -0,0 +1,3 @@ +* 如果离线联系人之前已宣布支持,则显示呼叫按钮 +* 通话接通后,后退按钮不再结束通话 +* bug 修复 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42000.txt b/fastlane/metadata/android/zh-CN/changelogs/42000.txt new file mode 100644 index 000000000..8aedfaed8 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42000.txt @@ -0,0 +1,4 @@ +* 能够选择来电铃声 +* 修复 OpenKeychain 5.6+ 的 OpenPGP 密钥 ID 发现问题 +* 正确验证 punycode TLS 证书 +* 提高 RTP 会话建立(呼叫)的稳定性 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42006.txt b/fastlane/metadata/android/zh-CN/changelogs/42006.txt new file mode 100644 index 000000000..d9f60038f --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42006.txt @@ -0,0 +1,2 @@ +* 使用预先存在的 OMEMO 会话验证音频/视频通话 +* 提高与非 libwebrtc WebRTC 实现的兼容性 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42010.txt b/fastlane/metadata/android/zh-CN/changelogs/42010.txt new file mode 100644 index 000000000..09fddc261 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42010.txt @@ -0,0 +1,2 @@ +* 修复了有关 Tor 支持的各种错误 +* 改进与 Dino 的通话兼容性 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42012.txt b/fastlane/metadata/android/zh-CN/changelogs/42012.txt new file mode 100644 index 000000000..9c15d9ceb --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42012.txt @@ -0,0 +1 @@ +* 修复不信任系统证书颁发机构的用户的 HTTP 上传/下载问题 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42013.txt b/fastlane/metadata/android/zh-CN/changelogs/42013.txt new file mode 100644 index 000000000..801cf42fb --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42013.txt @@ -0,0 +1 @@ +* 修复了 Android 7.1 上的“无连接”问题 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42014.txt b/fastlane/metadata/android/zh-CN/changelogs/42014.txt new file mode 100644 index 000000000..2784135e0 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42014.txt @@ -0,0 +1,2 @@ +* 始终验证域名。没有用户覆盖 +* 支持花名册预验证 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42015.txt b/fastlane/metadata/android/zh-CN/changelogs/42015.txt new file mode 100644 index 000000000..b9ce380d6 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42015.txt @@ -0,0 +1 @@ +* 在音频和视频方面略有改进 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42018.txt b/fastlane/metadata/android/zh-CN/changelogs/42018.txt new file mode 100644 index 000000000..974bd0292 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42018.txt @@ -0,0 +1,3 @@ +* 当远程视频与屏幕宽高比不匹配时显示黑条 +* 提高搜索性能 +* 添加防止截图的设置 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42022.txt b/fastlane/metadata/android/zh-CN/changelogs/42022.txt new file mode 100644 index 000000000..08c03bff7 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42022.txt @@ -0,0 +1,2 @@ +* 修复某些视频无法压缩的问题 +* 修复打开通知时罕见的崩溃问题 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42023.txt b/fastlane/metadata/android/zh-CN/changelogs/42023.txt new file mode 100644 index 000000000..e24d2373f --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42023.txt @@ -0,0 +1,2 @@ +* 修复渲染某些引用时的崩溃问题 +* 修复欢迎屏幕崩溃问题 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42037.txt b/fastlane/metadata/android/zh-CN/changelogs/42037.txt new file mode 100644 index 000000000..bde374945 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42037.txt @@ -0,0 +1,11 @@ +版本2.10.9 +* 进行音视频通话时请求蓝牙权限(如果您不使用蓝牙耳机可以拒绝) +* 修复呼叫 Movim 时的错误 +* 修复群组聊天的显示错误头像的问题 +* 始终要求选择退出电池优化 +* 在“x 个已连接账号”通知上设置仅本地标志 +* 修复与 Google 地图分享位置插件的交互 +* 移除有关服务器费用的脚注 +* 将文件存储在适合 Android 11 的位置 +* 网络切换后尝试重新连接通话 +* 在来电屏幕中显示来电者JID和帐户JID diff --git a/fastlane/metadata/android/zh-CN/changelogs/42038.txt b/fastlane/metadata/android/zh-CN/changelogs/42038.txt new file mode 100644 index 000000000..99c2ed862 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42038.txt @@ -0,0 +1,2 @@ +* 修正了一些小错误 +* 恢复通过 JMP 和其他服务呼叫的能力(Playstore 版本) diff --git a/fastlane/metadata/android/zh-CN/changelogs/42041.txt b/fastlane/metadata/android/zh-CN/changelogs/42041.txt new file mode 100644 index 000000000..b809c9854 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42041.txt @@ -0,0 +1,5 @@ +* 实施可扩展 SASL Profile、Bind 2.0 和 Fast,以加快重新连接速度 +* 实现频道绑定 +* 增加从音频通话切换到视频通话的功能 +* 增加删除自己头像的功能 +* 增加未接来电通知功能 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42042.txt b/fastlane/metadata/android/zh-CN/changelogs/42042.txt new file mode 100644 index 000000000..2c5975c38 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42042.txt @@ -0,0 +1,2 @@ +* 修复仅支持 sm:2 的服务器上的重发循环 +* 仅当对方支持视频时才显示“切换到视频” diff --git a/fastlane/metadata/android/zh-CN/changelogs/42043.txt b/fastlane/metadata/android/zh-CN/changelogs/42043.txt new file mode 100644 index 000000000..d830a7f32 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42043.txt @@ -0,0 +1 @@ +* 修复了 P2P 文件传输中的缺陷 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42044.txt b/fastlane/metadata/android/zh-CN/changelogs/42044.txt new file mode 100644 index 000000000..f0e54c458 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42044.txt @@ -0,0 +1,3 @@ +* 修复使用 SASL2 时重新发送消息的问题 +* 修复部分设备之间的黑屏问题 +* 修复空密码崩溃问题 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42046.txt b/fastlane/metadata/android/zh-CN/changelogs/42046.txt new file mode 100644 index 000000000..a4a5e4c1a --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42046.txt @@ -0,0 +1 @@ +* 集成 UnifiedPush 分发程序,以便将消息推送到其他支持 UnifiedPush 的应用程序,例如 Tusky 和 Fedilab diff --git a/fastlane/metadata/android/zh-CN/changelogs/42047.txt b/fastlane/metadata/android/zh-CN/changelogs/42047.txt new file mode 100644 index 000000000..c67da1843 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42047.txt @@ -0,0 +1 @@ +* 修复 UnifiedPush 分发程序中的崩溃问题 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42050.txt b/fastlane/metadata/android/zh-CN/changelogs/42050.txt new file mode 100644 index 000000000..28c946e81 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42050.txt @@ -0,0 +1 @@ +* 增加个人资料图片的圆角半径 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42059.txt b/fastlane/metadata/android/zh-CN/changelogs/42059.txt new file mode 100644 index 000000000..faeaa2ff1 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42059.txt @@ -0,0 +1,2 @@ +* 将 Target SDK 再次提升至 33 +* 修复支持 SASL2 且不支持内联流管理的服务器上的问题 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42060.txt b/fastlane/metadata/android/zh-CN/changelogs/42060.txt new file mode 100644 index 000000000..72a78b5cb --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42060.txt @@ -0,0 +1 @@ +* 修复“q”被错误识别为西里尔字母的问题 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42061.txt b/fastlane/metadata/android/zh-CN/changelogs/42061.txt new file mode 100644 index 000000000..4cf6ebea1 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42061.txt @@ -0,0 +1 @@ +* 从 Google Play 版本中移除频道发现功能 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42062.txt b/fastlane/metadata/android/zh-CN/changelogs/42062.txt new file mode 100644 index 000000000..e15f64892 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42062.txt @@ -0,0 +1 @@ +* 禁止从文件管理器打开备份文件(.ceb) diff --git a/fastlane/metadata/android/zh-CN/changelogs/42065.txt b/fastlane/metadata/android/zh-CN/changelogs/42065.txt new file mode 100644 index 000000000..419c47a53 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42065.txt @@ -0,0 +1 @@ +* 引入新的备份文件格式 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42068.txt b/fastlane/metadata/android/zh-CN/changelogs/42068.txt new file mode 100644 index 000000000..3c7020ba0 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42068.txt @@ -0,0 +1,2 @@ +* 支持每个对话通知设置 +* 在 Android 10 上使用 opus 发送语音消息 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42072.txt b/fastlane/metadata/android/zh-CN/changelogs/42072.txt new file mode 100644 index 000000000..481587e68 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42072.txt @@ -0,0 +1,3 @@ +* 将 libwebrtc 依赖项提升到 M117 并提升 libvpx +* 回到 AAC 语音消息 +* 支持每个应用程序语言设置 diff --git a/fastlane/metadata/android/zh-CN/changelogs/42074.txt b/fastlane/metadata/android/zh-CN/changelogs/42074.txt new file mode 100644 index 000000000..12acff938 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/42074.txt @@ -0,0 +1,3 @@ +* 支持私人 DNS(DNS over TLS) +* 支持主题启动器图标 +* 修复在 Android 11+ 分享文件时罕见的权限问题 diff --git a/fastlane/metadata/android/zh-CN/changelogs/4207704.txt b/fastlane/metadata/android/zh-CN/changelogs/4207704.txt new file mode 100644 index 000000000..4c33d6b99 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4207704.txt @@ -0,0 +1,3 @@ +* 支持私人 DNS(DNS over TLS) +* 支持主题启动器图标 +* 修复在 Android 11+ 上分享文件时罕见的权限问题 diff --git a/fastlane/metadata/android/zh-CN/changelogs/4208104.txt b/fastlane/metadata/android/zh-CN/changelogs/4208104.txt new file mode 100644 index 000000000..7e5f80a13 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4208104.txt @@ -0,0 +1,4 @@ +* 更容易访问“显示二维码” +* 支持 PEP Native Bookmarks +* 添加对 SDP 请求/响应模型的支持(由 SIP 网关使用) +* 将目标 API 提升到 Android 14 diff --git a/fastlane/metadata/android/zh-TW/changelogs/349.txt b/fastlane/metadata/android/zh-TW/changelogs/349.txt new file mode 100644 index 000000000..af5136714 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/349.txt @@ -0,0 +1,4 @@ +* 引入專家設置,以在本地伺服器上執行通道發現,而非 search.jabber.network +* 默認啟用傳送檢查標記,並刪除相應設置 +* 默認啟用「發送按鈕顯示狀態」,並刪除相應設置 +* 將備份和前景服務設置移至主畫面 diff --git a/fastlane/metadata/android/zh-TW/changelogs/351.txt b/fastlane/metadata/android/zh-TW/changelogs/351.txt new file mode 100644 index 000000000..bf292b34a --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/351.txt @@ -0,0 +1,3 @@ +* 修復 Jingle IBB 檔案傳輸問題 +* 修復重複更正填充數據庫的問題 +* 切換至 Last Message Correction 版本 1.1 diff --git a/fastlane/metadata/android/zh-TW/changelogs/353.txt b/fastlane/metadata/android/zh-TW/changelogs/353.txt new file mode 100644 index 000000000..5aeb2bb42 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/353.txt @@ -0,0 +1,4 @@ +* 允許用戶設置自己的暱稱 +* 恢復下載 OMEMO 加密文件 +* 頻道頭像中現在使用 '#' 符號 +* Quicksy 將「總是」用作 OMEMO 加密的默認值(隱藏鎖定圖標) diff --git a/fastlane/metadata/android/zh-TW/changelogs/360.txt b/fastlane/metadata/android/zh-TW/changelogs/360.txt new file mode 100644 index 000000000..6fff27fea --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/360.txt @@ -0,0 +1 @@ +* 支援 ?register 和 ?register;preauth XMPP URI 參數 diff --git a/fastlane/metadata/android/zh-TW/changelogs/362.txt b/fastlane/metadata/android/zh-TW/changelogs/362.txt new file mode 100644 index 000000000..7919d5550 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/362.txt @@ -0,0 +1 @@ +在 Android 10 上支援自動主題切換 diff --git a/fastlane/metadata/android/zh-TW/changelogs/364.txt b/fastlane/metadata/android/zh-TW/changelogs/364.txt new file mode 100644 index 000000000..b46a5c3e2 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/364.txt @@ -0,0 +1,2 @@ +* 在 Android 5+ 上提供 PDF 預覽 +* 在 OMEMO 中使用 12 字節的 IV diff --git a/fastlane/metadata/android/zh-TW/changelogs/367.txt b/fastlane/metadata/android/zh-TW/changelogs/367.txt new file mode 100644 index 000000000..6ace96e8b --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/367.txt @@ -0,0 +1,2 @@ +* 修復在某些 Android 10 設備上的頭像選擇問題 +* 修復傳輸較大文件的問題 diff --git a/fastlane/metadata/android/zh-TW/changelogs/379.txt b/fastlane/metadata/android/zh-TW/changelogs/379.txt new file mode 100644 index 000000000..aae54ffb2 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/379.txt @@ -0,0 +1 @@ +* 音訊/視訊通話(需要伺服器支援,以 STUN 和 TURN 伺服器的形式通過 XEP-0215 可發現) diff --git a/fastlane/metadata/android/zh-TW/changelogs/381.txt b/fastlane/metadata/android/zh-TW/changelogs/381.txt new file mode 100644 index 000000000..c714a43f2 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/381.txt @@ -0,0 +1,2 @@ +* 語音通話的聲音回饋(撥號、通話開始、通話結束) +* 修復重試失敗的視訊通話問題 diff --git a/fastlane/metadata/android/zh-TW/changelogs/382.txt b/fastlane/metadata/android/zh-TW/changelogs/382.txt new file mode 100644 index 000000000..b026a4707 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/382.txt @@ -0,0 +1,2 @@ +* 新增在視訊通話中切換攝像頭的按鈕 +* 修復平板電腦上的語音通話問題 diff --git a/fastlane/metadata/android/zh-TW/changelogs/383.txt b/fastlane/metadata/android/zh-TW/changelogs/383.txt new file mode 100644 index 000000000..027684eed --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/383.txt @@ -0,0 +1,3 @@ +* 將通話圖標移至左側,以保持其他工具欄圖標的一致位置 +* 在語音通話期間顯示通話持續時間 +* 視訊/音訊通話的分開處理(同時互打電話的兩人的處理) diff --git a/fastlane/metadata/android/zh-TW/changelogs/387.txt b/fastlane/metadata/android/zh-TW/changelogs/387.txt new file mode 100644 index 000000000..38f22df14 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/387.txt @@ -0,0 +1,2 @@ +* 重做憑證登錄的使用者介面 +* 新增置頂聊天的功能(加入最愛) diff --git a/fastlane/metadata/android/zh-TW/changelogs/388.txt b/fastlane/metadata/android/zh-TW/changelogs/388.txt new file mode 100644 index 000000000..3ea17c53f --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/388.txt @@ -0,0 +1,3 @@ +* 在某些設備上減少通話中的回音 +* 修復當密碼包含特殊字符時的登錄問題 +* 在視訊通話期間在揚聲器上播放撥號和忙音 diff --git a/fastlane/metadata/android/zh-TW/changelogs/390.txt b/fastlane/metadata/android/zh-TW/changelogs/390.txt new file mode 100644 index 000000000..8452f3854 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/390.txt @@ -0,0 +1 @@ +* 在被呼叫方忙線時提供錄製語音訊息的選項 diff --git a/fastlane/metadata/android/zh-TW/changelogs/393.txt b/fastlane/metadata/android/zh-TW/changelogs/393.txt new file mode 100644 index 000000000..855909e6e --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/393.txt @@ -0,0 +1,3 @@ +* 如果音訊/視訊通話失敗,顯示求助按鈕 +* 修復一些令人困擾的崩潰問題 +* 修復使用裸 JIDs 的 Jingle 連接(檔案傳輸 + 通話) diff --git a/fastlane/metadata/android/zh-TW/changelogs/394.txt b/fastlane/metadata/android/zh-TW/changelogs/394.txt new file mode 100644 index 000000000..4e2764249 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/394.txt @@ -0,0 +1,2 @@ +* 修復在某些情況下通知未顯示的問題 +* 修復與音訊/視訊通話相關的相容性問題和崩潰 diff --git a/fastlane/metadata/android/zh-TW/changelogs/395.txt b/fastlane/metadata/android/zh-TW/changelogs/395.txt new file mode 100644 index 000000000..09ebd7faa --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/395.txt @@ -0,0 +1,3 @@ +* 在音訊通話畫面中新增「返回聊天」選項 +* 改進鍵盤快捷鍵 +* 修復錯誤 diff --git a/fastlane/metadata/android/zh-TW/changelogs/397.txt b/fastlane/metadata/android/zh-TW/changelogs/397.txt new file mode 100644 index 000000000..0575a7378 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/397.txt @@ -0,0 +1,3 @@ +* 處理 GPX 檔案 +* 改善備份與還原的效能 +* 錯誤修正 diff --git a/fastlane/metadata/android/zh-TW/changelogs/398.txt b/fastlane/metadata/android/zh-TW/changelogs/398.txt new file mode 100644 index 000000000..a728131a9 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/398.txt @@ -0,0 +1,4 @@ +* 搜尋個別對話 +* 如果消息未能傳遞,通知使用者 +* 在重新啟動後保留來自 Quicksy 使用者的顯示名稱(暱稱) +* 如果需要,新增從通知啟動 Orbot(洋蔥路由器)的按鈕 diff --git a/fastlane/metadata/android/zh-TW/changelogs/401.txt b/fastlane/metadata/android/zh-TW/changelogs/401.txt new file mode 100644 index 000000000..b24fec2cb --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/401.txt @@ -0,0 +1,2 @@ +* 修復在 Android <= 5 上的搜尋問題 +* 優化內存消耗 diff --git a/fastlane/metadata/android/zh-TW/changelogs/402.txt b/fastlane/metadata/android/zh-TW/changelogs/402.txt new file mode 100644 index 000000000..1897128a2 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/402.txt @@ -0,0 +1,3 @@ +* 在支援的伺服器上提供簡易邀請生成 +* 顯示從 Movim 發送的 GIF +* 將頭像存儲在快取中 diff --git a/fastlane/metadata/android/zh-TW/changelogs/403.txt b/fastlane/metadata/android/zh-TW/changelogs/403.txt new file mode 100644 index 000000000..e3e5e57f1 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/403.txt @@ -0,0 +1,3 @@ +* 修復使用不同 SCRAM 機制的不同帳戶時的連接問題 +* 新增對 SCRAM-SHA-512 的支援 +* 允許自己與自己進行 P2P(Jingle)文件傳輸 diff --git a/fastlane/metadata/android/zh-TW/changelogs/404.txt b/fastlane/metadata/android/zh-TW/changelogs/404.txt new file mode 100644 index 000000000..60d555681 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/404.txt @@ -0,0 +1 @@ +* 對音訊/視訊通話進行了小幅度的穩定性改進 diff --git a/fastlane/metadata/android/zh-TW/changelogs/405.txt b/fastlane/metadata/android/zh-TW/changelogs/405.txt new file mode 100644 index 000000000..189b5a041 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/405.txt @@ -0,0 +1 @@ +* Quicksy:自動接收驗證簡訊 diff --git a/fastlane/metadata/android/zh-TW/changelogs/407.txt b/fastlane/metadata/android/zh-TW/changelogs/407.txt new file mode 100644 index 000000000..bcc3aca98 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/407.txt @@ -0,0 +1,3 @@ +* 如果離線的聯絡人之前宣告支援,則顯示通話按鈕 +* 當通話已連接時,返回按鈕不再結束通話 +* 錯誤修復 diff --git a/fastlane/metadata/android/zh-TW/changelogs/42000.txt b/fastlane/metadata/android/zh-TW/changelogs/42000.txt new file mode 100644 index 000000000..3e4923271 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/42000.txt @@ -0,0 +1,4 @@ +* 可選擇來電鈴聲 +* 修復 開放金鑰匙圈 5.6+ 的 開放PGP 金鑰 ID 查找問題 +* 正確驗證 punycode TLS 證書 +* 改進 RTP 會話建立的穩定性(呼叫) diff --git a/fastlane/metadata/android/zh-TW/changelogs/42006.txt b/fastlane/metadata/android/zh-TW/changelogs/42006.txt new file mode 100644 index 000000000..d435a9bfb --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/42006.txt @@ -0,0 +1,2 @@ +* 使用預先存在的 OMEMO 會話驗證音訊/視訊通話 +* 改進與非 libwebrtc WebRTC 實現的相容性 diff --git a/fastlane/metadata/android/zh-TW/changelogs/42010.txt b/fastlane/metadata/android/zh-TW/changelogs/42010.txt new file mode 100644 index 000000000..2ff727219 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/42010.txt @@ -0,0 +1,2 @@ +* 針對 Tor 支援進行了各種錯誤修復 +* 改進與 Dino 的通話相容性 diff --git a/fastlane/metadata/android/zh-TW/changelogs/42012.txt b/fastlane/metadata/android/zh-TW/changelogs/42012.txt new file mode 100644 index 000000000..5e5b341fb --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/42012.txt @@ -0,0 +1 @@ +* 修復對不信任系統 CA 的用戶的 HTTP 上傳/下載問題 diff --git a/fastlane/metadata/android/zh-TW/changelogs/42013.txt b/fastlane/metadata/android/zh-TW/changelogs/42013.txt new file mode 100644 index 000000000..51d6c51b6 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/42013.txt @@ -0,0 +1 @@ +* 修復在 Android 7.1 上的「無連線」問題 diff --git a/fastlane/metadata/android/zh-TW/changelogs/42014.txt b/fastlane/metadata/android/zh-TW/changelogs/42014.txt new file mode 100644 index 000000000..f2106a3bd --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/42014.txt @@ -0,0 +1,2 @@ +* 總是驗證域名。不允許使用者覆寫 +* 支援花名冊預先驗證 diff --git a/fastlane/metadata/android/zh-TW/changelogs/42047.txt b/fastlane/metadata/android/zh-TW/changelogs/42047.txt new file mode 100644 index 000000000..a63fcf276 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/42047.txt @@ -0,0 +1 @@ +* 修正 UnifiedPush 散發者當機 diff --git a/fastlane/metadata/android/zh-TW/changelogs/42050.txt b/fastlane/metadata/android/zh-TW/changelogs/42050.txt new file mode 100644 index 000000000..5e10020c6 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/42050.txt @@ -0,0 +1 @@ +* 增加設定檔圖片的圓角半徑 diff --git a/gradle.properties b/gradle.properties index 431e485f2..b0323e6c6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,5 @@ android.useAndroidX=true android.enableJetifier=true +android.nonTransitiveRClass=true +android.nonFinalResIds=false org.gradle.jvmargs=-Xmx4096m diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9d41003b2..f3491507e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Nov 14 09:59:55 CET 2020 +#Sun Dec 17 21:30:13 CET 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/proguard-rules.pro b/proguard-rules.pro index 7e4d7d31d..03044d525 100644 --- a/proguard-rules.pro +++ b/proguard-rules.pro @@ -64,7 +64,21 @@ -dontwarn retrofit2.KotlinExtensions -dontwarn retrofit2.KotlinExtensions$* + # With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy # and replaces all potential values with null. Explicitly keeping the interfaces prevents this. -if interface * { @retrofit2.http.* ; } -keep,allowobfuscation interface <1> + +# Keep inherited services. +-if interface * { @retrofit2.http.* ; } +-keep,allowobfuscation interface * extends <1> + +# With R8 full mode generic signatures are stripped for classes that are not +# kept. Suspend functions are wrapped in continuations where the type argument +# is used. +-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation + +# R8 full mode strips generic signatures from return types if not kept. +-if interface * { @retrofit2.http.* public *** *(...); } +-keep,allowoptimization,allowshrinking,allowobfuscation class <3> diff --git a/src/conversations/AndroidManifest.xml b/src/conversations/AndroidManifest.xml index c79e4e265..87c925fe6 100644 --- a/src/conversations/AndroidManifest.xml +++ b/src/conversations/AndroidManifest.xml @@ -24,57 +24,8 @@ android:launchMode="singleTask" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:launchMode="singleTask" /> diff --git a/fastlane/metadata/android/da-DK/short_description.txt b/src/conversations/fastlane/metadata/android/da-DK/short_description.txt similarity index 100% rename from fastlane/metadata/android/da-DK/short_description.txt rename to src/conversations/fastlane/metadata/android/da-DK/short_description.txt diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/src/conversations/fastlane/metadata/android/de-DE/full_description.txt similarity index 100% rename from fastlane/metadata/android/de-DE/full_description.txt rename to src/conversations/fastlane/metadata/android/de-DE/full_description.txt diff --git a/fastlane/metadata/android/de-DE/short_description.txt b/src/conversations/fastlane/metadata/android/de-DE/short_description.txt similarity index 100% rename from fastlane/metadata/android/de-DE/short_description.txt rename to src/conversations/fastlane/metadata/android/de-DE/short_description.txt diff --git a/fastlane/metadata/android/en-US/full_description.txt b/src/conversations/fastlane/metadata/android/en-US/full_description.txt similarity index 100% rename from fastlane/metadata/android/en-US/full_description.txt rename to src/conversations/fastlane/metadata/android/en-US/full_description.txt diff --git a/src/conversations/fastlane/metadata/android/en-US/images/icon.png b/src/conversations/fastlane/metadata/android/en-US/images/icon.png new file mode 100644 index 000000000..046dbfb3a Binary files /dev/null and b/src/conversations/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/fastlane/metadata/android/en-US/short_description.txt b/src/conversations/fastlane/metadata/android/en-US/short_description.txt similarity index 100% rename from fastlane/metadata/android/en-US/short_description.txt rename to src/conversations/fastlane/metadata/android/en-US/short_description.txt diff --git a/src/conversations/fastlane/metadata/android/eo/short_description.txt b/src/conversations/fastlane/metadata/android/eo/short_description.txt new file mode 100644 index 000000000..514cca5bd --- /dev/null +++ b/src/conversations/fastlane/metadata/android/eo/short_description.txt @@ -0,0 +1 @@ +Ĉifrita, facile uzebla XMPP tujmesaĝilo por via poŝtelefono diff --git a/src/conversations/fastlane/metadata/android/es-ES/full_description.txt b/src/conversations/fastlane/metadata/android/es-ES/full_description.txt new file mode 100644 index 000000000..853549547 --- /dev/null +++ b/src/conversations/fastlane/metadata/android/es-ES/full_description.txt @@ -0,0 +1,39 @@ +Fácil de usar, fiable y con poca batería. Con soporte integrado para imágenes, chats de grupo y cifrado e2e. + +Principios de diseño: + +* Ser lo más bonito y fácil de usar posible sin sacrificar la seguridad ni la privacidad. +* Basarse en protocolos existentes y bien establecidos. +* No requerir una cuenta de Google o, específicamente, Google Cloud Messaging (GCM). +* Requerir el menor número de permisos posible + +Características: + +* Cifrado de extremo a extremo con OMEMO o OpenPGP. +* Envío y recepción de imágenes +* Llamadas de audio y vídeo cifradas (DTLS-SRTP) +* Interfaz de usuario intuitiva que sigue las directrices de diseño de Android +* Imágenes / Avatares para tus contactos +* Sincronización con el cliente de escritorio +* Conferencias (con soporte para marcadores) +* Integración de la libreta de direcciones +* Múltiples cuentas / bandeja de entrada unificada +* Muy bajo impacto en la duración de la batería + +Conversations hace que sea muy fácil crear una cuenta en el servidor gratuito conversations.im. Sin embargo, Conversations también funciona con cualquier otro servidor XMPP. Muchos servidores XMPP están gestionados por voluntarios y son gratuitos. + +Características de XMPP: + +Conversations funciona con todos los servidores XMPP existentes. Sin embargo, XMPP es un protocolo extensible. Estas extensiones también están estandarizadas en los llamados XEP. Conversations soporta un par de ellas para mejorar la experiencia general del usuario. Existe la posibilidad de que su actual servidor XMPP no soporte estas extensiones. Por lo tanto, para sacar el máximo provecho de Conversaciones deberías considerar o bien cambiar a un servidor XMPP que lo haga o - mejor aún - ejecutar tu propio servidor XMPP para ti y tus amigos. + +Estos XEPs son (por el momento): + +* XEP-0065: SOCKS5 Bytestreams (o mod_proxy65). Se utilizará para transferir archivos si ambas partes están detrás de un cortafuegos (NAT). +* XEP-0163: Protocolo de Evento Personal para avatares +* XEP-0191: El comando de bloqueo te permite hacer una lista negra de spammers o bloquear contactos sin eliminarlos de tu lista. +* XEP-0198: Stream Management permite a XMPP sobrevivir a pequeños cortes de red y cambios de la conexión TCP subyacente. +* XEP-0280: Message Carbons que sincroniza automáticamente los mensajes que envías a tu cliente de escritorio y por lo tanto te permite cambiar sin problemas de tu cliente móvil a tu cliente de escritorio y viceversa en una sola conversación. +* XEP-0237: Versionado de listas, principalmente para ahorrar ancho de banda en conexiones móviles deficientes. +* XEP-0313: Gestión de Archivo de Mensajes sincroniza el historial de mensajes con el servidor. Ponerse al día con los mensajes que fueron enviados mientras Conversaciones estaba fuera de línea. +* XEP-0352: Indicación del Estado del Cliente permite al servidor saber si Conversaciones está o no en segundo plano. Permite al servidor ahorrar ancho de banda reteniendo paquetes sin importancia. +* XEP-0363: Carga de Archivos HTTP permite compartir archivos en conferencias y con contactos sin conexión. Requiere un componente adicional en su servidor. diff --git a/src/conversations/fastlane/metadata/android/es-ES/short_description.txt b/src/conversations/fastlane/metadata/android/es-ES/short_description.txt new file mode 100644 index 000000000..7ed047672 --- /dev/null +++ b/src/conversations/fastlane/metadata/android/es-ES/short_description.txt @@ -0,0 +1 @@ +Mensajería instantánea XMPP cifrada y fácil de usar para tu dispositivo móvil diff --git a/src/conversations/fastlane/metadata/android/fi-FI/full_description.txt b/src/conversations/fastlane/metadata/android/fi-FI/full_description.txt new file mode 100644 index 000000000..8071c92d0 --- /dev/null +++ b/src/conversations/fastlane/metadata/android/fi-FI/full_description.txt @@ -0,0 +1,39 @@ +Helppokäyttöinen, luotettava ja vähän akkua käyttävä. Sisäänrakennettu tuki kuville, ryhmille ja päästä päähän -salaukselle. + +Periaatteet: + +* Ole niin kaunis ja helppokäyttöinen kuin on mahdolista turvallisuudesta ja ykstyisyydestä tinkimättä +* Käytä valmiita ja vakiintuneita protokollia +* Älä riipu Google-tunnuksesta äläkä Google Cloud Messaging -palvelusta (GCM) +* Vaadi niin vähän käyttöoikeuksia kuin mahdollista + +Ominaisuudet: + +* Päästä päähän -salaus joko OMEMO:lla tai OpenPGP:llä +* Lähetä ja vastaanota kuvia +* Salatut ääni- ja videopuhelut (DTLS-SRTP) +* Intuitiivinen käyttöliittymä joka noudattaa Androidin muotoilukieltä +* Profiilikuvat yhteystiedoille +* Synkronoi työpöytäversion kanssa +* Konferenssit (kirjanmerkkituella) +* Osoitekirjaintegrointi +* Useampi tili yhdessä näkymässä +* Todella pieni akun kulutus + +Conversations:lla on helppo luoda tili conversations.im-palvelimella. Silti Conversations toimii myös minkä tahansa muun XMPP-palvelimen kanssa. Monia XMPP-palvelimia ylläpidetään ilmaiseksi vapaaehtoisvoimin. + +XMPP-ominaisuudet: + +Conversations toimii kaikkien XMPP-palvelinten kanssa. XMPP on kuitenkin laajennettava protokolla. Nämä laajennukset on standardoitu niin kutsuttuina XEP:inä. Conversations tukee muutamaa näistä tehdäkseen käyttäjäkokemuksesta paremman. On mahdollista että nykyinen XMPP-palvelimesi ei tue kaikkia näitä laajennoksia. Siispä saadaksesi kaiken ilon irti Conversationsista kannattaa harkita joko sellaiseen palvelimeen, joka tukee näitä, vaihtamista tai oman XMPP-palvelimen ylläpitämistä itsellesi ja kavereillesi. + +XEP:t ovat tällä hetkellä: + +* XEP-0065: SOCKS5 Bytestreams (tai mod_proxy65). Käytetään tiedostojen siirtoon jos molemmat osapuolet ovat palomuurin tai NAT:n takana. +* XEP-0163: Personal Eventing Protocol profiilikuville +* XEP-0191: Blocking command lets you blacklist spammers or block contacts without removing them from your roster. +* XEP-0198: Stream Management mahdollistaa XMPP:n selviämisen pienestä verkon pätkimisestä ja TCP-yhteyden muutoksista. +* XEP-0280: Kopiot lähettämistäsi viesteistä muille laitteillesi. Mahdolistaa laitteiden vaihdon kesken keskustelun täysin saumoitta. +* XEP-0237: Roster Versioning säästää dataa heikoila yhteyksillä +* XEP-0313: Message Archive Management synchronize message history with the server. Catch up with messages that were sent while Conversations was offline. +* XEP-0352: Client State Indication lets the server know whether or not Conversations is in the background. Allows the server to save bandwidth by withholding unimportant packages. +* XEP-0363: HTTP File Upload allows you to share files in conferences and with offline contacts. Requires an additional component on your server. diff --git a/src/conversations/fastlane/metadata/android/fi-FI/short_description.txt b/src/conversations/fastlane/metadata/android/fi-FI/short_description.txt new file mode 100644 index 000000000..2713b6efd --- /dev/null +++ b/src/conversations/fastlane/metadata/android/fi-FI/short_description.txt @@ -0,0 +1 @@ +Salattu ja helppokäyttöinen XMPP-pikaviestin mobiililaitteellesi diff --git a/src/conversations/fastlane/metadata/android/gl-ES/full_description.txt b/src/conversations/fastlane/metadata/android/gl-ES/full_description.txt new file mode 100644 index 000000000..9bdcf9042 --- /dev/null +++ b/src/conversations/fastlane/metadata/android/gl-ES/full_description.txt @@ -0,0 +1,40 @@ +Fácil de usar, fiable, baixo consumo de batería. Con soporte para imaxes, conversas en grupo e cifraxe e2e. + +Principios do deseño: + +* Ser tan fermosa e doada de usar como sexa posible sen sacrificar a seguridade ou privacidade +* Apoiarse en protocolos existentes e ben establecidos +* Non precisar dunha Conta de Google ou concretamente Google Cloud Messaging (GCM) +* Solicitar os mínimos permisos posibles + +Características: + +* Cifraxe extremo-a-extremo, ben con OMEMO ou con OpenPGP +* Enviar e recibir imaxes +* Chamadas de audio e vídeo cifradas (DTLS-SRTP) +* Interface intuitiva seguindo as recomendacións Android Design +* Imaxes/Avatares para os Contactos +* Sicronizada co cliente de escritorio +* Conferencias (con soporte para marcadores) +* Integración coa Libreta de enderezos +* Varias contas cunha lista de conversas unificada +* Consumo de enerxía moi baixo + +Con Conversations é moi doado crear unha conta no servidor gratuíto conversations.im. Con todo, Conversations funcionará igualmente con calquera outro servidor XMPP. Existen moitos servidores XMPP xestionados por voluntarios e gratuítos. + +Características de XMPP: + +Conversations funciona con calquera sevidor XMPP, mais XMPP é un protocolo extensible. Estas extensións tamén están estadarizadas nos chamados XEP's. +Conversations da soporte a un par delas que axudan a mellorar a experiencia de uso da aplicación. Pode acontecer que o teu servidor XMPP actual non dé soporte para estas extensións. Por tanto para obter o mellor resultado ao usar Conversations debes ter considerar usar un servidor XMPP que si o faga - ou incluso mellor - xestionar o teu propio servidor para as túas amizades. + +Estes XEPs son - neste intre: + +* XEP-0065: SOCKS5 Bytestreams (ou mod_proxy65). Usado para a transferencia de ficheiros se as dúas partes están detrás dun cortalumes (NAT). +* XEP-0163: Personal Eventing Protocol para os avatares +* XEP-0191: O bloqueo de ordes permiteche bloquear spammer ou contactos sen eliminalos das túas listaxes. +* XEP-0198: Stream Management permite que XMPP sobreviva a caídas da rede e cambios na conexión TCP. +* XEP-0280: Message Carbons permite sincronizar automáticamente as mensaxes co teu cliente de escritorio e por tanto cambiar dun a outro sen perder mensaxes da conversa. +* XEP-0237: Roster Versioning fundamentalmente para aforrar datos en conexións móbiles +* XEP-0313: Message Archive Management sincroniza o historial de mensaxes co servidor. Para obter as mensaxes recibidas cando Conversations non teña conexión. +* XEP-0352: Client State Indication permítelle ao servidor saber se Conversations está a funcionar en segundo plano. Permítelle ao servidor aforrar ancho de banda retendo paquetes de datos de pouca importancia. +* XEP-0363: HTTP File Upload permíteche compartir ficheiros en salas de conferencia e con contactos que non están conectados. Require un compoñente adicional no teu servidor. diff --git a/src/conversations/fastlane/metadata/android/gl-ES/short_description.txt b/src/conversations/fastlane/metadata/android/gl-ES/short_description.txt new file mode 100644 index 000000000..79c77166e --- /dev/null +++ b/src/conversations/fastlane/metadata/android/gl-ES/short_description.txt @@ -0,0 +1 @@ +Mensaxería instantánea XMPP cifrada e fácil de usar para o teu dispositivo móbil diff --git a/fastlane/metadata/android/it-IT/full_description.txt b/src/conversations/fastlane/metadata/android/it-IT/full_description.txt similarity index 100% rename from fastlane/metadata/android/it-IT/full_description.txt rename to src/conversations/fastlane/metadata/android/it-IT/full_description.txt diff --git a/fastlane/metadata/android/it-IT/short_description.txt b/src/conversations/fastlane/metadata/android/it-IT/short_description.txt similarity index 100% rename from fastlane/metadata/android/it-IT/short_description.txt rename to src/conversations/fastlane/metadata/android/it-IT/short_description.txt diff --git a/fastlane/metadata/android/pl-PL/full_description.txt b/src/conversations/fastlane/metadata/android/pl-PL/full_description.txt similarity index 100% rename from fastlane/metadata/android/pl-PL/full_description.txt rename to src/conversations/fastlane/metadata/android/pl-PL/full_description.txt diff --git a/fastlane/metadata/android/pl-PL/short_description.txt b/src/conversations/fastlane/metadata/android/pl-PL/short_description.txt similarity index 100% rename from fastlane/metadata/android/pl-PL/short_description.txt rename to src/conversations/fastlane/metadata/android/pl-PL/short_description.txt diff --git a/src/conversations/fastlane/metadata/android/ro/full_description.txt b/src/conversations/fastlane/metadata/android/ro/full_description.txt new file mode 100644 index 000000000..2d4ae419d --- /dev/null +++ b/src/conversations/fastlane/metadata/android/ro/full_description.txt @@ -0,0 +1,38 @@ +Ușor de utilizat, fiabil, prietenos cu bateria. Cu suport încorporat pentru imagini, discuții de grup și criptare E2E. + +Principii de proiectare: + +* Să fie cât mai frumos și mai ușor de utilizat posibil, fără a sacrifica securitatea sau confidențialitatea. +* Să se bazeze pe protocoale existente și bine stabilite +* Nu necesită un cont Google sau în mod specific Google Cloud Messaging (GCM). +* Să necesite cât mai puține permisiuni posibil + +Caracteristici: + +* Criptare de la un capăt-la-altul (E2E) cu OMEMO sau OpenPGP +* Trimiterea și primirea de imagini +* Apeluri audio și video criptate (DTLS-SRTP) +* Interfață intuitivă care respectă liniile directoare Android Design +* Imagini / Avataruri pentru contactele dvs. +* Se sincronizează cu clientul desktop +* Conferințe (cu suport pentru marcaje) +* Integrare cu lista de contacte +* Conturi multiple / căsuță de mesaje unificată +* Impact foarte redus asupra duratei de viață a bateriei + +Conversations face foarte ușoară crearea unui cont pe serverul gratuit conversations.im. Cu toate acestea, Conversations va funcționa și cu orice alt server XMPP. O mulțime de servere XMPP sunt administrate de voluntari și sunt gratuite. + +Caracteristici XMPP: + +Conversations funcționează cu orice server XMPP existent. Cu toate acestea, XMPP este un protocol extensibil. Aceste extensii sunt, de asemenea, standardizate în așa-numitele XEP-uri. Conversations suportă câteva dintre acestea pentru a îmbunătăți experiența generală a utilizatorului. Există o șansă ca serverul XMPP actual să nu suporte aceste extensii. Prin urmare, pentru a profita la maximum de Conversations, ar trebui să luați în considerare fie trecerea la un server XMPP care să suporte aceste extensii, fie - și mai bine - să rulați propriul server XMPP pentru dumneavoastră și prietenii dumneavoastră. + +Aceste XEP-uri sunt - deocamdată: +* XEP-0065: SOCKS5 Bytestreams (sau mod_proxy65). Va fi utilizat pentru a transfera fișiere dacă ambele părți se află în spatele unui firewall (NAT). +* XEP-0163: Protocol de evenimente personale pentru avatare. +* XEP-0191: Comanda de blocare vă permite să puneți pe lista neagră spamerii sau să blocați contactele fără a le elimina din listă. +* XEP-0198: Stream Management permite XMPP să supraviețuiască unor mici întreruperi de rețea și schimbărilor conexiunii TCP de bază. +* XEP-0280: Message Carbons, care sincronizează automat mesajele pe care le trimiteți în clientul desktop și vă permite astfel să treceți fără probleme de la clientul mobil la clientul desktop și înapoi în cadrul unei singure conversații. +* XEP-0237: Roster Versioning în principal pentru a economisi lățimea de bandă în cazul conexiunilor mobile slabe +* XEP-0313: Gestionarea arhivei de mesaje sincronizează istoricul mesajelor cu serverul. Recuperați mesajele care au fost trimise în timp ce Conversations era deconectat. +* XEP-0352: Client State Indication permite serverului să știe dacă Conversations este sau nu în fundal. Permite serverului să economisească lățimea de bandă prin reținerea pachetelor neimportante. +* XEP-0363: HTTP File Upload vă permite să partajați fișiere în cadrul conferințelor și cu contactele deconectate. Necesită o componentă suplimentară pe serverul dumneavoastră. diff --git a/fastlane/metadata/android/ro/short_description.txt b/src/conversations/fastlane/metadata/android/ro/short_description.txt similarity index 100% rename from fastlane/metadata/android/ro/short_description.txt rename to src/conversations/fastlane/metadata/android/ro/short_description.txt diff --git a/fastlane/metadata/android/sq/full_description.txt b/src/conversations/fastlane/metadata/android/sq/full_description.txt similarity index 100% rename from fastlane/metadata/android/sq/full_description.txt rename to src/conversations/fastlane/metadata/android/sq/full_description.txt diff --git a/fastlane/metadata/android/sq/short_description.txt b/src/conversations/fastlane/metadata/android/sq/short_description.txt similarity index 100% rename from fastlane/metadata/android/sq/short_description.txt rename to src/conversations/fastlane/metadata/android/sq/short_description.txt diff --git a/fastlane/metadata/android/sv-SE/full_description.txt b/src/conversations/fastlane/metadata/android/sv-SE/full_description.txt similarity index 100% rename from fastlane/metadata/android/sv-SE/full_description.txt rename to src/conversations/fastlane/metadata/android/sv-SE/full_description.txt diff --git a/fastlane/metadata/android/sv-SE/short_description.txt b/src/conversations/fastlane/metadata/android/sv-SE/short_description.txt similarity index 100% rename from fastlane/metadata/android/sv-SE/short_description.txt rename to src/conversations/fastlane/metadata/android/sv-SE/short_description.txt diff --git a/src/conversations/fastlane/metadata/android/uk/full_description.txt b/src/conversations/fastlane/metadata/android/uk/full_description.txt new file mode 100644 index 000000000..39971ed55 --- /dev/null +++ b/src/conversations/fastlane/metadata/android/uk/full_description.txt @@ -0,0 +1,39 @@ +Надійний, простий у використанні, ощадливо витрачає заряд акумулятора. Має вбудовану підтримку зображень, групових чатів і наскрізного шифрування. + +Принципи проєктування: + +* Бути максимально красивим та простим у використанні, не жертвуючи безпекою чи конфіденційністю +* Покладатися на існуючі, добре встановлені протоколи +* Не вимагати облікового запису Google, зокрема Google Cloud Messaging (GCM) +* Вимагати якомога менше дозволів + +Функції: + +* Наскрізне шифрування (від відправника до одержувача) за допомогою OMEMO або OpenPGP +* Надсилання та отримання зображень +* Зашифровані голосові та відеодзвінки (DTLS-SRTP) +* Інтуїтивно зрозумілий інтерфейс користувача, який відповідає вказівкам Android Design +* Зображення / Аватари для Ваших контактів +* Синхронізація з настільним клієнтом +* Конференції (з підтримкою закладок) +* Інтеграція адресної книги +* Кілька облікових записів / єдина папка вхідних +* Дуже низький вплив на термін служби акумулятора + +Conversations дозволяє легко створити обліковий запис на безкоштовному сервері conversations.im. Однак Conversations працюватиме також із будь-яким іншим XMPP-сервером. Чимало серверів XMPP обслуговуються волонтерами і є безкоштовними. + +Функції XMPP: + +Conversations працює з будь-яким сервером XMPP. Проте XMPP — розширюваний протокол. Розширення також стандартизовані в так званих XEP. Conversations підтримує кілька з них, щоб покращити загальний досвід користування. Може виявитися, що Ваш поточний сервер XMPP не підтримує цих розширень. Тому, щоб отримати максимум від Conversations, розгляньте перехід на XMPP-сервер з підтримкою цих розширень або — ще краще — запускайте власний сервер XMPP для себе і своїх друзів. + +На даний час підтримуються такі XEP: + +* XEP-0065: SOCKS5 Bytestreams (або mod_proxy65). Використовується для передачі файлів, якщо обидві сторони знаходяться за брандмауером (NAT). +* XEP-0163: персональний протокол подій для аватарів +* XEP-0191: команда блокування дозволяє Вам заносити спамерів у чорний список або блокувати контакти, не видаляючи їх зі свого списку. +* XEP-0198: керування потоками дозволяє XMPP витримувати невеликі перебої в мережі та зміни основного TCP-з'єднання. +* XEP-0280: Message Carbons, який автоматично синхронізує повідомлення, які Ви надсилаєте, на настільний клієнт і, таким чином, дозволяє плавно переключатися з мобільного клієнта на клієнт для настільного ПК і назад протягом однієї розмови. +* XEP-0237: версія списку в основному для економії пропускної здатності при поганих мобільних з'єднаннях +* XEP-0313: керування архівом повідомлень синхронізує історію повідомлень із сервером. Дізнавайтеся про повідомлення, надіслані, поки Conversations був офлайн. +* XEP-0352: індикація стану клієнта повідомляє серверу, чи працює Conversations у фоновому режимі. Дозволяє серверу заощаджувати пропускну здатність, утримуючи неважливі пакети. +* XEP-0363: завантаження файлів HTTP дозволяє обмінюватися файлами в конференціях і з офлайн-контактами. Потрібен додатковий компонент на Вашому сервері. diff --git a/src/conversations/fastlane/metadata/android/uk/short_description.txt b/src/conversations/fastlane/metadata/android/uk/short_description.txt new file mode 100644 index 000000000..300b89277 --- /dev/null +++ b/src/conversations/fastlane/metadata/android/uk/short_description.txt @@ -0,0 +1 @@ +Простий у використанні XMPP-клієнт з підтримкою шифрування для Вашого телефона diff --git a/src/conversations/fastlane/metadata/android/zh-CN/full_description.txt b/src/conversations/fastlane/metadata/android/zh-CN/full_description.txt new file mode 100644 index 000000000..085d21191 --- /dev/null +++ b/src/conversations/fastlane/metadata/android/zh-CN/full_description.txt @@ -0,0 +1,39 @@ +易于使用、性能可靠、电池友好。内置支持图片、群聊和 e2e 加密功能。 + +设计原则: + +* 在不牺牲安全性和隐私性的前提下,尽可能美观易用 +* 依赖现有的、完善的协议 +* 不需要 Google 账号或特定的 Google 云通讯服务(GCM) +* 要求尽可能少的权限 + +特点: + +* 使用 OMEMOOpenPGP 进行端对端加密 +* 发送和接收图片 +* 加密音视频通话(DTLS-SRTP) +* 直观的用户界面,遵循 Android 设计准则 +* 为您的联系人添加图片/头像 +* 与桌面客户端同步 +* 群聊(支持书签功能) +* 通讯录集成 +* 多账号/统一消息栏 +* 对电池寿命的影响非常小 + +Conversations 使在免费的 conversations.im 服务器上创建账号变得非常简单。不过,Conversations 也适用于任何其他 XMPP 服务器。许多 XMPP 服务器都是由志愿者免费运行的。 + +XMPP 功能: + +Conversations 适用于所有 XMPP 服务器。然而,XMPP 是一种可扩展的协议。这些扩展在所谓的 XEP 中也是标准化的。Conversations 支持其中的一些扩展,以使整体用户体验更好。有一种可能是您当前的 XMPP 服务器不支持这些扩展。因此,要想充分使用 Conversations 的功能,您应该考虑切换到支持这些扩展的 XMPP 服务器,甚至有更好的方式,或者为您和您的朋友运行自己的 XMPP 服务器。 + +到目前为止,这些 XEP 是: + +* XEP-0065:SOCKS5 字节流(or mod_proxy65)。如果双方都在防火墙(NAT)后面,将用于传输文件。 +* XEP-0163:个人事件协议(头像) +* XEP-0191:屏蔽命令可让您将垃圾消息发送者列入黑名单或屏蔽的联系人中,而不会将其从花名册中删除。 +* XEP-0198:流管理允许 XMPP 在小规模网络中断和底层 TCP 连接发生变化时继续运行。 +* XEP-0280:消息抄送,可自动将您发送的消息同步到桌面客户端,因此您可以在一次对话中从手机客户端无缝切换到桌面客户端,然后再返回。 +* XEP-0237:花名册版本控制主要是为了在移动连接不佳的情况下节省带宽 +* XEP-0313:消息存档管理与服务器同步消息历史记录。补发 Conversations 离线时发送的消息。 +* XEP-0352:客户端状态指示让服务器知道 Conversations 是否在后台。允许服务器保留不重要的数据包,从而节省带宽。 +* XEP-0363:通过 HTTP 文件上传功能,您可以在群聊中与离线联系人分享文件。需要在服务器上安装额外组件。 diff --git a/src/conversations/fastlane/metadata/android/zh-CN/short_description.txt b/src/conversations/fastlane/metadata/android/zh-CN/short_description.txt new file mode 100644 index 000000000..913d97480 --- /dev/null +++ b/src/conversations/fastlane/metadata/android/zh-CN/short_description.txt @@ -0,0 +1 @@ +加密、易于使用的 XMPP 即时通讯软件,适用于您的移动设备 diff --git a/src/conversations/fastlane/metadata/android/zh-TW/full_description.txt b/src/conversations/fastlane/metadata/android/zh-TW/full_description.txt new file mode 100644 index 000000000..0e9114c98 --- /dev/null +++ b/src/conversations/fastlane/metadata/android/zh-TW/full_description.txt @@ -0,0 +1,39 @@ +易於使用、可靠、省電,且帶有內建圖像支援、群組聊天和端對端加密的 XMPP 用戶端。 + +設計原則: + +* 在不犧牲安全或隱私權的前提下,盡可能地保持美觀性和易用性 +* 仰賴現存的、已建立的通訊協定 +* 不需要 Google 帳戶或特別的 Google 雲端訊息 (GCM) +* 需要少量可能的權限 + +功能: + +* OMEMOOpenPGP 端對端加密 +* 傳送並接收圖像 +* 加密的音訊和視訊通話 (DTLS-SRTP) +* 依循 Android 設計指南的直覺化 UI +* 為您的聯絡人顯示圖片/ 頭像 +* 與桌面用戶端同步 +* 會議 (書籤支援) +* 通訊錄整合 +* 多個帳戶/整合收件匣 +* 對電池壽命的極低影響 + +Conversations 使在免費的 conversations.im 伺服器上建立一個帳戶變得極為輕易。然而 Conversations 也可在其他 XMPP 伺服器上運作,很多 XMPP 伺服器是由志工驅動的,並且完全免費。 + +XMPP 功能: + +Conversations 可以在所有 XMPP 伺服器上運作。然而,XMPP 是一個可以擴充的通訊協定,這些擴充功能在所謂的 XEP 中也是標準化的。Conversations 支援其中的幾個,已使使用者體驗更佳。有可能您目前的 XMPP 伺服器並不支援這些擴充功能,因此,為了最大限度的發揮 Conversations 的作用,您應該考慮切換到一個支援這些擴充功能的 XMPP 伺服器,或者甚至更好——為您和您的朋友驅動您自己的 XMPP 伺服器。 + +如下 XEP - 截止目前: + +* XEP-0065:SOCKS5 位元資料流 (或 mod_proxy65),將被用於傳輸檔案,如果雙方都在防火牆之後 (NAT)。 +* XEP-0163:用於虛擬化身的私人活動通訊協定 +* XEP-0191:封鎖命令可讓您將濫發垃圾郵件者列入黑名單,或封鎖聯絡人而不把他們從名冊中移除。 +* XEP-0198:串流管理允許 XMPP 在小型網路中斷和基礎 TCP 連線的變更中生存。 +* XEP-0280:訊息副本,自動將您傳送的訊息同步至桌面用戶端,從而允許您在一次會話中從您的行動用戶端無縫切換到您的桌面用戶端。 +* XEP-0237:名冊版本管理,主要是為了節省行動連線不佳時的頻寬。 +* XEP-0313:訊息封存管理將訊息記錄與伺服器同步,隨時掌握離線傳送的訊息。 +* XEP-0352:用戶端狀態指示可讓伺服器知道 Conversations 是否在背景,允許伺服器透過扣留不必要的封裝來節省頻寬。 +* XEP-0363:HTTP 檔案上傳允許您在會議中或與離線聯絡人分享檔案,需要在您的伺服器上有一個額外的元件。 diff --git a/fastlane/metadata/android/zh-TW/short_description.txt b/src/conversations/fastlane/metadata/android/zh-TW/short_description.txt similarity index 100% rename from fastlane/metadata/android/zh-TW/short_description.txt rename to src/conversations/fastlane/metadata/android/zh-TW/short_description.txt diff --git a/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java b/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java index c118d7375..377070941 100644 --- a/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java +++ b/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java @@ -6,6 +6,7 @@ import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; @@ -22,6 +23,21 @@ import androidx.core.app.NotificationManagerCompat; import com.google.common.base.Charsets; import com.google.common.base.Stopwatch; import com.google.common.io.CountingInputStream; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.persistance.DatabaseBackend; +import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.ui.ManageAccountActivity; +import eu.siacs.conversations.utils.BackupFileHeader; +import eu.siacs.conversations.utils.SerialSingleThreadExecutor; +import eu.siacs.conversations.xmpp.Jid; import org.bouncycastle.crypto.engines.AESEngine; import org.bouncycastle.crypto.io.CipherInputStream; @@ -40,50 +56,47 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; import java.util.zip.ZipException; import javax.crypto.BadPaddingException; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.persistance.DatabaseBackend; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.ui.ManageAccountActivity; -import eu.siacs.conversations.utils.BackupFileHeader; -import eu.siacs.conversations.utils.SerialSingleThreadExecutor; -import eu.siacs.conversations.xmpp.Jid; - public class ImportBackupService extends Service { private static final int NOTIFICATION_ID = 21; private static final AtomicBoolean running = new AtomicBoolean(false); private final ImportBackupServiceBinder binder = new ImportBackupServiceBinder(); - private final SerialSingleThreadExecutor executor = new SerialSingleThreadExecutor(getClass().getSimpleName()); - private final Set mOnBackupProcessedListeners = Collections.newSetFromMap(new WeakHashMap<>()); + private final SerialSingleThreadExecutor executor = + new SerialSingleThreadExecutor(getClass().getSimpleName()); + private final Set mOnBackupProcessedListeners = + Collections.newSetFromMap(new WeakHashMap<>()); private DatabaseBackend mDatabaseBackend; private NotificationManager notificationManager; - private static int count(String input, char c) { - int count = 0; - for (char aChar : input.toCharArray()) { - if (aChar == c) { - ++count; - } - } - return count; - } + private static final Collection TABLE_ALLOW_LIST = + Arrays.asList( + Account.TABLENAME, + Conversation.TABLENAME, + Message.TABLENAME, + SQLiteAxolotlStore.PREKEY_TABLENAME, + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + SQLiteAxolotlStore.SESSION_TABLENAME, + SQLiteAxolotlStore.IDENTITIES_TABLENAME); + private static final Pattern COLUMN_PATTERN = Pattern.compile("^[a-zA-Z_]+$"); @Override public void onCreate() { mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext()); - notificationManager = (android.app.NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager = + (android.app.NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); } @Override @@ -105,16 +118,17 @@ public class ImportBackupService extends Service { return START_NOT_STICKY; } if (running.compareAndSet(false, true)) { - executor.execute(() -> { - startForegroundService(); - final boolean success = importBackup(uri, password); - stopForeground(true); - running.set(false); - if (success) { - notifySuccess(); - } - stopSelf(); - }); + executor.execute( + () -> { + startForegroundService(); + final boolean success = importBackup(uri, password); + stopForeground(true); + running.set(false); + if (success) { + notifySuccess(); + } + stopSelf(); + }); } else { Log.d(Config.LOGTAG, "backup already running"); } @@ -126,42 +140,62 @@ public class ImportBackupService extends Service { } public void loadBackupFiles(final OnBackupFilesLoaded onBackupFilesLoaded) { - executor.execute(() -> { - 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))); - 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) { - continue; - } - for (final File file : files) { - if (file.isFile() && file.getName().endsWith(".ceb")) { - try { - final BackupFile backupFile = BackupFile.read(file); - if (accounts.contains(backupFile.getHeader().getJid())) { - Log.d(Config.LOGTAG, "skipping backup for " + backupFile.getHeader().getJid()); - } else { - backupFiles.add(backupFile); + executor.execute( + () -> { + 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))); + 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) { + continue; + } + Log.d(Config.LOGTAG, "looking for backups in " + directory); + for (final File file : files) { + if (file.isFile() && file.getName().endsWith(".ceb")) { + try { + final BackupFile backupFile = BackupFile.read(file); + if (accounts.contains(backupFile.getHeader().getJid())) { + Log.d( + Config.LOGTAG, + "skipping backup for " + + backupFile.getHeader().getJid()); + } else { + backupFiles.add(backupFile); + } + } catch (final IOException + | IllegalArgumentException + | BackupFileHeader.OutdatedBackupFileVersion e) { + Log.d(Config.LOGTAG, "unable to read backup file ", e); + } } - } catch (IOException | IllegalArgumentException e) { - Log.d(Config.LOGTAG, "unable to read backup file ", e); } } - } - } - Collections.sort(backupFiles, (a, b) -> a.header.getJid().toString().compareTo(b.header.getJid().toString())); - onBackupFilesLoaded.onBackupFilesLoaded(backupFiles); - }); + Collections.sort( + backupFiles, + (a, b) -> + a.header + .getJid() + .toString() + .compareTo(b.header.getJid().toString())); + onBackupFilesLoaded.onBackupFilesLoaded(backupFiles); + }); } private void startForegroundService() { @@ -180,14 +214,16 @@ public class ImportBackupService extends Service { } final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); try { - notificationManager.notify(NOTIFICATION_ID, createImportBackupNotification(max, progress)); + notificationManager.notify( + NOTIFICATION_ID, createImportBackupNotification(max, progress)); } catch (final RuntimeException e) { Log.d(Config.LOGTAG, "unable to make notification", e); } } private Notification createImportBackupNotification(final int max, final int progress) { - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup"); + NotificationCompat.Builder mBuilder = + new NotificationCompat.Builder(getBaseContext(), "backup"); mBuilder.setContentTitle(getString(R.string.restoring_backup)) .setSmallIcon(R.drawable.ic_unarchive_white_24dp) .setProgress(max, progress, max == 1 && progress == 0); @@ -212,7 +248,9 @@ public class ImportBackupService extends Service { fileSize = 0; } else { returnCursor.moveToFirst(); - fileSize = returnCursor.getLong(returnCursor.getColumnIndex(OpenableColumns.SIZE)); + fileSize = + returnCursor.getLong( + returnCursor.getColumnIndexOrThrow(OpenableColumns.SIZE)); returnCursor.close(); } inputStream = getContentResolver().openInputStream(uri); @@ -242,40 +280,46 @@ public class ImportBackupService extends Service { final byte[] key = ExportBackupService.getKey(password, backupFileHeader.getSalt()); final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); - cipher.init(false, new AEADParameters(new KeyParameter(key), 128, backupFileHeader.getIv())); - final CipherInputStream cipherInputStream = new CipherInputStream(countingInputStream, cipher); + cipher.init( + false, + new AEADParameters(new KeyParameter(key), 128, backupFileHeader.getIv())); + final CipherInputStream cipherInputStream = + new CipherInputStream(countingInputStream, cipher); final GZIPInputStream gzipInputStream = new GZIPInputStream(cipherInputStream); - final BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream, Charsets.UTF_8)); + final BufferedReader reader = + new BufferedReader(new InputStreamReader(gzipInputStream, Charsets.UTF_8)); + final JsonReader jsonReader = new JsonReader(reader); + if (jsonReader.peek() == JsonToken.BEGIN_ARRAY) { + jsonReader.beginArray(); + } else { + throw new IllegalStateException("Backup file did not begin with array"); + } db.beginTransaction(); - String line; - StringBuilder multiLineQuery = null; - while ((line = reader.readLine()) != null) { - int count = count(line, '\''); - if (multiLineQuery != null) { - multiLineQuery.append('\n'); - multiLineQuery.append(line); - if (count % 2 == 1) { - db.execSQL(multiLineQuery.toString()); - multiLineQuery = null; - updateImportBackupNotification(fileSize, countingInputStream.getCount()); - } - } else { - if (count % 2 == 0) { - db.execSQL(line); - updateImportBackupNotification(fileSize, countingInputStream.getCount()); - } else { - multiLineQuery = new StringBuilder(line); - } + while (jsonReader.hasNext()) { + if (jsonReader.peek() == JsonToken.BEGIN_OBJECT) { + importRow(db, jsonReader, backupFileHeader.getJid(), password); + } else if (jsonReader.peek() == JsonToken.END_ARRAY) { + jsonReader.endArray(); + continue; } + updateImportBackupNotification(fileSize, countingInputStream.getCount()); } db.setTransactionSuccessful(); db.endTransaction(); final Jid jid = backupFileHeader.getJid(); - final Cursor countCursor = db.rawQuery("select count(messages.uuid) from messages join conversations on conversations.uuid=messages.conversationUuid join accounts on conversations.accountUuid=accounts.uuid where accounts.username=? and accounts.server=?", new String[]{jid.getEscapedLocal(), jid.getDomain().toEscapedString()}); + final Cursor countCursor = + db.rawQuery( + "select count(messages.uuid) from messages join conversations on conversations.uuid=messages.conversationUuid join accounts on conversations.accountUuid=accounts.uuid where accounts.username=? and accounts.server=?", + new String[] { + jid.getEscapedLocal(), jid.getDomain().toEscapedString() + }); countCursor.moveToFirst(); final int count = countCursor.getInt(0); - Log.d(Config.LOGTAG, String.format("restored %d messages in %s", count, stopwatch.stop().toString())); + Log.d( + Config.LOGTAG, + String.format( + "restored %d messages in %s", count, stopwatch.stop().toString())); countCursor.close(); stopBackgroundService(); synchronized (mOnBackupProcessedListeners) { @@ -286,7 +330,8 @@ public class ImportBackupService extends Service { return true; } catch (final Exception e) { final Throwable throwable = e.getCause(); - final boolean reasonWasCrypto = throwable instanceof BadPaddingException || e instanceof ZipException; + final boolean reasonWasCrypto = + throwable instanceof BadPaddingException || e instanceof ZipException; synchronized (mOnBackupProcessedListeners) { for (OnBackupProcessed l : mOnBackupProcessedListeners) { if (reasonWasCrypto) { @@ -301,14 +346,75 @@ public class ImportBackupService extends Service { } } + private void importRow( + final SQLiteDatabase db, + final JsonReader jsonReader, + final Jid account, + final String passphrase) + throws IOException { + jsonReader.beginObject(); + final String firstParameter = jsonReader.nextName(); + if (!firstParameter.equals("table")) { + throw new IllegalStateException("Expected key 'table'"); + } + final String table = jsonReader.nextString(); + if (!TABLE_ALLOW_LIST.contains(table)) { + throw new IOException(String.format("%s is not recognized for import", table)); + } + final ContentValues contentValues = new ContentValues(); + final String secondParameter = jsonReader.nextName(); + if (!secondParameter.equals("values")) { + throw new IllegalStateException("Expected key 'values'"); + } + jsonReader.beginObject(); + while (jsonReader.peek() != JsonToken.END_OBJECT) { + final String name = jsonReader.nextName(); + if (COLUMN_PATTERN.matcher(name).matches()) { + if (jsonReader.peek() == JsonToken.NULL) { + jsonReader.nextNull(); + contentValues.putNull(name); + } else if (jsonReader.peek() == JsonToken.NUMBER) { + contentValues.put(name, jsonReader.nextLong()); + } else { + contentValues.put(name, jsonReader.nextString()); + } + } else { + throw new IOException(String.format("Unexpected column name %s", name)); + } + } + jsonReader.endObject(); + jsonReader.endObject(); + if (Account.TABLENAME.equals(table)) { + final Jid jid = + Jid.of( + contentValues.getAsString(Account.USERNAME), + contentValues.getAsString(Account.SERVER), + null); + final String password = contentValues.getAsString(Account.PASSWORD); + if (jid.equals(account) && passphrase.equals(password)) { + Log.d(Config.LOGTAG, "jid and password from backup header had matching row"); + } else { + throw new IOException("jid or password in table did not match backup"); + } + } + db.insert(table, null, contentValues); + } + private void notifySuccess() { - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup"); + NotificationCompat.Builder mBuilder = + new NotificationCompat.Builder(getBaseContext(), "backup"); mBuilder.setContentTitle(getString(R.string.notification_restored_backup_title)) .setContentText(getString(R.string.notification_restored_backup_subtitle)) .setAutoCancel(true) - .setContentIntent(PendingIntent.getActivity(this, 145, new Intent(this, ManageAccountActivity.class), s() - ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT)) + .setContentIntent( + PendingIntent.getActivity( + this, + 145, + new Intent(this, ManageAccountActivity.class), + s() + ? PendingIntent.FLAG_IMMUTABLE + | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT)) .setSmallIcon(R.drawable.ic_unarchive_white_24dp); notificationManager.notify(NOTIFICATION_ID, mBuilder.build()); } @@ -391,4 +497,4 @@ public class ImportBackupService extends Service { return ImportBackupService.this; } } -} \ No newline at end of file +} diff --git a/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java b/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java index 0e4772862..342aa0144 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java @@ -30,6 +30,7 @@ import eu.siacs.conversations.databinding.DialogEnterPasswordBinding; import eu.siacs.conversations.services.ImportBackupService; import eu.siacs.conversations.ui.adapter.BackupFileAdapter; import eu.siacs.conversations.ui.util.SettingsUtils; +import eu.siacs.conversations.utils.BackupFileHeader; import eu.siacs.conversations.utils.ThemeHelper; public class ImportBackupActivity extends ActionBarActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed { @@ -135,6 +136,8 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo try { final ImportBackupService.BackupFile backupFile = ImportBackupService.BackupFile.read(this, uri); showEnterPasswordDialog(backupFile, finishOnCancel); + } catch (final BackupFileHeader.OutdatedBackupFileVersion e) { + Snackbar.make(binding.coordinator, R.string.outdated_backup_file_format, Snackbar.LENGTH_LONG).show(); } catch (final IOException | IllegalArgumentException e) { Log.d(Config.LOGTAG, "unable to open backup file " + uri, e); Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG).show(); diff --git a/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java index c8de942b6..22e208b90 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -372,6 +372,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda private void enableAccount(Account account) { account.setOption(Account.OPTION_DISABLED, false); + account.setOption(Account.OPTION_SOFT_DISABLED, false); final XmppConnection connection = account.getXmppConnection(); if (connection != null) { connection.resetEverything(); diff --git a/src/conversations/res/drawable/ic_launcher_foreground.xml b/src/conversations/res/drawable/ic_launcher_foreground.xml index 07a7ee7eb..5851e5f2c 100644 --- a/src/conversations/res/drawable/ic_launcher_foreground.xml +++ b/src/conversations/res/drawable/ic_launcher_foreground.xml @@ -1,24 +1,13 @@ - - - + android:width="108dp" + android:height="108dp" + android:viewportWidth="1146.7721" + android:viewportHeight="1146.7721"> + + diff --git a/src/conversations/res/drawable/ic_launcher_monochrome.xml b/src/conversations/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 000000000..56895d605 --- /dev/null +++ b/src/conversations/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/src/conversations/res/layout/dialog_enter_password.xml b/src/conversations/res/layout/dialog_enter_password.xml index 40f3ba34d..29b50d4ef 100644 --- a/src/conversations/res/layout/dialog_enter_password.xml +++ b/src/conversations/res/layout/dialog_enter_password.xml @@ -22,6 +22,13 @@ android:text="@string/restore_warning" android:textAppearance="@style/TextAppearance.Conversations.Body1"/> + + Πατήστε το πλήκτρο διαμοιρασμού για να στείλετε στην επαφή σας μια πρόσκληση στο %1$s. Αν η επαφή σας βρίσκεται κοντά σας, μπορεί επίσης να σαρώσει τον κωδικό παρακάτω για να αποδεχτεί την πρόσκλησή σας. Μπείτε στο %1$s και συνομιλήστε μαζί μου: %2$s - Διαμοιρασμός πρόσκλησης με... + Διαμοιρασμός πρόσκλησης με… \ No newline at end of file diff --git a/src/conversations/res/values-eo/strings.xml b/src/conversations/res/values-eo/strings.xml new file mode 100644 index 000000000..d5fc7d80e --- /dev/null +++ b/src/conversations/res/values-eo/strings.xml @@ -0,0 +1,20 @@ + + + Uzi conversations.im + Aliĝu al %1$s kaj babilu kun mi: %2$s + Vi estis invitita al %1$s. Ni gvidos vin tra la procezo de kreado de konto. +\nElektante %1$s kiel provizanton vi povos komuniki kun uzantoj de aliaj provizantoj donante al ili vian plenan XMPP-adreson. + Ĉu vi jam havas XMPP-konton\? Ĉi tio povus esti la kazo se vi jam uzas alian XMPP-klienton aŭ antaŭe uzis Conversations. Se ne, vi povas krei novan XMPP-konton nun. +\nKonsileto: Iuj retpoŝtaj provizantoj ankaŭ provizas XMPP-kontojn. + Se via kontakto estas proksime, ili ankaŭ povas skani la suban kodon por akcepti vian inviton. + Elekti vian XMPP-provizanton + Kunhavigi inviton kun… + XMPP estas provizanta sendependa tujmesaĝa reto. Vi povas uzi ĉi tiun klienton per kia ajn XMPP-servilo, kiun vi elektas. +\nTamen por via komforto ni faciligis krei konton ĉe conversations.im; provizanto speciale taŭga por la uzo kun Conversations. + Vi estis invitita al %1$s. Uzantnomo jam estas elektita por vi. Ni gvidos vin tra la procezo de kreado de konto. +\nVi povos komuniki kun uzantoj de aliaj provizantoj donante al ili vian plenan XMPP-adreson. + Nedece formatita provizokodo + Premu la kunhavigi butonon por sendi al via kontakto inviton al %1$s. + Via servila invito + Krei novan konton + \ No newline at end of file diff --git a/src/conversations/res/values-fr/strings.xml b/src/conversations/res/values-fr/strings.xml index 47badf219..f0c96726f 100644 --- a/src/conversations/res/values-fr/strings.xml +++ b/src/conversations/res/values-fr/strings.xml @@ -9,8 +9,8 @@ Vous avez été invité à %1$s. Un nom d’utilisateur a déjà été choisi pour vous. Nous allons vous guider à travers le processus de création d’un compte.\nVous pourrez communiquer avec les utilisateurs des autres fournisseurs en leur donnant votre adresse XMPP complète. Votre invitation au serveur Code de provisionnement mal formaté - Appuyez sur le bouton partager pour envoyer à votre contact une invitation pour %1$s - Si vos contacts sont à votre côté, ils peuvent aussi scanner le code ci dessous pour accepter votre invitation + Appuyez sur le bouton partager pour envoyer à votre contact une invitation pour %1$s. + Si vos contacts sont à proximité, ils peuvent aussi scanner le code ci-dessous pour accepter votre invitation. Rejoignez %1$set discutez avec moi : %2$s - Partager une invitation avec ... + Partager une invitation avec … \ No newline at end of file diff --git a/src/conversations/res/values-sk/strings.xml b/src/conversations/res/values-sk/strings.xml index ed58bbefb..e280344c4 100644 --- a/src/conversations/res/values-sk/strings.xml +++ b/src/conversations/res/values-sk/strings.xml @@ -10,5 +10,5 @@ Ťuknite na tlačidlo zdieľať na odoslanie pozvánky do %1$s vášmu kontaktu. Ak je váš kontakt blízko, na prijatie vašej pozvánky si môže nasnímať kód nižšie. Pripojte sa k %1$sa rozprávajte sa so mnou: %2$s - Zdieľať pozvánku s... + Zdieľať pozvánku s… \ No newline at end of file diff --git a/src/conversations/res/values-tr-rTR/strings.xml b/src/conversations/res/values-tr-rTR/strings.xml index 6fb383cf7..415bc89e0 100644 --- a/src/conversations/res/values-tr-rTR/strings.xml +++ b/src/conversations/res/values-tr-rTR/strings.xml @@ -11,6 +11,6 @@ Yanlış ayarlanmış düzenleme kodu Kişinize, %1$s grubuna davet etmek için Paylaş düğmesine basın. Kişiniz yakınınızda ise, aşağıdaki kodu tarayak daveti kabul edebilirler. - %1$s grubuna katıl ve benimle sohpet et: %2$s - Daveti şununla paylaş... + %1$s grubuna katıl ve benimle sohbet et: %2$s + Daveti şununla paylaş… \ No newline at end of file diff --git a/src/conversations/res/values-uk/strings.xml b/src/conversations/res/values-uk/strings.xml index 3b855ab5c..f9e37cea7 100644 --- a/src/conversations/res/values-uk/strings.xml +++ b/src/conversations/res/values-uk/strings.xml @@ -3,10 +3,18 @@ Виберіть постачальника послуг обміну повідомленнями XMPP Скористатися conversations.im Створити новий обліковий запис - Вже маєте обліковий запис XMPP? Можливо, користуєтеся іншою програмою XMPP або користувалися цією програмою раніше. Якщо ні, можете створити новий обліковий запис XMPP просто зараз.\nЗверніть увагу, що деякі постачальники електронної пошти у той же час надають облікові записи XMPP. - XMPP — це мережа обміну повідомленнями, незалежна від постачальників. Можете використовувати цю програму з будь-яким XMPP сервером, який оберете.\nПроте, для зручності, ми спростили створення облікового запису на conversations.im — у постачальника, який спеціально налаштований на роботу з цією програмою. - Вас запросили до %1$s. Ми проведемо вас крок за кроком, щоб створити обліковий запис.\nОбираючи %1$s в якості свого постачальника, ви зможете спілкуватися з користувачами інших постачальників, для цього повідомте їм свою повну адресу XMPP. - Вас запросили до %1$s. Для вас створено ім\'я користувача. Ми проведемо вас крок за кроком, щоб створити обліковий запис.\nВи зможете спілкуватися з користувачами інших постачальників, для цього повідомите їм свою повну адресу XMPP. + Уже маєте обліковий запис XMPP\? Можливо, користуєтеся іншою програмою XMPP або користувалися Conversations раніше. Якщо ні, можете створити новий обліковий запис XMPP просто зараз. +\nЗверніть увагу, що деякі постачальники електронної пошти у той же час надають облікові записи XMPP. + XMPP — це мережа обміну повідомленнями, незалежна від постачальників. Можете використовувати цю програму з будь-яким XMPP-сервером, який оберете. +\nПроте для зручності ми спростили створення облікового запису на conversations.im — у постачальника, спеціально налаштованого на роботу з Conversations. + Вас запросили до %1$s. Ми проведемо Вас крок за кроком, щоб створити обліковий запис. +\nОбравши %1$s в якості свого постачальника, Ви зможете спілкуватися з користувачами інших постачальників, для цього повідомте їм свою повну адресу XMPP. + Вас запросили до %1$s. Для Вас створено ім\'я користувача. Ми проведемо Вас крок за кроком, щоб створити обліковий запис. +\nВи зможете спілкуватися з користувачами інших постачальників, для цього повідомте їм свою повну адресу XMPP. Ваше запрошення до сервера Неправильно відформатований код забезпечення - \ No newline at end of file + Якщо контакт поблизу, він також може прийняти запрошення, відсканувавши код нижче. + Приєднуйтеся до %1$s і спілкуйтеся зі мною: %2$s + Запросити… + Натисніть «Поділитися», щоб надіслати Вашому контакту запрошення до %1$s. + \ No newline at end of file diff --git a/src/conversations/res/values-vi/strings.xml b/src/conversations/res/values-vi/strings.xml index f80ceacf8..851ad0927 100644 --- a/src/conversations/res/values-vi/strings.xml +++ b/src/conversations/res/values-vi/strings.xml @@ -1,16 +1,20 @@ Chọn nhà cung cấp XMPP của bạn - Sử dụng conversations.im + Sử dụng “conversations.im” Tạo tài khoản mới - Bạn đã có tài khoản XMPP chưa? Điều này có thể đúng nếu bạn đang dùng một ứng dụng khách cho XMPP khác hoặc đã sử dụng Conversations trước đó. Nếu không, bạn có thể tạo tài khoản XMPP mới ngay bây giờ.\nGợi ý: Một số nhà cung cấp email cũng cung cấp tài khoản XMPP. - XMPP là một mạng nhắn tin ngay lập tức không phụ thuộc vào nhà cung cấp. Bạn có thể sử dụng ứng dụng khách này với bất kỳ máy chủ XMPP nào mà bạn chọn.\nTuy nhiên, vì sự thuận tiện của bạn, chúng tôi đã làm cho việc tạo tài khoản trên conversations.im được dễ dàng; một nhà cung cấp đặc biệt phù hợp với việc sử dụng Conversations. - Bạn đã được mời vào %1$s. Chúng tôi sẽ hướng dẫn bạn trong quá trình tạo tài khoản.\nKhi chọn %1$s là nhà cung cấp, bạn sẽ có thể giao tiếp với những người dùng của các nhà cung cấp khác bằng cách đưa cho họ địa chỉ XMPP đầy đủ của bạn. - Bạn đã được mời vào %1$s. Một tên người dùng đã được chọn sẵn cho bạn. Chúng tôi sẽ hướng dẫn bạn trong quá trình tạo tài khoản.\nBạn sẽ có thể giao tiếp với những người dùng của các nhà cung cấp khác bằng cách đưa cho họ địa chỉ XMPP đầy đủ của bạn. + Bạn đã có sẵn một tài khoản XMPP chưa\? Nếu bạn đang dùng một ứng dụng XMPP khác dành cho máy khách (client) hoặc đã sử dụng Conversations trước đó. Nếu chưa có, bạn có thể tạo tài khoản XMPP mới ngay bây giờ. +\nGợi ý: Một số nhà cung cấp dịch vụ email cũng cung cấp tài khoản XMPP. + XMPP là một dịch vụ mạng tin nhắn không phụ thuộc vào nhà cung cấp nào. Bạn có thể sử dụng ứng dụng máy khách này với bất kỳ máy chủ XMPP nào mà bạn chọn. +\nĐể thuận tiện hơn cho bạn, chúng tôi đã đơn giản hóa khâu tạo tài khoản trên conversations.im – một nhà cung cấp đặc biệt phù hợp cho việc sử dụng Conversations. + Bạn đã được mời vào “ %1$s”. Chúng tôi sẽ hướng dẫn bạn xuyên suốt quá trình tạo tài khoản. +\nKhi chọn “%1$s” làm nhà cung cấp, bạn sẽ có thể liên lạc với những người dùng của các nhà cung cấp khác bằng cách đưa cho họ địa chỉ XMPP đầy đủ của bạn. + Bạn đã được mời vào “%1$s.” Một tên người dùng đã được chọn sẵn cho bạn. Chúng tôi sẽ hướng dẫn bạn xuyên suốt quá trình tạo tài khoản. +\nBạn sẽ có thể với những người dùng của các nhà cung cấp khác bằng cách đưa cho họ địa chỉ XMPP đầy đủ của bạn. Lời mời vào máy chủ của bạn - Mã cung cấp không được định dạng đúng - Nhấn nút chia sẻ để gửi lời mời vào %1$s đến liên hệ của bạn. - Nếu liên hệ của bạn ở gần đây, họ cũng có thể quét mã ở dưới để chấp nhận lời mời của bạn. - Hãy tham gia vào %1$s và trò chuyện với tôi: %2$s - Chia sẻ lời mời với... + Mã cung cấp sai định dạng + Nhấn nút chia sẻ để gửi đến liên hệ của bạn một lời mời vào “%1$s”. + Nếu liên hệ của bạn đang ở gần bên bạn, họ có thể quét mã ở dưới để chấp nhận lời mời của bạn. + Hãy tham gia vào “%1$s” và trò chuyện với tôi: %2$s + Chia sẻ lời mời với… \ No newline at end of file diff --git a/src/conversations/res/values-zh-rCN/strings.xml b/src/conversations/res/values-zh-rCN/strings.xml index 961973b8c..34cf35734 100644 --- a/src/conversations/res/values-zh-rCN/strings.xml +++ b/src/conversations/res/values-zh-rCN/strings.xml @@ -2,15 +2,19 @@ 选择您的 XMPP 提供者 使用 conversations.im - 创建新账户 - 您有 XMPP 账户吗?如果您之前使用过其他的 XMPP 客户端,那么您已经拥有这种账户了。如果没有的话,您现在可以创建一个。\n提示:有些电子邮件服务也提供XMPP账户。 - XMPP 是独立于提供者的即时消息网络。您可以将此客户端与任意 XMPP 服务器一同使用。\n不过,您可以很容易地在 conversations.im 上创建账户;它是特别适合与“Conversations”一起使用的提供者。 - 您已受邀加入 %1$s。我们将指导您完成创建帐户的过程。\n使用 %1$s 作为提供者时,您可以通过您的完整 XMPP 地址与其他提供者的用户进行交流。 - 您已受邀加入 %1$s。已为您选择了一个用户名。我们将指导您完成创建帐户的过程。\n您可以使用完整的 XMPP 地址来与其他提供者的用户进行交流。 - 你的服务器邀请 - 格式不正确的配置代码 - 点击分享按钮向您的联系人发送加入 %1$s 的邀请。 - 如果你的联系人在附近,他们也可以扫描下面的代码来接受你的邀请。 + 创建新账号 + 您已经有 XMPP 账号了吗?如果您之前使用过 Conversations 或其他 XMPP 客户端,那么您已经有账号了。如果没有,您可以立即创建一个。 +\n提示:一些电子邮件服务也提供 XMPP 账号。 + XMPP 是独立于提供者的即时通讯网络。您选择的任何 XMPP 服务器都可以使用此客户端。 +\n不过,您可以轻松地在 conversations.im 上创建账号;特别适合与 Conversations 使用的提供者。 + 您已受邀加入 %1$s。我们将指导您创建账号。 +\n当选择 %1$s 作为提供者时,向其他 XMPP 用户提供您的完整地址,就能和对方交流。 + 您已受邀加入 %1$s。已为您选择了用户名。我们将指导您创建账号。 +\n向其他 XMPP 用户提供您的完整地址,就能和对方交流。 + 您的服务器邀请 + 配置代码格式不正确 + 轻击分享按钮,向您的联系人发送加入 %1$s 的邀请。 + 如果您的联系人在附近,对方也可以扫描下方二维码接受邀请。 加入 %1$s 和我聊天:%2$s - 分享邀请… + 分享邀请至… \ No newline at end of file diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 3d4c53d54..d848e4027 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -19,7 +19,6 @@ - - + + + + + + + + + + + - + @@ -77,30 +89,60 @@ + tools:targetApi="tiramisu"> - + + + + + + + + + + + + + + android:exported="false"> - + @@ -114,10 +156,12 @@ + + - + @@ -145,7 +189,6 @@ + android:exported="true"> @@ -260,7 +302,6 @@ @@ -298,7 +339,7 @@ android:value="eu.siacs.conversations.ui.SettingsActivity" /> @@ -315,17 +356,6 @@ - - - - - - - - QUERY_CACHE = + new LruCache<>(1024); + private final Context context; + private final NetworkDataSource networkDataSource = new NetworkDataSource(); + private boolean askForDnssec = false; + + public AndroidDNSClient(final Context context) { + super(); + this.setDataSource(networkDataSource); + this.context = context; + } + + private static String getPrivateDnsServerName(final LinkProperties linkProperties) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + return linkProperties.getPrivateDnsServerName(); + } else { + return null; + } + } + + private static boolean isPrivateDnsActive(final LinkProperties linkProperties) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + return linkProperties.isPrivateDnsActive(); + } else { + return false; + } + } + + @Override + protected DNSMessage.Builder newQuestion(final DNSMessage.Builder message) { + message.setRecursionDesired(true); + message.getEdnsBuilder() + .setUdpPayloadSize(networkDataSource.getUdpPayloadSize()) + .setDnssecOk(askForDnssec); + return message; + } + + @Override + protected DNSMessage query(final DNSMessage.Builder queryBuilder) throws IOException { + final DNSMessage question = newQuestion(queryBuilder).build(); + for (final DNSServer dnsServer : getDNSServers()) { + final QuestionServerTuple cacheKey = new QuestionServerTuple(dnsServer, question); + final DNSMessage cachedResponse = queryCache(cacheKey); + if (cachedResponse != null) { + return cachedResponse; + } + final DNSMessage response = this.networkDataSource.query(question, dnsServer); + if (response == null) { + continue; + } + switch (response.responseCode) { + case NO_ERROR: + case NX_DOMAIN: + break; + default: + continue; + } + cacheQuery(cacheKey, response); + return response; + } + return null; + } + + public boolean isAskForDnssec() { + return askForDnssec; + } + + public void setAskForDnssec(boolean askForDnssec) { + this.askForDnssec = askForDnssec; + } + + private List getDNSServers() { + final ImmutableList.Builder dnsServerBuilder = new ImmutableList.Builder<>(); + final ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + final Network[] networks = getActiveNetworks(connectivityManager); + for (final Network network : networks) { + final LinkProperties linkProperties = connectivityManager.getLinkProperties(network); + if (linkProperties == null) { + continue; + } + final String privateDnsServerName = getPrivateDnsServerName(linkProperties); + if (Strings.isNullOrEmpty(privateDnsServerName)) { + final boolean isPrivateDns = isPrivateDnsActive(linkProperties); + for (final InetAddress dnsServer : linkProperties.getDnsServers()) { + if (isPrivateDns) { + dnsServerBuilder.add(new DNSServer(dnsServer, Transport.TLS)); + } else { + dnsServerBuilder.add(new DNSServer(dnsServer)); + } + } + } else { + dnsServerBuilder.add(new DNSServer(privateDnsServerName, Transport.TLS)); + } + } + return dnsServerBuilder.build(); + } + + private Network[] getActiveNetworks(final ConnectivityManager connectivityManager) { + if (connectivityManager == null) { + return new Network[0]; + } + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + final Network activeNetwork = connectivityManager.getActiveNetwork(); + if (activeNetwork != null) { + return new Network[] {activeNetwork}; + } + } + return connectivityManager.getAllNetworks(); + } + + private DNSMessage queryCache(final QuestionServerTuple key) { + final DNSMessage cachedResponse; + synchronized (QUERY_CACHE) { + cachedResponse = QUERY_CACHE.get(key); + if (cachedResponse == null) { + return null; + } + final long expiresIn = expiresIn(cachedResponse); + if (expiresIn < 0) { + QUERY_CACHE.remove(key); + return null; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Log.d( + Config.LOGTAG, + "DNS query came from cache. expires in " + Duration.ofMillis(expiresIn)); + } + } + return cachedResponse; + } + + private void cacheQuery(final QuestionServerTuple key, final DNSMessage response) { + if (response.receiveTimestamp <= 0) { + return; + } + synchronized (QUERY_CACHE) { + QUERY_CACHE.put(key, response); + } + } + + private static long ttl(final DNSMessage dnsMessage) { + final List> answerSection = dnsMessage.answerSection; + if (answerSection == null || answerSection.isEmpty()) { + final List> authoritySection = dnsMessage.authoritySection; + if (authoritySection == null || authoritySection.isEmpty()) { + return 0; + } else { + return Collections.min(Collections2.transform(authoritySection, d -> d.ttl)); + } + + } else { + return Collections.min(Collections2.transform(answerSection, d -> d.ttl)); + } + } + + private static long expiresAt(final DNSMessage dnsMessage) { + return dnsMessage.receiveTimestamp + (Math.min(DNS_MAX_TTL, ttl(dnsMessage)) * 1000L); + } + + private static long expiresIn(final DNSMessage dnsMessage) { + return expiresAt(dnsMessage) - System.currentTimeMillis(); + } + + private static class QuestionServerTuple { + private final DNSServer dnsServer; + private final DNSMessage question; + + private QuestionServerTuple(final DNSServer dnsServer, final DNSMessage question) { + this.dnsServer = dnsServer; + this.question = question.asNormalizedVersion(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + QuestionServerTuple that = (QuestionServerTuple) o; + return Objects.equal(dnsServer, that.dnsServer) + && Objects.equal(question, that.question); + } + + @Override + public int hashCode() { + return Objects.hashCode(dnsServer, question); + } + } +} diff --git a/src/main/java/de/gultsch/minidns/DNSServer.java b/src/main/java/de/gultsch/minidns/DNSServer.java new file mode 100644 index 000000000..7486ec2c6 --- /dev/null +++ b/src/main/java/de/gultsch/minidns/DNSServer.java @@ -0,0 +1,104 @@ +package de.gultsch.minidns; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterables; + +import java.net.InetAddress; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.annotation.Nonnull; + +public final class DNSServer { + + public final InetAddress inetAddress; + public final String hostname; + public final int port; + public final List transports; + + public DNSServer(InetAddress inetAddress, Integer port, Transport transport) { + this.inetAddress = inetAddress; + this.port = port == null ? 0 : port; + this.transports = Collections.singletonList(transport); + this.hostname = null; + } + + public DNSServer(final String hostname, final Integer port, final Transport transport) { + Preconditions.checkArgument( + Arrays.asList(Transport.HTTPS, Transport.TLS).contains(transport), + "hostname validation only works with TLS based transports"); + this.hostname = hostname; + this.port = port == null ? 0 : port; + this.transports = Collections.singletonList(transport); + this.inetAddress = null; + } + + public DNSServer(final String hostname, final Transport transport) { + this(hostname, Transport.DEFAULT_PORTS.get(transport), transport); + } + + public DNSServer(InetAddress inetAddress, Transport transport) { + this(inetAddress, Transport.DEFAULT_PORTS.get(transport), transport); + } + + public DNSServer(final InetAddress inetAddress) { + this(inetAddress, 53, Arrays.asList(Transport.UDP, Transport.TCP)); + } + + public DNSServer(final InetAddress inetAddress, int port, List transports) { + this(inetAddress, null, port, transports); + } + + private DNSServer( + final InetAddress inetAddress, + final String hostname, + final int port, + final List transports) { + this.inetAddress = inetAddress; + this.hostname = hostname; + this.port = port; + this.transports = transports; + } + + public Transport uniqueTransport() { + return Iterables.getOnlyElement(this.transports); + } + + public DNSServer asUniqueTransport(final Transport transport) { + Preconditions.checkArgument( + this.transports.contains(transport), + "This DNS server does not have transport ", + transport); + return new DNSServer(inetAddress, hostname, port, Collections.singletonList(transport)); + } + + @Override + @Nonnull + public String toString() { + return MoreObjects.toStringHelper(this) + .add("inetAddress", inetAddress) + .add("hostname", hostname) + .add("port", port) + .add("transports", transports) + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DNSServer dnsServer = (DNSServer) o; + return port == dnsServer.port + && Objects.equal(inetAddress, dnsServer.inetAddress) + && Objects.equal(hostname, dnsServer.hostname) + && Objects.equal(transports, dnsServer.transports); + } + + @Override + public int hashCode() { + return Objects.hashCode(inetAddress, hostname, port, transports); + } +} diff --git a/src/main/java/de/gultsch/minidns/DNSSocket.java b/src/main/java/de/gultsch/minidns/DNSSocket.java new file mode 100644 index 000000000..e3d86b80c --- /dev/null +++ b/src/main/java/de/gultsch/minidns/DNSSocket.java @@ -0,0 +1,200 @@ +package de.gultsch.minidns; + +import android.util.Log; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; + +import de.measite.minidns.DNSMessage; + +import eu.siacs.conversations.Config; + +import org.conscrypt.OkHostnameVerifier; + +import java.io.Closeable; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +final class DNSSocket implements Closeable { + + public static final int QUERY_TIMEOUT = 5_000; + + private final Semaphore semaphore = new Semaphore(1); + private final Map> inFlightQueries = new HashMap<>(); + private final Socket socket; + private final DataInputStream dataInputStream; + private final DataOutputStream dataOutputStream; + + private DNSSocket( + final Socket socket, + final DataInputStream dataInputStream, + final DataOutputStream dataOutputStream) { + this.socket = socket; + this.dataInputStream = dataInputStream; + this.dataOutputStream = dataOutputStream; + new Thread(this::readDNSMessages).start(); + } + + private void readDNSMessages() { + try { + while (socket.isConnected()) { + final DNSMessage response = readDNSMessage(); + final SettableFuture future; + synchronized (inFlightQueries) { + future = inFlightQueries.remove(response.id); + } + if (future != null) { + future.set(response); + } else { + Log.e(Config.LOGTAG, "no in flight query found for response id " + response.id); + } + } + evictInFlightQueries(new EOFException()); + } catch (final IOException e) { + evictInFlightQueries(e); + } + } + + private void evictInFlightQueries(final Exception e) { + synchronized (inFlightQueries) { + final Iterator>> iterator = + inFlightQueries.entrySet().iterator(); + while (iterator.hasNext()) { + final Map.Entry> entry = iterator.next(); + entry.getValue().setException(e); + iterator.remove(); + } + } + } + + private static DNSSocket of(final Socket socket) throws IOException { + final DataInputStream dataInputStream = new DataInputStream(socket.getInputStream()); + final DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream()); + return new DNSSocket(socket, dataInputStream, dataOutputStream); + } + + public static DNSSocket connect(final DNSServer dnsServer) throws IOException { + switch (dnsServer.uniqueTransport()) { + case TCP: + return connectTcpSocket(dnsServer); + case TLS: + return connectTlsSocket(dnsServer); + default: + throw new IllegalStateException("This is not a socket based transport"); + } + } + + private static DNSSocket connectTcpSocket(final DNSServer dnsServer) throws IOException { + Preconditions.checkArgument(dnsServer.uniqueTransport() == Transport.TCP); + final SocketAddress socketAddress = + new InetSocketAddress(dnsServer.inetAddress, dnsServer.port); + final Socket socket = new Socket(); + socket.connect(socketAddress, QUERY_TIMEOUT / 2); + socket.setSoTimeout(QUERY_TIMEOUT); + return DNSSocket.of(socket); + } + + private static DNSSocket connectTlsSocket(final DNSServer dnsServer) throws IOException { + Preconditions.checkArgument(dnsServer.uniqueTransport() == Transport.TLS); + final SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + final SSLSocket sslSocket = (SSLSocket) factory.createSocket(); + if (Strings.isNullOrEmpty(dnsServer.hostname)) { + final SocketAddress socketAddress = + new InetSocketAddress(dnsServer.inetAddress, dnsServer.port); + sslSocket.connect(socketAddress, QUERY_TIMEOUT / 2); + sslSocket.setSoTimeout(QUERY_TIMEOUT); + sslSocket.startHandshake(); + } else { + final SocketAddress socketAddress = + new InetSocketAddress(dnsServer.hostname, dnsServer.port); + sslSocket.connect(socketAddress, QUERY_TIMEOUT / 2); + sslSocket.setSoTimeout(QUERY_TIMEOUT); + sslSocket.startHandshake(); + final SSLSession session = sslSocket.getSession(); + final Certificate[] peerCertificates = session.getPeerCertificates(); + if (peerCertificates.length == 0 || !(peerCertificates[0] instanceof X509Certificate)) { + throw new IOException("Peer did not provide X509 certificates"); + } + final X509Certificate certificate = (X509Certificate) peerCertificates[0]; + if (!OkHostnameVerifier.strictInstance().verify(dnsServer.hostname, certificate)) { + throw new SSLPeerUnverifiedException("Peer did not provide valid certificates"); + } + } + return DNSSocket.of(sslSocket); + } + + public DNSMessage query(final DNSMessage query) throws IOException, InterruptedException { + try { + return queryAsync(query).get(QUERY_TIMEOUT, TimeUnit.MILLISECONDS); + } catch (final ExecutionException e) { + final Throwable cause = e.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } else { + throw new IOException(e); + } + } catch (final TimeoutException e) { + throw new IOException(e); + } + } + + public ListenableFuture queryAsync(final DNSMessage query) + throws InterruptedException, IOException { + final SettableFuture responseFuture = SettableFuture.create(); + synchronized (this.inFlightQueries) { + this.inFlightQueries.put(query.id, responseFuture); + } + this.semaphore.acquire(); + try { + query.writeTo(this.dataOutputStream); + this.dataOutputStream.flush(); + } finally { + this.semaphore.release(); + } + return responseFuture; + } + + private DNSMessage readDNSMessage() throws IOException { + final int length = this.dataInputStream.readUnsignedShort(); + byte[] data = new byte[length]; + int read = 0; + while (read < length) { + read += this.dataInputStream.read(data, read, length - read); + } + return NetworkDataSource.readDNSMessage(data); + } + + @Override + public void close() throws IOException { + this.socket.close(); + } + + public void closeQuietly() { + try { + this.socket.close(); + } catch (final IOException ignored) { + + } + } +} diff --git a/src/main/java/de/gultsch/minidns/NetworkDataSource.java b/src/main/java/de/gultsch/minidns/NetworkDataSource.java new file mode 100644 index 000000000..67a8f8c33 --- /dev/null +++ b/src/main/java/de/gultsch/minidns/NetworkDataSource.java @@ -0,0 +1,169 @@ +package de.gultsch.minidns; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.google.common.base.Throwables; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.cache.RemovalListener; +import com.google.common.collect.ImmutableList; + +import de.measite.minidns.DNSMessage; +import de.measite.minidns.MiniDNSException; +import de.measite.minidns.source.DNSDataSource; +import de.measite.minidns.util.MultipleIoException; + +import eu.siacs.conversations.Config; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +public class NetworkDataSource extends DNSDataSource { + + private static final LoadingCache socketCache = + CacheBuilder.newBuilder() + .removalListener( + (RemovalListener) + notification -> { + final DNSServer dnsServer = notification.getKey(); + final DNSSocket dnsSocket = notification.getValue(); + if (dnsSocket == null) { + return; + } + Log.d(Config.LOGTAG, "closing connection to " + dnsServer); + dnsSocket.closeQuietly(); + }) + .expireAfterAccess(5, TimeUnit.MINUTES) + .build( + new CacheLoader() { + @Override + @NonNull + public DNSSocket load(@NonNull final DNSServer dnsServer) + throws Exception { + Log.d(Config.LOGTAG, "establishing connection to " + dnsServer); + return DNSSocket.connect(dnsServer); + } + }); + + private static List transportsForPort(final int port) { + final ImmutableList.Builder transportBuilder = new ImmutableList.Builder<>(); + for (final Map.Entry entry : Transport.DEFAULT_PORTS.entrySet()) { + if (entry.getValue().equals(port)) { + transportBuilder.add(entry.getKey()); + } + } + return transportBuilder.build(); + } + + @Override + public DNSMessage query(final DNSMessage message, final InetAddress address, final int port) + throws IOException { + final List transports = transportsForPort(port); + Log.w( + Config.LOGTAG, + "using legacy DataSource interface. guessing transports " + + transports + + " from port"); + if (transports.isEmpty()) { + throw new IOException(String.format("No transports found for port %d", port)); + } + return query(message, new DNSServer(address, port, transports)); + } + + public DNSMessage query(final DNSMessage message, final DNSServer dnsServer) + throws IOException { + Log.d(Config.LOGTAG, "using " + dnsServer); + final List ioExceptions = new ArrayList<>(); + for (final Transport transport : dnsServer.transports) { + try { + final DNSMessage response = + queryWithUniqueTransport(message, dnsServer.asUniqueTransport(transport)); + if (response != null && !response.truncated) { + return response; + } + } catch (final IOException e) { + ioExceptions.add(e); + } catch (final InterruptedException e) { + throw new IOException(e); + } + } + MultipleIoException.throwIfRequired(ioExceptions); + return null; + } + + private DNSMessage queryWithUniqueTransport(final DNSMessage message, final DNSServer dnsServer) + throws IOException, InterruptedException { + final Transport transport = dnsServer.uniqueTransport(); + switch (transport) { + case UDP: + return queryUdp(message, dnsServer.inetAddress, dnsServer.port); + case TCP: + case TLS: + return queryDnsSocket(message, dnsServer); + default: + throw new IOException( + String.format("Transport %s has not been implemented", transport)); + } + } + + protected DNSMessage queryUdp( + final DNSMessage message, final InetAddress address, final int port) + throws IOException { + final DatagramPacket request = message.asDatagram(address, port); + final byte[] buffer = new byte[udpPayloadSize]; + try (final DatagramSocket socket = new DatagramSocket()) { + socket.setSoTimeout(timeout); + socket.send(request); + final DatagramPacket response = new DatagramPacket(buffer, buffer.length); + socket.receive(response); + final DNSMessage dnsMessage = readDNSMessage(response.getData()); + if (dnsMessage.id != message.id) { + throw new MiniDNSException.IdMismatch(message, dnsMessage); + } + return dnsMessage; + } + } + + protected DNSMessage queryDnsSocket(final DNSMessage message, final DNSServer dnsServer) + throws IOException, InterruptedException { + final DNSSocket cachedDnsSocket = socketCache.getIfPresent(dnsServer); + if (cachedDnsSocket != null) { + try { + return cachedDnsSocket.query(message); + } catch (final IOException e) { + Log.d( + Config.LOGTAG, + "IOException occurred at cached socket. invalidating and falling through to new socket creation"); + socketCache.invalidate(dnsServer); + } + } + try { + return socketCache.get(dnsServer).query(message); + } catch (final ExecutionException e) { + final Throwable cause = e.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } else { + throw new IOException(cause); + } + } + } + + public static DNSMessage readDNSMessage(final byte[] bytes) throws IOException { + try { + return new DNSMessage(bytes); + } catch (final IllegalArgumentException e) { + throw new IOException(Throwables.getRootCause(e)); + } + } +} diff --git a/src/main/java/de/gultsch/minidns/Transport.java b/src/main/java/de/gultsch/minidns/Transport.java new file mode 100644 index 000000000..3aabfacaa --- /dev/null +++ b/src/main/java/de/gultsch/minidns/Transport.java @@ -0,0 +1,23 @@ +package de.gultsch.minidns; + +import com.google.common.collect.ImmutableMap; + +import java.util.Map; + +public enum Transport { + UDP, + TCP, + TLS, + HTTPS; + + public static final Map DEFAULT_PORTS; + + static { + final ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + builder.put(Transport.UDP, 53); + builder.put(Transport.TCP, 53); + builder.put(Transport.TLS, 853); + builder.put(Transport.HTTPS, 443); + DEFAULT_PORTS = builder.build(); + } +} diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 43984cb0e..a19710688 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -41,6 +41,8 @@ public final class Config { public static final String LOGTAG = BuildConfig.APP_NAME.toLowerCase(Locale.US); + public static final boolean QUICK_LOG = false; + public static final Jid BUG_REPORTS = Jid.of("bugs@conversations.im"); public static final Uri HELP = Uri.parse("https://help.conversations.im"); @@ -83,6 +85,8 @@ public final class Config { public static final boolean XEP_0392 = true; //enables XEP-0392 v0.6.0 + + // media file formats. Homogenous Android or Conversations only deployments can switch to opus and webp public static final int AVATAR_SIZE = 192; public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.JPEG; public static final int AVATAR_CHAR_LIMIT = 9400; @@ -91,6 +95,8 @@ public final class Config { public static final Bitmap.CompressFormat IMAGE_FORMAT = Bitmap.CompressFormat.JPEG; public static final int IMAGE_QUALITY = 75; + public static final boolean USE_OPUS_VOICE_MESSAGES = false; + public static final int MESSAGE_MERGE_WINDOW = 30; public static final int PAGE_SIZE = 50; @@ -111,9 +117,6 @@ public final class Config { public static final boolean OMEMO_PADDING = false; public static final boolean PUT_AUTH_TAG_INTO_KEY = true; public static final boolean AUTOMATICALLY_COMPLETE_SESSIONS = true; - - public static final boolean USE_BOOKMARKS2 = false; - public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true; public static final boolean DISABLE_HTTP_UPLOAD = false; diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java index db84e0cf4..68447e552 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java @@ -156,7 +156,8 @@ public class PgpDecryptionService { && manager.getAutoAcceptFileSize() > 0) { manager.createNewDownloadConnection(message); } - } catch (IOException e) { + } catch (final IOException e) { + Log.d(Config.LOGTAG,"decryption failed", e); message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED); } mXmppConnectionService.updateMessage(message); @@ -170,6 +171,7 @@ public class PgpDecryptionService { } break; case OpenPgpApi.RESULT_CODE_ERROR: + Log.d(Config.LOGTAG,"decryption failed (api error)"); message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED); mXmppConnectionService.updateMessage(message); break; diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java index 9652ad3eb..d3588a995 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java @@ -285,7 +285,9 @@ public class PgpEngine { Intent params = new Intent(); params.setAction(OpenPgpApi.ACTION_GET_KEY); params.putExtra(OpenPgpApi.EXTRA_KEY_ID, pgpKeyId); - Intent result = api.executeApi(params, null, null); + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + final ByteArrayInputStream inputStream = new ByteArrayInputStream(new byte[0]); + Intent result = api.executeApi(params, inputStream, outputStream); return (PendingIntent) result.getParcelableExtra(OpenPgpApi.RESULT_INTENT); } } diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 2cab10373..46343d61a 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -736,8 +736,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return axolotlStore.getFingerprintCertificate(fingerprint); } - public void setFingerprintTrust(String fingerprint, FingerprintStatus status) { + public void setFingerprintTrust(final String fingerprint, final FingerprintStatus status) { axolotlStore.setFingerprintStatus(fingerprint, status); + // TODO we decided to call this after a fingerprint gets toggled to update the 'your contact + // is using unverified devices text'; however this means the entire screen gets redrawn + // after a toggle which might be annoying or cause other weird UI glitches + mXmppConnectionService.updateAccountUi(); } private ListenableFuture verifySessionWithPEP(final XmppAxolotlSession session) { diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java index 2f1856d09..dffde90a1 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java @@ -97,6 +97,10 @@ public class FingerprintStatus implements Comparable { return trust == Trust.TRUSTED || isVerified(); } + public boolean isUnverified() { + return trust == Trust.TRUSTED; + } + public boolean isVerified() { return trust == Trust.VERIFIED || trust == Trust.VERIFIED_X509; } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java index 216f3d7f8..2eb5e39fb 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java @@ -117,4 +117,14 @@ public enum ChannelBinding { throw new AssertionError("Missing short name for " + channelBinding); } } + + public static int priority(final ChannelBinding channelBinding) { + if (Arrays.asList(TLS_EXPORTER,TLS_UNIQUE).contains(channelBinding)) { + return 2; + } else if (channelBinding == ChannelBinding.TLS_SERVER_END_POINT) { + return 1; + } else { + return 0; + } + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java index b94210a60..7343eb86e 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java @@ -97,4 +97,13 @@ public interface ChannelBindingMechanism { messageDigest.update(encodedCertificate); return messageDigest.digest(); } + + static int getPriority(final SaslMechanism mechanism) { + if (mechanism instanceof ChannelBindingMechanism) { + final ChannelBindingMechanism channelBindingMechanism = (ChannelBindingMechanism) mechanism; + return ChannelBinding.priority(channelBindingMechanism.getChannelBinding()); + } else { + return 0; + } + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java index 2ca27570f..4490d7621 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java @@ -27,7 +27,7 @@ public class ScramSha1Plus extends ScramPlusMechanism { @Override public int getPriority() { - return 35; // higher than SCRAM-SHA512 (30) + return 35 + ChannelBinding.priority(this.channelBinding); // higher than SCRAM-SHA512 (30) } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java index 4db33a2fa..eafc86fbc 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java @@ -27,7 +27,7 @@ public class ScramSha256Plus extends ScramPlusMechanism { @Override public int getPriority() { - return 40; + return 40 + ChannelBinding.priority(this.channelBinding); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java index 5d8461973..d110e7708 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java @@ -27,7 +27,7 @@ public class ScramSha512Plus extends ScramPlusMechanism { @Override public int getPriority() { - return 45; + return 45 + ChannelBinding.priority(this.channelBinding); } @Override diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index bfbe817cb..3146abe46 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -31,7 +31,6 @@ import eu.siacs.conversations.crypto.sasl.HashedToken; import eu.siacs.conversations.crypto.sasl.HashedTokenSha256; import eu.siacs.conversations.crypto.sasl.HashedTokenSha512; import eu.siacs.conversations.crypto.sasl.SaslMechanism; -import eu.siacs.conversations.crypto.sasl.ScramPlusMechanism; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.UIHelper; @@ -71,6 +70,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public static final int OPTION_UNVERIFIED = 8; public static final int OPTION_FIXED_USERNAME = 9; public static final int OPTION_QUICKSTART_AVAILABLE = 10; + public static final int OPTION_SOFT_DISABLED = 11; private static final String KEY_PGP_SIGNATURE = "pgp_signature"; private static final String KEY_PGP_ID = "pgp_id"; @@ -249,11 +249,18 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable return !isOptionSet(Account.OPTION_DISABLED); } + public boolean isConnectionEnabled() { + return !isOptionSet(Account.OPTION_DISABLED) && !isOptionSet(Account.OPTION_SOFT_DISABLED); + } + public boolean isOptionSet(final int option) { return ((options & (1 << option)) != 0); } public boolean setOption(final int option, final boolean value) { + if (value && (option == OPTION_DISABLED || option == OPTION_SOFT_DISABLED)) { + this.setStatus(State.OFFLINE); + } final int before = this.options; if (value) { this.options |= 1 << option; @@ -323,11 +330,17 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public State getStatus() { if (isOptionSet(OPTION_DISABLED)) { return State.DISABLED; + } else if (isOptionSet(OPTION_SOFT_DISABLED)) { + return State.LOGGED_OUT; } else { return this.status; } } + public boolean unauthorized() { + return this.status == State.UNAUTHORIZED || this.lastErrorStatus == State.UNAUTHORIZED; + } + public State getLastErrorStatus() { return this.lastErrorStatus; } @@ -762,6 +775,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public enum State { DISABLED(false, false), + LOGGED_OUT(false,false), OFFLINE(false), CONNECTING(false), ONLINE(false), @@ -787,6 +801,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable BIND_FAILURE, HOST_UNKNOWN, STREAM_ERROR, + SEE_OTHER_HOST, STREAM_OPENING_ERROR, POLICY_VIOLATION, PAYMENT_REQUIRED, @@ -820,6 +835,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable switch (this) { case DISABLED: return R.string.account_status_disabled; + case LOGGED_OUT: + return R.string.account_state_logged_out; case ONLINE: return R.string.account_status_online; case CONNECTING: @@ -874,6 +891,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable return R.string.account_status_stream_opening_error; case PAYMENT_REQUIRED: return R.string.payment_required; + case SEE_OTHER_HOST: + return R.string.reconnect_on_other_host; case MISSING_INTERNET_PERMISSION: return R.string.missing_internet_permission; case TEMPORARY_AUTH_FAILURE: diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index cc1c358de..c408d147f 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -170,7 +170,9 @@ public class MucOptions { } public boolean participantsCanChangeSubject() { - final Field field = getRoomInfoForm().getFieldByName("muc#roominfo_changesubject"); + final Field configField = getRoomInfoForm().getFieldByName("muc#roomconfig_changesubject"); + final Field infoField = getRoomInfoForm().getFieldByName("muc#roominfo_changesubject"); + final Field field = configField != null ? configField : infoField; return field != null && "1".equals(field.getValue()); } diff --git a/src/main/java/eu/siacs/conversations/entities/Presences.java b/src/main/java/eu/siacs/conversations/entities/Presences.java index e32e7e7a0..838b85f6c 100644 --- a/src/main/java/eu/siacs/conversations/entities/Presences.java +++ b/src/main/java/eu/siacs/conversations/entities/Presences.java @@ -63,6 +63,21 @@ public class Presences { } } + public boolean anySupport(final String namespace) { + synchronized (this.presences) { + if (this.presences.size() == 0) { + return true; + } + for (Presence presence : this.presences.values()) { + ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult(); + if (disco != null && disco.getFeatures().contains(namespace)) { + return true; + } + } + } + return false; + } + public Presence.Status getShownStatus() { Presence.Status status = Presence.Status.OFFLINE; synchronized (this.presences) { @@ -134,20 +149,6 @@ public class Presences { return true; } - public boolean anySupport(final String namespace) { - synchronized (this.presences) { - if (this.presences.size() == 0) { - return true; - } - for (Presence presence : this.presences.values()) { - ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult(); - if (disco != null && disco.getFeatures().contains(namespace)) { - return true; - } - } - } - return false; - } public String firstWhichSupport(final String namespace) { for (Map.Entry entry : this.presences.entrySet()) { diff --git a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java index f6e482e53..90fb0c5ee 100644 --- a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java +++ b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java @@ -17,6 +17,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; import eu.siacs.conversations.xml.Element; @@ -100,9 +101,9 @@ public class ServiceDiscoveryResult { public ServiceDiscoveryResult(Cursor cursor) throws JSONException { this( - cursor.getString(cursor.getColumnIndex(HASH)), - Base64.decode(cursor.getString(cursor.getColumnIndex(VER)), Base64.DEFAULT), - new JSONObject(cursor.getString(cursor.getColumnIndex(RESULT))) + cursor.getString(cursor.getColumnIndexOrThrow(HASH)), + Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(VER)), Base64.DEFAULT), + new JSONObject(cursor.getString(cursor.getColumnIndexOrThrow(RESULT))) ); } @@ -213,24 +214,23 @@ public class ServiceDiscoveryResult { .append("<"); } - List features = this.getFeatures(); + final List features = this.getFeatures(); Collections.sort(features); - - for (String feature : features) { + for (final String feature : features) { s.append(clean(feature)).append("<"); } - Collections.sort(forms, (lhs, rhs) -> lhs.getFormType().compareTo(rhs.getFormType())); - - for (Data form : forms) { + Collections.sort(forms, Comparator.comparing(Data::getFormType)); + for (final Data form : forms) { s.append(clean(form.getFormType())).append("<"); - List fields = form.getFields(); - Collections.sort(fields, (lhs, rhs) -> Strings.nullToEmpty(lhs.getFieldName()).compareTo(Strings.nullToEmpty(rhs.getFieldName()))); - for (Field field : fields) { + final List fields = form.getFields(); + Collections.sort( + fields, Comparator.comparing(lhs -> Strings.nullToEmpty(lhs.getFieldName()))); + for (final Field field : fields) { s.append(Strings.nullToEmpty(field.getFieldName())).append("<"); - List values = field.getValues(); - Collections.sort(values); - for (String value : values) { + final List values = field.getValues(); + Collections.sort(values, Comparator.comparing(ServiceDiscoveryResult::blankNull)); + for (final String value : values) { s.append(blankNull(value)).append("<"); } } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 5008dc516..a2466e7c5 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -344,12 +344,18 @@ public class IqGenerator extends AbstractGenerator { return iq; } - public IqPacket generateSetBlockRequest(final Jid jid, boolean reportSpam) { + public IqPacket generateSetBlockRequest(final Jid jid, final boolean reportSpam, final String serverMsgId) { final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); final Element block = iq.addChild("block", Namespace.BLOCKING); final Element item = block.addChild("item").setAttribute("jid", jid); if (reportSpam) { - item.addChild("report", "urn:xmpp:reporting:0").addChild("spam"); + final Element report = item.addChild("report", Namespace.REPORTING); + report.setAttribute("reason", Namespace.REPORTING_REASON_SPAM); + if (serverMsgId != null) { + final Element stanzaId = report.addChild("stanza-id", Namespace.STANZA_IDS); + stanzaId.setAttribute("by", jid); + stanzaId.setAttribute("id", serverMsgId); + } } Log.d(Config.LOGTAG, iq.toString()); return iq; diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 19f4a505c..944e36d15 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -199,8 +199,8 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket invite(Conversation conversation, Jid contact) { - MessagePacket packet = new MessagePacket(); + public MessagePacket invite(final Conversation conversation, final Jid contact) { + final MessagePacket packet = new MessagePacket(); packet.setTo(conversation.getJid().asBareJid()); packet.setFrom(conversation.getAccount().getJid()); Element x = new Element("x"); diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 486550918..d3728531e 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -52,7 +52,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH); - private static final List JINGLE_MESSAGE_ELEMENT_NAMES = Arrays.asList("accept", "propose", "proceed", "reject", "retract"); + private static final List JINGLE_MESSAGE_ELEMENT_NAMES = + Arrays.asList("accept", "propose", "proceed", "reject", "retract", "ringing"); public MessageParser(XmppConnectionService service) { super(service); @@ -169,14 +170,19 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece return null; } - private Invite extractInvite(Element message) { + private Invite extractInvite(final Element message) { final Element mucUser = message.findChild("x", Namespace.MUC_USER); if (mucUser != null) { - Element invite = mucUser.findChild("invite"); + final Element invite = mucUser.findChild("invite"); if (invite != null) { - String password = mucUser.findChildContent("password"); - Jid from = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("from")); - Jid room = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from")); + final String password = mucUser.findChildContent("password"); + final Jid from = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("from")); + final Jid to = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("to")); + if (to != null && from == null) { + Log.d(Config.LOGTAG,"do not parse outgoing mediated invite "+message); + return null; + } + final Jid room = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from")); if (room == null) { return null; } @@ -453,8 +459,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece final Invite invite = extractInvite(packet); if (invite != null) { - if (isTypeGroupChat) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring invite to " + invite.jid + " because type=groupchat"); + if (invite.jid.asBareJid().equals(account.getJid().asBareJid())) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignore invite to "+invite.jid+" because it matches account"); + } else if (isTypeGroupChat) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring invite to " + invite.jid + " because it was received as group chat"); } else if (invite.direct && (mucUserElement != null || invite.inviter == null || mXmppConnectionService.isMuc(account, invite.inviter))) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring direct invite to " + invite.jid + " because it was received in MUC"); } else { @@ -871,9 +879,22 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece if (serverMsgId == null) { serverMsgId = extractStanzaId(account, packet); } - mXmppConnectionService.getJingleConnectionManager().deliverMessage(account, packet.getTo(), packet.getFrom(), child, remoteMsgId, serverMsgId, timestamp); - if (!account.getJid().asBareJid().equals(from.asBareJid()) && remoteMsgId != null) { - processMessageReceipts(account, packet, remoteMsgId, query); + mXmppConnectionService + .getJingleConnectionManager() + .deliverMessage( + account, + packet.getTo(), + packet.getFrom(), + child, + remoteMsgId, + serverMsgId, + timestamp); + final Contact contact = account.getRoster().getContact(from); + if (mXmppConnectionService.confirmMessages() + && !contact.isSelf() + && remoteMsgId != null + && contact.showInContactList()) { + processMessageReceipts(account, packet, remoteMsgId, null); } } else if (query.isCatchup()) { if ("propose".equals(action)) { @@ -1106,22 +1127,26 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece this.inviter = inviter; } - public boolean execute(Account account) { - if (jid != null) { - Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid, null, true, false, false, null); - if (conversation.getMucOptions().online()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received invite to " + jid + " but muc is considered to be online"); - mXmppConnectionService.mucSelfPingAndRejoin(conversation); - } else { - conversation.getMucOptions().setPassword(password); - mXmppConnectionService.databaseBackend.updateConversation(conversation); - final Contact contact = inviter != null ? account.getRoster().getContactFromContactList(inviter) : null; - mXmppConnectionService.joinMuc(conversation, contact != null && contact.mutualPresenceSubscription()); - mXmppConnectionService.updateConversationUi(); - } - return true; + public boolean execute(final Account account) { + if (this.jid == null) { + return false; } - return false; + final Contact contact = this.inviter != null ? account.getRoster().getContact(this.inviter) : null; + if (contact != null && contact.isBlocked()) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignore invite from "+contact.getJid()+" because contact is blocked"); + return false; + } + Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid, null, true, false, false, null); + if (conversation.getMucOptions().online()) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received invite to " + jid + " but muc is considered to be online"); + mXmppConnectionService.mucSelfPingAndRejoin(conversation); + } else { + conversation.getMucOptions().setPassword(password); + mXmppConnectionService.databaseBackend.updateConversation(conversation); + mXmppConnectionService.joinMuc(conversation, contact != null && contact.showInContactList()); + mXmppConnectionService.updateConversationUi(); + } + return true; } } } diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 2817457e0..e356824da 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -655,8 +655,13 @@ public class FileBackend { } } - public String getOriginalPath(Uri uri) { - return FileUtils.getPath(mXmppConnectionService, uri); + public String getOriginalPath(final Uri uri) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // On Android 11+ we don’t have access to the original file + return null; + } else { + return FileUtils.getPath(mXmppConnectionService, uri); + } } private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException { diff --git a/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java b/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java index f36506bd1..fd7b27db4 100644 --- a/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java +++ b/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java @@ -16,6 +16,7 @@ import com.google.common.collect.ImmutableList; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; import java.util.List; import eu.siacs.conversations.Config; @@ -129,6 +130,23 @@ public class UnifiedPushDatabase extends SQLiteOpenHelper { return null; } + public List deletePushTargets() { + final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + try (final Cursor cursor = sqLiteDatabase.query("push",new String[]{"application","instance"},null,null,null,null,null)) { + if (cursor != null && cursor.moveToFirst()) { + builder.add(new PushTarget( + cursor.getString(cursor.getColumnIndexOrThrow("application")), + cursor.getString(cursor.getColumnIndexOrThrow("instance")))); + } + } catch (final Exception e) { + Log.d(Config.LOGTAG,"unable to retrieve push targets",e); + return builder.build(); + } + sqLiteDatabase.delete("push",null,null); + return builder.build(); + } + public boolean hasEndpoints(final UnifiedPushBroker.Transport transport) { final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); try (final Cursor cursor = diff --git a/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java b/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java index d05e40ae8..5d6f8eee5 100644 --- a/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java +++ b/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.services; import android.content.Intent; +import android.os.Build; import eu.siacs.conversations.BuildConfig; @@ -25,6 +26,10 @@ public abstract class AbstractQuickConversationsService { return "conversations".equals(BuildConfig.FLAVOR_mode); } + public static boolean isPlayStoreFlavor() { + return "playstore".equals(BuildConfig.FLAVOR_distribution); + } + public abstract void signalAccountStateChange(); public abstract boolean isSynchronizing(); diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java index 752137ec6..99f447275 100644 --- a/src/main/java/eu/siacs/conversations/services/AvatarService.java +++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java @@ -124,6 +124,17 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded { return avatar; } + public Bitmap getRoundedShortcut(final MucOptions mucOptions, Jid nextCounterpart) { + final DisplayMetrics metrics = mXmppConnectionService.getResources().getDisplayMetrics(); + final int size = Math.round(metrics.density * 48); + final Bitmap bitmap = get(mucOptions, size, false, nextCounterpart); + final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(output); + final Paint paint = new Paint(); + drawAvatar(bitmap, canvas, paint); + return output; + } + public Bitmap getRoundedShortcut(final Contact contact) { return getRoundedShortcut(contact, false); } @@ -147,7 +158,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded { return output; } - private void drawAvatar(Bitmap bitmap, Canvas canvas, Paint paint) { + private static void drawAvatar(Bitmap bitmap, Canvas canvas, Paint paint) { final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); paint.setAntiAlias(true); canvas.drawARGB(0, 0, 0, 0); diff --git a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java index 9826ecbc2..1462b5614 100644 --- a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java +++ b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java @@ -19,18 +19,25 @@ import androidx.core.app.NotificationCompat; import com.google.common.base.CharMatcher; import com.google.common.base.Strings; +import com.google.gson.stream.JsonWriter; import java.io.DataOutputStream; import java.io.File; import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.List; +import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; import java.util.zip.GZIPOutputStream; @@ -54,6 +61,8 @@ import eu.siacs.conversations.utils.Compatibility; public class ExportBackupService extends Service { + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + public static final String KEYTYPE = "AES"; public static final String CIPHERMODE = "AES/GCM/NoPadding"; public static final String PROVIDER = "BC"; @@ -61,16 +70,16 @@ public class ExportBackupService extends Service { public static final String MIME_TYPE = "application/vnd.conversations.backup"; private static final int NOTIFICATION_ID = 19; - private static final int PAGE_SIZE = 20; private static final AtomicBoolean RUNNING = new AtomicBoolean(false); private DatabaseBackend mDatabaseBackend; private List mAccounts; private NotificationManager notificationManager; - private static List getPossibleFileOpenIntents(final Context context, final String path) { + private static List getPossibleFileOpenIntents( + final Context context, final String path) { - //http://www.openintents.org/action/android-intent-action-view/file-directory - //do not use 'vnd.android.document/directory' since this will trigger system file manager + // http://www.openintents.org/action/android-intent-action-view/file-directory + // do not use 'vnd.android.document/directory' since this will trigger system file manager final Intent openIntent = new Intent(Intent.ACTION_VIEW); openIntent.addCategory(Intent.CATEGORY_DEFAULT); if (Compatibility.runsAndTargetsTwentyFour(context)) { @@ -83,134 +92,95 @@ public class ExportBackupService extends Service { final Intent amazeIntent = new Intent(Intent.ACTION_VIEW); amazeIntent.setDataAndType(Uri.parse("com.amaze.filemanager:" + path), "resource/folder"); - //will open a file manager at root and user can navigate themselves + // will open a file manager at root and user can navigate themselves final Intent systemFallBack = new Intent(Intent.ACTION_VIEW); systemFallBack.addCategory(Intent.CATEGORY_DEFAULT); - systemFallBack.setData(Uri.parse("content://com.android.externalstorage.documents/root/primary")); + systemFallBack.setData( + Uri.parse("content://com.android.externalstorage.documents/root/primary")); return Arrays.asList(openIntent, amazeIntent, systemFallBack); } - private static void accountExport(final SQLiteDatabase db, final String uuid, final PrintWriter writer) { - final StringBuilder builder = new StringBuilder(); - final Cursor accountCursor = db.query(Account.TABLENAME, null, Account.UUID + "=?", new String[]{uuid}, null, null, null); + private static void accountExport( + final SQLiteDatabase db, final String uuid, final JsonWriter writer) + throws IOException { + final Cursor accountCursor = + db.query( + Account.TABLENAME, + null, + Account.UUID + "=?", + new String[] {uuid}, + null, + null, + null); while (accountCursor != null && accountCursor.moveToNext()) { - builder.append("INSERT INTO ").append(Account.TABLENAME).append("("); + writer.beginObject(); + writer.name("table"); + writer.value(Account.TABLENAME); + writer.name("values"); + writer.beginObject(); for (int i = 0; i < accountCursor.getColumnCount(); ++i) { - if (i != 0) { - builder.append(','); - } - builder.append(accountCursor.getColumnName(i)); - } - builder.append(") VALUES("); - for (int i = 0; i < accountCursor.getColumnCount(); ++i) { - if (i != 0) { - builder.append(','); - } + final String name = accountCursor.getColumnName(i); + writer.name(name); final String value = accountCursor.getString(i); if (value == null || Account.ROSTERVERSION.equals(accountCursor.getColumnName(i))) { - builder.append("NULL"); - } else if (Account.OPTIONS.equals(accountCursor.getColumnName(i)) && value.matches("\\d+")) { + writer.nullValue(); + } else if (Account.OPTIONS.equals(accountCursor.getColumnName(i)) + && value.matches("\\d+")) { int intValue = Integer.parseInt(value); intValue |= 1 << Account.OPTION_DISABLED; - builder.append(intValue); + writer.value(intValue); } else { - appendEscapedSQLString(builder, value); + writer.value(value); } } - builder.append(")"); - builder.append(';'); - builder.append('\n'); + writer.endObject(); + writer.endObject(); } if (accountCursor != null) { accountCursor.close(); } - writer.append(builder.toString()); } - private static void appendEscapedSQLString(final StringBuilder sb, final String sqlString) { - DatabaseUtils.appendEscapedSQLString(sb, CharMatcher.is('\u0000').removeFrom(sqlString)); - } - - private static void simpleExport(SQLiteDatabase db, String table, String column, String uuid, PrintWriter writer) { - final Cursor cursor = db.query(table, null, column + "=?", new String[]{uuid}, null, null, null); + private static void simpleExport( + final SQLiteDatabase db, + final String table, + final String column, + final String uuid, + final JsonWriter writer) + throws IOException { + final Cursor cursor = + db.query(table, null, column + "=?", new String[] {uuid}, null, null, null); while (cursor != null && cursor.moveToNext()) { - writer.write(cursorToString(table, cursor, PAGE_SIZE)); + writer.beginObject(); + writer.name("table"); + writer.value(table); + writer.name("values"); + writer.beginObject(); + for (int i = 0; i < cursor.getColumnCount(); ++i) { + final String name = cursor.getColumnName(i); + writer.name(name); + final String value = cursor.getString(i); + writer.value(value); + } + writer.endObject(); + writer.endObject(); } if (cursor != null) { cursor.close(); } } - public static byte[] getKey(final String password, final byte[] salt) throws InvalidKeySpecException { + public static byte[] getKey(final String password, final byte[] salt) + throws InvalidKeySpecException { final SecretKeyFactory factory; try { factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(e); } - return factory.generateSecret(new PBEKeySpec(password.toCharArray(), salt, 1024, 128)).getEncoded(); - } - - private static String cursorToString(final String table, final Cursor cursor, final int max) { - return cursorToString(table, cursor, max, false); - } - - private static String cursorToString(final String table, final Cursor cursor, int max, boolean ignore) { - final boolean identities = SQLiteAxolotlStore.IDENTITIES_TABLENAME.equals(table); - StringBuilder builder = new StringBuilder(); - builder.append("INSERT "); - if (ignore) { - builder.append("OR IGNORE "); - } - builder.append("INTO ").append(table).append("("); - int skipColumn = -1; - for (int i = 0; i < cursor.getColumnCount(); ++i) { - final String name = cursor.getColumnName(i); - if (identities && SQLiteAxolotlStore.TRUSTED.equals(name)) { - skipColumn = i; - continue; - } - if (i != 0) { - builder.append(','); - } - builder.append(name); - } - builder.append(") VALUES"); - for (int i = 0; i < max; ++i) { - if (i != 0) { - builder.append(','); - } - appendValues(cursor, builder, skipColumn); - if (i < max - 1 && !cursor.moveToNext()) { - break; - } - } - builder.append(';'); - builder.append('\n'); - return builder.toString(); - } - - private static void appendValues(final Cursor cursor, final StringBuilder builder, final int skipColumn) { - builder.append("("); - for (int i = 0; i < cursor.getColumnCount(); ++i) { - if (i == skipColumn) { - continue; - } - if (i != 0) { - builder.append(','); - } - final String value = cursor.getString(i); - if (value == null) { - builder.append("NULL"); - } else if (value.matches("[0-9]+")) { - builder.append(value); - } else { - appendEscapedSQLString(builder, value); - } - } - builder.append(")"); - + return factory.generateSecret(new PBEKeySpec(password.toCharArray(), salt, 1024, 128)) + .getEncoded(); } @Override @@ -223,49 +193,69 @@ public class ExportBackupService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { if (RUNNING.compareAndSet(false, true)) { - new Thread(() -> { - boolean success; - List files; - try { - files = export(); - success = true; - } catch (final Exception e) { - Log.d(Config.LOGTAG, "unable to create backup", e); - success = false; - files = Collections.emptyList(); - } - stopForeground(true); - RUNNING.set(false); - if (success) { - notifySuccess(files); - } - stopSelf(); - }).start(); + new Thread( + () -> { + boolean success; + List files; + try { + files = export(); + success = true; + } catch (final Exception e) { + Log.d(Config.LOGTAG, "unable to create backup", e); + success = false; + files = Collections.emptyList(); + } + stopForeground(true); + RUNNING.set(false); + if (success) { + notifySuccess(files); + } + stopSelf(); + }) + .start(); return START_STICKY; } else { - Log.d(Config.LOGTAG, "ExportBackupService. ignoring start command because already running"); + Log.d( + Config.LOGTAG, + "ExportBackupService. ignoring start command because already running"); } return START_NOT_STICKY; } - private void messageExport(SQLiteDatabase db, String uuid, PrintWriter writer, Progress progress) { - Cursor cursor = db.rawQuery("select messages.* from messages join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?", new String[]{uuid}); + private void messageExport( + final SQLiteDatabase db, + final String uuid, + final JsonWriter writer, + final Progress progress) + throws IOException { + Cursor cursor = + db.rawQuery( + "select messages.* from messages join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?", + new String[] {uuid}); int size = cursor != null ? cursor.getCount() : 0; Log.d(Config.LOGTAG, "exporting " + size + " messages for account " + uuid); int i = 0; int p = 0; while (cursor != null && cursor.moveToNext()) { - writer.write(cursorToString(Message.TABLENAME, cursor, PAGE_SIZE, false)); - if (i + PAGE_SIZE > size) { - i = size; - } else { - i += PAGE_SIZE; + writer.beginObject(); + writer.name("table"); + writer.value(Message.TABLENAME); + writer.name("values"); + writer.beginObject(); + for (int j = 0; j < cursor.getColumnCount(); ++j) { + final String name = cursor.getColumnName(j); + writer.name(name); + final String value = cursor.getString(j); + writer.value(value); } + writer.endObject(); + writer.endObject(); final int percentage = i * 100 / size; if (p < percentage) { p = percentage; notificationManager.notify(NOTIFICATION_ID, progress.build(p)); } + i++; } if (cursor != null) { cursor.close(); @@ -273,7 +263,8 @@ public class ExportBackupService extends Service { } private List export() throws Exception { - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup"); + NotificationCompat.Builder mBuilder = + new NotificationCompat.Builder(getBaseContext(), "backup"); mBuilder.setContentTitle(getString(R.string.notification_create_backup_title)) .setSmallIcon(R.drawable.ic_archive_white_24dp) .setProgress(1, 0, false); @@ -286,17 +277,38 @@ public class ExportBackupService extends Service { for (final Account account : this.mAccounts) { final String password = account.getPassword(); if (Strings.nullToEmpty(password).trim().isEmpty()) { - Log.d(Config.LOGTAG, String.format("skipping backup for %s because password is empty. unable to encrypt", account.getJid().asBareJid())); + Log.d( + Config.LOGTAG, + String.format( + "skipping backup for %s because password is empty. unable to encrypt", + account.getJid().asBareJid())); continue; } - Log.d(Config.LOGTAG, String.format("exporting data for account %s (%s)", account.getJid().asBareJid(), account.getUuid())); + Log.d( + Config.LOGTAG, + String.format( + "exporting data for account %s (%s)", + account.getJid().asBareJid(), account.getUuid())); final byte[] IV = new byte[12]; final byte[] salt = new byte[16]; secureRandom.nextBytes(IV); secureRandom.nextBytes(salt); - final BackupFileHeader backupFileHeader = new BackupFileHeader(getString(R.string.app_name), account.getJid(), System.currentTimeMillis(), IV, salt); + final BackupFileHeader backupFileHeader = + new BackupFileHeader( + getString(R.string.app_name), + account.getJid(), + System.currentTimeMillis(), + IV, + salt); final Progress progress = new Progress(mBuilder, max, count); - final File file = new File(FileBackend.getBackupDirectory(this), account.getJid().asBareJid().toEscapedString() + ".ceb"); + final String filename = + String.format( + "%s.%s.ceb", + account.getJid().asBareJid().toEscapedString(), + DATE_FORMAT.format(new Date())); + final File file = + new File( + FileBackend.getBackupDirectory(this), filename); files.add(file); final File directory = file.getParentFile(); if (directory != null && directory.mkdirs()) { @@ -307,25 +319,38 @@ public class ExportBackupService extends Service { backupFileHeader.write(dataOutputStream); dataOutputStream.flush(); - final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER); + final Cipher cipher = + Compatibility.twentyEight() + ? Cipher.getInstance(CIPHERMODE) + : Cipher.getInstance(CIPHERMODE, PROVIDER); final byte[] key = getKey(password, salt); SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE); IvParameterSpec ivSpec = new IvParameterSpec(IV); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); - CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher); + CipherOutputStream cipherOutputStream = + new CipherOutputStream(fileOutputStream, cipher); - GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream); - PrintWriter writer = new PrintWriter(gzipOutputStream); - SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase(); + final GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream); + final JsonWriter jsonWriter = + new JsonWriter( + new OutputStreamWriter(gzipOutputStream, StandardCharsets.UTF_8)); + jsonWriter.beginArray(); + final SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase(); final String uuid = account.getUuid(); - accountExport(db, uuid, writer); - simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, writer); - messageExport(db, uuid, writer, progress); - for (String table : Arrays.asList(SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) { - simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, writer); + accountExport(db, uuid, jsonWriter); + simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, jsonWriter); + messageExport(db, uuid, jsonWriter, progress); + for (final String table : + Arrays.asList( + SQLiteAxolotlStore.PREKEY_TABLENAME, + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + SQLiteAxolotlStore.SESSION_TABLENAME, + SQLiteAxolotlStore.IDENTITIES_TABLENAME)) { + simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, jsonWriter); } - writer.flush(); - writer.close(); + jsonWriter.endArray(); + jsonWriter.flush(); + jsonWriter.close(); mediaScannerScanFile(file); Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile()); count++; @@ -346,9 +371,15 @@ public class ExportBackupService extends Service { for (final Intent intent : getPossibleFileOpenIntents(this, path)) { if (intent.resolveActivityInfo(getPackageManager(), 0) != null) { - openFolderIntent = PendingIntent.getActivity(this, 189, intent, s() - ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); + openFolderIntent = + PendingIntent.getActivity( + this, + 189, + intent, + s() + ? PendingIntent.FLAG_IMMUTABLE + | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); break; } } @@ -363,22 +394,39 @@ public class ExportBackupService extends Service { intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setType(MIME_TYPE); - final Intent chooser = Intent.createChooser(intent, getString(R.string.share_backup_files)); - shareFilesIntent = PendingIntent.getActivity(this, 190, chooser, s() - ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); + final Intent chooser = + Intent.createChooser(intent, getString(R.string.share_backup_files)); + shareFilesIntent = + PendingIntent.getActivity( + this, + 190, + chooser, + s() + ? PendingIntent.FLAG_IMMUTABLE + | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); } - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup"); + NotificationCompat.Builder mBuilder = + new NotificationCompat.Builder(getBaseContext(), "backup"); mBuilder.setContentTitle(getString(R.string.notification_backup_created_title)) .setContentText(getString(R.string.notification_backup_created_subtitle, path)) - .setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_backup_created_subtitle, FileBackend.getBackupDirectory(this).getAbsolutePath()))) + .setStyle( + new NotificationCompat.BigTextStyle() + .bigText( + getString( + R.string.notification_backup_created_subtitle, + FileBackend.getBackupDirectory(this) + .getAbsolutePath()))) .setAutoCancel(true) .setContentIntent(openFolderIntent) .setSmallIcon(R.drawable.ic_archive_white_24dp); if (shareFilesIntent != null) { - mBuilder.addAction(R.drawable.ic_share_white_24dp, getString(R.string.share_backup_files), shareFilesIntent); + mBuilder.addAction( + R.drawable.ic_share_white_24dp, + getString(R.string.share_backup_files), + shareFilesIntent); } notificationManager.notify(NOTIFICATION_ID, mBuilder.build()); diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index c261f5219..dc265755d 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -10,6 +10,7 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.ShortcutManager; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Typeface; @@ -21,6 +22,7 @@ import android.os.Build; import android.os.SystemClock; import android.os.Vibrator; import android.preference.PreferenceManager; +import android.provider.Settings; import android.text.SpannableString; import android.text.style.StyleSpan; import android.util.DisplayMetrics; @@ -34,11 +36,13 @@ import androidx.core.app.NotificationManagerCompat; import androidx.core.app.Person; import androidx.core.app.RemoteInput; import androidx.core.content.ContextCompat; +import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.graphics.drawable.IconCompat; import com.google.android.material.color.MaterialColors; import com.google.common.base.Joiner; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import java.io.File; @@ -642,6 +646,7 @@ public class NotificationService { createCallAction( id.sessionId, XmppConnectionService.ACTION_END_CALL, 104)) .build()); + builder.setLocalOnly(true); return builder.build(); } @@ -772,6 +777,25 @@ public class NotificationService { } } + public void clearMissedCall(final Message message) { + synchronized (mMissedCalls) { + final Iterator> iterator = mMissedCalls.entrySet().iterator(); + while (iterator.hasNext()) { + final Map.Entry entry = iterator.next(); + final Conversational conversational = entry.getKey(); + final MissedCallsInfo missedCallsInfo = entry.getValue(); + if (conversational.getUuid().equals(message.getConversation().getUuid())) { + if (missedCallsInfo.removeMissedCall()) { + cancel(conversational.getUuid(), MISSED_CALL_NOTIFICATION_ID); + Log.d(Config.LOGTAG,conversational.getAccount().getJid().asBareJid()+": dismissed missed call because call was picked up on other device"); + iterator.remove(); + } + } + } + updateMissedCallNotifications(null); + } + } + public void clearMissedCalls() { synchronized (mMissedCalls) { for (final Conversational conversation : mMissedCalls.keySet()) { @@ -1285,17 +1309,30 @@ public class NotificationService { } } } + final ShortcutInfoCompat info; if (conversation.getMode() == Conversation.MODE_SINGLE) { - Contact contact = conversation.getContact(); - Uri systemAccount = contact.getSystemAccount(); + final Contact contact = conversation.getContact(); + final Uri systemAccount = contact.getSystemAccount(); if (systemAccount != null) { mBuilder.addPerson(systemAccount.toString()); } + info = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact); + } else { + info = + mXmppConnectionService + .getShortcutService() + .getShortcutInfoCompat(conversation.getMucOptions(), conversation.getNextCounterpart()); } mBuilder.setWhen(conversation.getLatestMessage().getTimeSent()); mBuilder.setSmallIcon(R.drawable.ic_notification); mBuilder.setDeleteIntent(createDeleteIntent(conversation)); mBuilder.setContentIntent(createContentIntent(conversation)); + mBuilder.setShortcutInfo(info); + if (Build.VERSION.SDK_INT >= 30) { + mXmppConnectionService + .getSystemService(ShortcutManager.class) + .pushDynamicShortcut(info.toShortcutInfo()); + } } return mBuilder; } @@ -1629,12 +1666,25 @@ public class NotificationService { } private PendingIntent createCallAction(String sessionId, final String action, int requestCode) { - final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); + return pendingServiceIntent(mXmppConnectionService, action, requestCode, ImmutableMap.of(RtpSessionActivity.EXTRA_SESSION_ID, sessionId)); + } + + private PendingIntent createSnoozeIntent(final Conversation conversation) { + return pendingServiceIntent(mXmppConnectionService, XmppConnectionService.ACTION_SNOOZE, generateRequestCode(conversation,22),ImmutableMap.of("uuid",conversation.getUuid())); + } + + private static PendingIntent pendingServiceIntent(final Context context, final String action, final int requestCode) { + return pendingServiceIntent(context, action, requestCode, ImmutableMap.of()); + } + + private static PendingIntent pendingServiceIntent(final Context context, final String action, final int requestCode, final Map extras) { + final Intent intent = new Intent(context, XmppConnectionService.class); intent.setAction(action); - intent.setPackage(mXmppConnectionService.getPackageName()); - intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId); + for(final Map.Entry entry : extras.entrySet()) { + intent.putExtra(entry.getKey(), entry.getValue()); + } return PendingIntent.getService( - mXmppConnectionService, + context, requestCode, intent, s() @@ -1642,44 +1692,6 @@ public class NotificationService { : PendingIntent.FLAG_UPDATE_CURRENT); } - private PendingIntent createSnoozeIntent(Conversation conversation) { - final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_SNOOZE); - intent.putExtra("uuid", conversation.getUuid()); - intent.setPackage(mXmppConnectionService.getPackageName()); - return PendingIntent.getService( - mXmppConnectionService, - generateRequestCode(conversation, 22), - intent, - s() - ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - } - - private PendingIntent createTryAgainIntent() { - final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN); - return PendingIntent.getService( - mXmppConnectionService, - 45, - intent, - s() - ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - } - - private PendingIntent createDismissErrorIntent() { - final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS); - return PendingIntent.getService( - mXmppConnectionService, - 69, - intent, - s() - ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - } - private boolean wasHighlightedOrPrivate(final Message message) { if (message.getConversation() instanceof Conversation) { Conversation conversation = (Conversation) message.getConversation(); @@ -1724,17 +1736,15 @@ public class NotificationService { final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService); mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name)); final List accounts = mXmppConnectionService.getAccounts(); - int enabled = 0; - int connected = 0; - if (accounts != null) { - for (Account account : accounts) { - if (account.isOnlineAndConnected()) { - connected++; - enabled++; - } else if (account.isEnabled()) { - enabled++; - } - } + final int enabled; + final int connected; + if (accounts == null) { + enabled = 0; + connected = 0; + } else { + enabled = Iterables.size(Iterables.filter(accounts, Account::isEnabled)); + connected = + Iterables.size(Iterables.filter(accounts, Account::isOnlineAndConnected)); } mBuilder.setContentText( mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled)); @@ -1752,11 +1762,36 @@ public class NotificationService { if (Compatibility.runsTwentySix()) { mBuilder.setChannelId("foreground"); + mBuilder.addAction( + R.drawable.ic_logout_white_24dp, + mXmppConnectionService.getString(R.string.log_out), + pendingServiceIntent( + mXmppConnectionService, + XmppConnectionService.ACTION_TEMPORARILY_DISABLE, + 87)); + mBuilder.addAction( + R.drawable.ic_notifications_off_white_24dp, + mXmppConnectionService.getString(R.string.hide_notification), + pendingNotificationSettingsIntent(mXmppConnectionService)); } return mBuilder.build(); } + @RequiresApi(api = Build.VERSION_CODES.O) + private static PendingIntent pendingNotificationSettingsIntent(final Context context) { + final Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS); + intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName()); + intent.putExtra(Settings.EXTRA_CHANNEL_ID, "foreground"); + return PendingIntent.getActivity( + context, + 89, + intent, + s() + ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); + } + private PendingIntent createOpenConversationsIntent() { try { return PendingIntent.getActivity( @@ -1807,7 +1842,7 @@ public class NotificationService { mBuilder.addAction( R.drawable.ic_autorenew_white_24dp, mXmppConnectionService.getString(R.string.try_again), - createTryAgainIntent()); + pendingServiceIntent(mXmppConnectionService, XmppConnectionService.ACTION_TRY_AGAIN, 45)); if (torNotAvailable) { if (TorServiceUtils.isOrbotInstalled(mXmppConnectionService)) { mBuilder.addAction( @@ -1835,7 +1870,7 @@ public class NotificationService { : PendingIntent.FLAG_UPDATE_CURRENT)); } } - mBuilder.setDeleteIntent(createDismissErrorIntent()); + mBuilder.setDeleteIntent(pendingServiceIntent(mXmppConnectionService,XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS, 69)); mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE); mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp); mBuilder.setLocalOnly(true); @@ -1938,6 +1973,11 @@ public class NotificationService { lastTime = time; } + public boolean removeMissedCall() { + --numberOfCalls; + return numberOfCalls <= 0; + } + public int getNumberOfCalls() { return numberOfCalls; } diff --git a/src/main/java/eu/siacs/conversations/services/ShortcutService.java b/src/main/java/eu/siacs/conversations/services/ShortcutService.java index 6b7106f74..41fdbf754 100644 --- a/src/main/java/eu/siacs/conversations/services/ShortcutService.java +++ b/src/main/java/eu/siacs/conversations/services/ShortcutService.java @@ -11,6 +11,9 @@ import android.os.Build; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.core.content.pm.ShortcutInfoCompat; +import androidx.core.graphics.drawable.IconCompat; import java.util.ArrayList; import java.util.HashMap; @@ -19,6 +22,7 @@ import java.util.List; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.ui.StartConversationActivity; import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor; import eu.siacs.conversations.xmpp.Jid; @@ -88,13 +92,45 @@ public class ShortcutService { } } - @TargetApi(Build.VERSION_CODES.N_MR1) - private ShortcutInfo getShortcutInfo(Contact contact) { - return new ShortcutInfo.Builder(xmppConnectionService, getShortcutId(contact)) + public ShortcutInfoCompat getShortcutInfoCompat(final Contact contact) { + final ShortcutInfoCompat.Builder builder = + new ShortcutInfoCompat.Builder(xmppConnectionService, getShortcutId(contact)) .setShortLabel(contact.getDisplayName()) .setIntent(getShortcutIntent(contact)) - .setIcon(Icon.createWithBitmap(xmppConnectionService.getAvatarService().getRoundedShortcut(contact))) - .build(); + .setIsConversation(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + builder.setIcon( + IconCompat.createFromIcon( + xmppConnectionService, + Icon.createWithBitmap( + xmppConnectionService + .getAvatarService() + .getRoundedShortcut(contact)))); + } + return builder.build(); + } + + public ShortcutInfoCompat getShortcutInfoCompat(final MucOptions mucOptions, Jid nextCounterpart) { + final ShortcutInfoCompat.Builder builder = + new ShortcutInfoCompat.Builder(xmppConnectionService, getShortcutId(mucOptions)) + .setShortLabel(mucOptions.getConversation().getName()) + .setIntent(getShortcutIntent(mucOptions)) + .setIsConversation(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + builder.setIcon( + IconCompat.createFromIcon( + xmppConnectionService, + Icon.createWithBitmap( + xmppConnectionService + .getAvatarService() + .getRoundedShortcut(mucOptions, nextCounterpart)))); + } + return builder.build(); + } + + @TargetApi(Build.VERSION_CODES.N_MR1) + private ShortcutInfo getShortcutInfo(final Contact contact) { + return getShortcutInfoCompat(contact).toShortcutInfo(); } private static boolean contactsChanged(List needles, List haystack) { @@ -120,12 +156,40 @@ public class ShortcutService { return contact.getAccount().getJid().asBareJid().toEscapedString()+"#"+contact.getJid().asBareJid().toEscapedString(); } - private Intent getShortcutIntent(Contact contact) { + private static String getShortcutId(final MucOptions mucOptions) { + final Account account = mucOptions.getAccount(); + final Jid jid = mucOptions.getConversation().getJid(); + return account.getJid().asBareJid().toEscapedString() + + "#" + + jid.asBareJid().toEscapedString(); + } + + private Intent getShortcutIntent(final MucOptions mucOptions) { + final Account account = mucOptions.getAccount(); + return getShortcutIntent( + account, + Uri.parse( + String.format( + "xmpp:%s?join", + mucOptions + .getConversation() + .getJid() + .asBareJid() + .toEscapedString()))); + } + + private Intent getShortcutIntent(final Contact contact) { + return getShortcutIntent( + contact.getAccount(), + Uri.parse("xmpp:" + contact.getJid().asBareJid().toEscapedString())); + } + + private Intent getShortcutIntent(final Account account, final Uri uri) { Intent intent = new Intent(xmppConnectionService, StartConversationActivity.class); intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse("xmpp:"+contact.getJid().asBareJid().toEscapedString())); - intent.putExtra("account",contact.getAccount().getJid().asBareJid().toString()); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP| Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.setData(uri); + intent.putExtra("account", account.getJid().asBareJid().toString()); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); return intent; } diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java index 7d2d90dd5..d152c5d07 100644 --- a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java +++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java @@ -1,15 +1,27 @@ package eu.siacs.conversations.services; +import android.app.PendingIntent; import android.content.ComponentName; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; import android.preference.PreferenceManager; import android.util.Log; + +import androidx.annotation.NonNull; + import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.google.common.io.BaseEncoding; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; + import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; @@ -62,7 +74,7 @@ public class UnifiedPushBroker { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": trigger endpoint renewal on bind"); - renewUnifiedEndpoint(transportOptional.get()); + renewUnifiedEndpoint(transportOptional.get(), null); } } } @@ -74,21 +86,43 @@ public class UnifiedPushBroker { } public Optional renewUnifiedPushEndpoints() { + return renewUnifiedPushEndpoints(null); + } + + public Optional renewUnifiedPushEndpoints(final PushTargetMessenger pushTargetMessenger) { final Optional transportOptional = getTransport(); if (transportOptional.isPresent()) { final Transport transport = transportOptional.get(); if (transport.account.isEnabled()) { - renewUnifiedEndpoint(transportOptional.get()); + renewUnifiedEndpoint(transportOptional.get(), pushTargetMessenger); } else { + if (pushTargetMessenger.messenger != null) { + sendRegistrationDelayed(pushTargetMessenger.messenger,"account is disabled"); + } Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. Account is disabled"); } } else { + if (pushTargetMessenger.messenger != null) { + sendRegistrationDelayed(pushTargetMessenger.messenger,"no transport selected"); + } Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. No transport selected"); } return transportOptional; } - private void renewUnifiedEndpoint(final Transport transport) { + private void sendRegistrationDelayed(final Messenger messenger, final String error) { + final Intent intent = new Intent(UnifiedPushDistributor.ACTION_REGISTRATION_DELAYED); + intent.putExtra(UnifiedPushDistributor.EXTRA_MESSAGE, error); + final var message = new Message(); + message.obj = intent; + try { + messenger.send(message); + } catch (final RemoteException e) { + Log.d(Config.LOGTAG,"unable to tell messenger of delayed registration",e); + } + } + + private void renewUnifiedEndpoint(final Transport transport, final PushTargetMessenger pushTargetMessenger) { final Account account = transport.account; final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service); final List renewals = @@ -105,6 +139,7 @@ public class UnifiedPushBroker { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": try to renew UnifiedPush " + renewal); + UnifiedPushDistributor.quickLog(service,String.format("%s: try to renew UnifiedPush %s", account.getJid(), renewal.toString())); final String hashedApplication = UnifiedPushDistributor.hash(account.getUuid(), renewal.application); final String hashedInstance = @@ -114,16 +149,23 @@ public class UnifiedPushBroker { final Element register = registration.addChild("register", Namespace.UNIFIED_PUSH); register.setAttribute("application", hashedApplication); register.setAttribute("instance", hashedInstance); + final Messenger messenger; + if (pushTargetMessenger != null && renewal.equals(pushTargetMessenger.pushTarget)) { + messenger = pushTargetMessenger.messenger; + } else { + messenger = null; + } this.service.sendIqPacket( account, registration, - (a, response) -> processRegistration(transport, renewal, response)); + (a, response) -> processRegistration(transport, renewal, messenger, response)); } } private void processRegistration( final Transport transport, final UnifiedPushDatabase.PushTarget renewal, + final Messenger messenger, final IqPacket response) { if (response.getType() == IqPacket.TYPE.RESULT) { final Element registered = response.findChild("registered", Namespace.UNIFIED_PUSH); @@ -142,7 +184,7 @@ public class UnifiedPushBroker { Log.d(Config.LOGTAG, "could not parse expiration", e); return; } - renewUnifiedPushEndpoint(transport, renewal, endpoint, expiration); + renewUnifiedPushEndpoint(transport, renewal, messenger, endpoint, expiration); } else { Log.d(Config.LOGTAG, "could not register UP endpoint " + response.getErrorCondition()); } @@ -151,6 +193,7 @@ public class UnifiedPushBroker { private void renewUnifiedPushEndpoint( final Transport transport, final UnifiedPushDatabase.PushTarget renewal, + final Messenger messenger, final String endpoint, final long expiration) { Log.d(Config.LOGTAG, "registered endpoint " + endpoint + " expiration=" + expiration); @@ -171,15 +214,42 @@ public class UnifiedPushBroker { + renewal.instance + " was updated to " + endpoint); - broadcastEndpoint( - renewal.instance, - new UnifiedPushDatabase.ApplicationEndpoint(renewal.application, endpoint)); + UnifiedPushDistributor.quickLog( + service, + "endpoint for " + + renewal.application + + "/" + + renewal.instance + + " was updated to " + + endpoint); + final UnifiedPushDatabase.ApplicationEndpoint applicationEndpoint = + new UnifiedPushDatabase.ApplicationEndpoint(renewal.application, endpoint); + sendEndpoint(messenger, renewal.instance, applicationEndpoint); + } + } + + private void sendEndpoint(final Messenger messenger, String instance, final UnifiedPushDatabase.ApplicationEndpoint applicationEndpoint) { + if (messenger != null) { + Log.d(Config.LOGTAG,"using messenger instead of broadcast to communicate endpoint to "+applicationEndpoint.application); + final Message message = new Message(); + message.obj = endpointIntent(instance, applicationEndpoint); + try { + messenger.send(message); + } catch (final RemoteException e) { + Log.d(Config.LOGTAG,"messenger failed. falling back to broadcast"); + broadcastEndpoint(instance, applicationEndpoint); + } + } else { + broadcastEndpoint(instance, applicationEndpoint); } } public boolean reconfigurePushDistributor() { final boolean enabled = getTransport().isPresent(); setUnifiedPushDistributorEnabled(enabled); + if (!enabled) { + unregisterCurrentPushTargets(); + } return enabled; } @@ -202,6 +272,43 @@ public class UnifiedPushBroker { } } + private void unregisterCurrentPushTargets() { + final var future = deletePushTargets(); + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess( + final List pushTargets) { + broadcastUnregistered(pushTargets); + } + + @Override + public void onFailure(@NonNull Throwable throwable) { + Log.d( + Config.LOGTAG, + "could not delete endpoints after UnifiedPushDistributor was disabled"); + } + }, + MoreExecutors.directExecutor()); + } + + private ListenableFuture> deletePushTargets() { + return Futures.submit(() -> UnifiedPushDatabase.getInstance(service).deletePushTargets(),SCHEDULER); + } + + private void broadcastUnregistered(final List pushTargets) { + for(final UnifiedPushDatabase.PushTarget pushTarget : pushTargets) { + Log.d(Config.LOGTAG,"sending unregistered to "+pushTarget); + broadcastUnregistered(pushTarget); + } + } + + private void broadcastUnregistered(final UnifiedPushDatabase.PushTarget pushTarget) { + final var intent = unregisteredIntent(pushTarget); + service.sendBroadcast(intent); + } + public boolean processPushMessage( final Account account, final Jid transport, final Element push) { final String instance = push.getAttribute("instance"); @@ -296,20 +403,45 @@ public class UnifiedPushBroker { updateIntent.putExtra("token", target.instance); updateIntent.putExtra("bytesMessage", payload); updateIntent.putExtra("message", new String(payload, StandardCharsets.UTF_8)); + // TODO add distributor verification? service.sendBroadcast(updateIntent); } private void broadcastEndpoint( final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) { Log.d(Config.LOGTAG, "broadcasting endpoint to " + endpoint.application); - final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_NEW_ENDPOINT); - updateIntent.setPackage(endpoint.application); - updateIntent.putExtra("token", instance); - updateIntent.putExtra("endpoint", endpoint.endpoint); + final Intent updateIntent = endpointIntent(instance, endpoint); service.sendBroadcast(updateIntent); } - public void rebroadcastEndpoint(final String instance, final Transport transport) { + private Intent endpointIntent(final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) { + final Intent intent = new Intent(UnifiedPushDistributor.ACTION_NEW_ENDPOINT); + intent.setPackage(endpoint.application); + intent.putExtra("token", instance); + intent.putExtra("endpoint", endpoint.endpoint); + final var distributorVerificationIntent = new Intent(); + distributorVerificationIntent.setPackage(service.getPackageName()); + final var pendingIntent = + PendingIntent.getBroadcast( + service, 0, distributorVerificationIntent, PendingIntent.FLAG_IMMUTABLE); + intent.putExtra("distributor", pendingIntent); + return intent; + } + + private Intent unregisteredIntent(final UnifiedPushDatabase.PushTarget pushTarget) { + final Intent intent = new Intent(UnifiedPushDistributor.ACTION_UNREGISTERED); + intent.setPackage(pushTarget.application); + intent.putExtra("token", pushTarget.instance); + final var distributorVerificationIntent = new Intent(); + distributorVerificationIntent.setPackage(service.getPackageName()); + final var pendingIntent = + PendingIntent.getBroadcast( + service, 0, distributorVerificationIntent, PendingIntent.FLAG_IMMUTABLE); + intent.putExtra("distributor", pendingIntent); + return intent; + } + + public void rebroadcastEndpoint(final Messenger messenger, final String instance, final Transport transport) { final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service); final UnifiedPushDatabase.ApplicationEndpoint endpoint = unifiedPushDatabase.getEndpoint( @@ -317,7 +449,7 @@ public class UnifiedPushBroker { transport.transport.toEscapedString(), instance); if (endpoint != null) { - broadcastEndpoint(instance, endpoint); + sendEndpoint(messenger, instance, endpoint); } } @@ -330,4 +462,14 @@ public class UnifiedPushBroker { this.transport = transport; } } + + public static class PushTargetMessenger { + private final UnifiedPushDatabase.PushTarget pushTarget; + public final Messenger messenger; + + public PushTargetMessenger(UnifiedPushDatabase.PushTarget pushTarget, Messenger messenger) { + this.pushTarget = pushTarget; + this.messenger = messenger; + } + } } diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java index 64c16dbcd..b47a61a53 100644 --- a/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java +++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java @@ -1,10 +1,15 @@ package eu.siacs.conversations.services; +import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.pm.ResolveInfo; import android.net.Uri; +import android.os.Message; +import android.os.Messenger; +import android.os.Parcelable; +import android.os.RemoteException; import android.util.Log; import com.google.common.base.Charsets; @@ -24,16 +29,30 @@ import eu.siacs.conversations.utils.Compatibility; public class UnifiedPushDistributor extends BroadcastReceiver { + // distributor actions (these are actios used for connector->distributor broadcasts) + // we, the distributor, have a broadcast receiver listening for those actions + public static final String ACTION_REGISTER = "org.unifiedpush.android.distributor.REGISTER"; public static final String ACTION_UNREGISTER = "org.unifiedpush.android.distributor.UNREGISTER"; + + + // connector actions (these are actions used for distributor->connector broadcasts) + public static final String ACTION_UNREGISTERED = "org.unifiedpush.android.connector.UNREGISTERED"; public static final String ACTION_BYTE_MESSAGE = "org.unifiedpush.android.distributor.feature.BYTES_MESSAGE"; public static final String ACTION_REGISTRATION_FAILED = "org.unifiedpush.android.connector.REGISTRATION_FAILED"; + + // this action is only used in 'messenger' communication to tell the app that a registration is + // probably fine but can not be processed right now; for example due to spotty internet + public static final String ACTION_REGISTRATION_DELAYED = + "org.unifiedpush.android.connector.REGISTRATION_DELAYED"; public static final String ACTION_MESSAGE = "org.unifiedpush.android.connector.MESSAGE"; public static final String ACTION_NEW_ENDPOINT = "org.unifiedpush.android.connector.NEW_ENDPOINT"; + public static final String EXTRA_MESSAGE = "message"; + public static final String PREFERENCE_ACCOUNT = "up_push_account"; public static final String PREFERENCE_PUSH_SERVER = "up_push_server"; @@ -46,22 +65,24 @@ public class UnifiedPushDistributor extends BroadcastReceiver { return; } final String action = intent.getAction(); - final String application = intent.getStringExtra("application"); + final String application; + final Parcelable appVerification = intent.getParcelableExtra("app"); + if (appVerification instanceof PendingIntent pendingIntent) { + application = pendingIntent.getIntentSender().getCreatorPackage(); + Log.d(Config.LOGTAG,"received application name via pending intent "+ application); + } else { + application = intent.getStringExtra("application"); + } + final Parcelable messenger = intent.getParcelableExtra("messenger"); final String instance = intent.getStringExtra("token"); final List features = intent.getStringArrayListExtra("features"); switch (Strings.nullToEmpty(action)) { - case ACTION_REGISTER: - register(context, application, instance, features); - break; - case ACTION_UNREGISTER: - unregister(context, instance); - break; - case Intent.ACTION_PACKAGE_FULLY_REMOVED: - unregisterApplication(context, intent.getData()); - break; - default: - Log.d(Config.LOGTAG, "UnifiedPushDistributor received unknown action " + action); - break; + case ACTION_REGISTER -> register(context, application, instance, features, messenger); + case ACTION_UNREGISTER -> unregister(context, instance); + case Intent.ACTION_PACKAGE_FULLY_REMOVED -> + unregisterApplication(context, intent.getData()); + default -> + Log.d(Config.LOGTAG, "UnifiedPushDistributor received unknown action " + action); } } @@ -69,7 +90,8 @@ public class UnifiedPushDistributor extends BroadcastReceiver { final Context context, final String application, final String instance, - final Collection features) { + final Collection features, + final Parcelable messenger) { if (Strings.isNullOrEmpty(application) || Strings.isNullOrEmpty(instance)) { Log.w(Config.LOGTAG, "ignoring invalid UnifiedPush registration"); return; @@ -89,18 +111,37 @@ public class UnifiedPushDistributor extends BroadcastReceiver { Log.d( Config.LOGTAG, "successfully created UnifiedPush entry. waking up XmppConnectionService"); + quickLog(context, String.format("successfully registered %s (token = %s) for UnifiedPushed", application, instance)); final Intent serviceIntent = new Intent(context, XmppConnectionService.class); serviceIntent.setAction(XmppConnectionService.ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS); serviceIntent.putExtra("instance", instance); + serviceIntent.putExtra("application", application); + if (messenger instanceof Messenger) { + serviceIntent.putExtra("messenger", messenger); + } Compatibility.startService(context, serviceIntent); } else { Log.d(Config.LOGTAG, "not successful. sending error message back to application"); final Intent registrationFailed = new Intent(ACTION_REGISTRATION_FAILED); + registrationFailed.putExtra(EXTRA_MESSAGE, "instance already exits"); registrationFailed.setPackage(application); registrationFailed.putExtra("token", instance); - context.sendBroadcast(registrationFailed); + if (messenger instanceof Messenger m) { + final var message = new Message(); + message.obj = registrationFailed; + try { + m.send(message); + } catch (final RemoteException e) { + context.sendBroadcast(registrationFailed); + } + } else { + context.sendBroadcast(registrationFailed); + } } } else { + if (messenger instanceof Messenger m) { + sendRegistrationFailed(m,"Your application is not registered to receive messages"); + } Log.d( Config.LOGTAG, "ignoring invalid UnifiedPush registration. Unknown application " @@ -108,6 +149,18 @@ public class UnifiedPushDistributor extends BroadcastReceiver { } } + private void sendRegistrationFailed(final Messenger messenger, final String error) { + final Intent intent = new Intent(ACTION_REGISTRATION_FAILED); + intent.putExtra(EXTRA_MESSAGE, error); + final var message = new Message(); + message.obj = intent; + try { + messenger.send(message); + } catch (final RemoteException e) { + Log.d(Config.LOGTAG,"unable to tell messenger of failed registration",e); + } + } + private List getBroadcastReceivers(final Context context, final String application) { final Intent messageIntent = new Intent(ACTION_MESSAGE); messageIntent.setPackage(application); @@ -124,7 +177,9 @@ public class UnifiedPushDistributor extends BroadcastReceiver { } final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(context); if (unifiedPushDatabase.deleteInstance(instance)) { + quickLog(context, String.format("successfully unregistered token %s from UnifiedPushed (application requested unregister)", instance)); Log.d(Config.LOGTAG, "successfully removed " + instance + " from UnifiedPush"); + // TODO send UNREGISTERED broadcast back to app?! } } @@ -137,6 +192,7 @@ public class UnifiedPushDistributor extends BroadcastReceiver { Log.d(Config.LOGTAG, "app " + application + " has been removed from the system"); final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(context); if (database.deleteApplication(application)) { + quickLog(context, String.format("successfully removed %s from UnifiedPushed (ACTION_PACKAGE_FULLY_REMOVED)", application)); Log.d(Config.LOGTAG, "successfully removed " + application + " from UnifiedPush"); } } @@ -149,4 +205,11 @@ public class UnifiedPushDistributor extends BroadcastReceiver { .hashString(Joiner.on('\0').join(components), Charsets.UTF_8) .asBytes()); } + + public static void quickLog(final Context context, final String message) { + final Intent intent = new Intent(context, XmppConnectionService.class); + intent.setAction(XmppConnectionService.ACTION_QUICK_LOG); + intent.putExtra("message", message); + context.startService(intent); + } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 99bf83a83..8484498e4 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -19,6 +19,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; import android.database.ContentObserver; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; @@ -34,6 +35,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.IBinder; +import android.os.Messenger; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.os.SystemClock; @@ -60,6 +62,7 @@ import com.google.common.base.Optional; import com.google.common.base.Strings; import org.conscrypt.Conscrypt; +import org.jxmpp.stringprep.libidn.LibIdnXmppStringprep; import org.openintents.openpgp.IOpenPgpService2; import org.openintents.openpgp.util.OpenPgpApi; import org.openintents.openpgp.util.OpenPgpServiceConnection; @@ -68,7 +71,6 @@ import java.io.File; import java.security.Security; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; -import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -82,11 +84,12 @@ import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; -import java.util.concurrent.Callable; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -126,6 +129,7 @@ import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.persistance.UnifiedPushDatabase; import eu.siacs.conversations.ui.ChooseAccountForProfilePictureActivity; +import eu.siacs.conversations.ui.ConversationsActivity; import eu.siacs.conversations.ui.RtpSessionActivity; import eu.siacs.conversations.ui.SettingsActivity; import eu.siacs.conversations.ui.UiCallback; @@ -189,7 +193,11 @@ public class XmppConnectionService extends Service { public static final String ACTION_CLEAR_MISSED_CALL_NOTIFICATION = "clear_missed_call_notification"; public static final String ACTION_DISMISS_ERROR_NOTIFICATIONS = "dismiss_error"; public static final String ACTION_TRY_AGAIN = "try_again"; + + public static final String ACTION_TEMPORARILY_DISABLE = "temporarily_disable"; + public static final String ACTION_PING = "ping"; public static final String ACTION_IDLE_PING = "idle_ping"; + public static final String ACTION_INTERNAL_PING = "internal_ping"; public static final String ACTION_FCM_TOKEN_REFRESH = "fcm_token_refresh"; public static final String ACTION_FCM_MESSAGE_RECEIVED = "fcm_message_received"; public static final String ACTION_DISMISS_CALL = "dismiss_call"; @@ -197,12 +205,15 @@ public class XmppConnectionService extends Service { public static final String ACTION_PROVISION_ACCOUNT = "provision_account"; private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE"; public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS = "eu.siacs.conversations.UNIFIED_PUSH_RENEW"; + public static final String ACTION_QUICK_LOG = "eu.siacs.conversations.QUICK_LOG"; private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp"; public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1); private final static Executor FILE_OBSERVER_EXECUTOR = Executors.newSingleThreadExecutor(); private final static Executor FILE_ATTACHMENT_EXECUTOR = Executors.newSingleThreadExecutor(); + + private final ScheduledExecutorService internalPingExecutor = Executors.newSingleThreadScheduledExecutor(); private final static SerialSingleThreadExecutor VIDEO_COMPRESSION_EXECUTOR = new SerialSingleThreadExecutor("VideoCompression"); private final SerialSingleThreadExecutor mDatabaseWriterExecutor = new SerialSingleThreadExecutor("DatabaseWriter"); private final SerialSingleThreadExecutor mDatabaseReaderExecutor = new SerialSingleThreadExecutor("DatabaseReader"); @@ -457,9 +468,9 @@ public class XmppConnectionService extends Service { joinMuc(conversation); } scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode()); - } else if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED) { + } else if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED || account.getStatus() == Account.State.LOGGED_OUT) { resetSendingToWaiting(account); - if (account.isEnabled() && isInLowPingTimeoutMode(account)) { + if (account.isConnectionEnabled() && isInLowPingTimeoutMode(account)) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": went into offline state during low ping mode. reconnecting now"); reconnectAccount(account, true, false); } else { @@ -472,15 +483,24 @@ public class XmppConnectionService extends Service { } else if (account.getStatus() != Account.State.CONNECTING && account.getStatus() != Account.State.NO_INTERNET) { resetSendingToWaiting(account); if (connection != null && account.getStatus().isAttemptReconnect()) { - final int next = connection.getTimeToNextAttempt(); + final boolean aggressive = account.getStatus() == Account.State.SEE_OTHER_HOST + || hasJingleRtpConnection(account); + final int next = connection.getTimeToNextAttempt(aggressive); final boolean lowPingTimeoutMode = isInLowPingTimeoutMode(account); if (next <= 0) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error connecting account. reconnecting now. lowPingTimeout=" + lowPingTimeoutMode); reconnectAccount(account, true, false); } else { final int attempt = connection.getAttempt() + 1; - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error connecting account. try again in " + next + "s for the " + attempt + " time. lowPingTimeout=" + lowPingTimeoutMode); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error connecting account. try again in " + next + "s for the " + attempt + " time. lowPingTimeout=" + lowPingTimeoutMode+", aggressive="+aggressive); scheduleWakeUpCall(next, account.getUuid().hashCode()); + if (aggressive) { + internalPingExecutor.schedule( + XmppConnectionService.this::manageAccountConnectionStatesInternal, + (next * 1000L) + 50, + TimeUnit.MILLISECONDS + ); + } } } } @@ -493,6 +513,7 @@ public class XmppConnectionService extends Service { private LruCache mBitmapCache; private LruCache mDrawableCache; private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver(); + private final BroadcastReceiver mInternalRestrictedEventReceiver = new RestrictedEventReceiver(Arrays.asList(TorServiceUtils.ACTION_STATUS)); private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver(); private static String generateFetchKey(Account account, final Avatar avatar) { @@ -665,223 +686,275 @@ public class XmppConnectionService extends Service { } @Override - public int onStartCommand(Intent intent, int flags, int startId) { - final String action = intent == null ? null : intent.getAction(); + public int onStartCommand(final Intent intent, int flags, int startId) { + final String action = Strings.nullToEmpty(intent == null ? null : intent.getAction()); final boolean needsForegroundService = intent != null && intent.getBooleanExtra(EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE, false); if (needsForegroundService) { Log.d(Config.LOGTAG, "toggle forced foreground service after receiving event (action=" + action + ")"); toggleForegroundService(true); } - String pushedAccountHash = null; - boolean interactive = false; - if (action != null) { - final String uuid = intent.getStringExtra("uuid"); - switch (action) { - case QuickConversationsService.SMS_RETRIEVED_ACTION: - mQuickConversationsService.handleSmsReceived(intent); - break; - case ConnectivityManager.CONNECTIVITY_ACTION: - if (hasInternetConnection()) { - if (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0) { - schedulePostConnectivityChange(); - } - if (Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) { - resetAllAttemptCounts(true, false); - } - Resolver.clearCache(); + final String uuid = intent == null ? null : intent.getStringExtra("uuid"); + switch (action) { + case QuickConversationsService.SMS_RETRIEVED_ACTION: + mQuickConversationsService.handleSmsReceived(intent); + break; + case ConnectivityManager.CONNECTIVITY_ACTION: + if (hasInternetConnection()) { + if (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0) { + schedulePostConnectivityChange(); } - break; - case Intent.ACTION_SHUTDOWN: - logoutAndSave(true); - return START_NOT_STICKY; - case ACTION_CLEAR_MESSAGE_NOTIFICATION: - mNotificationExecutor.execute(() -> { - try { - final Conversation c = findConversationByUuid(uuid); - if (c != null) { - mNotificationService.clearMessages(c); - } else { - mNotificationService.clearMessages(); - } - restoredFromDatabaseLatch.await(); - - } catch (InterruptedException e) { - Log.d(Config.LOGTAG, "unable to process clear message notification"); - } - }); - break; - case ACTION_CLEAR_MISSED_CALL_NOTIFICATION: - mNotificationExecutor.execute(() -> { - try { - final Conversation c = findConversationByUuid(uuid); - if (c != null) { - mNotificationService.clearMissedCalls(c); - } else { - mNotificationService.clearMissedCalls(); - } - restoredFromDatabaseLatch.await(); - - } catch (InterruptedException e) { - Log.d(Config.LOGTAG, "unable to process clear missed call notification"); - } - }); - break; - case ACTION_DISMISS_CALL: { - final String sessionId = intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID); - Log.d(Config.LOGTAG, "received intent to dismiss call with session id " + sessionId); - mJingleConnectionManager.rejectRtpSession(sessionId); - break; - } - case TorServiceUtils.ACTION_STATUS: - final String status = intent.getStringExtra(TorServiceUtils.EXTRA_STATUS); - //TODO port and host are in 'extras' - but this may not be a reliable source? - if ("ON".equals(status)) { - handleOrbotStartedEvent(); - return START_STICKY; + if (Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) { + resetAllAttemptCounts(true, false); } - break; - case ACTION_END_CALL: { - final String sessionId = intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID); - Log.d(Config.LOGTAG, "received intent to end call with session id " + sessionId); - mJingleConnectionManager.endRtpSession(sessionId); + Resolver.clearCache(); } break; - case ACTION_PROVISION_ACCOUNT: { - final String address = intent.getStringExtra("address"); - final String password = intent.getStringExtra("password"); - if (QuickConversationsService.isQuicksy() || Strings.isNullOrEmpty(address) || Strings.isNullOrEmpty(password)) { - break; - } - provisionAccount(address, password); - break; - } - case ACTION_DISMISS_ERROR_NOTIFICATIONS: - dismissErrorNotifications(); - break; - case ACTION_TRY_AGAIN: - resetAllAttemptCounts(false, true); - interactive = true; - break; - case ACTION_REPLY_TO_CONVERSATION: - Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); - if (remoteInput == null) { - break; - } - final CharSequence body = remoteInput.getCharSequence("text_reply"); - final boolean dismissNotification = intent.getBooleanExtra("dismiss_notification", false); - final String lastMessageUuid = intent.getStringExtra("last_message_uuid"); - if (body == null || body.length() <= 0) { - break; - } - mNotificationExecutor.execute(() -> { - try { - restoredFromDatabaseLatch.await(); - final Conversation c = findConversationByUuid(uuid); - if (c != null) { - directReply(c, body.toString(), lastMessageUuid, dismissNotification); - } - } catch (InterruptedException e) { - Log.d(Config.LOGTAG, "unable to process direct reply"); - } - }); - break; - case ACTION_MARK_AS_READ: - mNotificationExecutor.execute(() -> { + case Intent.ACTION_SHUTDOWN: + logoutAndSave(true); + return START_NOT_STICKY; + case ACTION_CLEAR_MESSAGE_NOTIFICATION: + mNotificationExecutor.execute(() -> { + try { final Conversation c = findConversationByUuid(uuid); - if (c == null) { - Log.d(Config.LOGTAG, "received mark read intent for unknown conversation (" + uuid + ")"); - return; - } - try { - restoredFromDatabaseLatch.await(); - sendReadMarker(c, null); - } catch (InterruptedException e) { - Log.d(Config.LOGTAG, "unable to process notification read marker for conversation " + c.getName()); + if (c != null) { + mNotificationService.clearMessages(c); + } else { + mNotificationService.clearMessages(); } + restoredFromDatabaseLatch.await(); - }); - break; - case ACTION_SNOOZE: - mNotificationExecutor.execute(() -> { + } catch (InterruptedException e) { + Log.d(Config.LOGTAG, "unable to process clear message notification"); + } + }); + break; + case ACTION_CLEAR_MISSED_CALL_NOTIFICATION: + mNotificationExecutor.execute(() -> { + try { final Conversation c = findConversationByUuid(uuid); - if (c == null) { - Log.d(Config.LOGTAG, "received snooze intent for unknown conversation (" + uuid + ")"); - return; + if (c != null) { + mNotificationService.clearMissedCalls(c); + } else { + mNotificationService.clearMissedCalls(); } - c.setMutedTill(System.currentTimeMillis() + 30 * 60 * 1000); - mNotificationService.clearMessages(c); - updateConversation(c); - }); - case AudioManager.RINGER_MODE_CHANGED_ACTION: - case NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED: - if (dndOnSilentMode()) { - refreshAllPresences(); + restoredFromDatabaseLatch.await(); + + } catch (InterruptedException e) { + Log.d(Config.LOGTAG, "unable to process clear missed call notification"); } + }); + break; + case ACTION_DISMISS_CALL: { + if (intent == null) { break; - case Intent.ACTION_SCREEN_ON: - deactivateGracePeriod(); - case Intent.ACTION_USER_PRESENT: - case Intent.ACTION_SCREEN_OFF: - if (awayWhenScreenLocked()) { - refreshAllPresences(); - } - break; - case ACTION_FCM_TOKEN_REFRESH: - refreshAllFcmTokens(); - break; - case ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS: - final String instance = intent.getStringExtra("instance"); - final Optional transport = renewUnifiedPushEndpoints(); - if (instance != null && transport.isPresent()) { - unifiedPushBroker.rebroadcastEndpoint(instance, transport.get()); - } - break; - case ACTION_IDLE_PING: - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - scheduleNextIdlePing(); - } - break; - case ACTION_FCM_MESSAGE_RECEIVED: - pushedAccountHash = intent.getStringExtra("account"); - Log.d(Config.LOGTAG, "push message arrived in service. account=" + pushedAccountHash); - break; - case Intent.ACTION_SEND: - Uri uri = intent.getData(); - if (uri != null) { - Log.d(Config.LOGTAG, "received uri permission for " + uri); - } - return START_STICKY; - } - } - synchronized (this) { - WakeLockHelper.acquire(wakeLock); - boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action) || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0 && ACTION_POST_CONNECTIVITY_CHANGE.equals(action)); - final HashSet pingCandidates = new HashSet<>(); - final String androidId = PhoneHelper.getAndroidId(this); - for (Account account : accounts) { - final boolean pushWasMeantForThisAccount = CryptoHelper.getAccountFingerprint(account, androidId).equals(pushedAccountHash); - pingNow |= processAccountState(account, - interactive, - "ui".equals(action), - pushWasMeantForThisAccount, - pingCandidates); - } - if (pingNow) { - for (Account account : pingCandidates) { - final boolean lowTimeout = isInLowPingTimeoutMode(account); - account.getXmppConnection().sendPing(); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + " send ping (action=" + action + ",lowTimeout=" + lowTimeout + ")"); - scheduleWakeUpCall(lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT, account.getUuid().hashCode()); } + final String sessionId = intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID); + Log.d(Config.LOGTAG, "received intent to dismiss call with session id " + sessionId); + mJingleConnectionManager.rejectRtpSession(sessionId); + break; } - WakeLockHelper.release(wakeLock); + case TorServiceUtils.ACTION_STATUS: + final String status = intent == null ? null : intent.getStringExtra(TorServiceUtils.EXTRA_STATUS); + //TODO port and host are in 'extras' - but this may not be a reliable source? + if ("ON".equals(status)) { + handleOrbotStartedEvent(); + return START_STICKY; + } + break; + case ACTION_END_CALL: { + if (intent == null) { + break; + } + final String sessionId = intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID); + Log.d(Config.LOGTAG, "received intent to end call with session id " + sessionId); + mJingleConnectionManager.endRtpSession(sessionId); + } + break; + case ACTION_PROVISION_ACCOUNT: { + if (intent == null) { + break; + } + final String address = intent.getStringExtra("address"); + final String password = intent.getStringExtra("password"); + if (QuickConversationsService.isQuicksy() || Strings.isNullOrEmpty(address) || Strings.isNullOrEmpty(password)) { + break; + } + provisionAccount(address, password); + break; + } + case ACTION_DISMISS_ERROR_NOTIFICATIONS: + dismissErrorNotifications(); + break; + case ACTION_TRY_AGAIN: + resetAllAttemptCounts(false, true); + break; + case ACTION_REPLY_TO_CONVERSATION: + final Bundle remoteInput = intent == null ? null : RemoteInput.getResultsFromIntent(intent); + if (remoteInput == null) { + break; + } + final CharSequence body = remoteInput.getCharSequence("text_reply"); + final boolean dismissNotification = intent.getBooleanExtra("dismiss_notification", false); + final String lastMessageUuid = intent.getStringExtra("last_message_uuid"); + if (body == null || body.length() <= 0) { + break; + } + mNotificationExecutor.execute(() -> { + try { + restoredFromDatabaseLatch.await(); + final Conversation c = findConversationByUuid(uuid); + if (c != null) { + directReply(c, body.toString(), lastMessageUuid, dismissNotification); + } + } catch (InterruptedException e) { + Log.d(Config.LOGTAG, "unable to process direct reply"); + } + }); + break; + case ACTION_MARK_AS_READ: + mNotificationExecutor.execute(() -> { + final Conversation c = findConversationByUuid(uuid); + if (c == null) { + Log.d(Config.LOGTAG, "received mark read intent for unknown conversation (" + uuid + ")"); + return; + } + try { + restoredFromDatabaseLatch.await(); + sendReadMarker(c, null); + } catch (InterruptedException e) { + Log.d(Config.LOGTAG, "unable to process notification read marker for conversation " + c.getName()); + } + + }); + break; + case ACTION_SNOOZE: + mNotificationExecutor.execute(() -> { + final Conversation c = findConversationByUuid(uuid); + if (c == null) { + Log.d(Config.LOGTAG, "received snooze intent for unknown conversation (" + uuid + ")"); + return; + } + c.setMutedTill(System.currentTimeMillis() + 30 * 60 * 1000); + mNotificationService.clearMessages(c); + updateConversation(c); + }); + case AudioManager.RINGER_MODE_CHANGED_ACTION: + case NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED: + if (dndOnSilentMode()) { + refreshAllPresences(); + } + break; + case Intent.ACTION_SCREEN_ON: + deactivateGracePeriod(); + case Intent.ACTION_USER_PRESENT: + case Intent.ACTION_SCREEN_OFF: + if (awayWhenScreenLocked()) { + refreshAllPresences(); + } + break; + case ACTION_FCM_TOKEN_REFRESH: + refreshAllFcmTokens(); + break; + case ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS: + if (intent == null) { + break; + } + final String instance = intent.getStringExtra("instance"); + final String application = intent.getStringExtra("application"); + final Messenger messenger = intent.getParcelableExtra("messenger"); + final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger; + if (messenger != null && application != null && instance != null) { + pushTargetMessenger = new UnifiedPushBroker.PushTargetMessenger(new UnifiedPushDatabase.PushTarget(application, instance),messenger); + Log.d(Config.LOGTAG,"found push target messenger"); + } else { + pushTargetMessenger = null; + } + final Optional transport = renewUnifiedPushEndpoints(pushTargetMessenger); + if (instance != null && transport.isPresent()) { + unifiedPushBroker.rebroadcastEndpoint(messenger, instance, transport.get()); + } + break; + case ACTION_IDLE_PING: + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + scheduleNextIdlePing(); + } + break; + case ACTION_FCM_MESSAGE_RECEIVED: + Log.d(Config.LOGTAG, "push message arrived in service. account"); + break; + case ACTION_QUICK_LOG: + final String message = intent == null ? null : intent.getStringExtra("message"); + if (message != null && Config.QUICK_LOG) { + quickLog(message); + } + break; + case Intent.ACTION_SEND: + final Uri uri = intent == null ? null : intent.getData(); + if (uri != null) { + Log.d(Config.LOGTAG, "received uri permission for " + uri); + } + return START_STICKY; + case ACTION_TEMPORARILY_DISABLE: + toggleSoftDisabled(true); + if (checkListeners()) { + stopSelf(); + } + return START_NOT_STICKY; } + manageAccountConnectionStates(action, intent == null ? null : intent.getExtras()); if (SystemClock.elapsedRealtime() - mLastExpiryRun.get() >= Config.EXPIRY_INTERVAL) { expireOldMessages(); } return START_STICKY; } + private void quickLog(final String message) { + if (Strings.isNullOrEmpty(message)) { + return; + } + final Account account = AccountUtils.getFirstEnabled(this); + if (account == null) { + return; + } + final Conversation conversation = + findOrCreateConversation(account, Config.BUG_REPORTS, null, false, false, true, null); + final Message report = new Message(conversation, message, Message.ENCRYPTION_NONE); + report.setStatus(Message.STATUS_RECEIVED); + conversation.add(report); + databaseBackend.createMessage(report); + updateConversationUi(); + } + + private void manageAccountConnectionStatesInternal() { + manageAccountConnectionStates(ACTION_INTERNAL_PING, null); + } + + private synchronized void manageAccountConnectionStates(final String action, final Bundle extras) { + final String pushedAccountHash = extras == null ? null : extras.getString("account"); + final boolean interactive = Arrays.asList(ACTION_TRY_AGAIN).contains(action); + WakeLockHelper.acquire(wakeLock); + boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action) || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0 && ACTION_POST_CONNECTIVITY_CHANGE.equals(action)); + final HashSet pingCandidates = new HashSet<>(); + final String androidId = PhoneHelper.getAndroidId(this); + for (final Account account : accounts) { + final boolean pushWasMeantForThisAccount = CryptoHelper.getAccountFingerprint(account, androidId).equals(pushedAccountHash); + pingNow |= processAccountState(account, + interactive, + "ui".equals(action), + pushWasMeantForThisAccount, + pingCandidates); + } + if (pingNow) { + for (Account account : pingCandidates) { + final boolean lowTimeout = isInLowPingTimeoutMode(account); + account.getXmppConnection().sendPing(); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + " send ping (action=" + action + ",lowTimeout=" + lowTimeout + ")"); + scheduleWakeUpCall(lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT, account.getUuid().hashCode()); + } + } + WakeLockHelper.release(wakeLock); + } + private void handleOrbotStartedEvent() { for (final Account account : accounts) { if (account.getStatus() == Account.State.TOR_NOT_AVAILABLE) { @@ -891,78 +964,85 @@ public class XmppConnectionService extends Service { } private boolean processAccountState(Account account, boolean interactive, boolean isUiAction, boolean isAccountPushed, HashSet pingCandidates) { - boolean pingNow = false; - if (account.getStatus().isAttemptReconnect()) { - if (!hasInternetConnection()) { - account.setStatus(Account.State.NO_INTERNET); - if (statusListener != null) { - statusListener.onStatusChanged(account); - } - } else { - if (account.getStatus() == Account.State.NO_INTERNET) { - account.setStatus(Account.State.OFFLINE); - if (statusListener != null) { - statusListener.onStatusChanged(account); - } - } - if (account.getStatus() == Account.State.ONLINE) { - synchronized (mLowPingTimeoutMode) { - long lastReceived = account.getXmppConnection().getLastPacketReceived(); - long lastSent = account.getXmppConnection().getLastPingSent(); - long pingInterval = isUiAction ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000; - long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime(); - int pingTimeout = mLowPingTimeoutMode.contains(account.getJid().asBareJid()) ? Config.LOW_PING_TIMEOUT * 1000 : Config.PING_TIMEOUT * 1000; - long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime(); - if (lastSent > lastReceived) { - if (pingTimeoutIn < 0) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping timeout"); - this.reconnectAccount(account, true, interactive); - } else { - int secs = (int) (pingTimeoutIn / 1000); - this.scheduleWakeUpCall(secs, account.getUuid().hashCode()); - } + if (!account.getStatus().isAttemptReconnect()) { + return false; + } + if (!hasInternetConnection()) { + account.setStatus(Account.State.NO_INTERNET); + statusListener.onStatusChanged(account); + } else { + if (account.getStatus() == Account.State.NO_INTERNET) { + account.setStatus(Account.State.OFFLINE); + statusListener.onStatusChanged(account); + } + if (account.getStatus() == Account.State.ONLINE) { + synchronized (mLowPingTimeoutMode) { + long lastReceived = account.getXmppConnection().getLastPacketReceived(); + long lastSent = account.getXmppConnection().getLastPingSent(); + long pingInterval = isUiAction ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000; + long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime(); + int pingTimeout = mLowPingTimeoutMode.contains(account.getJid().asBareJid()) ? Config.LOW_PING_TIMEOUT * 1000 : Config.PING_TIMEOUT * 1000; + long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime(); + if (lastSent > lastReceived) { + if (pingTimeoutIn < 0) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping timeout"); + this.reconnectAccount(account, true, interactive); } else { - pingCandidates.add(account); - if (isAccountPushed) { - pingNow = true; - if (mLowPingTimeoutMode.add(account.getJid().asBareJid())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": entering low ping timeout mode"); - } - } else if (msToNextPing <= 0) { - pingNow = true; - } else { - this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode()); - if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": leaving low ping timeout mode"); - } + int secs = (int) (pingTimeoutIn / 1000); + this.scheduleWakeUpCall(secs, account.getUuid().hashCode()); + } + } else { + pingCandidates.add(account); + if (isAccountPushed) { + if (mLowPingTimeoutMode.add(account.getJid().asBareJid())) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": entering low ping timeout mode"); + } + return true; + } else if (msToNextPing <= 0) { + return true; + } else { + this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode()); + if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": leaving low ping timeout mode"); } } } - } else if (account.getStatus() == Account.State.OFFLINE) { + } + } else if (account.getStatus() == Account.State.OFFLINE) { + reconnectAccount(account, true, interactive); + } else if (account.getStatus() == Account.State.CONNECTING) { + long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000; + long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000; + long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco; + long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect; + if (timeout < 0) { + Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting (secondsSinceLast=" + secondsSinceLastConnect + ")"); + account.getXmppConnection().resetAttemptCount(false); reconnectAccount(account, true, interactive); - } else if (account.getStatus() == Account.State.CONNECTING) { - long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000; - long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000; - long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco; - long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect; - if (timeout < 0) { - Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting (secondsSinceLast=" + secondsSinceLastConnect + ")"); - account.getXmppConnection().resetAttemptCount(false); - reconnectAccount(account, true, interactive); - } else if (discoTimeout < 0) { - account.getXmppConnection().sendDiscoTimeout(); - scheduleWakeUpCall((int) Math.min(timeout, discoTimeout), account.getUuid().hashCode()); - } else { - scheduleWakeUpCall((int) Math.min(timeout, discoTimeout), account.getUuid().hashCode()); - } + } else if (discoTimeout < 0) { + account.getXmppConnection().sendDiscoTimeout(); + scheduleWakeUpCall((int) Math.min(timeout, discoTimeout), account.getUuid().hashCode()); } else { - if (account.getXmppConnection().getTimeToNextAttempt() <= 0) { - reconnectAccount(account, true, interactive); - } + scheduleWakeUpCall((int) Math.min(timeout, discoTimeout), account.getUuid().hashCode()); + } + } else { + final boolean aggressive = account.getStatus() == Account.State.SEE_OTHER_HOST || hasJingleRtpConnection(account); + if (account.getXmppConnection().getTimeToNextAttempt(aggressive) <= 0) { + reconnectAccount(account, true, interactive); + } + } + } + return false; + } + + private void toggleSoftDisabled(final boolean softDisabled) { + for(final Account account : this.accounts) { + if (account.isEnabled()) { + if (account.setOption(Account.OPTION_SOFT_DISABLED, softDisabled)) { + updateAccount(account); } } } - return pingNow; } public boolean processUnifiedPushMessage(final Account account, final Jid transport, final Element push) { @@ -1166,6 +1246,7 @@ public class XmppConnectionService extends Service { @SuppressLint("TrulyRandom") @Override public void onCreate() { + LibIdnXmppStringprep.setup(); if (Compatibility.runsTwentySix()) { mNotificationService.initializeChannels(); } @@ -1238,7 +1319,7 @@ public class XmppConnectionService extends Service { if (Config.supportOpenPgp()) { this.pgpServiceConnection = new OpenPgpServiceConnection(this, "org.sufficientlysecure.keychain", new OpenPgpServiceConnection.OnBound() { @Override - public void onBound(IOpenPgpService2 service) { + public void onBound(final IOpenPgpService2 service) { for (Account account : accounts) { final PgpDecryptionService pgp = account.getPgpDecryptionService(); if (pgp != null) { @@ -1248,7 +1329,8 @@ public class XmppConnectionService extends Service { } @Override - public void onError(Exception e) { + public void onError(final Exception exception) { + Log.e(Config.LOGTAG,"could not bind to OpenKeyChain", exception); } }); this.pgpServiceConnection.bindToService(); @@ -1260,19 +1342,30 @@ public class XmppConnectionService extends Service { toggleForegroundService(); updateUnreadCountBadge(); toggleScreenEventReceiver(); - final IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(TorServiceUtils.ACTION_STATUS); + final IntentFilter systemBroadcastFilter = new IntentFilter(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { scheduleNextIdlePing(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + systemBroadcastFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); } - intentFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED); + systemBroadcastFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED); } - registerReceiver(this.mInternalEventReceiver, intentFilter); + ContextCompat.registerReceiver( + this, + this.mInternalEventReceiver, + systemBroadcastFilter, + ContextCompat.RECEIVER_NOT_EXPORTED); + final IntentFilter exportedBroadcastFilter = new IntentFilter(); + exportedBroadcastFilter.addAction(TorServiceUtils.ACTION_STATUS); + ContextCompat.registerReceiver( + this, + this.mInternalRestrictedEventReceiver, + exportedBroadcastFilter, + ContextCompat.RECEIVER_EXPORTED); mForceDuringOnCreate.set(false); toggleForegroundService(); setupPhoneStateListener(); + internalPingExecutor.scheduleAtFixedRate(this::manageAccountConnectionStatesInternal,10,10,TimeUnit.SECONDS); } @@ -1339,12 +1432,14 @@ public class XmppConnectionService extends Service { public void onDestroy() { try { unregisterReceiver(this.mInternalEventReceiver); + unregisterReceiver(this.mInternalRestrictedEventReceiver); unregisterReceiver(this.mInternalScreenEventReceiver); } catch (final IllegalArgumentException e) { //ignored } destroyed = false; fileObserver.stopWatching(); + internalPingExecutor.shutdown(); super.onDestroy(); } @@ -1393,12 +1488,12 @@ public class XmppConnectionService extends Service { if (ongoing != null) { notification = this.mNotificationService.getOngoingCallNotification(ongoing); id = NotificationService.ONGOING_CALL_NOTIFICATION_ID; - startForeground(id, notification); + startForegroundOrCatch(id, notification); mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID); } else { notification = this.mNotificationService.createForegroundNotification(); id = NotificationService.FOREGROUND_NOTIFICATION_ID; - startForeground(id, notification); + startForegroundOrCatch(id, notification); } if (!mForceForegroundService.get()) { @@ -1418,6 +1513,32 @@ public class XmppConnectionService extends Service { Log.d(Config.LOGTAG, "ForegroundService: " + (status ? "on" : "off")); } + private void startForegroundOrCatch(final int id, final Notification notification) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + final int foregroundServiceType; + if (getSystemService(PowerManager.class) + .isIgnoringBatteryOptimizations(getPackageName())) { + foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED; + } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) + == PackageManager.PERMISSION_GRANTED) { + foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; + } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED) { + foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA; + } else { + foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE; + Log.w(Config.LOGTAG,"falling back to special use foreground service type"); + } + startForeground(id, notification, foregroundServiceType); + } else { + startForeground(id, notification); + } + } catch (final IllegalStateException | SecurityException e) { + Log.e(Config.LOGTAG, "Could not start foreground service", e); + } + } + public boolean foregroundNotificationNeedsUpdatingWhenErrorStateChanges() { return !mForceForegroundService.get() && ongoingCall.get() == null && Compatibility.keepForegroundService(this) && hasEnabledAccounts(); } @@ -1435,7 +1556,7 @@ public class XmppConnectionService extends Service { private void logoutAndSave(boolean stop) { int activeAccounts = 0; for (final Account account : accounts) { - if (account.getStatus() != Account.State.DISABLED) { + if (account.isConnectionEnabled()) { databaseBackend.writeRoster(account.getRoster()); activeAccounts++; } @@ -1471,25 +1592,18 @@ public class XmppConnectionService extends Service { } } - public void scheduleWakeUpCall(int seconds, int requestCode) { + public void scheduleWakeUpCall(final int seconds, final int requestCode) { final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000L; final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); if (alarmManager == null) { return; } final Intent intent = new Intent(this, EventReceiver.class); - intent.setAction("ping"); + intent.setAction(ACTION_PING); try { - final PendingIntent pendingIntent; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - pendingIntent = - PendingIntent.getBroadcast( - this, requestCode, intent, PendingIntent.FLAG_IMMUTABLE); - } else { - pendingIntent = - PendingIntent.getBroadcast( - this, requestCode, intent, 0); - } + final PendingIntent pendingIntent = + PendingIntent.getBroadcast( + this, requestCode, intent, PendingIntent.FLAG_IMMUTABLE); alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent); } catch (RuntimeException e) { Log.e(Config.LOGTAG, "unable to schedule alarm for ping", e); @@ -1883,6 +1997,7 @@ public class XmppConnectionService extends Service { if (connection == null) { Log.d(Config.LOGTAG, account.getJid().asBareJid()+": no connection. ignoring bookmark creation"); } else if (connection.getFeatures().bookmarks2()) { + Log.d(Config.LOGTAG,account.getJid().asBareJid() + ": pushing bookmark via Bookmarks 2"); final Element item = mIqGenerator.publishBookmarkItem(bookmark); pushNodeAndEnforcePublishOptions(account, Namespace.BOOKMARKS2, item, bookmark.getJid().asBareJid().toEscapedString(), PublishOptions.persistentWhitelistAccessMaxItems()); } else if (connection.getFeatures().bookmarksConversion()) { @@ -1896,7 +2011,8 @@ public class XmppConnectionService extends Service { account.removeBookmark(bookmark); final XmppConnection connection = account.getXmppConnection(); if (connection.getFeatures().bookmarks2()) { - IqPacket request = mIqGenerator.deleteItem(Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toEscapedString()); + final IqPacket request = mIqGenerator.deleteItem(Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toEscapedString()); + Log.d(Config.LOGTAG,account.getJid().asBareJid() + ": removing bookmark via Bookmarks 2"); sendIqPacket(account, request, (a, response) -> { if (response.getType() == IqPacket.TYPE.ERROR) { Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": unable to delete bookmark " + response.getErrorCondition()); @@ -2498,8 +2614,12 @@ public class XmppConnectionService extends Service { return this.unifiedPushBroker.reconfigurePushDistributor(); } + private Optional renewUnifiedPushEndpoints(final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger) { + return this.unifiedPushBroker.renewUnifiedPushEndpoints(pushTargetMessenger); + } + public Optional renewUnifiedPushEndpoints() { - return this.unifiedPushBroker.renewUnifiedPushEndpoints(); + return this.unifiedPushBroker.renewUnifiedPushEndpoints(null); } private void provisionAccount(final String address, final String password) { @@ -2890,6 +3010,7 @@ public class XmppConnectionService extends Service { } private void switchToForeground() { + toggleSoftDisabled(false); final boolean broadcastLastActivity = broadcastLastActivity(); for (Conversation conversation : getConversations()) { if (conversation.getMode() == Conversation.MODE_MULTI) { @@ -3270,8 +3391,8 @@ public class XmppConnectionService extends Service { if (this.accounts == null) { return false; } - for (Account account : this.accounts) { - if (account.isEnabled()) { + for (final Account account : this.accounts) { + if (account.isConnectionEnabled()) { return true; } } @@ -3687,23 +3808,23 @@ public class XmppConnectionService extends Service { }); } - private void disconnect(Account account, boolean force) { - if ((account.getStatus() == Account.State.ONLINE) - || (account.getStatus() == Account.State.DISABLED)) { - final XmppConnection connection = account.getXmppConnection(); - if (!force) { - List conversations = getConversations(); - for (Conversation conversation : conversations) { - if (conversation.getAccount() == account) { - if (conversation.getMode() == Conversation.MODE_MULTI) { - leaveMuc(conversation, true); - } + private void disconnect(final Account account, boolean force) { + final XmppConnection connection = account.getXmppConnection(); + if (connection == null) { + return; + } + if (!force) { + final List conversations = getConversations(); + for (Conversation conversation : conversations) { + if (conversation.getAccount() == account) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + leaveMuc(conversation, true); } } - sendOfflinePresence(account); } - connection.disconnect(force); + sendOfflinePresence(account); } + connection.disconnect(force); } @Override @@ -4187,13 +4308,18 @@ public class XmppConnectionService extends Service { private void reconnectAccount(final Account account, final boolean force, final boolean interactive) { synchronized (account) { - XmppConnection connection = account.getXmppConnection(); - if (connection == null) { + final XmppConnection existingConnection = account.getXmppConnection(); + final XmppConnection connection; + if (existingConnection != null) { + connection = existingConnection; + } else if (account.isConnectionEnabled()) { connection = createConnection(account); account.setXmppConnection(connection); + } else { + return; } - boolean hasInternet = hasInternetConnection(); - if (account.isEnabled() && hasInternet) { + final boolean hasInternet = hasInternetConnection(); + if (account.isConnectionEnabled() && hasInternet) { if (!force) { disconnect(account, false); } @@ -4689,7 +4815,7 @@ public class XmppConnectionService extends Service { public void refreshAllPresences() { boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity(); for (Account account : getAccounts()) { - if (account.isEnabled()) { + if (account.isConnectionEnabled()) { sendPresence(account, includeIdleTimestamp); } } @@ -4730,6 +4856,10 @@ public class XmppConnectionService extends Service { return this.mJingleConnectionManager; } + private boolean hasJingleRtpConnection(final Account account) { + return this.mJingleConnectionManager.hasJingleRtpConnection(account); + } + public MessageArchiveService getMessageArchiveService() { return this.mMessageArchiveService; } @@ -4800,10 +4930,10 @@ public class XmppConnectionService extends Service { mDatabaseWriterExecutor.execute(runnable); } - public boolean sendBlockRequest(final Blockable blockable, boolean reportSpam) { + public boolean sendBlockRequest(final Blockable blockable, final boolean reportSpam, final String serverMsgId) { if (blockable != null && blockable.getBlockedJid() != null) { final Jid jid = blockable.getBlockedJid(); - this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid, reportSpam), (a, response) -> { + this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid, reportSpam, serverMsgId), (a, response) -> { if (response.getType() == IqPacket.TYPE.RESULT) { a.getBlocklist().add(jid); updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); @@ -5196,11 +5326,30 @@ public class XmppConnectionService extends Service { private class InternalEventReceiver extends BroadcastReceiver { @Override - public void onReceive(Context context, Intent intent) { + public void onReceive(final Context context, final Intent intent) { onStartCommand(intent, 0, 0); } } + private class RestrictedEventReceiver extends BroadcastReceiver { + + private final Collection allowedActions; + + private RestrictedEventReceiver(final Collection allowedActions) { + this.allowedActions = allowedActions; + } + + @Override + public void onReceive(final Context context, final Intent intent) { + final String action = intent == null ? null : intent.getAction(); + if (allowedActions.contains(action)) { + onStartCommand(intent,0,0); + } else { + Log.e(Config.LOGTAG,"restricting broadcast of event "+action); + } + } + } + public static class OngoingCall { public final AbstractJingleConnection.Id id; public final Set media; @@ -5225,4 +5374,18 @@ public class XmppConnectionService extends Service { return Objects.hashCode(id, media, reconnecting); } } + + public static void toggleForegroundService(final XmppConnectionService service) { + if (service == null) { + return; + } + service.toggleForegroundService(); + } + + public static void toggleForegroundService(final ConversationsActivity activity) { + if (activity == null) { + return; + } + toggleForegroundService(activity.xmppConnectionService); + } } diff --git a/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java b/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java index 6f4d77c51..986aeb563 100644 --- a/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java @@ -14,13 +14,27 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.ui.util.JidDialog; public final class BlockContactDialog { + public static void show(final XmppActivity xmppActivity, final Blockable blockable) { + show(xmppActivity, blockable, null); + } + public static void show(final XmppActivity xmppActivity, final Blockable blockable, final String serverMsgId) { final AlertDialog.Builder builder = new AlertDialog.Builder(xmppActivity); final boolean isBlocked = blockable.isBlocked(); builder.setNegativeButton(R.string.cancel, null); DialogBlockContactBinding binding = DataBindingUtil.inflate(xmppActivity.getLayoutInflater(), R.layout.dialog_block_contact, null, false); final boolean reporting = blockable.getAccount().getXmppConnection().getFeatures().spamReporting(); - binding.reportSpam.setVisibility(!isBlocked && reporting ? View.VISIBLE : View.GONE); + if (reporting && !isBlocked) { + binding.reportSpam.setVisibility(View.VISIBLE); + if (serverMsgId != null) { + binding.reportSpam.setChecked(true); + binding.reportSpam.setEnabled(false); + } else { + binding.reportSpam.setEnabled(true); + } + } else { + binding.reportSpam.setVisibility(View.GONE); + } builder.setView(binding.getRoot()); final String value; @@ -34,8 +48,18 @@ public final class BlockContactDialog { value =blockable.getJid().getDomain().toEscapedString(); res = isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text; } else { - int resBlockAction = blockable instanceof Conversation && ((Conversation) blockable).isWithStranger() ? R.string.block_stranger : R.string.action_block_contact; - builder.setTitle(isBlocked ? R.string.action_unblock_contact : resBlockAction); + if (isBlocked) { + builder.setTitle(R.string.action_unblock_contact); + } else if (serverMsgId != null) { + builder.setTitle(R.string.report_spam_and_block); + } else { + final int resBlockAction = + blockable instanceof Conversation + && ((Conversation) blockable).isWithStranger() + ? R.string.block_stranger + : R.string.action_block_contact; + builder.setTitle(resBlockAction); + } value = blockable.getJid().asBareJid().toEscapedString(); res = isBlocked ? R.string.unblock_contact_text : R.string.block_contact_text; } @@ -45,7 +69,7 @@ public final class BlockContactDialog { xmppActivity.xmppConnectionService.sendUnblockRequest(blockable); } else { boolean toastShown = false; - if (xmppActivity.xmppConnectionService.sendBlockRequest(blockable, binding.reportSpam.isChecked())) { + if (xmppActivity.xmppConnectionService.sendBlockRequest(blockable, binding.reportSpam.isChecked(), serverMsgId)) { Toast.makeText(xmppActivity, R.string.corresponding_conversations_closed, Toast.LENGTH_SHORT).show(); toastShown = true; } diff --git a/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java b/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java index 52e12d960..21a90ffd8 100644 --- a/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java @@ -89,7 +89,7 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid, x, y) -> { Blockable blockable = new RawBlockable(account, contactJid); - if (xmppConnectionService.sendBlockRequest(blockable, false)) { + if (xmppConnectionService.sendBlockRequest(blockable, false, null)) { Toast.makeText(BlocklistActivity.this, R.string.corresponding_conversations_closed, Toast.LENGTH_SHORT).show(); } return true; diff --git a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java index 94f7aa9d7..d6c80b61b 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java @@ -279,7 +279,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im return; } for (final Account account : xmppConnectionService.getAccounts()) { - if (account.getStatus() != Account.State.DISABLED) { + if (account.isEnabled()) { for (final Contact contact : account.getRoster().getContacts()) { if (contact.showInContactList() && !filterContacts.contains(contact.getJid().asBareJid().toString()) @@ -382,7 +382,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im filterContacts(); this.mActivatedAccounts.clear(); for (Account account : xmppConnectionService.getAccounts()) { - if (account.getStatus() != Account.State.DISABLED) { + if (account.isEnabled()) { if (Config.DOMAIN_LOCK != null) { this.mActivatedAccounts.add(account.getJid().getEscapedLocal()); } else { @@ -402,6 +402,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity im @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); ScanActivity.onRequestPermissionResult(this, requestCode, grantResults); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 13ba3f2bb..aaf5c9b8a 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -156,8 +156,9 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers builder.setMultiChoiceItems(configuration.names, values, (dialog, which, isChecked) -> values[which] = isChecked); builder.setNegativeButton(R.string.cancel, null); builder.setPositiveButton(R.string.confirm, (dialog, which) -> { - Bundle options = configuration.toBundle(values); + final Bundle options = configuration.toBundle(values); options.putString("muc#roomconfig_persistentroom", "1"); + options.putString("{http://prosody.im/protocol/muc}roomconfig_allowmemberinvites", options.getString("muc#roomconfig_allowinvites")); xmppConnectionService.pushConferenceConfiguration(mConversation, options, ConferenceDetailsActivity.this); diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 4f67e66f0..f184358fe 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -539,6 +539,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp } boolean skippedInactive = false; boolean showsInactive = false; + boolean showUnverifiedWarning = false; for (final XmppAxolotlSession session : sessions) { final FingerprintStatus trust = session.getTrust(); hasKeys |= !trust.isCompromised(); @@ -554,7 +555,11 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp boolean highlight = session.getFingerprint().equals(messageFingerprint); addFingerprintRow(binding.detailsContactKeys, session, highlight); } + if (trust.isUnverified()) { + showUnverifiedWarning = true; + } } + binding.unverifiedWarning.setVisibility(showUnverifiedWarning ? View.VISIBLE : View.GONE); if (showsInactive || skippedInactive) { binding.showInactiveDevices.setText(showsInactive ? R.string.hide_inactive_devices : R.string.show_inactive_devices); binding.showInactiveDevices.setVisibility(View.VISIBLE); @@ -564,7 +569,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp } else { binding.showInactiveDevices.setVisibility(View.GONE); } - binding.scanButton.setVisibility(hasKeys && isCameraFeatureAvailable() ? View.VISIBLE : View.GONE); + final boolean isCameraFeatureAvailable = isCameraFeatureAvailable(); + binding.scanButton.setVisibility(hasKeys && isCameraFeatureAvailable ? View.VISIBLE : View.GONE); if (hasKeys) { binding.scanButton.setOnClickListener((v) -> ScanActivity.scan(this)); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index e4b71c0da..121395d0a 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -5,6 +5,8 @@ import static eu.siacs.conversations.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSAT import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard; import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.showKeyboard; import static eu.siacs.conversations.utils.PermissionUtils.allGranted; +import static eu.siacs.conversations.utils.PermissionUtils.audioGranted; +import static eu.siacs.conversations.utils.PermissionUtils.cameraGranted; import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied; import static eu.siacs.conversations.utils.PermissionUtils.writeGranted; @@ -170,6 +172,19 @@ import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; import eu.siacs.conversations.xmpp.jingle.RtpCapability; import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; + public class ConversationFragment extends XmppFragment implements EditMessage.KeyboardListener, MessageAdapter.OnContactPictureLongClicked, @@ -489,6 +504,7 @@ public class ConversationFragment extends XmppFragment public void onClick(View v) { final Account account = conversation == null ? null : conversation.getAccount(); if (account != null) { + account.setOption(Account.OPTION_SOFT_DISABLED, false); account.setOption(Account.OPTION_DISABLED, false); activity.xmppConnectionService.updateAccount(account); } @@ -551,7 +567,8 @@ public class ConversationFragment extends XmppFragment null, 0, 0, - 0); + 0, + Compatibility.pgpStartIntentSenderOptions()); } catch (SendIntentException e) { Toast.makeText( getActivity(), @@ -1702,6 +1719,7 @@ public class ConversationFragment extends XmppFragment || t instanceof HttpDownloadConnection); activity.getMenuInflater().inflate(R.menu.message_context, menu); menu.setHeaderTitle(R.string.message_options); + final MenuItem reportAndBlock = menu.findItem(R.id.action_report_and_block); MenuItem openWith = menu.findItem(R.id.open_with); MenuItem copyMessage = menu.findItem(R.id.copy_message); MenuItem copyLink = menu.findItem(R.id.copy_link); @@ -1721,6 +1739,17 @@ public class ConversationFragment extends XmppFragment m.getStatus() == Message.STATUS_SEND_FAILED && m.getErrorMessage() != null && !Message.ERROR_MESSAGE_CANCELLED.equals(m.getErrorMessage()); + final Conversational conversational = m.getConversation(); + if (m.getStatus() == Message.STATUS_RECEIVED && conversational instanceof Conversation c) { + final XmppConnection connection = c.getAccount().getXmppConnection(); + if (c.isWithStranger() + && m.getServerMsgId() != null + && !c.isBlocked() + && connection != null + && connection.getFeatures().spamReporting()) { + reportAndBlock.setVisible(true); + } + } if (!m.isFileOrImage() && !encrypted && !m.isGeoUri() @@ -1849,6 +1878,9 @@ public class ConversationFragment extends XmppFragment case R.id.open_with: openWith(selectedMessage); return true; + case R.id.action_report_and_block: + reportMessage(selectedMessage); + return true; default: return super.onContextItemSelected(item); } @@ -2033,8 +2065,12 @@ public class ConversationFragment extends XmppFragment .show(); return; } + final Account account = conversation.getAccount(); + if (account.setOption(Account.OPTION_SOFT_DISABLED, false)) { + activity.xmppConnectionService.updateAccount(account); + } final Contact contact = conversation.getContact(); - if (contact.getPresences().anySupport(Namespace.JINGLE_MESSAGE)) { + if (RtpCapability.jmiSupport(contact)) { triggerRtpSession(contact.getAccount(), contact.getJid().asBareJid(), action); } else { final RtpCapability.Capability capability; @@ -2289,6 +2325,9 @@ public class ConversationFragment extends XmppFragment } refresh(); } + if (cameraGranted(grantResults, permissions) || audioGranted(grantResults, permissions)) { + XmppConnectionService.toggleForegroundService(activity); + } } public void startDownloadable(Message message) { @@ -2546,6 +2585,10 @@ public class ConversationFragment extends XmppFragment this.messageListAdapter.notifyDataSetChanged(); } + private void reportMessage(final Message message) { + BlockContactDialog.show(activity, conversation.getContact(), message.getServerMsgId()); + } + private void showErrorMessage(final Message message) { AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); builder.setTitle(R.string.error_message); @@ -3213,6 +3256,8 @@ public class ConversationFragment extends XmppFragment R.string.this_account_is_disabled, R.string.enable, this.mEnableAccountListener); + } else if (account.getStatus() == Account.State.LOGGED_OUT) { + showSnackbar(R.string.this_account_is_logged_out,R.string.log_in,this.mEnableAccountListener); } else if (conversation.isBlocked()) { showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener); } else if (contact != null @@ -3956,7 +4001,7 @@ public class ConversationFragment extends XmppFragment try { getActivity() .startIntentSenderForResult( - pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0); + pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions()); } catch (final SendIntentException ignored) { } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index 58d035baa..bb76ea271 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -67,6 +67,7 @@ import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import eu.siacs.conversations.Config; @@ -322,14 +323,16 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio } } - private void handleActivityResult(ActivityResult activityResult) { + private void handleActivityResult(final ActivityResult activityResult) { if (activityResult.resultCode == Activity.RESULT_OK) { handlePositiveActivityResult(activityResult.requestCode, activityResult.data); } else { handleNegativeActivityResult(activityResult.requestCode); } if (activityResult.requestCode == REQUEST_BATTERY_OP) { + // the result code is always 0 even when battery permission were granted requestNotificationPermissionIfNeeded(); + XmppConnectionService.toggleForegroundService(xmppConnectionService); } } diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index b4812477f..741cbcce5 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -49,6 +49,7 @@ import java.util.concurrent.atomic.AtomicInteger; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.databinding.ActivityEditAccountBinding; import eu.siacs.conversations.databinding.DialogPresenceBinding; @@ -66,6 +67,7 @@ import eu.siacs.conversations.ui.util.AvatarWorkerTask; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; import eu.siacs.conversations.ui.util.PendingItem; import eu.siacs.conversations.ui.util.SoftKeyboardUtils; +import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.Resolver; import eu.siacs.conversations.utils.SignupUtils; @@ -149,7 +151,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat if (mInitMode && mAccount != null) { mAccount.setOption(Account.OPTION_DISABLED, false); } - if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !accountInfoEdited) { + if (mAccount != null && Arrays.asList(Account.State.DISABLED, Account.State.LOGGED_OUT).contains(mAccount.getStatus()) && !accountInfoEdited) { + mAccount.setOption(Account.OPTION_SOFT_DISABLED, false); mAccount.setOption(Account.OPTION_DISABLED, false); if (!xmppConnectionService.updateAccount(mAccount)) { Toast.makeText(EditAccountActivity.this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show(); @@ -471,6 +474,10 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat if (requestCode == REQUEST_BATTERY_OP || requestCode == REQUEST_DATA_SAVER) { updateAccountInformation(mAccount == null); } + if (requestCode == REQUEST_BATTERY_OP) { + // the result code is always 0 even when battery permission were granted + XmppConnectionService.toggleForegroundService(xmppConnectionService); + } if (requestCode == REQUEST_CHANGE_STATUS) { PresenceTemplate template = mPendingPresenceTemplate.pop(); if (template != null && resultCode == Activity.RESULT_OK) { @@ -624,6 +631,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat this.binding.accountRegisterNew.setVisibility(View.GONE); } this.binding.actionEditYourName.setOnClickListener(this::onEditYourNameClicked); + this.binding.scanButton.setOnClickListener((v) -> ScanActivity.scan(this)); } private void onEditYourNameClicked(View view) { @@ -977,7 +985,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat public void userInputRequired(PendingIntent pi, String object) { mPendingPresenceTemplate.push(template); try { - startIntentSenderForResult(pi.getIntentSender(), REQUEST_CHANGE_STATUS, null, 0, 0, 0); + startIntentSenderForResult(pi.getIntentSender(), REQUEST_CHANGE_STATUS, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions()); } catch (final IntentSender.SendIntentException ignored) { } } @@ -1025,7 +1033,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat final boolean togglePassword = mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) || !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY); - final boolean editPassword = !mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) || (!mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY) && QuickConversationsService.isConversations()) || mAccount.getLastErrorStatus() == Account.State.UNAUTHORIZED; + final boolean neverLoggedIn = !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY) && QuickConversationsService.isConversations(); + final boolean editPassword = mAccount.unauthorized() || neverLoggedIn; this.binding.accountPasswordLayout.setPasswordVisibilityToggleEnabled(togglePassword); @@ -1153,19 +1162,24 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat this.binding.ownFingerprintDesc.setText(R.string.omemo_fingerprint); } this.binding.axolotlFingerprint.setText(CryptoHelper.prettifyFingerprint(ownAxolotlFingerprint.substring(2))); - this.binding.actionCopyAxolotlToClipboard.setVisibility(View.VISIBLE); - this.binding.actionCopyAxolotlToClipboard.setOnClickListener(v -> copyOmemoFingerprint(ownAxolotlFingerprint)); + this.binding.showQrCodeButton.setVisibility(View.VISIBLE); + this.binding.showQrCodeButton.setOnClickListener(v -> showQrCode()); } else { this.binding.axolotlFingerprintBox.setVisibility(View.GONE); } boolean hasKeys = false; + boolean showUnverifiedWarning = false; binding.otherDeviceKeys.removeAllViews(); - for (XmppAxolotlSession session : mAccount.getAxolotlService().findOwnSessions()) { - if (!session.getTrust().isCompromised()) { + for (final XmppAxolotlSession session : mAccount.getAxolotlService().findOwnSessions()) { + final FingerprintStatus trust = session.getTrust(); + if (!trust.isCompromised()) { boolean highlight = session.getFingerprint().equals(messageFingerprint); addFingerprintRow(binding.otherDeviceKeys, session, highlight); hasKeys = true; } + if (trust.isUnverified()) { + showUnverifiedWarning = true; + } } if (hasKeys && Config.supportOmemo()) { //TODO: either the button should be visible if we print an active device or the device list should be fed with reactived devices this.binding.otherDeviceKeysCard.setVisibility(View.VISIBLE); @@ -1175,6 +1189,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } else { binding.clearDevices.setVisibility(View.VISIBLE); } + binding.unverifiedWarning.setVisibility(showUnverifiedWarning ? View.VISIBLE : View.GONE); + binding.scanButton.setVisibility(showUnverifiedWarning ? View.VISIBLE : View.GONE); } else { this.binding.otherDeviceKeysCard.setVisibility(View.GONE); } diff --git a/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java b/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java index 2f05be90f..e759ee18e 100644 --- a/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java @@ -13,8 +13,13 @@ import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.databinding.DataBindingUtil; +import com.google.common.collect.Collections2; +import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; + import java.util.ArrayList; import java.util.Collections; import java.util.Locale; @@ -56,30 +61,38 @@ public class MucUsersActivity extends XmppActivity implements XmppConnectionServ private void loadAndSubmitUsers() { if (mConversation != null) { allUsers = mConversation.getMucOptions().getUsers(); - Collections.sort(allUsers); submitFilteredList(mSearchEditText != null ? mSearchEditText.getText().toString() : null); } } - private void submitFilteredList(String search) { + private void submitFilteredList(final String search) { if (TextUtils.isEmpty(search)) { - userAdapter.submitList(allUsers); + userAdapter.submitList(Ordering.natural().immutableSortedCopy(allUsers)); } else { final String needle = search.toLowerCase(Locale.getDefault()); - ArrayList filtered = new ArrayList<>(); - for(MucOptions.User user : allUsers) { - final String name = user.getName(); - final Contact contact = user.getContact(); - if (name != null && name.toLowerCase(Locale.getDefault()).contains(needle) || contact != null && contact.getDisplayName().toLowerCase(Locale.getDefault()).contains(needle)) { - filtered.add(user); - } - } - userAdapter.submitList(filtered); + userAdapter.submitList( + Ordering.natural() + .immutableSortedCopy( + Collections2.filter( + this.allUsers, + user -> { + final String name = user.getName(); + final Contact contact = user.getContact(); + return name != null + && name.toLowerCase( + Locale.getDefault()) + .contains(needle) + || contact != null + && contact.getDisplayName() + .toLowerCase( + Locale.getDefault()) + .contains(needle); + }))); } } @Override - public boolean onContextItemSelected(MenuItem item) { + public boolean onContextItemSelected(@NonNull MenuItem item) { if (!MucDetailsContextMenuHelper.onContextItemSelected(item, userAdapter.getSelectedUser(), this)) { return super.onContextItemSelected(item); } diff --git a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java index 89fdae333..44af0d0b2 100644 --- a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java @@ -205,6 +205,7 @@ public abstract class OmemoActivity extends XmppActivity { @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); ScanActivity.onRequestPermissionResult(this, requestCode, grantResults); } } diff --git a/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java index 81b0ae15c..658567aa6 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java @@ -40,8 +40,6 @@ import android.widget.Toast; import androidx.annotation.StringRes; import androidx.databinding.DataBindingUtil; -import com.theartofdev.edmodo.cropper.CropImage; - import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityPublishProfilePictureBinding; @@ -51,6 +49,8 @@ import eu.siacs.conversations.ui.util.PendingItem; import static eu.siacs.conversations.ui.PublishProfilePictureActivity.REQUEST_CHOOSE_PICTURE; +import com.canhub.cropper.CropImage; + public class PublishGroupChatProfilePictureActivity extends XmppActivity implements OnAvatarPublication { private final PendingItem pendingConversationUuid = new PendingItem<>(); private ActivityPublishProfilePictureBinding binding; diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java index 16607b81e..b6822b301 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -19,7 +19,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.StringRes; -import com.theartofdev.edmodo.cropper.CropImage; +import com.canhub.cropper.CropImage; import java.util.concurrent.atomic.AtomicBoolean; diff --git a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java index e6e61b773..de2254b4d 100644 --- a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java @@ -104,11 +104,20 @@ public class RecordingActivity extends Activity implements View.OnClickListener private boolean startRecording() { mRecorder = new MediaRecorder(); mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); - mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); - mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); - mRecorder.setAudioEncodingBitRate(96000); - mRecorder.setAudioSamplingRate(22050); - setupOutputFile(); + final int outputFormat; + if (Config.USE_OPUS_VOICE_MESSAGES && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + outputFormat = MediaRecorder.OutputFormat.OGG; + mRecorder.setOutputFormat(outputFormat); + mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS); + mRecorder.setAudioEncodingBitRate(32000); + } else { + outputFormat = MediaRecorder.OutputFormat.MPEG_4; + mRecorder.setOutputFormat(outputFormat); + mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); + mRecorder.setAudioEncodingBitRate(96000); + mRecorder.setAudioSamplingRate(22050); + } + setupOutputFile(outputFormat); mRecorder.setOutputFile(mOutputFile.getAbsolutePath()); try { @@ -116,10 +125,10 @@ public class RecordingActivity extends Activity implements View.OnClickListener mRecorder.start(); mStartTime = SystemClock.elapsedRealtime(); mHandler.postDelayed(mTickExecutor, 100); - Log.d("Voice Recorder", "started recording to " + mOutputFile.getAbsolutePath()); + Log.d(Config.LOGTAG, "started recording to " + mOutputFile.getAbsolutePath()); return true; } catch (Exception e) { - Log.e("Voice Recorder", "prepare() failed " + e.getMessage()); + Log.e(Config.LOGTAG, "prepare() failed ", e); return false; } } @@ -181,9 +190,18 @@ public class RecordingActivity extends Activity implements View.OnClickListener } } - private File generateOutputFilename() { + private File generateOutputFilename(final int outputFormat) { final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); - final String filename = "RECORDING_" + dateFormat.format(new Date()) + ".m4a"; + final String extension; + if (outputFormat == MediaRecorder.OutputFormat.MPEG_4) { + extension = "m4a"; + } else if (outputFormat == MediaRecorder.OutputFormat.OGG) { + extension = "oga"; + } else { + throw new IllegalStateException("Unrecognized output format"); + } + final String filename = + String.format("RECORDING_%s.%s", dateFormat.format(new Date()), extension); final File parentDirectory; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { parentDirectory = @@ -196,8 +214,8 @@ public class RecordingActivity extends Activity implements View.OnClickListener return new File(conversationsDirectory, filename); } - private void setupOutputFile() { - mOutputFile = generateOutputFilename(); + private void setupOutputFile(final int outputFormat) { + mOutputFile = generateOutputFilename(outputFormat); final File parentDirectory = mOutputFile.getParentFile(); if (Objects.requireNonNull(parentDirectory).mkdirs()) { Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath()); diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index e982d9054..9759841df 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -1,8 +1,9 @@ package eu.siacs.conversations.ui; -import static java.util.Arrays.asList; import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied; +import static java.util.Arrays.asList; + import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; @@ -64,15 +65,25 @@ import eu.siacs.conversations.ui.util.MainThreadExecutor; import eu.siacs.conversations.ui.util.Rationals; import eu.siacs.conversations.utils.PermissionUtils; import eu.siacs.conversations.utils.TimeFrameUtils; -import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import eu.siacs.conversations.xmpp.jingle.ContentAddition; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; +import eu.siacs.conversations.xmpp.jingle.RtpCapability; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; +import org.webrtc.RendererCommon; +import org.webrtc.SurfaceViewRenderer; +import org.webrtc.VideoTrack; + +import java.lang.ref.WeakReference; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate, eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged { @@ -107,9 +118,7 @@ public class RtpSessionActivity extends XmppActivity RtpEndUserState.RECONNECTING, RtpEndUserState.INCOMING_CONTENT_ADD); private static final List STATES_CONSIDERED_CONNECTED = - Arrays.asList( - RtpEndUserState.CONNECTED, - RtpEndUserState.RECONNECTING); + Arrays.asList(RtpEndUserState.CONNECTED, RtpEndUserState.RECONNECTING); private static final List STATES_SHOWING_PIP_PLACEHOLDER = Arrays.asList( RtpEndUserState.ACCEPTING_CALL, @@ -260,21 +269,22 @@ public class RtpSessionActivity extends XmppActivity } public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case R.id.action_help: - launchHelpInBrowser(); - return true; - case R.id.action_goto_chat: - switchToConversation(); - return true; - case R.id.action_dialpad: - toggleDialpadVisibility(); - return true; - case R.id.action_switch_to_video: - requestPermissionAndSwitchToVideo(); - return true; + final var itemItem = item.getItemId(); + if (itemItem == R.id.action_help) { + launchHelpInBrowser(); + return true; + } else if (itemItem == R.id.action_goto_chat) { + switchToConversation(); + return true; + } else if (itemItem == R.id.action_switch_to_video) { + requestPermissionAndSwitchToVideo(); + return true; + } else if (itemItem == R.id.action_dialpad) { + toggleDialpadVisibility(); + return true; + } else { + return super.onOptionsItemSelected(item); } - return super.onOptionsItemSelected(item); } private void launchHelpInBrowser() { @@ -351,8 +361,9 @@ public class RtpSessionActivity extends XmppActivity } private void acceptContentAdd(final ContentAddition contentAddition) { - if (contentAddition == null || contentAddition.direction != ContentAddition.Direction.INCOMING) { - Log.d(Config.LOGTAG,"ignore press on content-accept button"); + if (contentAddition == null + || contentAddition.direction != ContentAddition.Direction.INCOMING) { + Log.d(Config.LOGTAG, "ignore press on content-accept button"); return; } requestPermissionAndAcceptContentAdd(contentAddition); @@ -361,7 +372,11 @@ public class RtpSessionActivity extends XmppActivity private void requestPermissionAndAcceptContentAdd(final ContentAddition contentAddition) { final List permissions = permissions(contentAddition.media()); if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CONTENT)) { - requireRtpConnection().acceptContentAdd(contentAddition.summary); + try { + requireRtpConnection().acceptContentAdd(contentAddition.summary); + } catch (final IllegalStateException e) { + Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); + } } } @@ -704,8 +719,10 @@ public class RtpSessionActivity extends XmppActivity private boolean isConnected() { final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; - final RtpEndUserState endUserState = connection == null ? null : connection.getEndUserState(); - return STATES_CONSIDERED_CONNECTED.contains(endUserState) || endUserState == RtpEndUserState.INCOMING_CONTENT_ADD; + final RtpEndUserState endUserState = + connection == null ? null : connection.getEndUserState(); + return STATES_CONSIDERED_CONNECTED.contains(endUserState) + || endUserState == RtpEndUserState.INCOMING_CONTENT_ADD; } private boolean switchToPictureInPicture() { @@ -864,68 +881,43 @@ public class RtpSessionActivity extends XmppActivity updateStateDisplay(state, Collections.emptySet(), null); } - private void updateStateDisplay(final RtpEndUserState state, final Set media, final ContentAddition contentAddition) { + private void updateStateDisplay( + final RtpEndUserState state, + final Set media, + final ContentAddition contentAddition) { switch (state) { - case INCOMING_CALL: + case INCOMING_CALL -> { Preconditions.checkArgument(media.size() > 0, "Media must not be empty"); if (media.contains(Media.VIDEO)) { setTitle(R.string.rtp_state_incoming_video_call); } else { setTitle(R.string.rtp_state_incoming_call); } - break; - case INCOMING_CONTENT_ADD: + } + case INCOMING_CONTENT_ADD -> { if (contentAddition != null && contentAddition.media().contains(Media.VIDEO)) { setTitle(R.string.rtp_state_content_add_video); } else { setTitle(R.string.rtp_state_content_add); } - break; - case CONNECTING: - setTitle(R.string.rtp_state_connecting); - break; - case CONNECTED: - setTitle(R.string.rtp_state_connected); - break; - case RECONNECTING: - setTitle(R.string.rtp_state_reconnecting); - break; - case ACCEPTING_CALL: - setTitle(R.string.rtp_state_accepting_call); - break; - case ENDING_CALL: - setTitle(R.string.rtp_state_ending_call); - break; - case FINDING_DEVICE: - setTitle(R.string.rtp_state_finding_device); - break; - case RINGING: - setTitle(R.string.rtp_state_ringing); - break; - case DECLINED_OR_BUSY: - setTitle(R.string.rtp_state_declined_or_busy); - break; - case CONNECTIVITY_ERROR: - setTitle(R.string.rtp_state_connectivity_error); - break; - case CONNECTIVITY_LOST_ERROR: - setTitle(R.string.rtp_state_connectivity_lost_error); - break; - case RETRACTED: - setTitle(R.string.rtp_state_retracted); - break; - case APPLICATION_ERROR: - setTitle(R.string.rtp_state_application_failure); - break; - case SECURITY_ERROR: - setTitle(R.string.rtp_state_security_error); - break; - case ENDED: - throw new IllegalStateException( - "Activity should have called finishAndReleaseWakeLock();"); - default: - throw new IllegalStateException( - String.format("State %s has not been handled in UI", state)); + } + case CONNECTING -> setTitle(R.string.rtp_state_connecting); + case CONNECTED -> setTitle(R.string.rtp_state_connected); + case RECONNECTING -> setTitle(R.string.rtp_state_reconnecting); + case ACCEPTING_CALL -> setTitle(R.string.rtp_state_accepting_call); + case ENDING_CALL -> setTitle(R.string.rtp_state_ending_call); + case FINDING_DEVICE -> setTitle(R.string.rtp_state_finding_device); + case RINGING -> setTitle(R.string.rtp_state_ringing); + case DECLINED_OR_BUSY -> setTitle(R.string.rtp_state_declined_or_busy); + case CONNECTIVITY_ERROR -> setTitle(R.string.rtp_state_connectivity_error); + case CONNECTIVITY_LOST_ERROR -> setTitle(R.string.rtp_state_connectivity_lost_error); + case RETRACTED -> setTitle(R.string.rtp_state_retracted); + case APPLICATION_ERROR -> setTitle(R.string.rtp_state_application_failure); + case SECURITY_ERROR -> setTitle(R.string.rtp_state_security_error); + case ENDED -> throw new IllegalStateException( + "Activity should have called finishAndReleaseWakeLock();"); + default -> throw new IllegalStateException( + String.format("State %s has not been handled in UI", state)); } } @@ -981,7 +973,10 @@ public class RtpSessionActivity extends XmppActivity } @SuppressLint("RestrictedApi") - private void updateButtonConfiguration(final RtpEndUserState state, final Set media, final ContentAddition contentAddition) { + private void updateButtonConfiguration( + final RtpEndUserState state, + final Set media, + final ContentAddition contentAddition) { if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) { this.binding.rejectCall.setVisibility(View.INVISIBLE); this.binding.endCall.setVisibility(View.INVISIBLE); @@ -997,7 +992,8 @@ public class RtpSessionActivity extends XmppActivity this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp); this.binding.acceptCall.setVisibility(View.VISIBLE); } else if (state == RtpEndUserState.INCOMING_CONTENT_ADD) { - this.binding.rejectCall.setContentDescription(getString(R.string.reject_switch_to_video)); + this.binding.rejectCall.setContentDescription( + getString(R.string.reject_switch_to_video)); this.binding.rejectCall.setOnClickListener(this::rejectContentAdd); this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp); this.binding.rejectCall.setVisibility(View.VISIBLE); @@ -1089,7 +1085,7 @@ public class RtpSessionActivity extends XmppActivity private void updateInCallButtonConfigurationSpeaker( final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices) { switch (selectedAudioDevice) { - case EARPIECE: + case EARPIECE -> { this.binding.inCallActionRight.setImageResource( R.drawable.ic_volume_off_black_24dp); if (numberOfChoices >= 2) { @@ -1098,13 +1094,13 @@ public class RtpSessionActivity extends XmppActivity this.binding.inCallActionRight.setOnClickListener(null); this.binding.inCallActionRight.setClickable(false); } - break; - case WIRED_HEADSET: + } + case WIRED_HEADSET -> { this.binding.inCallActionRight.setImageResource(R.drawable.ic_headset_black_24dp); this.binding.inCallActionRight.setOnClickListener(null); this.binding.inCallActionRight.setClickable(false); - break; - case SPEAKER_PHONE: + } + case SPEAKER_PHONE -> { this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_up_black_24dp); if (numberOfChoices >= 2) { this.binding.inCallActionRight.setOnClickListener(this::switchToEarpiece); @@ -1112,13 +1108,13 @@ public class RtpSessionActivity extends XmppActivity this.binding.inCallActionRight.setOnClickListener(null); this.binding.inCallActionRight.setClickable(false); } - break; - case BLUETOOTH: + } + case BLUETOOTH -> { this.binding.inCallActionRight.setImageResource( R.drawable.ic_bluetooth_audio_black_24dp); this.binding.inCallActionRight.setOnClickListener(null); this.binding.inCallActionRight.setClickable(false); - break; + } } this.binding.inCallActionRight.setVisibility(View.VISIBLE); } @@ -1147,10 +1143,10 @@ public class RtpSessionActivity extends XmppActivity private void switchCamera(final View view) { Futures.addCallback( requireRtpConnection().switchCamera(), - new FutureCallback() { + new FutureCallback<>() { @Override public void onSuccess(@Nullable Boolean isFrontCamera) { - binding.localVideo.setMirror(isFrontCamera); + binding.localVideo.setMirror(Boolean.TRUE.equals(isFrontCamera)); } @Override @@ -1514,10 +1510,7 @@ public class RtpSessionActivity extends XmppActivity final Account account, Jid with, final RtpEndUserState state, final Set media) { final Intent intent = new Intent(Intent.ACTION_VIEW); intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString()); - if (account.getRoster() - .getContact(with) - .getPresences() - .anySupport(Namespace.JINGLE_MESSAGE)) { + if (RtpCapability.jmiSupport(account.getRoster().getContact(with))) { intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString()); } else { intent.putExtra(EXTRA_WITH, with.toEscapedString()); diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index 5b9665455..2f1dd3bd3 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -138,6 +138,7 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference changeOmemoSettingSummary(); if (QuickConversationsService.isQuicksy() + || QuickConversationsService.isPlayStoreFlavor() || Strings.isNullOrEmpty(Config.CHANNEL_DISCOVERY)) { final PreferenceCategory groupChats = (PreferenceCategory) mSettingsFragment.findPreference("group_chats"); diff --git a/src/main/java/eu/siacs/conversations/ui/ShortcutActivity.java b/src/main/java/eu/siacs/conversations/ui/ShortcutActivity.java index 33164c95c..ff935beae 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShortcutActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShortcutActivity.java @@ -61,7 +61,7 @@ public class ShortcutActivity extends AbstractSearchableListItemActivity { return; } for (final Account account : xmppConnectionService.getAccounts()) { - if (account.getStatus() != Account.State.DISABLED) { + if (account.isEnabled()) { for (final Contact contact : account.getRoster().getContacts()) { if (contact.showInContactList() && contact.match(this, needle)) { diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 2907575b3..32074a522 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -359,7 +359,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } switch (actionItem.getId()) { case R.id.discover_public_channels: - startActivity(new Intent(this, ChannelDiscoveryActivity.class)); + if (QuickConversationsService.isPlayStoreFlavor()) { + throw new IllegalStateException("Channel discovery is not available on Google Play flavor"); + } else { + startActivity(new Intent(this, ChannelDiscoveryActivity.class)); + } break; case R.id.join_public_channel: showJoinConferenceDialog(prefilled); @@ -385,6 +389,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne final Menu menu = popupMenu.getMenu(); for (int i = 0; i < menu.size(); i++) { final MenuItem menuItem = menu.getItem(i); + if (QuickConversationsService.isPlayStoreFlavor() && menuItem.getItemId() == R.id.discover_public_channels) { + continue; + } final SpeedDialActionItem actionItem = new SpeedDialActionItem.Builder(menuItem.getItemId(), menuItem.getIcon()) .setLabel(menuItem.getTitle() != null ? menuItem.getTitle().toString() : null) .setFabImageTintColor(ContextCompat.getColor(this, R.color.white)) @@ -895,6 +902,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (grantResults.length > 0) if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { ScanActivity.onRequestPermissionResult(this, requestCode, grantResults); @@ -1059,8 +1067,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne this.contacts.clear(); ArrayList tags = new ArrayList<>(); final List accounts = xmppConnectionService.getAccounts(); - for (Account account : accounts) { - if (account.getStatus() != Account.State.DISABLED) { + for (final Account account : accounts) { + if (account.isEnabled()) { for (Contact contact : account.getRoster().getContacts()) { Presence.Status s = contact.getShownStatus(); if (contact.showInContactList() && contact.match(this, needle) @@ -1105,7 +1113,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne protected void filterConferences(String needle) { this.conferences.clear(); for (final Account account : xmppConnectionService.getAccounts()) { - if (account.getStatus() != Account.State.DISABLED) { + if (account.isEnabled()) { for (final Bookmark bookmark : account.getBookmarks()) { if (bookmark.match(this, needle)) { this.conferences.add(bookmark); diff --git a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java index 75cb0994e..b1b91c99c 100644 --- a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java @@ -18,13 +18,6 @@ import androidx.databinding.DataBindingUtil; import com.google.common.base.Strings; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityUriHandlerBinding; @@ -36,12 +29,20 @@ import eu.siacs.conversations.utils.SignupUtils; import eu.siacs.conversations.utils.ThemeHelper; import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xmpp.Jid; + import okhttp3.Call; import okhttp3.Callback; import okhttp3.HttpUrl; import okhttp3.Request; import okhttp3.Response; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + public class UriHandlerActivity extends AppCompatActivity { public static final String ACTION_SCAN_QR_CODE = "scan_qr_code"; @@ -59,7 +60,9 @@ public class UriHandlerActivity extends AppCompatActivity { } public static void scan(final Activity activity, final boolean provisioning) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M + || ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED) { final Intent intent = new Intent(activity, UriHandlerActivity.class); intent.setAction(UriHandlerActivity.ACTION_SCAN_QR_CODE); if (provisioning) { @@ -69,14 +72,17 @@ public class UriHandlerActivity extends AppCompatActivity { activity.startActivity(intent); } else { activity.requestPermissions( - new String[]{Manifest.permission.CAMERA}, - provisioning ? REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION : REQUEST_CAMERA_PERMISSIONS_TO_SCAN - ); + new String[] {Manifest.permission.CAMERA}, + provisioning + ? REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION + : REQUEST_CAMERA_PERMISSIONS_TO_SCAN); } } - public static void onRequestPermissionResult(Activity activity, int requestCode, int[] grantResults) { - if (requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN && requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION) { + public static void onRequestPermissionResult( + Activity activity, int requestCode, int[] grantResults) { + if (requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN + && requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION) { return; } if (grantResults.length > 0) { @@ -87,7 +93,11 @@ public class UriHandlerActivity extends AppCompatActivity { scan(activity); } } else { - Toast.makeText(activity, R.string.qr_code_scanner_needs_access_to_camera, Toast.LENGTH_SHORT).show(); + Toast.makeText( + activity, + R.string.qr_code_scanner_needs_access_to_camera, + Toast.LENGTH_SHORT) + .show(); } } } @@ -124,7 +134,7 @@ public class UriHandlerActivity extends AppCompatActivity { private boolean handleUri(final Uri uri, final boolean scanned) { final Intent intent; final XmppUri xmppUri = new XmppUri(uri); - final List accounts = DatabaseBackend.getInstance(this).getAccountJids(true); + final List accounts = DatabaseBackend.getInstance(this).getAccountJids(false); if (SignupUtils.isSupportTokenRegistry() && xmppUri.isValidJid()) { final String preAuth = xmppUri.getParameter(XmppUri.PARAMETER_PRE_AUTH); @@ -138,7 +148,12 @@ public class UriHandlerActivity extends AppCompatActivity { startActivity(intent); return true; } - if (accounts.size() == 0 && xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) { + if (accounts.size() == 0 + && xmppUri.isAction(XmppUri.ACTION_ROSTER) + && "y" + .equalsIgnoreCase( + Strings.nullToEmpty(xmppUri.getParameter(XmppUri.PARAMETER_IBR)) + .trim())) { intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth); intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString()); startActivity(intent); @@ -205,29 +220,28 @@ public class UriHandlerActivity extends AppCompatActivity { private void checkForLinkHeader(final HttpUrl url) { Log.d(Config.LOGTAG, "checking for link header on " + url); - this.call = HttpConnectionManager.OK_HTTP_CLIENT.newCall(new Request.Builder() - .url(url) - .head() - .build()); - this.call.enqueue(new Callback() { - @Override - public void onFailure(@NotNull Call call, @NotNull IOException e) { - Log.d(Config.LOGTAG, "unable to check HTTP url", e); - showError(R.string.no_xmpp_adddress_found); - } - - @Override - public void onResponse(@NotNull Call call, @NotNull Response response) { - if (response.isSuccessful()) { - final String linkHeader = response.header("Link"); - if (linkHeader != null && processLinkHeader(linkHeader)) { - return; + this.call = + HttpConnectionManager.OK_HTTP_CLIENT.newCall( + new Request.Builder().url(url).head().build()); + this.call.enqueue( + new Callback() { + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + Log.d(Config.LOGTAG, "unable to check HTTP url", e); + showError(R.string.no_xmpp_adddress_found); } - } - showError(R.string.no_xmpp_adddress_found); - } - }); + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) { + if (response.isSuccessful()) { + final String linkHeader = response.header("Link"); + if (linkHeader != null && processLinkHeader(linkHeader)) { + return; + } + } + showError(R.string.no_xmpp_adddress_found); + } + }); } private boolean processLinkHeader(final String header) { @@ -264,7 +278,8 @@ public class UriHandlerActivity extends AppCompatActivity { } switch (action) { case Intent.ACTION_MAIN: - binding.progress.setVisibility(call != null && !call.isCanceled() ? View.VISIBLE : View.INVISIBLE); + binding.progress.setVisibility( + call != null && !call.isCanceled() ? View.VISIBLE : View.INVISIBLE); break; case Intent.ACTION_VIEW: case Intent.ACTION_SENDTO: @@ -288,7 +303,8 @@ public class UriHandlerActivity extends AppCompatActivity { private boolean allowProvisioning() { final Intent launchIntent = getIntent(); - return launchIntent != null && launchIntent.getBooleanExtra(EXTRA_ALLOW_PROVISIONING, false); + return launchIntent != null + && launchIntent.getBooleanExtra(EXTRA_ALLOW_PROVISIONING, false); } @Override @@ -311,13 +327,17 @@ public class UriHandlerActivity extends AppCompatActivity { showError(R.string.no_xmpp_adddress_found); } return; - } else if (QuickConversationsService.isConversations() && looksLikeJsonObject(result) && allowProvisioning) { + } else if (QuickConversationsService.isConversations() + && looksLikeJsonObject(result) + && allowProvisioning) { ProvisioningUtils.provision(this, result); finish(); return; } final Uri uri = Uri.parse(result.trim()); - if (allowProvisioning && "https".equalsIgnoreCase(uri.getScheme()) && !XmppUri.INVITE_DOMAIN.equalsIgnoreCase(uri.getHost())) { + if (allowProvisioning + && "https".equalsIgnoreCase(uri.getScheme()) + && !XmppUri.INVITE_DOMAIN.equalsIgnoreCase(uri.getHost())) { final HttpUrl httpUrl = HttpUrl.parse(uri.toString()); if (httpUrl != null) { checkForLinkHeader(httpUrl); diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 49647efaa..e817b760f 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -48,7 +48,6 @@ import android.widget.Toast; import androidx.annotation.BoolRes; import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog.Builder; @@ -81,17 +80,23 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; import eu.siacs.conversations.ui.util.PresenceSelector; +import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.ui.util.SoftKeyboardUtils; import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.ExceptionHelper; -import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.utils.SignupUtils; import eu.siacs.conversations.utils.ThemeHelper; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; + public abstract class XmppActivity extends ActionBarActivity { public static final String EXTRA_ACCOUNT = "account"; @@ -323,22 +328,24 @@ public abstract class XmppActivity extends ActionBarActivity { button.setText(R.string.please_wait); button.setEnabled(false); xmppConnectionService.unregisterAccount(account, result -> { - if (result) { - dialog.dismiss(); - if (postDelete != null) { - postDelete.run(); + runOnUiThread(()->{ + if (result) { + dialog.dismiss(); + if (postDelete != null) { + postDelete.run(); + } + if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) { + final Intent intent = SignupUtils.getSignUpIntent(this); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + } + } else { + deleteFromServer.setEnabled(true); + button.setText(R.string.delete); + button.setEnabled(true); + Toast.makeText(this,R.string.could_not_delete_account_from_server,Toast.LENGTH_LONG).show(); } - if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) { - final Intent intent = SignupUtils.getSignUpIntent(this); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - startActivity(intent); - } - } else { - deleteFromServer.setEnabled(true); - button.setText(R.string.delete); - button.setEnabled(true); - Toast.makeText(this,R.string.could_not_delete_account_from_server,Toast.LENGTH_LONG).show(); - } + }); }); } else { Toast.makeText(this,R.string.not_connected_try_again,Toast.LENGTH_LONG).show(); @@ -686,9 +693,9 @@ public abstract class XmppActivity extends ActionBarActivity { xmppConnectionService.getPgpEngine().generateSignature(intent, account, status, new UiCallback() { @Override - public void userInputRequired(PendingIntent pi, String signature) { + public void userInputRequired(final PendingIntent pi, final String signature) { try { - startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); + startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0,Compatibility.pgpStartIntentSenderOptions()); } catch (final SendIntentException ignored) { } } @@ -749,7 +756,7 @@ public abstract class XmppActivity extends ActionBarActivity { public void userInputRequired(PendingIntent pi, Account object) { try { startIntentSenderForResult(pi.getIntentSender(), - REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0); + REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions()); } catch (final SendIntentException ignored) { } } @@ -923,8 +930,9 @@ public abstract class XmppActivity extends ActionBarActivity { try { startIntentSenderForResult( pgp.getIntentForKey(keyId).getIntentSender(), 0, null, 0, - 0, 0); - } catch (Throwable e) { + 0, 0, Compatibility.pgpStartIntentSenderOptions()); + } catch (final Throwable e) { + Log.d(Config.LOGTAG,"could not launch OpenKeyChain", e); Toast.makeText(XmppActivity.this, R.string.openpgp_error, Toast.LENGTH_SHORT).show(); } } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java index b070a63bb..038f255ea 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java @@ -59,6 +59,7 @@ public class AccountAdapter extends ArrayAdapter { viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorOnline)); break; case DISABLED: + case LOGGED_OUT: case CONNECTING: viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, android.R.attr.textColorSecondary)); break; diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java index 2733e7b8b..2683876c7 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java @@ -74,6 +74,8 @@ public class MediaAdapter extends RecyclerView.Adapter implements View.OnCreateContextMenuListener { @@ -104,7 +105,7 @@ public class UserAdapter extends ListAdapter accounts = service.getAccounts(); - for(Account account : accounts) { + for (Account account : accounts) { if (account.isOptionSet(Account.OPTION_DISABLED)) { return false; } @@ -42,20 +45,22 @@ public class AccountUtils { } catch (final IllegalArgumentException e) { return account.getUuid(); } - final UUID publicDeviceId = getUuid(uuid.getLeastSignificantBits(), uuid.getLeastSignificantBits()); - return publicDeviceId.toString(); - } - - protected static UUID getUuid(final long msb, final long lsb) { - final long msb0 = (msb & 0xffffffffffff0fffL) | 4; // set version - final long lsb0 = (lsb & 0x3fffffffffffffffL) | 0x8000000000000000L; // set variant - return new UUID(msb0, lsb0); + final byte[] bytes = + Bytes.concat( + Longs.toByteArray(uuid.getLeastSignificantBits()), + Longs.toByteArray(uuid.getLeastSignificantBits())); + bytes[6] &= 0x0f; /* clear version */ + bytes[6] |= 0x40; /* set to version 4 */ + bytes[8] &= 0x3f; /* clear variant */ + bytes[8] |= 0x80; /* set to IETF variant */ + final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + return new UUID(byteBuffer.getLong(), byteBuffer.getLong()).toString(); } public static List getEnabledAccounts(final XmppConnectionService service) { - ArrayList accounts = new ArrayList<>(); - for (Account account : service.getAccounts()) { - if (account.getStatus() != Account.State.DISABLED) { + final ArrayList accounts = new ArrayList<>(); + for (final Account account : service.getAccounts()) { + if (account.isEnabled()) { if (Config.DOMAIN_LOCK != null) { accounts.add(account.getJid().getEscapedLocal()); } else { @@ -68,7 +73,7 @@ public class AccountUtils { public static Account getFirstEnabled(XmppConnectionService service) { final List accounts = service.getAccounts(); - for(Account account : accounts) { + for (Account account : accounts) { if (!account.isOptionSet(Account.OPTION_DISABLED)) { return account; } @@ -78,7 +83,7 @@ public class AccountUtils { public static Account getFirst(XmppConnectionService service) { final List accounts = service.getAccounts(); - for(Account account : accounts) { + for (Account account : accounts) { return account; } return null; diff --git a/src/main/java/eu/siacs/conversations/utils/BackupFileHeader.java b/src/main/java/eu/siacs/conversations/utils/BackupFileHeader.java index 54387a8ee..3b536c27a 100644 --- a/src/main/java/eu/siacs/conversations/utils/BackupFileHeader.java +++ b/src/main/java/eu/siacs/conversations/utils/BackupFileHeader.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.utils; +import androidx.annotation.NonNull; + import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; @@ -8,7 +10,7 @@ import eu.siacs.conversations.xmpp.Jid; public class BackupFileHeader { - private static final int VERSION = 1; + private static final int VERSION = 2; private final String app; private final Jid jid; @@ -17,6 +19,7 @@ public class BackupFileHeader { private final byte[] salt; + @NonNull @Override public String toString() { return "BackupFileHeader{" + @@ -47,17 +50,19 @@ public class BackupFileHeader { public static BackupFileHeader read(DataInputStream inputStream) throws IOException { final int version = inputStream.readInt(); - if (version > VERSION) { - throw new IllegalArgumentException("Backup File version was " + version + " but app only supports up to version " + VERSION); - } - String app = inputStream.readUTF(); - String jid = inputStream.readUTF(); + final String app = inputStream.readUTF(); + final String jid = inputStream.readUTF(); long timestamp = inputStream.readLong(); - byte[] iv = new byte[12]; + final byte[] iv = new byte[12]; inputStream.readFully(iv); - byte[] salt = new byte[16]; + final byte[] salt = new byte[16]; inputStream.readFully(salt); - + if (version < VERSION) { + throw new OutdatedBackupFileVersion(); + } + if (version != VERSION) { + throw new IllegalArgumentException("Backup File version was " + version + " but app only supports version " + VERSION); + } return new BackupFileHeader(app, Jid.of(jid), timestamp, iv, salt); } @@ -81,4 +86,8 @@ public class BackupFileHeader { public long getTimestamp() { return timestamp; } + + public static class OutdatedBackupFileVersion extends RuntimeException { + + } } diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java index 04ffc7763..23c935a2b 100644 --- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java +++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java @@ -3,6 +3,7 @@ package eu.siacs.conversations.utils; import static eu.siacs.conversations.services.EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE; import android.annotation.SuppressLint; +import android.app.ActivityOptions; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -10,6 +11,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.net.ConnectivityManager; import android.os.Build; +import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceCategory; import android.preference.PreferenceManager; @@ -20,15 +22,15 @@ import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.core.content.ContextCompat; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.ui.SettingsActivity; import eu.siacs.conversations.ui.SettingsFragment; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + public class Compatibility { private static final List UNUSED_SETTINGS_POST_TWENTYSIX = @@ -41,7 +43,8 @@ public class Compatibility { Collections.singletonList("message_notification_settings"); public static boolean hasStoragePermission(final Context context) { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + return Build.VERSION.SDK_INT < Build.VERSION_CODES.M + || Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU || ContextCompat.checkSelfPermission( context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; @@ -177,4 +180,15 @@ public class Compatibility { return ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED; } } + + public static Bundle pgpStartIntentSenderOptions() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + return ActivityOptions.makeBasic() + .setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) + .toBundle(); + } else { + return null; + } + } } diff --git a/src/main/java/eu/siacs/conversations/utils/FileUtils.java b/src/main/java/eu/siacs/conversations/utils/FileUtils.java index 43e3da118..e439ab030 100644 --- a/src/main/java/eu/siacs/conversations/utils/FileUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/FileUtils.java @@ -34,10 +34,8 @@ public class FileUtils { return null; } - final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; - // DocumentProvider - if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { + if (DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider if (isExternalStorageDocument(uri)) { final String docId = DocumentsContract.getDocumentId(uri); diff --git a/src/main/java/eu/siacs/conversations/utils/IP.java b/src/main/java/eu/siacs/conversations/utils/IP.java index a7182e207..948f7537a 100644 --- a/src/main/java/eu/siacs/conversations/utils/IP.java +++ b/src/main/java/eu/siacs/conversations/utils/IP.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.utils; +import com.google.common.net.InetAddresses; + import java.util.regex.Pattern; public class IP { @@ -27,4 +29,14 @@ public class IP { } } + public static String unwrapIPv6(final String host) { + if (host.length() > 2 && host.charAt(0) == '[' && host.charAt(host.length() - 1) == ']') { + final String ip = host.substring(1,host.length() -1); + if (InetAddresses.isInetAddress(ip)) { + return ip; + } + } + return host; + } + } diff --git a/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java b/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java index d3aade355..04573ef4c 100644 --- a/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java +++ b/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java @@ -60,7 +60,7 @@ public class IrregularUnicodeDetector { private static final Map NORMALIZATION_MAP; private static final LruCache CACHE = new LruCache<>(4096); - private static final List AMBIGUOUS_CYRILLIC = Arrays.asList("а","г","е","ѕ","і","q","о","р","с","у"); + private static final List AMBIGUOUS_CYRILLIC = Arrays.asList("а","г","е","ѕ","і","ј","ķ","ԛ","о","р","с","у","х"); static { Map temp = new HashMap<>(); diff --git a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java index d112a9224..c90872028 100644 --- a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java @@ -28,6 +28,8 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -260,7 +262,8 @@ public final class MimeUtils { add("audio/mpeg", "mpga"); add("audio/mpeg", "mpega"); add("audio/mpeg", "mp2"); - add("audio/mpeg", "m4a"); + add("audio/mp4", "m4a"); + add("audio/x-m4b", "m4b"); add("audio/mpegurl", "m3u"); add("audio/ogg", "oga"); add("audio/opus", "opus"); @@ -413,6 +416,9 @@ public final class MimeUtils { applyOverrides(); } + // mime types that are more reliant by path + private static final Collection PATH_PRECEDENCE_MIME_TYPE = Arrays.asList("audio/x-m4b"); + private static void add(String mimeType, String extension) { // If we have an existing x -> y mapping, we do not want to // override it with another mapping x -> y2. @@ -537,44 +543,49 @@ public final class MimeUtils { } public static String guessMimeTypeFromUriAndMime(final Context context, final Uri uri, final String mime) { - Log.d(Config.LOGTAG, "guessMimeTypeFromUriAndMime " + uri + " and mime=" + mime); - if (mime == null || mime.equals("application/octet-stream")) { - final String guess = guessMimeTypeFromUri(context, uri); - if (guess != null) { - return guess; - } else { - return mime; - } + Log.d(Config.LOGTAG, "guessMimeTypeFromUriAndMime(" + uri + "," + mime+")"); + final String mimeFromUri = guessMimeTypeFromUri(context, uri); + Log.d(Config.LOGTAG,"mimeFromUri:"+mimeFromUri); + if (PATH_PRECEDENCE_MIME_TYPE.contains(mimeFromUri)) { + return mimeFromUri; + } else if (mime == null || mime.equals("application/octet-stream")) { + return mimeFromUri; + } else { + return mime; } - return guessMimeTypeFromUri(context, uri); } - public static String guessMimeTypeFromUri(Context context, Uri uri) { - // try the content resolver - String mimeType; + public static String guessMimeTypeFromUri(final Context context, final Uri uri) { + final String mimeTypeContentResolver = guessFromContentResolver(context, uri); + final String mimeTypeFromQueryParameter = uri.getQueryParameter("mimeType"); + final String name = "content".equals(uri.getScheme()) ? getDisplayName(context, uri) : null; + final String mimeTypeFromName = Strings.isNullOrEmpty(name) ? null : guessFromPath(name); + final String path = uri.getPath(); + final String mimeTypeFromPath = Strings.isNullOrEmpty(path) ? null : guessFromPath(path); + if (PATH_PRECEDENCE_MIME_TYPE.contains(mimeTypeFromName)) { + return mimeTypeFromName; + } + if (PATH_PRECEDENCE_MIME_TYPE.contains(mimeTypeFromPath)) { + return mimeTypeFromPath; + } + if (mimeTypeContentResolver != null && !"application/octet-stream".equals(mimeTypeContentResolver)) { + return mimeTypeContentResolver; + } + if (mimeTypeFromName != null) { + return mimeTypeFromName; + } + if (mimeTypeFromQueryParameter != null) { + return mimeTypeFromQueryParameter; + } + return mimeTypeFromPath; + } + + private static String guessFromContentResolver(final Context context, final Uri uri) { try { - mimeType = context.getContentResolver().getType(uri); - } catch (final Throwable throwable) { - mimeType = null; + return context.getContentResolver().getType(uri); + } catch (final Throwable e) { + return null; } - // try the extension - if (mimeType == null || mimeType.equals("application/octet-stream")) { - final String path = uri.getPath(); - if (path != null) { - mimeType = guessFromPath(path); - } - } - if (mimeType == null && "content".equals(uri.getScheme())) { - final String name = getDisplayName(context, uri); - if (name != null) { - mimeType = guessFromPath(name); - } - } - // sometimes this works (as with the commit content api) - if (mimeType == null) { - mimeType = uri.getQueryParameter("mimeType"); - } - return mimeType; } private static String getDisplayName(final Context context, final Uri uri) { diff --git a/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java b/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java index dd74dc57c..21d7f42a1 100644 --- a/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/PermissionUtils.java @@ -24,9 +24,23 @@ public class PermissionUtils { return true; } - public static boolean writeGranted(int[] grantResults, String[] permission) { + public static boolean writeGranted(final int[] grantResults, final String[] permissions) { + return permissionGranted( + Manifest.permission.WRITE_EXTERNAL_STORAGE, grantResults, permissions); + } + + public static boolean audioGranted(final int[] grantResults, final String[] permissions) { + return permissionGranted(Manifest.permission.RECORD_AUDIO, grantResults, permissions); + } + + public static boolean cameraGranted(final int[] grantResults, final String[] permissions) { + return permissionGranted(Manifest.permission.CAMERA, grantResults, permissions); + } + + private static boolean permissionGranted( + final String permission, final int[] grantResults, final String[] permissions) { for (int i = 0; i < grantResults.length; ++i) { - if (Manifest.permission.WRITE_EXTERNAL_STORAGE.equals(permission[i])) { + if (permission.equals(permissions[i])) { return grantResults[i] == PackageManager.PERMISSION_GRANTED; } } @@ -72,7 +86,7 @@ public class PermissionUtils { public static boolean hasPermission( final Activity activity, final List permissions, final int requestCode) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { final ImmutableList.Builder missingPermissions = new ImmutableList.Builder<>(); for (final String permission : permissions) { if (ActivityCompat.checkSelfPermission(activity, permission) diff --git a/src/main/java/eu/siacs/conversations/utils/Resolver.java b/src/main/java/eu/siacs/conversations/utils/Resolver.java index 463d6eb73..915209413 100644 --- a/src/main/java/eu/siacs/conversations/utils/Resolver.java +++ b/src/main/java/eu/siacs/conversations/utils/Resolver.java @@ -6,6 +6,11 @@ import android.util.Log; import androidx.annotation.NonNull; +import com.google.common.base.Strings; +import com.google.common.base.Throwables; +import com.google.common.net.InetAddresses; +import com.google.common.primitives.Ints; + import java.io.IOException; import java.lang.reflect.Field; import java.net.Inet4Address; @@ -15,6 +20,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import de.gultsch.minidns.AndroidDNSClient; import de.measite.minidns.AbstractDNSClient; import de.measite.minidns.DNSCache; import de.measite.minidns.DNSClient; @@ -112,7 +118,7 @@ public class Resolver { return port == 443 || port == 5223; } - public static List resolve(String domain) { + public static List resolve(final String domain) { final List ipResults = fromIpAddress(domain); if (ipResults.size() > 0) { return ipResults; @@ -126,8 +132,10 @@ public class Resolver { synchronized (results) { results.addAll(list); } - } catch (Throwable throwable) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (direct TLS)", throwable); + } catch (final Throwable throwable) { + if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) { + Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (direct TLS)", throwable); + } } }); threads[1] = new Thread(() -> { @@ -136,8 +144,10 @@ public class Resolver { synchronized (results) { results.addAll(list); } - } catch (Throwable throwable) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (STARTTLS)", throwable); + } catch (final Throwable throwable) { + if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) { + Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (STARTTLS)", throwable); + } } }); threads[2] = new Thread(() -> { @@ -260,8 +270,10 @@ public class Resolver { results.addAll(resolveNoSrvRecords(cname.name, false)); } } - } catch (Throwable throwable) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable); + } catch (final Throwable throwable) { + if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) { + Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable); + } } results.add(Result.createDefault(dnsName)); return results; @@ -274,7 +286,9 @@ public class Resolver { private static ResolverResult resolveWithFallback(DNSName dnsName, Class type, boolean validateHostname) throws IOException { final Question question = new Question(dnsName, Record.TYPE.getType(type)); if (!validateHostname) { - return ResolverApi.INSTANCE.resolve(question); + final AndroidDNSClient androidDNSClient = new AndroidDNSClient(SERVICE); + final ResolverApi resolverApi = new ResolverApi(androidDNSClient); + return resolverApi.resolve(question); } try { return DnssecResolverApi.INSTANCE.resolveDnssecReliable(question); @@ -435,6 +449,65 @@ public class Resolver { contentValues.put(AUTHENTICATED, authenticated ? 1 : 0); return contentValues; } + + public Result seeOtherHost(final String seeOtherHost) { + final String hostname = seeOtherHost.trim(); + if (hostname.isEmpty()) { + return null; + } + final Result result = new Result(); + result.directTls = this.directTls; + final int portSegmentStart = hostname.lastIndexOf(':'); + if (hostname.charAt(hostname.length() - 1) != ']' + && portSegmentStart >= 0 + && hostname.length() >= portSegmentStart + 1) { + final String hostPart = hostname.substring(0, portSegmentStart); + final String portPart = hostname.substring(portSegmentStart + 1); + final Integer port = Ints.tryParse(portPart); + if (port == null || Strings.isNullOrEmpty(hostPart)) { + return null; + } + final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostPart); + result.port = port; + if (InetAddresses.isInetAddress(host)) { + final InetAddress inetAddress; + try { + inetAddress = InetAddresses.forString(host); + } catch (final IllegalArgumentException e) { + return null; + } + result.ip = inetAddress; + } else { + if (hostPart.trim().isEmpty()) { + return null; + } + try { + result.hostname = DNSName.from(hostPart.trim()); + } catch (final Exception e) { + return null; + } + } + } else { + final String host = eu.siacs.conversations.utils.IP.unwrapIPv6(hostname); + if (InetAddresses.isInetAddress(host)) { + final InetAddress inetAddress; + try { + inetAddress = InetAddresses.forString(host); + } catch (final IllegalArgumentException e) { + return null; + } + result.ip = inetAddress; + } else { + try { + result.hostname = DNSName.from(hostname); + } catch (final Exception e) { + return null; + } + } + result.port = port; + } + return result; + } } } diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index f5d69115e..173454f59 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -481,6 +481,8 @@ public class UIHelper { return context.getString(R.string.file); } else if (MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime)) { return context.getString(R.string.multimedia_file); + } else if (mime.equals("audio/x-m4b")) { + return context.getString(R.string.audiobook); } else if (mime.startsWith("audio/")) { return context.getString(R.string.audio); } else if (mime.startsWith("video/")) { diff --git a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java index 57a2f3dba..635afd145 100644 --- a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java +++ b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java @@ -29,7 +29,7 @@ public class LocalizedContent { final String childLanguage = child.getAttribute("xml:lang"); final String lang = childLanguage == null ? parentLanguage : childLanguage; final String content = child.getContent(); - if (content != null && (namespace == null || "jabber:client".equals(namespace))) { + if (content != null && (namespace == null || Namespace.JABBER_CLIENT.equals(namespace))) { if (contents.put(lang, content) != null) { //anything that has multiple contents for the same language is invalid return null; diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 4570033e4..7ccaab739 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -2,6 +2,7 @@ package eu.siacs.conversations.xml; public final class Namespace { public static final String STREAMS = "http://etherx.jabber.org/streams"; + public static final String JABBER_CLIENT = "jabber:client"; public static final String DISCO_ITEMS = "http://jabber.org/protocol/disco#items"; public static final String DISCO_INFO = "http://jabber.org/protocol/disco#info"; public static final String EXTERNAL_SERVICE_DISCOVERY = "urn:xmpp:extdisco:2"; @@ -65,5 +66,9 @@ public final class Namespace { public static final String PARS = "urn:xmpp:pars:0"; public static final String EASY_ONBOARDING_INVITE = "urn:xmpp:invite#invite"; public static final String OMEMO_DTLS_SRTP_VERIFICATION = "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification"; + public static final String JINGLE_TRANSPORT_ICE_OPTION = "http://gultsch.de/xmpp/drafts/jingle/transports/ice-udp/option"; public static final String UNIFIED_PUSH = "http://gultsch.de/xmpp/drafts/unified-push"; + public static final String REPORTING = "urn:xmpp:reporting:1"; + public static final String REPORTING_REASON_SPAM = "urn:xmpp:reporting:spam"; + public static final String SDP_OFFER_ANSWER = "urn:ietf:rfc:3264"; } diff --git a/src/main/java/eu/siacs/conversations/xml/Tag.java b/src/main/java/eu/siacs/conversations/xml/Tag.java index db2b11172..d8d98348e 100644 --- a/src/main/java/eu/siacs/conversations/xml/Tag.java +++ b/src/main/java/eu/siacs/conversations/xml/Tag.java @@ -43,6 +43,10 @@ public class Tag { return name; } + public String identifier() { + return String.format("%s#%s", name, this.attributes.get("xmlns")); + } + public String getAttribute(final String attrName) { return this.attributes.get(attrName); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 05927146d..edd67851c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -16,8 +16,11 @@ import android.util.SparseArray; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.common.base.MoreObjects; import com.google.common.base.Optional; +import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; import org.xmlpull.v1.XmlPullParserException; @@ -68,6 +71,7 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.XmppDomainVerifier; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.sasl.ChannelBinding; +import eu.siacs.conversations.crypto.sasl.ChannelBindingMechanism; import eu.siacs.conversations.crypto.sasl.HashedToken; import eu.siacs.conversations.crypto.sasl.SaslMechanism; import eu.siacs.conversations.entities.Account; @@ -165,7 +169,7 @@ public class XmppConnection implements Runnable { private boolean isBound = false; private Element streamFeatures; private Element boundStreamFeatures; - private String streamId = null; + private StreamId streamId = null; private int stanzasReceived = 0; private int stanzasSent = 0; private int stanzasSentBeforeAuthentication; @@ -188,10 +192,12 @@ public class XmppConnection implements Runnable { private OnStatusChanged statusListener = null; private OnBindListener bindListener = null; private OnMessageAcknowledged acknowledgedListener = null; - private SaslMechanism saslMechanism; + private LoginInfo loginInfo; private HashedToken.Mechanism hashTokenRequest; private HttpUrl redirectionUrl = null; private String verifiedHostname = null; + private Resolver.Result currentResolverResult; + private Resolver.Result seeOtherHostResolverResult; private volatile Thread mThread; private CountDownLatch mStreamCountDownLatch; private static ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1); @@ -234,10 +240,11 @@ public class XmppConnection implements Runnable { return; } if (account.getStatus() != nextStatus) { - if ((nextStatus == Account.State.OFFLINE) - && (account.getStatus() != Account.State.CONNECTING) - && (account.getStatus() != Account.State.ONLINE) - && (account.getStatus() != Account.State.DISABLED)) { + if (nextStatus == Account.State.OFFLINE + && account.getStatus() != Account.State.CONNECTING + && account.getStatus() != Account.State.ONLINE + && account.getStatus() != Account.State.DISABLED + && account.getStatus() != Account.State.LOGGED_OUT) { return; } if (nextStatus == Account.State.ONLINE) { @@ -364,7 +371,18 @@ public class XmppConnection implements Runnable { + storedBackupResult); } } - for (Iterator iterator = results.iterator(); + final StreamId streamId = this.streamId; + final Resolver.Result resumeLocation = streamId == null ? null : streamId.location; + if (resumeLocation != null) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": injected resume location on position 0"); + results.add(0, resumeLocation); + } + final Resolver.Result seeOtherHost = this.seeOtherHostResolverResult; + if (seeOtherHost != null) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": injected see-other-host on position 0"); + results.add(0, seeOtherHost); + } + for (final Iterator iterator = results.iterator(); iterator.hasNext(); ) { final Resolver.Result result = iterator.next(); if (Thread.currentThread().isInterrupted()) { @@ -378,7 +396,6 @@ public class XmppConnection implements Runnable { features.encryptionEnabled = result.isDirectTls(); verifiedHostname = result.isAuthenticated() ? result.getHostname().toString() : null; - Log.d(Config.LOGTAG, "verified hostname " + verifiedHostname); final InetSocketAddress addr; if (result.getIp() != null) { addr = new InetSocketAddress(result.getIp(), result.getPort()); @@ -426,6 +443,8 @@ public class XmppConnection implements Runnable { mXmppConnectionService.databaseBackend.saveResolverResult( domain, result); } + this.currentResolverResult = result; + this.seeOtherHostResolverResult = null; break; // successfully connected to server that speaks xmpp } else { FileBackend.close(localSocket); @@ -575,7 +594,6 @@ public class XmppConnection implements Runnable { if (processSuccess(success)) { break; } - } else if (nextTag.isStart("failure", Namespace.TLS)) { throw new StateChangingException(Account.State.TLS_ERROR); } else if (nextTag.isStart("failure")) { @@ -585,7 +603,7 @@ public class XmppConnection implements Runnable { // two step sasl2 - we don’t support this yet throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT); } else if (nextTag.isStart("challenge")) { - if (isSecure() && this.saslMechanism != null) { + if (isSecure() && this.loginInfo != null) { final Element challenge = tagReader.readElement(nextTag); processChallenge(challenge); } else { @@ -598,10 +616,10 @@ public class XmppConnection implements Runnable { } else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) { final Element enabled = tagReader.readElement(nextTag); processEnabled(enabled); - } else if (nextTag.isStart("resumed")) { + } else if (nextTag.isStart("resumed", Namespace.STREAM_MANAGEMENT)) { final Element resumed = tagReader.readElement(nextTag); processResumed(resumed); - } else if (nextTag.isStart("r")) { + } else if (nextTag.isStart("r", Namespace.STREAM_MANAGEMENT)) { tagReader.readElement(nextTag); if (Config.EXTENDED_SM_LOGGING) { Log.d( @@ -612,7 +630,7 @@ public class XmppConnection implements Runnable { } final AckPacket ack = new AckPacket(this.stanzasReceived); tagWriter.writeStanzaAsync(ack); - } else if (nextTag.isStart("a")) { + } else if (nextTag.isStart("a", Namespace.STREAM_MANAGEMENT)) { boolean accountUiNeedsRefresh = false; synchronized (NotificationService.CATCHUP_LOCK) { if (mWaitingForSmCatchup.compareAndSet(true, false)) { @@ -655,15 +673,22 @@ public class XmppConnection implements Runnable { if (acknowledgedMessages) { mXmppConnectionService.updateConversationUi(); } - } else if (nextTag.isStart("failed")) { + } else if (nextTag.isStart("failed", Namespace.STREAM_MANAGEMENT)) { final Element failed = tagReader.readElement(nextTag); processFailed(failed, true); - } else if (nextTag.isStart("iq")) { + } else if (nextTag.isStart("iq", Namespace.JABBER_CLIENT)) { processIq(nextTag); - } else if (nextTag.isStart("message")) { + } else if (nextTag.isStart("message", Namespace.JABBER_CLIENT)) { processMessage(nextTag); - } else if (nextTag.isStart("presence")) { + } else if (nextTag.isStart("presence", Namespace.JABBER_CLIENT)) { processPresence(nextTag); + } else { + Log.e( + Config.LOGTAG, + account.getJid().asBareJid() + + ": Encountered unknown stream element" + + nextTag.identifier()); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } nextTag = tagReader.readTag(); } @@ -688,7 +713,7 @@ public class XmppConnection implements Runnable { throw new AssertionError("Missing implementation for " + version); } try { - response.setContent(saslMechanism.getResponse(challenge.getContent(), sslSocketOrNull(socket))); + response.setContent(this.loginInfo.saslMechanism.getResponse(challenge.getContent(), sslSocketOrNull(socket))); } catch (final SaslMechanism.AuthenticationException e) { // TODO: Send auth abort tag. Log.e(Config.LOGTAG, e.toString()); @@ -705,8 +730,9 @@ public class XmppConnection implements Runnable { } catch (final IllegalArgumentException e) { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - final SaslMechanism currentSaslMechanism = this.saslMechanism; - if (currentSaslMechanism == null) { + final LoginInfo currentLoginInfo = this.loginInfo; + final SaslMechanism currentSaslMechanism = LoginInfo.mechanism(currentLoginInfo); + if (currentLoginInfo == null || currentSaslMechanism == null) { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } final String challenge; @@ -779,7 +805,6 @@ public class XmppConnection implements Runnable { + ": server sent bound and resumed in SASL2 success"); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - final boolean processNopStreamFeatures; if (resumed != null && streamId != null) { if (this.boundStreamFeatures != null) { this.streamFeatures = this.boundStreamFeatures; @@ -792,6 +817,8 @@ public class XmppConnection implements Runnable { if (bound != null) { clearIqCallbacks(); this.isBound = true; + processNopStreamFeatures(); + this.boundStreamFeatures = this.streamFeatures; final Element streamManagementEnabled = bound.findChild("enabled", Namespace.STREAM_MANAGEMENT); final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS); @@ -804,16 +831,25 @@ public class XmppConnection implements Runnable { //if we did not enable stream management in bind do it now waitForDisco = enableStreamManagement(); } + final boolean negotiatedCarbons; if (carbonsEnabled != null) { + negotiatedCarbons = true; Log.d( Config.LOGTAG, - account.getJid().asBareJid() + ": successfully enabled carbons"); + account.getJid().asBareJid() + + ": successfully enabled carbons (via Bind 2.0)"); features.carbonsEnabled = true; + } else if (loginInfo.inlineBindFeatures.contains(Namespace.CARBONS)) { + negotiatedCarbons = true; + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": successfully enabled carbons (via Bind 2.0/implicit)"); + features.carbonsEnabled = true; + } else { + negotiatedCarbons = false; } - sendPostBindInitialization(waitForDisco, carbonsEnabled != null); - processNopStreamFeatures = true; - } else { - processNopStreamFeatures = false; + sendPostBindInitialization(waitForDisco, negotiatedCarbons); } final HashedToken.Mechanism tokenMechanism; if (SaslMechanism.hashedToken(currentSaslMechanism)) { @@ -824,10 +860,15 @@ public class XmppConnection implements Runnable { tokenMechanism = null; } if (tokenMechanism != null && !Strings.isNullOrEmpty(token)) { - this.account.setFastToken(tokenMechanism, token); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": storing hashed token " + tokenMechanism); + if (ChannelBinding.priority(tokenMechanism.channelBinding) >= ChannelBindingMechanism.getPriority(currentSaslMechanism)) { + this.account.setFastToken(tokenMechanism, token); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": storing hashed token " + tokenMechanism); + } else { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": not accepting hashed token "+ tokenMechanism.name()+" for log in mechanism "+currentSaslMechanism.getMechanism()); + this.account.resetFastToken(); + } } else if (this.hashTokenRequest != null) { Log.w( Config.LOGTAG, @@ -835,11 +876,6 @@ public class XmppConnection implements Runnable { + ": no response to our hashed token request " + this.hashTokenRequest); } - // a successful resume will not send stream features - if (processNopStreamFeatures) { - processNopStreamFeatures(); - this.boundStreamFeatures = this.streamFeatures; - } } mXmppConnectionService.databaseBackend.updateAccount(account); this.quickStartInProgress = false; @@ -917,7 +953,7 @@ public class XmppConnection implements Runnable { } Log.d(Config.LOGTAG, failure.toString()); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version); - if (SaslMechanism.hashedToken(this.saslMechanism)) { + if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resetting token"); account.resetFastToken(); mXmppConnectionService.databaseBackend.updateAccount(account); @@ -943,7 +979,7 @@ public class XmppConnection implements Runnable { } } } - if (SaslMechanism.hashedToken(this.saslMechanism)) { + if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) { Log.d( Config.LOGTAG, account.getJid().asBareJid() @@ -963,18 +999,27 @@ public class XmppConnection implements Runnable { } private void processEnabled(final Element enabled) { - final String streamId; + final String id; if (enabled.getAttributeAsBoolean("resume")) { - streamId = enabled.getAttribute("id"); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid().toString() - + ": stream management enabled (resumable)"); + id = enabled.getAttribute("id"); + } else { + id = null; + } + final String locationAttribute = enabled.getAttribute("location"); + final Resolver.Result currentResolverResult = this.currentResolverResult; + final Resolver.Result location; + if (Strings.isNullOrEmpty(locationAttribute) || currentResolverResult == null) { + location = null; + } else { + location = currentResolverResult.seeOtherHost(locationAttribute); + } + final StreamId streamId = id == null ? null : new StreamId(id, location); + if (streamId == null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream management enabled"); } else { Log.d( Config.LOGTAG, - account.getJid().asBareJid().toString() + ": stream management enabled"); - streamId = null; + account.getJid().asBareJid() + ": stream management enabled. resume at: " + streamId.location); } this.streamId = streamId; this.stanzasReceived = 0; @@ -1257,8 +1302,9 @@ public class XmppConnection implements Runnable { tagReader.readTag(); final Socket socket = this.socket; final SSLSocket sslSocket = upgradeSocketToTls(socket); - tagReader.setInputStream(sslSocket.getInputStream()); - tagWriter.setOutputStream(sslSocket.getOutputStream()); + this.socket = sslSocket; + this.tagReader.setInputStream(sslSocket.getInputStream()); + this.tagWriter.setOutputStream(sslSocket.getOutputStream()); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established"); final boolean quickStart; try { @@ -1324,7 +1370,7 @@ public class XmppConnection implements Runnable { account.getJid().asBareJid() + ": quick start in progress. ignoring features: " + XmlHelper.printElementNames(this.streamFeatures)); - if (SaslMechanism.hashedToken(this.saslMechanism)) { + if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) { return; } if (isFastTokenAvailable( @@ -1382,7 +1428,7 @@ public class XmppConnection implements Runnable { + ": resuming after stanza #" + stanzasReceived); } - final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived); + final ResumePacket resume = new ResumePacket(this.streamId.id, stanzasReceived); this.mSmCatchupMessageCounter.set(0); this.mWaitingForSmCatchup.set(true); this.tagWriter.writeStanzaAsync(resume); @@ -1409,7 +1455,8 @@ public class XmppConnection implements Runnable { private void authenticate() throws IOException { final boolean isSecure = isSecure(); - if (isSecure && this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) {authenticate(SaslMechanism.Version.SASL_2); + if (isSecure && this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) { + authenticate(SaslMechanism.Version.SASL_2); } else if (isSecure && this.streamFeatures.hasChild("mechanisms", Namespace.SASL)) { authenticate(SaslMechanism.Version.SASL); } else { @@ -1434,10 +1481,10 @@ public class XmppConnection implements Runnable { final Collection channelBindings = ChannelBinding.of(cbElement); final SaslMechanism.Factory factory = new SaslMechanism.Factory(account); final SaslMechanism saslMechanism = factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket)); - this.saslMechanism = validate(saslMechanism, mechanisms); + this.validate(saslMechanism, mechanisms); final boolean quickStartAvailable; - final String firstMessage = this.saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)); - final boolean usingFast = SaslMechanism.hashedToken(this.saslMechanism); + final String firstMessage = saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)); + final boolean usingFast = SaslMechanism.hashedToken(saslMechanism); final Element authenticate; if (version == SaslMechanism.Version.SASL) { authenticate = new Element("auth", Namespace.SASL); @@ -1445,6 +1492,7 @@ public class XmppConnection implements Runnable { authenticate.setContent(firstMessage); } quickStartAvailable = false; + this.loginInfo = new LoginInfo(saslMechanism,version,Collections.emptyList()); } else if (version == SaslMechanism.Version.SASL_2) { final Element inline = authElement.findChild("inline", Namespace.SASL_2); final boolean sm = inline != null && inline.hasChild("sm", Namespace.STREAM_MANAGEMENT); @@ -1473,6 +1521,7 @@ public class XmppConnection implements Runnable { return; } } + this.loginInfo = new LoginInfo(saslMechanism,version,bindFeatures); this.hashTokenRequest = hashTokenRequest; authenticate = generateAuthenticationRequest(firstMessage, usingFast, hashTokenRequest, bindFeatures, sm); } else { @@ -1489,8 +1538,8 @@ public class XmppConnection implements Runnable { + ": Authenticating with " + version + "/" - + this.saslMechanism.getMechanism()); - authenticate.setAttribute("mechanism", this.saslMechanism.getMechanism()); + + LoginInfo.mechanism(this.loginInfo).getMechanism()); + authenticate.setAttribute("mechanism", LoginInfo.mechanism(this.loginInfo).getMechanism()); synchronized (this.mStanzaQueue) { this.stanzasSentBeforeAuthentication = this.stanzasSent; tagWriter.writeElement(authenticate); @@ -1502,8 +1551,7 @@ public class XmppConnection implements Runnable { return inline != null && inline.hasChild("fast", Namespace.FAST); } - @NonNull - private SaslMechanism validate(final @Nullable SaslMechanism saslMechanism, Collection mechanisms) throws StateChangingException { + private void validate(final @Nullable SaslMechanism saslMechanism, Collection mechanisms) throws StateChangingException { if (saslMechanism == null) { Log.d( Config.LOGTAG, @@ -1513,7 +1561,7 @@ public class XmppConnection implements Runnable { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } if (SaslMechanism.hashedToken(saslMechanism)) { - return saslMechanism; + return; } final int pinnedMechanism = account.getPinnedMechanismPriority(); if (pinnedMechanism > saslMechanism.getPriority()) { @@ -1528,7 +1576,6 @@ public class XmppConnection implements Runnable { + "). Possible downgrade attack?"); throw new StateChangingException(Account.State.DOWNGRADE_ATTACK); } - return saslMechanism; } private Element generateAuthenticationRequest(final String firstMessage, final boolean usingFast) { @@ -1555,11 +1602,14 @@ public class XmppConnection implements Runnable { .addChild("device") .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL)); } - if (bind != null) { + // do not include bind if 'inlineStreamManagement' is missing and we have a streamId + // (because we would rather just do a normal SM/resume) + final boolean mayAttemptBind = streamId == null || inlineStreamManagement; + if (bind != null && mayAttemptBind) { authenticate.addChild(generateBindRequest(bind)); } if (inlineStreamManagement && streamId != null) { - final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived); + final ResumePacket resume = new ResumePacket(this.streamId.id, stanzasReceived); this.mSmCatchupMessageCounter.set(0); this.mWaitingForSmCatchup.set(true); authenticate.addChild(resume); @@ -1731,7 +1781,7 @@ public class XmppConnection implements Runnable { synchronized (this.commands) { this.commands.clear(); } - this.saslMechanism = null; + this.loginInfo = null; } private void sendBindRequest() { @@ -2177,6 +2227,21 @@ public class XmppConnection implements Runnable { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": policy violation. " + text); failPendingMessages(text); throw new StateChangingException(Account.State.POLICY_VIOLATION); + } else if (streamError.hasChild("see-other-host")) { + final String seeOtherHost = streamError.findChildContent("see-other-host"); + final Resolver.Result currentResolverResult = this.currentResolverResult; + if (Strings.isNullOrEmpty(seeOtherHost) || currentResolverResult == null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError); + throw new StateChangingException(Account.State.STREAM_ERROR); + } + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": see other host: "+seeOtherHost+" "+currentResolverResult); + final Resolver.Result seeOtherResult = currentResolverResult.seeOtherHost(seeOtherHost); + if (seeOtherResult != null) { + this.seeOtherHostResolverResult = seeOtherResult; + throw new StateChangingException(Account.State.SEE_OTHER_HOST); + } else { + throw new StateChangingException(Account.State.STREAM_ERROR); + } } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError); throw new StateChangingException(Account.State.STREAM_ERROR); @@ -2200,15 +2265,19 @@ public class XmppConnection implements Runnable { private boolean establishStream(final SSLSockets.Version sslVersion) throws IOException, InterruptedException { - final SaslMechanism quickStartMechanism = - SaslMechanism.ensureAvailable(account.getQuickStartMechanism(), sslVersion); final boolean secureConnection = sslVersion != SSLSockets.Version.NONE; + final SaslMechanism quickStartMechanism; + if (secureConnection) { + quickStartMechanism = SaslMechanism.ensureAvailable(account.getQuickStartMechanism(), sslVersion); + } else { + quickStartMechanism = null; + } if (secureConnection && Config.QUICKSTART_ENABLED && quickStartMechanism != null && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) { mXmppConnectionService.restoredFromDatabaseLatch.await(); - this.saslMechanism = quickStartMechanism; + this.loginInfo = new LoginInfo(quickStartMechanism, SaslMechanism.Version.SASL_2, Bind2.QUICKSTART_FEATURES); final boolean usingFast = quickStartMechanism instanceof HashedToken; final Element authenticate = generateAuthenticationRequest(quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)), usingFast); @@ -2238,7 +2307,7 @@ public class XmppConnection implements Runnable { } stream.setAttribute("version", "1.0"); stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE); - stream.setAttribute("xmlns", "jabber:client"); + stream.setAttribute("xmlns", Namespace.JABBER_CLIENT); stream.setAttribute("xmlns:stream", Namespace.STREAMS); tagWriter.writeTag(stream, flush); } @@ -2512,10 +2581,15 @@ public class XmppConnection implements Runnable { return servers.size() > 0 ? servers.get(0) : null; } - public int getTimeToNextAttempt() { - final int additionalTime = - account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0; - final int interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300); + public int getTimeToNextAttempt(final boolean aggressive) { + final int interval; + if (aggressive) { + interval = Math.min((int) (3 * Math.pow(1.3,attempt)), 60); + } else { + final int additionalTime = + account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0; + interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300); + } final int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000); return interval - secondsSinceLast; @@ -2616,6 +2690,49 @@ public class XmppConnection implements Runnable { } } + private static class LoginInfo { + public final SaslMechanism saslMechanism; + public final SaslMechanism.Version saslVersion; + public final List inlineBindFeatures; + + private LoginInfo( + final SaslMechanism saslMechanism, + final SaslMechanism.Version saslVersion, + final Collection inlineBindFeatures) { + Preconditions.checkNotNull(saslMechanism, "SASL Mechanism must not be null"); + Preconditions.checkNotNull(saslVersion, "SASL version must not be null"); + this.saslMechanism = saslMechanism; + this.saslVersion = saslVersion; + this.inlineBindFeatures = + inlineBindFeatures == null + ? Collections.emptyList() + : ImmutableList.copyOf(inlineBindFeatures); + } + + public static SaslMechanism mechanism(final LoginInfo loginInfo) { + return loginInfo == null ? null : loginInfo.saslMechanism; + } + } + + private static class StreamId { + public final String id; + public final Resolver.Result location; + + private StreamId(String id, Resolver.Result location) { + this.id = id; + this.location = location; + } + + @NonNull + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .add("location", location) + .toString(); + } + } + private static class StateChangingError extends Error { private final Account.State state; @@ -2678,7 +2795,7 @@ public class XmppConnection implements Runnable { } public boolean spamReporting() { - return hasDiscoFeature(account.getDomain(), "urn:xmpp:reporting:reason:spam:0"); + return hasDiscoFeature(account.getDomain(), Namespace.REPORTING); } public boolean flexibleOfflineMessageRetrieval() { @@ -2819,7 +2936,7 @@ public class XmppConnection implements Runnable { } public boolean bookmarks2() { - return Config.USE_BOOKMARKS2 || hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT); + return pepPublishOptions() && hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT); } public boolean externalServiceDiscovery() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java b/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java index 21c957a0f..c3f847eca 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java +++ b/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.xmpp.bind; +import com.google.common.base.Predicates; import com.google.common.collect.Collections2; import java.util.Arrays; @@ -27,7 +28,12 @@ public class Bind2 { if (inlineBind2Inline == null) { return Collections.emptyList(); } - return Collections2.transform( - inlineBind2Inline.getChildren(), c -> c == null ? null : c.getAttribute("var")); + return Collections2.filter( + Collections2.transform( + Collections2.filter( + inlineBind2Inline.getChildren(), + c -> "feature".equals(c.getName())), + c -> c.getAttribute("var")), + Predicates.notNull()); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 2c359c7ec..6edeca462 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -12,20 +12,6 @@ import com.google.common.collect.Collections2; import com.google.common.collect.ComparisonChain; import com.google.common.collect.ImmutableSet; -import java.lang.ref.WeakReference; -import java.security.SecureRandom; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; @@ -39,7 +25,6 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; @@ -51,6 +36,19 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import java.lang.ref.WeakReference; +import java.security.SecureRandom; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + public class JingleConnectionManager extends AbstractConnectionManager { static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor(); @@ -73,7 +71,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { static String nextRandomId() { final byte[] id = new byte[16]; new SecureRandom().nextBytes(id); - return Base64.encodeToString(id, Base64.NO_WRAP | Base64.NO_PADDING); + return Base64.encodeToString(id, Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE); } public void deliverPacket(final Account account, final JinglePacket packet) { @@ -100,7 +98,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { this.terminatedSessions.asMap().containsKey(PersistableSessionId.of(id)); final boolean stranger = isWithStrangerAndStrangerNotificationsAreOff(account, id.with); - if (isBusy() || sessionEnded || stranger) { + final boolean busy = isBusy(); + if (busy || sessionEnded || stranger) { Log.d( Config.LOGTAG, id.account.getJid().asBareJid() @@ -117,6 +116,15 @@ public class JingleConnectionManager extends AbstractConnectionManager { sessionTermination.setTo(id.with); sessionTermination.setReason(Reason.BUSY, null); mXmppConnectionService.sendIqPacket(account, sessionTermination, null); + if (busy || stranger) { + writeLogMissedIncoming( + account, + id.with, + id.sessionId, + null, + System.currentTimeMillis(), + stranger); + } return; } connection = new JingleRtpConnection(this, id, from); @@ -158,10 +166,23 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } + public boolean hasJingleRtpConnection(final Account account) { + for (AbstractJingleConnection connection : this.connections.values()) { + if (connection instanceof JingleRtpConnection rtpConnection) { + if (rtpConnection.isTerminated()) { + continue; + } + if (rtpConnection.id.account == account) { + return true; + } + } + } + return false; + } + public void notifyPhoneCallStarted() { for (AbstractJingleConnection connection : connections.values()) { - if (connection instanceof JingleRtpConnection) { - final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection; + if (connection instanceof JingleRtpConnection rtpConnection) { if (rtpConnection.isTerminated()) { continue; } @@ -195,8 +216,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { private boolean hasMatchingRtpSession( final Account account, final Jid with, final Set media) { for (AbstractJingleConnection connection : this.connections.values()) { - if (connection instanceof JingleRtpConnection) { - final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection; + if (connection instanceof JingleRtpConnection rtpConnection) { if (rtpConnection.isTerminated()) { continue; } @@ -235,7 +255,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { final Element error = response.addChild("error"); error.setAttribute("type", conditionType); error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas"); - error.addChild(jingleCondition, Namespace.JINGLE_ERRORS); + if (jingleCondition != null) { + error.addChild(jingleCondition, Namespace.JINGLE_ERRORS); + } account.getXmppConnection().sendIqPacket(response, null); } @@ -254,8 +276,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { } if ("accept".equals(message.getName())) { for (AbstractJingleConnection connection : connections.values()) { - if (connection instanceof JingleRtpConnection) { - final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection; + if (connection instanceof JingleRtpConnection rtpConnection) { final AbstractJingleConnection.Id id = connection.getId(); if (id.account == account && id.sessionId.equals(sessionId)) { rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); @@ -266,6 +287,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { return; } final boolean fromSelf = from.asBareJid().equals(account.getJid().asBareJid()); + // XEP version 0.6.0 sends proceed, reject, ringing to bare jid final boolean addressedDirectly = to != null && to.equals(account.getJid()); final AbstractJingleConnection.Id id; if (fromSelf) { @@ -310,6 +332,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { Config.LOGTAG, id.account.getJid().asBareJid() + ": updated previous busy because call got picked up by another device"); + mXmppConnectionService.getNotificationService().clearMissedCall(previousBusy); return; } } @@ -366,12 +389,14 @@ public class JingleConnectionManager extends AbstractConnectionManager { this.connections.put(id, rtpConnection); rtpConnection.setProposedMedia(ImmutableSet.copyOf(media)); rtpConnection.deliveryMessage(from, message, serverMsgId, timestamp); + // TODO actually do the automatic accept?! } else { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": our session won tie break. waiting for other party to accept. winningSession=" + ourSessionId); + // TODO reject their session with ? } return; } @@ -379,7 +404,12 @@ public class JingleConnectionManager extends AbstractConnectionManager { isWithStrangerAndStrangerNotificationsAreOff(account, id.with); if (isBusy() || stranger) { writeLogMissedIncoming( - account, id.with.asBareJid(), id.sessionId, serverMsgId, timestamp); + account, + id.with.asBareJid(), + id.sessionId, + serverMsgId, + timestamp, + stranger); if (stranger) { Log.d( Config.LOGTAG, @@ -435,7 +465,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { Log.d( Config.LOGTAG, account.getJid().asBareJid() - + ": no rtp session proposal found for " + + ": no rtp session (" + + sessionId + + ") proposal found for " + from + " to deliver proceed"); if (remoteMsgId == null) { @@ -474,12 +506,19 @@ public class JingleConnectionManager extends AbstractConnectionManager { + " to deliver reject"); } } + } else if (addressedDirectly && "ringing".equals(message.getName())) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + from + " started ringing"); + updateProposedSessionDiscovered( + account, from, sessionId, DeviceDiscoveryState.DISCOVERED); } else { Log.d( Config.LOGTAG, - account.getJid().asBareJid() - + ": retrieved out of order jingle message" - + message); + account.getJid() + + ": retrieved out of order jingle message from " + + from + + message + + ", addressedDirectly=" + + addressedDirectly); } } @@ -514,10 +553,11 @@ public class JingleConnectionManager extends AbstractConnectionManager { private void writeLogMissedIncoming( final Account account, - Jid with, + final Jid with, final String sessionId, - String serverMsgId, - long timestamp) { + final String serverMsgId, + final long timestamp, + final boolean stranger) { final Conversation conversation = mXmppConnectionService.findOrCreateConversation( account, with.asBareJid(), null, false, false, false, null); @@ -527,7 +567,12 @@ public class JingleConnectionManager extends AbstractConnectionManager { message.setBody(new RtpSessionStatus(false, 0).toString()); message.setServerMsgId(serverMsgId); message.setTime(timestamp); + message.setCounterpart(with); writeMessage(message); + if (stranger) { + return; + } + mXmppConnectionService.getNotificationService().pushMissedCallNow(message); } private void writeMessage(final Message message) { @@ -593,15 +638,16 @@ public class JingleConnectionManager extends AbstractConnectionManager { final AbstractJingleConnection.Id id = connection.getId(); if (this.connections.remove(id) == null) { throw new IllegalStateException( - String.format("Unable to finish connection with id=%s", id.toString())); + String.format("Unable to finish connection with id=%s", id)); } + // update chat UI to remove 'ongoing call' icon + mXmppConnectionService.updateConversationUi(); } public boolean fireJingleRtpConnectionStateUpdates() { boolean firedUpdates = false; for (final AbstractJingleConnection connection : this.connections.values()) { - if (connection instanceof JingleRtpConnection) { - final JingleRtpConnection jingleRtpConnection = (JingleRtpConnection) connection; + if (connection instanceof JingleRtpConnection jingleRtpConnection) { if (jingleRtpConnection.isTerminated()) { continue; } @@ -620,63 +666,50 @@ public class JingleConnectionManager extends AbstractConnectionManager { listener.onPrimaryCandidateFound(false, null); return; } - if (!this.primaryCandidates.containsKey(account.getJid().asBareJid())) { - final Jid proxy = - account.getXmppConnection().findDiscoItemByFeature(Namespace.BYTE_STREAMS); - if (proxy != null) { - IqPacket iq = new IqPacket(IqPacket.TYPE.GET); - iq.setTo(proxy); - iq.query(Namespace.BYTE_STREAMS); - account.getXmppConnection() - .sendIqPacket( - iq, - new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived( - Account account, IqPacket packet) { - final Element streamhost = - packet.query() - .findChild( - "streamhost", - Namespace.BYTE_STREAMS); - final String host = - streamhost == null - ? null - : streamhost.getAttribute("host"); - final String port = - streamhost == null - ? null - : streamhost.getAttribute("port"); - if (host != null && port != null) { - try { - JingleCandidate candidate = - new JingleCandidate(nextRandomId(), true); - candidate.setHost(host); - candidate.setPort(Integer.parseInt(port)); - candidate.setType(JingleCandidate.TYPE_PROXY); - candidate.setJid(proxy); - candidate.setPriority( - 655360 + (initiator ? 30 : 0)); - primaryCandidates.put( - account.getJid().asBareJid(), candidate); - listener.onPrimaryCandidateFound(true, candidate); - } catch (final NumberFormatException e) { - listener.onPrimaryCandidateFound(false, null); - } - } else { - listener.onPrimaryCandidateFound(false, null); - } - } - }); - } else { - listener.onPrimaryCandidateFound(false, null); - } - - } else { + if (this.primaryCandidates.containsKey(account.getJid().asBareJid())) { listener.onPrimaryCandidateFound( true, this.primaryCandidates.get(account.getJid().asBareJid())); + return; } + + final Jid proxy = + account.getXmppConnection().findDiscoItemByFeature(Namespace.BYTE_STREAMS); + if (proxy == null) { + listener.onPrimaryCandidateFound(false, null); + return; + } + final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + iq.setTo(proxy); + iq.query(Namespace.BYTE_STREAMS); + account.getXmppConnection() + .sendIqPacket( + iq, + (a, response) -> { + final Element streamhost = + response.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(a.getJid().asBareJid(), candidate); + listener.onPrimaryCandidateFound(true, candidate); + } catch (final NumberFormatException e) { + listener.onPrimaryCandidateFound(false, null); + } + } else { + listener.onPrimaryCandidateFound(false, null); + } + }); } public void retractSessionProposal(final Account account, final Jid with) { @@ -795,13 +828,9 @@ public class JingleConnectionManager extends AbstractConnectionManager { } if (sid != null) { for (final AbstractJingleConnection connection : this.connections.values()) { - if (connection instanceof JingleFileTransferConnection) { - final JingleFileTransferConnection fileTransfer = - (JingleFileTransferConnection) connection; + if (connection instanceof JingleFileTransferConnection fileTransfer) { final JingleTransport transport = fileTransfer.getTransport(); - if (transport instanceof JingleInBandTransport) { - final JingleInBandTransport inBandTransport = - (JingleInBandTransport) transport; + if (transport instanceof JingleInBandTransport inBandTransport) { if (inBandTransport.matches(account, sid)) { inBandTransport.deliverPayload(packet, payload); } @@ -810,7 +839,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } } - Log.d(Config.LOGTAG, "unable to deliver ibb packet: " + packet.toString()); + Log.d(Config.LOGTAG, "unable to deliver ibb packet: " + packet); account.getXmppConnection() .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null); } @@ -916,7 +945,8 @@ public class JingleConnectionManager extends AbstractConnectionManager { } } - public void failProceed(Account account, final Jid with, final String sessionId, final String message) { + public void failProceed( + Account account, final Jid with, final String sessionId, final String message) { final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, with, sessionId); final AbstractJingleConnection existingJingleConnection = connections.get(id); @@ -990,15 +1020,11 @@ public class JingleConnectionManager extends AbstractConnectionManager { FAILED; public RtpEndUserState toEndUserState() { - switch (this) { - case SEARCHING: - case SEARCHING_ACKNOWLEDGED: - return RtpEndUserState.FINDING_DEVICE; - case DISCOVERED: - return RtpEndUserState.RINGING; - default: - return RtpEndUserState.CONNECTIVITY_ERROR; - } + return switch (this) { + case SEARCHING, SEARCHING_ACKNOWLEDGED -> RtpEndUserState.FINDING_DEVICE; + case DISCOVERED -> RtpEndUserState.RINGING; + default -> RtpEndUserState.CONNECTIVITY_ERROR; + }; } } @@ -1008,10 +1034,6 @@ public class JingleConnectionManager extends AbstractConnectionManager { public final Set media; private final Account account; - private RtpSessionProposal(Account account, Jid with, String sessionId) { - this(account, with, sessionId, Collections.emptySet()); - } - private RtpSessionProposal(Account account, Jid with, String sessionId, Set media) { this.account = account; this.with = with; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index b8630704f..d1cc7cada 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -14,7 +14,10 @@ import com.google.common.base.Throwables; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.primitives.Ints; import com.google.common.util.concurrent.FutureCallback; @@ -22,23 +25,6 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import org.webrtc.EglBase; -import org.webrtc.IceCandidate; -import org.webrtc.PeerConnection; -import org.webrtc.VideoTrack; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; @@ -68,6 +54,23 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import org.webrtc.EglBase; +import org.webrtc.IceCandidate; +import org.webrtc.PeerConnection; +import org.webrtc.VideoTrack; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + public class JingleRtpConnection extends AbstractJingleConnection implements WebRTCWrapper.EventCallback { @@ -190,61 +193,37 @@ public class JingleRtpConnection extends AbstractJingleConnection } private static State reasonToState(Reason reason) { - switch (reason) { - case SUCCESS: - return State.TERMINATED_SUCCESS; - case DECLINE: - case BUSY: - return State.TERMINATED_DECLINED_OR_BUSY; - case CANCEL: - case TIMEOUT: - return State.TERMINATED_CANCEL_OR_TIMEOUT; - case SECURITY_ERROR: - return State.TERMINATED_SECURITY_ERROR; - case FAILED_APPLICATION: - case UNSUPPORTED_TRANSPORTS: - case UNSUPPORTED_APPLICATIONS: - return State.TERMINATED_APPLICATION_FAILURE; - default: - return State.TERMINATED_CONNECTIVITY_ERROR; - } + return switch (reason) { + case SUCCESS -> State.TERMINATED_SUCCESS; + case DECLINE, BUSY -> State.TERMINATED_DECLINED_OR_BUSY; + case CANCEL, TIMEOUT -> State.TERMINATED_CANCEL_OR_TIMEOUT; + case SECURITY_ERROR -> State.TERMINATED_SECURITY_ERROR; + case FAILED_APPLICATION, UNSUPPORTED_TRANSPORTS, UNSUPPORTED_APPLICATIONS -> State + .TERMINATED_APPLICATION_FAILURE; + default -> State.TERMINATED_CONNECTIVITY_ERROR; + }; } @Override synchronized void deliverPacket(final JinglePacket jinglePacket) { switch (jinglePacket.getAction()) { - case SESSION_INITIATE: - receiveSessionInitiate(jinglePacket); - break; - case TRANSPORT_INFO: - receiveTransportInfo(jinglePacket); - break; - case SESSION_ACCEPT: - receiveSessionAccept(jinglePacket); - break; - case SESSION_TERMINATE: - receiveSessionTerminate(jinglePacket); - break; - case CONTENT_ADD: - receiveContentAdd(jinglePacket); - break; - case CONTENT_ACCEPT: - receiveContentAccept(jinglePacket); - break; - case CONTENT_REJECT: - receiveContentReject(jinglePacket); - break; - case CONTENT_REMOVE: - receiveContentRemove(jinglePacket); - break; - default: + case SESSION_INITIATE -> receiveSessionInitiate(jinglePacket); + case TRANSPORT_INFO -> receiveTransportInfo(jinglePacket); + case SESSION_ACCEPT -> receiveSessionAccept(jinglePacket); + case SESSION_TERMINATE -> receiveSessionTerminate(jinglePacket); + case CONTENT_ADD -> receiveContentAdd(jinglePacket); + case CONTENT_ACCEPT -> receiveContentAccept(jinglePacket); + case CONTENT_REJECT -> receiveContentReject(jinglePacket); + case CONTENT_REMOVE -> receiveContentRemove(jinglePacket); + case CONTENT_MODIFY -> receiveContentModify(jinglePacket); + default -> { respondOk(jinglePacket); Log.d( Config.LOGTAG, String.format( "%s: received unhandled jingle action %s", id.account.getJid().asBareJid(), jinglePacket.getAction())); - break; + } } } @@ -349,23 +328,39 @@ public class JingleRtpConnection extends AbstractJingleConnection 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 - if (checkForIceRestart(jinglePacket, contentMap)) { - return; - } - respondOk(jinglePacket); - 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"); - } - } else { + final RtpContentMap remote = getRemoteContentMap(); + final Set remoteContentIds = + remote == null ? Collections.emptySet() : remote.contents.keySet(); + if (Collections.disjoint(remoteContentIds, contentMap.contents.keySet())) { + Log.d( + Config.LOGTAG, + "received transport-info for unknown contents " + + contentMap.contents.keySet() + + " (known: " + + remoteContentIds + + ")"); respondOk(jinglePacket); pendingIceCandidates.addAll(candidates); + return; + } + if (this.state != State.SESSION_ACCEPTED) { + Log.d(Config.LOGTAG, "received transport-info prematurely. adding to backlog"); + respondOk(jinglePacket); + pendingIceCandidates.addAll(candidates); + return; + } + // zero candidates + modified credentials are an ICE restart offer + if (checkForIceRestart(jinglePacket, contentMap)) { + return; + } + respondOk(jinglePacket); + 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"); } } @@ -385,7 +380,34 @@ public class JingleRtpConnection extends AbstractJingleConnection return; } if (isInState(State.SESSION_ACCEPTED)) { - receiveContentAdd(jinglePacket, modification); + final boolean hasFullTransportInfo = modification.hasFullTransportInfo(); + final ListenableFuture future = + receiveRtpContentMap( + modification, + this.omemoVerification.hasFingerprint() && hasFullTransportInfo); + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess(final RtpContentMap rtpContentMap) { + receiveContentAdd(jinglePacket, rtpContentMap); + } + + @Override + public void onFailure(@NonNull Throwable throwable) { + respondOk(jinglePacket); + final Throwable rootCause = Throwables.getRootCause(throwable); + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": improperly formatted contents in content-add", + throwable); + webRTCWrapper.close(); + sendSessionTerminate( + Reason.ofThrowable(rootCause), rootCause.getMessage()); + } + }, + MoreExecutors.directExecutor()); } else { terminateWithOutOfOrder(jinglePacket); } @@ -470,7 +492,27 @@ public class JingleRtpConnection extends AbstractJingleConnection if (ourSummary.equals(ContentAddition.summary(receivedContentAccept))) { this.outgoingContentAdd = null; respondOk(jinglePacket); - receiveContentAccept(receivedContentAccept); + final boolean hasFullTransportInfo = receivedContentAccept.hasFullTransportInfo(); + final ListenableFuture future = + receiveRtpContentMap( + receivedContentAccept, + this.omemoVerification.hasFingerprint() && hasFullTransportInfo); + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess(final RtpContentMap result) { + receiveContentAccept(result); + } + + @Override + public void onFailure(@NonNull final Throwable throwable) { + webRTCWrapper.close(); + sendSessionTerminate( + Reason.ofThrowable(throwable), throwable.getMessage()); + } + }, + MoreExecutors.directExecutor()); } else { Log.d(Config.LOGTAG, "received content-accept did not match our outgoing content-add"); terminateWithOutOfOrder(jinglePacket); @@ -503,12 +545,123 @@ public class JingleRtpConnection extends AbstractJingleConnection sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage()); return; } - updateEndUserState(); Log.d( Config.LOGTAG, id.getAccount().getJid().asBareJid() + ": remote has accepted content-add " + ContentAddition.summary(receivedContentAccept)); + processCandidates(receivedContentAccept.contents.entrySet()); + updateEndUserState(); + } + + private void receiveContentModify(final JinglePacket jinglePacket) { + if (this.state != State.SESSION_ACCEPTED) { + terminateWithOutOfOrder(jinglePacket); + return; + } + final Map modification = + Maps.transformEntries( + jinglePacket.getJingleContents(), (key, value) -> value.getSenders()); + final boolean isInitiator = isInitiator(); + final RtpContentMap currentOutgoing = this.outgoingContentAdd; + final RtpContentMap remoteContentMap = this.getRemoteContentMap(); + final Set currentOutgoingMediaIds = + currentOutgoing == null + ? Collections.emptySet() + : currentOutgoing.contents.keySet(); + Log.d(Config.LOGTAG, "receiveContentModification(" + modification + ")"); + if (currentOutgoing != null && currentOutgoingMediaIds.containsAll(modification.keySet())) { + respondOk(jinglePacket); + final RtpContentMap modifiedContentMap; + try { + modifiedContentMap = + currentOutgoing.modifiedSendersChecked(isInitiator, modification); + } catch (final IllegalArgumentException e) { + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); + return; + } + this.outgoingContentAdd = modifiedContentMap; + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": processed content-modification for pending content-add"); + } else if (remoteContentMap != null + && remoteContentMap.contents.keySet().containsAll(modification.keySet())) { + respondOk(jinglePacket); + final RtpContentMap modifiedRemoteContentMap; + try { + modifiedRemoteContentMap = + remoteContentMap.modifiedSendersChecked(isInitiator, modification); + } catch (final IllegalArgumentException e) { + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); + return; + } + final SessionDescription offer; + try { + offer = SessionDescription.of(modifiedRemoteContentMap, !isInitiator()); + } catch (final IllegalArgumentException | NullPointerException e) { + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": unable convert offer from content-modify to SDP", + e); + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); + return; + } + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + ": auto accepting content-modification"); + this.autoAcceptContentModify(modifiedRemoteContentMap, offer); + } else { + Log.d(Config.LOGTAG, "received unsupported content modification " + modification); + respondWithItemNotFound(jinglePacket); + } + } + + private void autoAcceptContentModify( + final RtpContentMap modifiedRemoteContentMap, final SessionDescription offer) { + this.setRemoteContentMap(modifiedRemoteContentMap); + final org.webrtc.SessionDescription sdp = + new org.webrtc.SessionDescription( + org.webrtc.SessionDescription.Type.OFFER, offer.toString()); + try { + this.webRTCWrapper.setRemoteDescription(sdp).get(); + // auto accept is only done when we already have tracks + final SessionDescription answer = setLocalSessionDescription(); + final RtpContentMap rtpContentMap = RtpContentMap.of(answer, isInitiator()); + modifyLocalContentMap(rtpContentMap); + // we do not need to send an answer but do we have to resend the candidates currently in + // SDP? + // resendCandidatesFromSdp(answer); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); + } catch (final Exception e) { + Log.d(Config.LOGTAG, "unable to accept content add", Throwables.getRootCause(e)); + webRTCWrapper.close(); + sendSessionTerminate(Reason.FAILED_APPLICATION); + } + } + + private static ImmutableMultimap parseCandidates( + final SessionDescription answer) { + final ImmutableMultimap.Builder candidateBuilder = + new ImmutableMultimap.Builder<>(); + for (final SessionDescription.Media media : answer.media) { + final String mid = Iterables.getFirst(media.attributes.get("mid"), null); + if (Strings.isNullOrEmpty(mid)) { + continue; + } + for (final String sdpCandidate : media.attributes.get("candidate")) { + final IceUdpTransportInfo.Candidate candidate = + IceUdpTransportInfo.Candidate.fromSdpAttributeValue(sdpCandidate, null); + if (candidate != null) { + candidateBuilder.put(mid, candidate); + } + } + } + return candidateBuilder.build(); } private void receiveContentReject(final JinglePacket jinglePacket) { @@ -536,7 +689,7 @@ public class JingleRtpConnection extends AbstractJingleConnection if (ourSummary.equals(ContentAddition.summary(receivedContentReject))) { this.outgoingContentAdd = null; respondOk(jinglePacket); - Log.d(Config.LOGTAG,jinglePacket.toString()); + Log.d(Config.LOGTAG, jinglePacket.toString()); receiveContentReject(ourSummary); } else { Log.d(Config.LOGTAG, "received content-reject did not match our outgoing content-add"); @@ -605,7 +758,7 @@ public class JingleRtpConnection extends AbstractJingleConnection "%s only supports %s as a means to retract a not yet accepted %s", BuildConfig.APP_NAME, JinglePacket.Action.CONTENT_REMOVE, - JinglePacket.Action.CONTENT_ACCEPT)); + JinglePacket.Action.CONTENT_ADD)); } } @@ -672,7 +825,8 @@ public class JingleRtpConnection extends AbstractJingleConnection "Unexpected rollback condition. Senders were not uniformly none"); } - public synchronized void acceptContentAdd(@NonNull final Set contentAddition) { + public synchronized void acceptContentAdd( + @NonNull final Set contentAddition) { final RtpContentMap incomingContentAdd = this.incomingContentAdd; if (incomingContentAdd == null) { throw new IllegalStateException("No incoming content add"); @@ -680,22 +834,64 @@ public class JingleRtpConnection extends AbstractJingleConnection if (contentAddition.equals(ContentAddition.summary(incomingContentAdd))) { this.incomingContentAdd = null; - acceptContentAdd(contentAddition, incomingContentAdd); + final Set senders = incomingContentAdd.getSenders(); + Log.d(Config.LOGTAG, "senders of incoming content-add: " + senders); + if (senders.equals(Content.Senders.receiveOnly(isInitiator()))) { + Log.d( + Config.LOGTAG, + "content addition is receive only. we want to upgrade to 'both'"); + final RtpContentMap modifiedSenders = + incomingContentAdd.modifiedSenders(Content.Senders.BOTH); + final JinglePacket proposedContentModification = + modifiedSenders + .toStub() + .toJinglePacket(JinglePacket.Action.CONTENT_MODIFY, id.sessionId); + proposedContentModification.setTo(id.with); + xmppConnectionService.sendIqPacket( + id.account, + proposedContentModification, + (account, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": remote has accepted our upgrade to senders=both"); + acceptContentAdd( + ContentAddition.summary(modifiedSenders), modifiedSenders); + } else { + Log.d( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": remote has rejected our upgrade to senders=both"); + acceptContentAdd(contentAddition, incomingContentAdd); + } + }); + } else { + acceptContentAdd(contentAddition, incomingContentAdd); + } } else { - throw new IllegalStateException("Accepted content add does not match pending content-add"); + throw new IllegalStateException( + "Accepted content add does not match pending content-add"); } } - private void acceptContentAdd(@NonNull final Set contentAddition, final RtpContentMap incomingContentAdd) { + private void acceptContentAdd( + @NonNull final Set contentAddition, + final RtpContentMap incomingContentAdd) { final IceUdpTransportInfo.Setup setup = getPeerDtlsSetup(); - final RtpContentMap modifiedContentMap = getRemoteContentMap().addContent(incomingContentAdd, setup); + final RtpContentMap modifiedContentMap = + getRemoteContentMap().addContent(incomingContentAdd, setup); this.setRemoteContentMap(modifiedContentMap); final SessionDescription offer; try { offer = SessionDescription.of(modifiedContentMap, !isInitiator()); } catch (final IllegalArgumentException | NullPointerException e) { - Log.d(Config.LOGTAG, id.getAccount().getJid().asBareJid() + ": unable convert offer from content-add to SDP", e); + Log.d( + Config.LOGTAG, + id.getAccount().getJid().asBareJid() + + ": unable convert offer from content-add to SDP", + e); webRTCWrapper.close(); sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage()); return; @@ -726,13 +922,33 @@ public class JingleRtpConnection extends AbstractJingleConnection final RtpContentMap contentAcceptMap = rtpContentMap.toContentModification( Collections2.transform(contentAddition, ca -> ca.name)); + Log.d( Config.LOGTAG, id.getAccount().getJid().asBareJid() + ": sending content-accept " + ContentAddition.summary(contentAcceptMap)); + + addIceCandidatesFromBlackLog(); + modifyLocalContentMap(rtpContentMap); - sendContentAccept(contentAcceptMap); + final ListenableFuture future = + prepareOutgoingContentMap(contentAcceptMap); + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess(final RtpContentMap rtpContentMap) { + sendContentAccept(rtpContentMap); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); + } + + @Override + public void onFailure(@NonNull final Throwable throwable) { + failureToPerformAction(JinglePacket.Action.CONTENT_ACCEPT, throwable); + } + }, + MoreExecutors.directExecutor()); } catch (final Exception e) { Log.d(Config.LOGTAG, "unable to accept content add", Throwables.getRootCause(e)); webRTCWrapper.close(); @@ -741,7 +957,8 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void sendContentAccept(final RtpContentMap contentAcceptMap) { - final JinglePacket jinglePacket = contentAcceptMap.toJinglePacket(JinglePacket.Action.CONTENT_ACCEPT, id.sessionId); + final JinglePacket jinglePacket = + contentAcceptMap.toJinglePacket(JinglePacket.Action.CONTENT_ACCEPT, id.sessionId); send(jinglePacket); } @@ -787,6 +1004,9 @@ public class JingleRtpConnection extends AbstractJingleConnection // 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 isSignalStateStable = + this.webRTCWrapper.getSignalingState() == PeerConnection.SignalingState.STABLE; + // TODO a stable signal state can be another indicator that we have an offer to restart ICE final boolean isOffer = rtpContentMap.emptyCandidates(); final RtpContentMap restartContentMap; try { @@ -849,7 +1069,8 @@ public class JingleRtpConnection extends AbstractJingleConnection final RtpContentMap restartContentMap, final boolean isOffer) throws ExecutionException, InterruptedException { - final SessionDescription sessionDescription = SessionDescription.of(restartContentMap, !isInitiator()); + final SessionDescription sessionDescription = + SessionDescription.of(restartContentMap, !isInitiator()); final org.webrtc.SessionDescription.Type type = isOffer ? org.webrtc.SessionDescription.Type.OFFER @@ -866,7 +1087,6 @@ public class JingleRtpConnection extends AbstractJingleConnection webRTCWrapper.setRemoteDescription(sdp).get(); setRemoteContentMap(restartContentMap); if (isOffer) { - webRTCWrapper.setIsReadyToReceiveIceCandidates(false); final SessionDescription localSessionDescription = setLocalSessionDescription(); setLocalContentMap(RtpContentMap.of(localSessionDescription, isInitiator())); // We need to respond OK before sending any candidates @@ -944,12 +1164,22 @@ public class JingleRtpConnection extends AbstractJingleConnection private ListenableFuture receiveRtpContentMap( final JinglePacket jinglePacket, final boolean expectVerification) { - final RtpContentMap receivedContentMap; try { - receivedContentMap = RtpContentMap.of(jinglePacket); + return receiveRtpContentMap(RtpContentMap.of(jinglePacket), expectVerification); } catch (final Exception e) { return Futures.immediateFailedFuture(e); } + } + + private ListenableFuture receiveRtpContentMap( + final RtpContentMap receivedContentMap, final boolean expectVerification) { + Log.d( + Config.LOGTAG, + "receiveRtpContentMap(" + + receivedContentMap.getClass().getSimpleName() + + ",expectVerification=" + + expectVerification + + ")"); if (receivedContentMap instanceof OmemoVerifiedRtpContentMap) { final ListenableFuture> future = id.account @@ -998,7 +1228,7 @@ public class JingleRtpConnection extends AbstractJingleConnection final ListenableFuture future = receiveRtpContentMap(jinglePacket, false); Futures.addCallback( future, - new FutureCallback() { + new FutureCallback<>() { @Override public void onSuccess(@Nullable RtpContentMap rtpContentMap) { receiveSessionInitiate(jinglePacket, rtpContentMap); @@ -1087,7 +1317,7 @@ public class JingleRtpConnection extends AbstractJingleConnection receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint()); Futures.addCallback( future, - new FutureCallback() { + new FutureCallback<>() { @Override public void onSuccess(@Nullable RtpContentMap rtpContentMap) { receiveSessionAccept(jinglePacket, rtpContentMap); @@ -1220,8 +1450,9 @@ public class JingleRtpConnection extends AbstractJingleConnection + ": ICE servers got discovered when session was already terminated. nothing to do."); return; } + final boolean includeCandidates = remoteHasSdpOfferAnswer(); try { - setupWebRTC(media, iceServers); + setupWebRTC(media, iceServers, !includeCandidates); } catch (final WebRTCWrapper.InitializationException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to initialize WebRTC"); webRTCWrapper.close(); @@ -1235,8 +1466,8 @@ public class JingleRtpConnection extends AbstractJingleConnection this.webRTCWrapper.setRemoteDescription(sdp).get(); addIceCandidatesFromBlackLog(); org.webrtc.SessionDescription webRTCSessionDescription = - this.webRTCWrapper.setLocalDescription().get(); - prepareSessionAccept(webRTCSessionDescription); + this.webRTCWrapper.setLocalDescription(includeCandidates).get(); + prepareSessionAccept(webRTCSessionDescription, includeCandidates); } catch (final Exception e) { failureToAcceptSession(e); } @@ -1252,6 +1483,17 @@ public class JingleRtpConnection extends AbstractJingleConnection sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage()); } + private void failureToPerformAction( + final JinglePacket.Action action, final Throwable throwable) { + if (isTerminated()) { + return; + } + final Throwable rootCause = Throwables.getRootCause(throwable); + Log.d(Config.LOGTAG, "unable to send " + action, rootCause); + webRTCWrapper.close(); + sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage()); + } + private void addIceCandidatesFromBlackLog() { Map.Entry foo; while ((foo = this.pendingIceCandidates.poll()) != null) { @@ -1263,20 +1505,36 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void prepareSessionAccept( - final org.webrtc.SessionDescription webRTCSessionDescription) { + final org.webrtc.SessionDescription webRTCSessionDescription, + final boolean includeCandidates) { final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription, false); + final ImmutableMultimap candidates; + if (includeCandidates) { + candidates = parseCandidates(sessionDescription); + } else { + candidates = ImmutableMultimap.of(); + } this.responderRtpContentMap = respondingRtpContentMap; storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip()); final ListenableFuture outgoingContentMapFuture = prepareOutgoingContentMap(respondingRtpContentMap); Futures.addCallback( outgoingContentMapFuture, - new FutureCallback() { + new FutureCallback<>() { @Override public void onSuccess(final RtpContentMap outgoingContentMap) { - sendSessionAccept(outgoingContentMap); + if (includeCandidates) { + Log.d( + Config.LOGTAG, + "including " + + candidates.size() + + " candidates in session accept"); + sendSessionAccept(outgoingContentMap.withCandidates(candidates)); + } else { + sendSessionAccept(outgoingContentMap); + } webRTCWrapper.setIsReadyToReceiveIceCandidates(true); } @@ -1336,30 +1594,23 @@ public class JingleRtpConnection extends AbstractJingleConnection + ": delivered message to JingleRtpConnection " + message); switch (message.getName()) { - case "propose": - receivePropose(from, Propose.upgrade(message), serverMessageId, timestamp); - break; - case "proceed": - receiveProceed(from, Proceed.upgrade(message), serverMessageId, timestamp); - break; - case "retract": - receiveRetract(from, serverMessageId, timestamp); - break; - case "reject": - receiveReject(from, serverMessageId, timestamp); - break; - case "accept": - receiveAccept(from, serverMessageId, timestamp); - break; - default: - break; + case "propose" -> receivePropose( + from, Propose.upgrade(message), serverMessageId, timestamp); + case "proceed" -> receiveProceed( + from, Proceed.upgrade(message), serverMessageId, timestamp); + case "retract" -> receiveRetract(from, serverMessageId, timestamp); + case "reject" -> receiveReject(from, serverMessageId, timestamp); + case "accept" -> receiveAccept(from, serverMessageId, timestamp); } } void deliverFailedProceed(final String message) { Log.d( Config.LOGTAG, - id.account.getJid().asBareJid() + ": receive message error for proceed message ("+Strings.nullToEmpty(message)+")"); + id.account.getJid().asBareJid() + + ": receive message error for proceed message (" + + Strings.nullToEmpty(message) + + ")"); if (transition(State.TERMINATED_CONNECTIVITY_ERROR)) { webRTCWrapper.close(); Log.d( @@ -1381,6 +1632,7 @@ public class JingleRtpConnection extends AbstractJingleConnection id.account.getJid().asBareJid() + ": unable to transition to accept because already in state=" + this.state); + Log.d(Config.LOGTAG, id.account.getJid() + ": received accept from " + from); } } else { Log.d( @@ -1396,9 +1648,7 @@ public class JingleRtpConnection extends AbstractJingleConnection this.message.setTime(timestamp); 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(); } @@ -1508,6 +1758,9 @@ public class JingleRtpConnection extends AbstractJingleConnection } this.message.setTime(timestamp); startRinging(); + if (xmppConnectionService.confirmMessages() && id.getContact().showInContactList()) { + sendJingleMessage("ringing"); + } } else { Log.d( Config.LOGTAG, @@ -1533,14 +1786,14 @@ public class JingleRtpConnection extends AbstractJingleConnection private synchronized void ringingTimeout() { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": timeout reached for ringing"); switch (this.state) { - case PROPOSED: + case PROPOSED -> { message.markUnread(); rejectCallFromProposed(); - break; - case SESSION_INITIALIZED: + } + case SESSION_INITIALIZED -> { message.markUnread(); rejectCallFromSessionInitiate(); - break; + } } xmppConnectionService.getNotificationService().pushMissedCallNow(message); } @@ -1671,8 +1924,9 @@ public class JingleRtpConnection extends AbstractJingleConnection + ": ICE servers got discovered when session was already terminated. nothing to do."); return; } + final boolean includeCandidates = remoteHasSdpOfferAnswer(); try { - setupWebRTC(media, iceServers); + setupWebRTC(media, iceServers, !includeCandidates); } catch (final WebRTCWrapper.InitializationException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to initialize WebRTC"); webRTCWrapper.close(); @@ -1681,8 +1935,8 @@ public class JingleRtpConnection extends AbstractJingleConnection } try { org.webrtc.SessionDescription webRTCSessionDescription = - this.webRTCWrapper.setLocalDescription().get(); - prepareSessionInitiate(webRTCSessionDescription, targetState); + this.webRTCWrapper.setLocalDescription(includeCandidates).get(); + prepareSessionInitiate(webRTCSessionDescription, includeCandidates, targetState); } catch (final Exception e) { // TODO sending the error text is worthwhile as well. Especially for FailureToSet // exceptions @@ -1715,19 +1969,37 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void prepareSessionInitiate( - final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) { + final org.webrtc.SessionDescription webRTCSessionDescription, + final boolean includeCandidates, + final State targetState) { final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription, true); + final ImmutableMultimap candidates; + if (includeCandidates) { + candidates = parseCandidates(sessionDescription); + } else { + candidates = ImmutableMultimap.of(); + } this.initiatorRtpContentMap = rtpContentMap; final ListenableFuture outgoingContentMapFuture = encryptSessionInitiate(rtpContentMap); Futures.addCallback( outgoingContentMapFuture, - new FutureCallback() { + new FutureCallback<>() { @Override public void onSuccess(final RtpContentMap outgoingContentMap) { - sendSessionInitiate(outgoingContentMap, targetState); + if (includeCandidates) { + Log.d( + Config.LOGTAG, + "including " + + candidates.size() + + " candidates in session initiate"); + sendSessionInitiate( + outgoingContentMap.withCandidates(candidates), targetState); + } else { + sendSessionInitiate(outgoingContentMap, targetState); + } webRTCWrapper.setIsReadyToReceiveIceCandidates(true); } @@ -1807,7 +2079,6 @@ public class JingleRtpConnection extends AbstractJingleConnection final JinglePacket jinglePacket = new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); jinglePacket.setReason(reason, text); - Log.d(Config.LOGTAG, jinglePacket.toString()); send(jinglePacket); finish(); } @@ -1918,6 +2189,10 @@ public class JingleRtpConnection extends AbstractJingleConnection respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait"); } + private void respondWithItemNotFound(final JinglePacket jinglePacket) { + respondWithJingleError(jinglePacket, null, "item-not-found", "cancel"); + } + void respondWithJingleError( final IqPacket original, String jingleCondition, @@ -1934,59 +2209,62 @@ public class JingleRtpConnection extends AbstractJingleConnection public RtpEndUserState getEndUserState() { switch (this.state) { - case NULL: - case PROPOSED: - case SESSION_INITIALIZED: + case NULL, PROPOSED, SESSION_INITIALIZED -> { if (isInitiator()) { return RtpEndUserState.RINGING; } else { return RtpEndUserState.INCOMING_CALL; } - case PROCEED: + } + case PROCEED -> { if (isInitiator()) { return RtpEndUserState.RINGING; } else { return RtpEndUserState.ACCEPTING_CALL; } - case SESSION_INITIALIZED_PRE_APPROVED: + } + case SESSION_INITIALIZED_PRE_APPROVED -> { if (isInitiator()) { return RtpEndUserState.RINGING; } else { return RtpEndUserState.CONNECTING; } - case SESSION_ACCEPTED: + } + case SESSION_ACCEPTED -> { final ContentAddition ca = getPendingContentAddition(); if (ca != null && ca.direction == ContentAddition.Direction.INCOMING) { return RtpEndUserState.INCOMING_CONTENT_ADD; } return getPeerConnectionStateAsEndUserState(); - case REJECTED: - case REJECTED_RACED: - case TERMINATED_DECLINED_OR_BUSY: + } + case REJECTED, REJECTED_RACED, TERMINATED_DECLINED_OR_BUSY -> { if (isInitiator()) { return RtpEndUserState.DECLINED_OR_BUSY; } else { return RtpEndUserState.ENDED; } - case TERMINATED_SUCCESS: - case ACCEPTED: - case RETRACTED: - case TERMINATED_CANCEL_OR_TIMEOUT: + } + case TERMINATED_SUCCESS, ACCEPTED, RETRACTED, TERMINATED_CANCEL_OR_TIMEOUT -> { return RtpEndUserState.ENDED; - case RETRACTED_RACED: + } + case RETRACTED_RACED -> { if (isInitiator()) { return RtpEndUserState.ENDED; } else { return RtpEndUserState.RETRACTED; } - case TERMINATED_CONNECTIVITY_ERROR: + } + case TERMINATED_CONNECTIVITY_ERROR -> { return zeroDuration() ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR; - case TERMINATED_APPLICATION_FAILURE: + } + case TERMINATED_APPLICATION_FAILURE -> { return RtpEndUserState.APPLICATION_ERROR; - case TERMINATED_SECURITY_ERROR: + } + case TERMINATED_SECURITY_ERROR -> { return RtpEndUserState.SECURITY_ERROR; + } } throw new IllegalStateException( String.format("%s has no equivalent EndUserState", this.state)); @@ -2001,19 +2279,14 @@ public class JingleRtpConnection extends AbstractJingleConnection // be in SESSION_ACCEPTED even though the peerConnection has been torn down return RtpEndUserState.ENDING_CALL; } - switch (state) { - case CONNECTED: - return RtpEndUserState.CONNECTED; - case NEW: - case CONNECTING: - return RtpEndUserState.CONNECTING; - case CLOSED: - return RtpEndUserState.ENDING_CALL; - default: - return zeroDuration() - ? RtpEndUserState.CONNECTIVITY_ERROR - : RtpEndUserState.RECONNECTING; - } + return switch (state) { + case CONNECTED -> RtpEndUserState.CONNECTED; + case NEW, CONNECTING -> RtpEndUserState.CONNECTING; + case CLOSED -> RtpEndUserState.ENDING_CALL; + default -> zeroDuration() + ? RtpEndUserState.CONNECTIVITY_ERROR + : RtpEndUserState.RECONNECTING; + }; } public ContentAddition getPendingContentAddition() { @@ -2048,9 +2321,10 @@ public class JingleRtpConnection extends AbstractJingleConnection } else 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"); } } @@ -2070,35 +2344,29 @@ public class JingleRtpConnection extends AbstractJingleConnection throw new IllegalStateException(String.format("%s has already been proposed", media)); } // TODO add state protection - can only add while ACCEPTED or so - Log.d(Config.LOGTAG,"adding media: "+media); + Log.d(Config.LOGTAG, "adding media: " + media); return webRTCWrapper.addTrack(media); } public synchronized void acceptCall() { switch (this.state) { - case PROPOSED: + case PROPOSED -> { cancelRingingTimeout(); acceptCallFromProposed(); - break; - case SESSION_INITIALIZED: + } + case SESSION_INITIALIZED -> { cancelRingingTimeout(); acceptCallFromSessionInitialized(); - break; - case ACCEPTED: - Log.w( - Config.LOGTAG, - id.account.getJid().asBareJid() - + ": the call has already been accepted with another client. UI was just lagging behind"); - break; - case PROCEED: - case SESSION_ACCEPTED: - Log.w( - Config.LOGTAG, - id.account.getJid().asBareJid() - + ": the call has already been accepted. user probably double tapped the UI"); - break; - default: - throw new IllegalStateException("Can not accept call from " + this.state); + } + case ACCEPTED -> Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": the call has already been accepted with another client. UI was just lagging behind"); + case PROCEED, SESSION_ACCEPTED -> Log.w( + Config.LOGTAG, + id.account.getJid().asBareJid() + + ": the call has already been accepted. user probably double tapped the UI"); + default -> throw new IllegalStateException("Can not accept call from " + this.state); } } @@ -2120,14 +2388,9 @@ public class JingleRtpConnection extends AbstractJingleConnection return; } switch (this.state) { - case PROPOSED: - rejectCallFromProposed(); - break; - case SESSION_INITIALIZED: - rejectCallFromSessionInitiate(); - break; - default: - throw new IllegalStateException("Can not reject call from " + this.state); + case PROPOSED -> rejectCallFromProposed(); + case SESSION_INITIALIZED -> rejectCallFromSessionInitiate(); + default -> throw new IllegalStateException("Can not reject call from " + this.state); } } @@ -2192,10 +2455,15 @@ public class JingleRtpConnection extends AbstractJingleConnection finish(); } - private void setupWebRTC(final Set media, final List iceServers) throws WebRTCWrapper.InitializationException { + private void setupWebRTC( + final Set media, + final List iceServers, + final boolean trickle) + throws WebRTCWrapper.InitializationException { this.jingleConnectionManager.ensureConnectionIsRegistered(this); - this.webRTCWrapper.setup(this.xmppConnectionService, AppRTCAudioManager.SpeakerPhonePreference.of(media)); - this.webRTCWrapper.initializePeerConnection(media, iceServers); + this.webRTCWrapper.setup( + this.xmppConnectionService, AppRTCAudioManager.SpeakerPhonePreference.of(media)); + this.webRTCWrapper.initializePeerConnection(media, iceServers, trickle); } private void acceptCallFromProposed() { @@ -2354,7 +2622,7 @@ public class JingleRtpConnection extends AbstractJingleConnection private void restartIce() { this.stateHistory.clear(); - this.webRTCWrapper.restartIce(); + this.webRTCWrapper.restartIceAsync(); } @Override @@ -2368,8 +2636,15 @@ public class JingleRtpConnection extends AbstractJingleConnection sessionDescription = setLocalSessionDescription(); } catch (final Exception e) { final Throwable cause = Throwables.getRootCause(e); - Log.d(Config.LOGTAG, "failed to renegotiate", cause); webRTCWrapper.close(); + if (isTerminated()) { + Log.d( + Config.LOGTAG, + "failed to renegotiate. session was already terminated", + cause); + return; + } + Log.d(Config.LOGTAG, "failed to renegotiate. sending session-terminate", cause); sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage()); return; } @@ -2447,6 +2722,27 @@ public class JingleRtpConnection extends AbstractJingleConnection private void sendContentAdd(final RtpContentMap rtpContentMap, final Collection added) { final RtpContentMap contentAdd = rtpContentMap.toContentModification(added); this.outgoingContentAdd = contentAdd; + final ListenableFuture outgoingContentMapFuture = + prepareOutgoingContentMap(contentAdd); + Futures.addCallback( + outgoingContentMapFuture, + new FutureCallback<>() { + @Override + public void onSuccess(final RtpContentMap outgoingContentMap) { + sendContentAdd(outgoingContentMap); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); + } + + @Override + public void onFailure(@NonNull Throwable throwable) { + failureToPerformAction(JinglePacket.Action.CONTENT_ADD, throwable); + } + }, + MoreExecutors.directExecutor()); + } + + private void sendContentAdd(final RtpContentMap contentAdd) { + final JinglePacket jinglePacket = contentAdd.toJinglePacket(JinglePacket.Action.CONTENT_ADD, id.sessionId); jinglePacket.setTo(id.with); @@ -2503,7 +2799,7 @@ public class JingleRtpConnection extends AbstractJingleConnection private SessionDescription setLocalSessionDescription() throws ExecutionException, InterruptedException { final org.webrtc.SessionDescription sessionDescription = - this.webRTCWrapper.setLocalDescription().get(); + this.webRTCWrapper.setLocalDescription(false).get(); return SessionDescription.parse(sessionDescription.description); } @@ -2627,6 +2923,7 @@ public class JingleRtpConnection extends AbstractJingleConnection if (port < 0 || port > 65535) { continue; } + if (Arrays.asList("stun", "stuns", "turn", "turns") .contains(type) && Arrays.asList("udp", "tcp").contains(transport)) { @@ -2638,20 +2935,26 @@ public class JingleRtpConnection extends AbstractJingleConnection + ": skipping invalid combination of udp/tls in external services"); continue; } - // TODO Starting on milestone 110, Chromium will perform - // stricter validation of TURN and STUN URLs passed to the - // constructor of an RTCPeerConnection. More specifically, - // STUN URLs will not support a query section, and TURN URLs - // will support only a transport parameter in their query - // section. + + // STUN URLs do not support a query section since M110 + final String uri; + if (Arrays.asList("stun", "stuns").contains(type)) { + uri = + String.format( + "%s:%s:%s", + type, IP.wrapIPv6(host), port); + } else { + uri = + String.format( + "%s:%s:%s?transport=%s", + type, + IP.wrapIPv6(host), + port, + transport); + } + final PeerConnection.IceServer.Builder iceServerBuilder = - PeerConnection.IceServer.builder( - String.format( - "%s:%s:%s?transport=%s", - type, - IP.wrapIPv6(host), - port, - transport)); + PeerConnection.IceServer.builder(uri); iceServerBuilder.setTlsCertPolicy( PeerConnection.TlsCertPolicy .TLS_CERT_POLICY_INSECURE_NO_CHECK); @@ -2785,6 +3088,14 @@ public class JingleRtpConnection extends AbstractJingleConnection } private boolean remoteHasVideoFeature() { + return remoteHasFeature(Namespace.JINGLE_FEATURE_VIDEO); + } + + private boolean remoteHasSdpOfferAnswer() { + return remoteHasFeature(Namespace.SDP_OFFER_ANSWER); + } + + private boolean remoteHasFeature(final String feature) { final Contact contact = id.getContact(); final Presence presence = contact.getPresences().get(Strings.nullToEmpty(id.with.getResource())); @@ -2792,7 +3103,7 @@ public class JingleRtpConnection extends AbstractJingleConnection presence == null ? null : presence.getServiceDiscoveryResult(); final List features = serviceDiscoveryResult == null ? null : serviceDiscoveryResult.getFeatures(); - return features != null && features.contains(Namespace.JINGLE_FEATURE_VIDEO); + return features != null && features.contains(feature); } private interface OnIceServersDiscovered { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java index 8a7581f04..b53f0b9e8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java @@ -1,9 +1,11 @@ package eu.siacs.conversations.xmpp.jingle; import com.google.common.base.Strings; +import com.google.common.collect.Collections2; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; @@ -77,6 +79,18 @@ public class RtpCapability { return result; } + // do all devices that support Rtp Call also support JMI? + public static boolean jmiSupport(final Contact contact) { + return !Collections2.transform( + Collections2.filter( + contact.getPresences().getPresences(), + p -> RtpCapability.check(p) != RtpCapability.Capability.NONE), + p -> { + ServiceDiscoveryResult disco = p.getServiceDiscoveryResult(); + return disco != null && disco.getFeatures().contains(Namespace.JINGLE_MESSAGE); + }).contains(false); + } + public enum Capability { NONE, AUDIO, VIDEO; 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 994c3a233..cfd4bef78 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -8,19 +8,12 @@ import com.google.common.base.Strings; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Sets; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.annotation.Nonnull; - import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; @@ -30,6 +23,14 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.Nonnull; + public class RtpContentMap { public final Group group; @@ -94,7 +95,7 @@ public class RtpContentMap { } public Set getSenders() { - return ImmutableSet.copyOf(Collections2.transform(contents.values(),dt -> dt.senders)); + return ImmutableSet.copyOf(Collections2.transform(contents.values(), dt -> dt.senders)); } public List getNames() { @@ -196,6 +197,24 @@ public class RtpContentMap { dt.senders, null, dt.transport.cloneWrapper()))); } + RtpContentMap withCandidates( + ImmutableMultimap candidates) { + final ImmutableMap.Builder contentBuilder = + new ImmutableMap.Builder<>(); + for (final Map.Entry entry : this.contents.entrySet()) { + final String name = entry.getKey(); + final DescriptionTransport descriptionTransport = entry.getValue(); + final var transport = descriptionTransport.transport; + contentBuilder.put( + name, + new DescriptionTransport( + descriptionTransport.senders, + descriptionTransport.description, + transport.withCandidates(candidates.get(name)))); + } + return new RtpContentMap(group, contentBuilder.build()); + } + public IceUdpTransportInfo.Credentials getDistinctCredentials() { final Set allCredentials = getCredentials(); final IceUdpTransportInfo.Credentials credentials = @@ -210,6 +229,12 @@ public class RtpContentMap { throw new IllegalStateException("Content map does not have distinct credentials"); } + private Set getCombinedIceOptions() { + final Collection> combinedIceOptions = + Collections2.transform(contents.values(), dt -> dt.transport.getIceOptions()); + return ImmutableSet.copyOf(Iterables.concat(combinedIceOptions)); + } + public Set getCredentials() { final Set credentials = ImmutableSet.copyOf( @@ -268,6 +293,11 @@ public class RtpContentMap { return count == 0; } + public boolean hasFullTransportInfo() { + return Collections2.transform(this.contents.values(), dt -> dt.transport.isStub()) + .contains(false); + } + public RtpContentMap modifiedCredentials( IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) { final ImmutableMap.Builder contentMapBuilder = @@ -294,14 +324,60 @@ public class RtpContentMap { dt -> new DescriptionTransport(senders, dt.description, dt.transport))); } + public RtpContentMap modifiedSendersChecked( + final boolean isInitiator, final Map modification) { + final ImmutableMap.Builder contentMapBuilder = + new ImmutableMap.Builder<>(); + for (final Map.Entry content : contents.entrySet()) { + final String id = content.getKey(); + final DescriptionTransport descriptionTransport = content.getValue(); + final Content.Senders currentSenders = descriptionTransport.senders; + final Content.Senders targetSenders = modification.get(id); + if (targetSenders == null || currentSenders == targetSenders) { + contentMapBuilder.put(id, descriptionTransport); + } else { + checkSenderModification(isInitiator, currentSenders, targetSenders); + contentMapBuilder.put( + id, + new DescriptionTransport( + targetSenders, + descriptionTransport.description, + descriptionTransport.transport)); + } + } + return new RtpContentMap(this.group, contentMapBuilder.build()); + } + + private static void checkSenderModification( + final boolean isInitiator, + final Content.Senders current, + final Content.Senders target) { + if (isInitiator) { + // we were both sending and now other party only wants to receive + if (current == Content.Senders.BOTH && target == Content.Senders.INITIATOR) { + return; + } + // only we were sending but now other party wants to send too + if (current == Content.Senders.INITIATOR && target == Content.Senders.BOTH) { + return; + } + } else { + // we were both sending and now other party only wants to receive + if (current == Content.Senders.BOTH && target == Content.Senders.RESPONDER) { + return; + } + // only we were sending but now other party wants to send too + if (current == Content.Senders.RESPONDER && target == Content.Senders.BOTH) { + return; + } + } + throw new IllegalArgumentException( + String.format("Unsupported senders modification %s -> %s", current, target)); + } + public RtpContentMap toContentModification(final Collection modifications) { return new RtpContentMap( - this.group, - Maps.transformValues( - Maps.filterKeys(contents, Predicates.in(modifications)), - dt -> - new DescriptionTransport( - dt.senders, dt.description, IceUdpTransportInfo.STUB))); + this.group, Maps.filterKeys(contents, Predicates.in(modifications))); } public RtpContentMap toStub() { @@ -317,7 +393,8 @@ public class RtpContentMap { } public RtpContentMap activeContents() { - return new RtpContentMap(group, Maps.filterValues(this.contents, dt -> dt.senders != Content.Senders.NONE)); + return new RtpContentMap( + group, Maps.filterValues(this.contents, dt -> dt.senders != Content.Senders.NONE)); } public Diff diff(final RtpContentMap rtpContentMap) { @@ -337,28 +414,48 @@ public class RtpContentMap { } public RtpContentMap addContent( - final RtpContentMap modification, final IceUdpTransportInfo.Setup setup) { - final IceUdpTransportInfo.Credentials credentials = getDistinctCredentials(); - final DTLS dtls = getDistinctDtls(); - final IceUdpTransportInfo iceUdpTransportInfo = - IceUdpTransportInfo.of(credentials, setup, dtls.hash, dtls.fingerprint); + final RtpContentMap modification, final IceUdpTransportInfo.Setup setupOverwrite) { final Map combined = merge(contents, modification.contents); - /*new ImmutableMap.Builder() - .putAll(contents) - .putAll(modification.contents) - .build();*/ final Map combinedFixedTransport = Maps.transformValues( combined, - dt -> - new DescriptionTransport( - dt.senders, dt.description, iceUdpTransportInfo)); - return new RtpContentMap(modification.group, combinedFixedTransport); + dt -> { + final IceUdpTransportInfo iceUdpTransportInfo; + if (dt.transport.isStub()) { + final IceUdpTransportInfo.Credentials credentials = + getDistinctCredentials(); + final Collection iceOptions = getCombinedIceOptions(); + final DTLS dtls = getDistinctDtls(); + iceUdpTransportInfo = + IceUdpTransportInfo.of( + credentials, + iceOptions, + setupOverwrite, + dtls.hash, + dtls.fingerprint); + } else { + final IceUdpTransportInfo.Fingerprint fp = + dt.transport.getFingerprint(); + final IceUdpTransportInfo.Setup setup = fp.getSetup(); + iceUdpTransportInfo = + IceUdpTransportInfo.of( + dt.transport.getCredentials(), + dt.transport.getIceOptions(), + setup == IceUdpTransportInfo.Setup.ACTPASS + ? setupOverwrite + : setup, + fp.getHash(), + fp.getContent()); + } + return new DescriptionTransport( + dt.senders, dt.description, iceUdpTransportInfo); + }); + return new RtpContentMap(modification.group, ImmutableMap.copyOf(combinedFixedTransport)); } private static Map merge( final Map a, final Map b) { - final Map combined = new HashMap<>(); + final Map combined = new LinkedHashMap<>(); combined.putAll(a); combined.putAll(b); return ImmutableMap.copyOf(combined); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index f0f98260b..2d2dc9570 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -11,23 +11,26 @@ import com.google.common.base.Strings; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; -import java.util.List; -import java.util.Locale; -import java.util.Map; - import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; + public class SessionDescription { public static final String LINE_DIVIDER = "\r\n"; private static final String HARDCODED_MEDIA_PROTOCOL = "UDP/TLS/RTP/SAVPF"; // probably only true for DTLS-SRTP aka when we have a fingerprint private static final int HARDCODED_MEDIA_PORT = 9; - private static final String HARDCODED_ICE_OPTIONS = "trickle"; + private static final Collection HARDCODED_ICE_OPTIONS = + Collections.singleton("trickle"); private static final String HARDCODED_CONNECTION = "IN IP4 0.0.0.0"; public final int version; @@ -128,7 +131,8 @@ public class SessionDescription { return sessionDescriptionBuilder.createSessionDescription(); } - public static SessionDescription of(final RtpContentMap contentMap, final boolean isInitiatorContentMap) { + public static SessionDescription of( + final RtpContentMap contentMap, final boolean isInitiatorContentMap) { final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder(); final ArrayListMultimap attributeMap = ArrayListMultimap.create(); final ImmutableList.Builder mediaListBuilder = new ImmutableList.Builder<>(); @@ -166,11 +170,21 @@ public class SessionDescription { } checkNoWhitespace(pwd, "pwd value must not contain any whitespaces"); mediaAttributes.put("ice-pwd", pwd); - mediaAttributes.put("ice-options", HARDCODED_ICE_OPTIONS); + final List negotiatedIceOptions = transport.getIceOptions(); + final Collection iceOptions = + negotiatedIceOptions.isEmpty() ? HARDCODED_ICE_OPTIONS : negotiatedIceOptions; + mediaAttributes.put("ice-options", Joiner.on(' ').join(iceOptions)); final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); if (fingerprint != null) { - mediaAttributes.put( - "fingerprint", fingerprint.getHash() + " " + fingerprint.getContent()); + final String hashFunction = fingerprint.getHash(); + final String hash = fingerprint.getContent(); + if (Strings.isNullOrEmpty(hashFunction) || Strings.isNullOrEmpty(hash)) { + throw new IllegalArgumentException("DTLS-SRTP missing hash"); + } + checkNoWhitespace( + hashFunction, "DTLS-SRTP hash function must not contain whitespace"); + checkNoWhitespace(hash, "DTLS-SRTP hash must not contain whitespace"); + mediaAttributes.put("fingerprint", hashFunction + " " + hash); final IceUdpTransportInfo.Setup setup = fingerprint.getSetup(); if (setup != null) { mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT)); @@ -207,12 +221,14 @@ public class SessionDescription { } checkNoWhitespace( type, "feedback negotiation type must not contain whitespace"); - mediaAttributes.put( - "rtcp-fb", - id - + " " - + type - + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); + if (Strings.isNullOrEmpty(subtype)) { + mediaAttributes.put("rtcp-fb", id + " " + type); + } else { + checkNoWhitespace( + subtype, + "feedback negotiation subtype must not contain whitespace"); + mediaAttributes.put("rtcp-fb", id + " " + type + " " + subtype); + } } for (RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : payloadType.feedbackNegotiationTrrInts()) { @@ -229,9 +245,13 @@ public class SessionDescription { throw new IllegalArgumentException("a feedback negotiation is missing type"); } checkNoWhitespace(type, "feedback negotiation type must not contain whitespace"); - mediaAttributes.put( - "rtcp-fb", - "* " + type + (Strings.isNullOrEmpty(subtype) ? "" : " " + subtype)); + if (Strings.isNullOrEmpty(subtype)) { + mediaAttributes.put("rtcp-fb", "* " + type); + } else { + checkNoWhitespace( + subtype, "feedback negotiation subtype must not contain whitespace"); + mediaAttributes.put("rtcp-fb", "* " + type + " " + subtype); /**/ + } } for (final RtpDescription.FeedbackNegotiationTrrInt feedbackNegotiationTrrInt : description.feedbackNegotiationTrrInts()) { @@ -268,6 +288,9 @@ public class SessionDescription { if (groups.size() == 0) { throw new IllegalArgumentException("A SSRC group is missing SSRC ids"); } + for (final String source : groups) { + checkNoWhitespace(source, "Sources must not contain whitespace"); + } mediaAttributes.put( "ssrc-group", String.format("%s %s", semantics, Joiner.on(' ').join(groups))); @@ -291,13 +314,21 @@ public class SessionDescription { throw new IllegalArgumentException( "A source specific media attribute is missing its value"); } - mediaAttributes.put("ssrc", id + " " + parameterName + ":" + parameterValue); + checkNoWhitespace( + parameterName, + "A source specific media attribute name not not contain whitespace"); + checkNoNewline( + parameterValue, + "A source specific media attribute value must not contain new lines"); + mediaAttributes.put( + "ssrc", id + " " + parameterName + ":" + parameterValue.trim()); } } mediaAttributes.put("mid", name); - mediaAttributes.put(descriptionTransport.senders.asMediaAttribute(isInitiatorContentMap), ""); + mediaAttributes.put( + descriptionTransport.senders.asMediaAttribute(isInitiatorContentMap), ""); if (description.hasChild("rtcp-mux", Namespace.JINGLE_APPS_RTP) || group != null) { mediaAttributes.put("rtcp-mux", ""); } @@ -329,6 +360,13 @@ public class SessionDescription { return input; } + public static String checkNoNewline(final String input, final String message) { + if (CharMatcher.anyOf("\r\n").matchesAnyOf(message)) { + throw new IllegalArgumentException(message); + } + return input; + } + public static int ignorantIntParser(final String input) { try { return Integer.parseInt(input); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java index 954ab6eb1..4b4b23d82 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java @@ -3,6 +3,7 @@ package eu.siacs.conversations.xmpp.jingle; import android.content.Context; import android.media.AudioManager; import android.media.ToneGenerator; +import android.os.Build; import android.util.Log; import java.util.Arrays; @@ -160,11 +161,17 @@ class ToneManager { if (currentTone != null) { currentTone.cancel(true); } - if (toneGenerator != null) { - // catch race condition with already-released generator - try { - toneGenerator.stopTone(); - } catch (final RuntimeException e) { } + stopTone(toneGenerator); + } + + private static void stopTone(final ToneGenerator toneGenerator) { + if (toneGenerator == null) { + return; + } + try { + toneGenerator.stopTone(); + } catch (final RuntimeException e) { + Log.w(Config.LOGTAG,"tone has already stopped"); } } @@ -182,7 +189,8 @@ class ToneManager { private static ToneGenerator getToneGenerator(final boolean ringerModeNormal) { try { - if (ringerModeNormal) { + // when silent and on Android 12+ use STREAM_MUSIC + if (ringerModeNormal || Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { return new ToneGenerator(AudioManager.STREAM_VOICE_CALL,60); } else { return new ToneGenerator(AudioManager.STREAM_MUSIC,100); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java index 31c3577ee..e62aa18fd 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/TrackWrapper.java @@ -6,6 +6,8 @@ import com.google.common.base.CaseFormat; import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import eu.siacs.conversations.Config; + import org.webrtc.MediaStreamTrack; import org.webrtc.PeerConnection; import org.webrtc.RtpSender; @@ -16,8 +18,6 @@ import java.util.UUID; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import eu.siacs.conversations.Config; - class TrackWrapper { public final T track; public final RtpSender rtpSender; @@ -43,7 +43,13 @@ class TrackWrapper { final RtpTransceiver transceiver = peerConnection == null ? null : getTransceiver(peerConnection, trackWrapper); if (transceiver == null) { - Log.w(Config.LOGTAG, "unable to detect transceiver for " + trackWrapper.rtpSender.id()); + final String id; + try { + id = trackWrapper.rtpSender.id(); + } catch (final IllegalStateException e) { + return Optional.absent(); + } + Log.w(Config.LOGTAG, "unable to detect transceiver for " + id); return Optional.of(trackWrapper.track); } final RtpTransceiver.RtpTransceiverDirection direction = transceiver.getDirection(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index fd649a425..5fd1a9ba3 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -16,6 +16,10 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.services.AppRTCAudioManager; +import eu.siacs.conversations.services.XmppConnectionService; + import org.webrtc.AudioSource; import org.webrtc.AudioTrack; import org.webrtc.CandidatePairChangeEvent; @@ -36,7 +40,6 @@ import org.webrtc.SdpObserver; import org.webrtc.SessionDescription; import org.webrtc.VideoTrack; import org.webrtc.audio.JavaAudioDeviceModule; -import org.webrtc.voiceengine.WebRtcAudioEffects; import java.util.LinkedList; import java.util.List; @@ -45,21 +48,21 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.services.AppRTCAudioManager; -import eu.siacs.conversations.services.XmppConnectionService; - @SuppressWarnings("UnstableApiUsage") public class WebRTCWrapper { private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName(); private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + private final ExecutorService localDescriptionExecutorService = + Executors.newSingleThreadExecutor(); private static final int TONE_DURATION = 500; private static final Map TONE_CODES; @@ -93,6 +96,7 @@ public class WebRTCWrapper { .add("E5823") // Sony z5 compact .add("Redmi Note 5") .add("FP2") // Fairphone FP2 + .add("FP4") // Fairphone FP4 .add("MI 5") .add("GT-I9515") // Samsung Galaxy S4 Value Edition (jfvelte) .add("GT-I9515L") // Samsung Galaxy S4 Value Edition (jfvelte) @@ -115,6 +119,8 @@ public class WebRTCWrapper { private TrackWrapper localAudioTrack = null; private TrackWrapper localVideoTrack = null; private VideoTrack remoteVideoTrack = null; + + private final SettableFuture iceGatheringComplete = SettableFuture.create(); private final PeerConnection.Observer peerConnectionObserver = new PeerConnection.Observer() { @Override @@ -149,8 +155,11 @@ public class WebRTCWrapper { @Override public void onIceGatheringChange( - PeerConnection.IceGatheringState iceGatheringState) { + final PeerConnection.IceGatheringState iceGatheringState) { Log.d(EXTENDED_LOGGING_TAG, "onIceGatheringChange(" + iceGatheringState + ")"); + if (iceGatheringState == PeerConnection.IceGatheringState.COMPLETE) { + iceGatheringComplete.set(null); + } } @Override @@ -277,15 +286,16 @@ public class WebRTCWrapper { } synchronized void initializePeerConnection( - final Set media, final List iceServers) + final Set media, + final List iceServers, + final boolean trickle) throws InitializationException { Preconditions.checkState(this.eglBase != null); Preconditions.checkNotNull(media); Preconditions.checkArgument( media.size() > 0, "media can not be empty when initializing peer connection"); final boolean setUseHardwareAcousticEchoCanceler = - WebRtcAudioEffects.canUseAcousticEchoCanceler() - && !HARDWARE_AEC_BLACKLIST.contains(Build.MODEL); + !HARDWARE_AEC_BLACKLIST.contains(Build.MODEL); Log.d( Config.LOGTAG, String.format( @@ -305,7 +315,7 @@ public class WebRTCWrapper { .createAudioDeviceModule()) .createPeerConnectionFactory(); - final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers); + final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers, trickle); final PeerConnection peerConnection = requirePeerConnectionFactory() .createPeerConnection(rtcConfig, peerConnectionObserver); @@ -420,38 +430,43 @@ public class WebRTCWrapper { } private static PeerConnection.RTCConfiguration buildConfiguration( - final List iceServers) { + final List iceServers, final boolean trickle) { final PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers); rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; // XEP-0176 doesn't support tcp - rtcConfig.continualGatheringPolicy = - PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY; + if (trickle) { + rtcConfig.continualGatheringPolicy = + PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY; + } else { + rtcConfig.continualGatheringPolicy = + PeerConnection.ContinualGatheringPolicy.GATHER_ONCE; + } rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN; rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.NEGOTIATE; rtcConfig.enableImplicitRollback = true; return rtcConfig; } - void reconfigurePeerConnection(final List iceServers) { - requirePeerConnection().setConfiguration(buildConfiguration(iceServers)); + void reconfigurePeerConnection( + final List iceServers, final boolean trickle) { + requirePeerConnection().setConfiguration(buildConfiguration(iceServers, trickle)); } - void restartIce() { - executorService.execute( - () -> { - final PeerConnection peerConnection; - try { - peerConnection = requirePeerConnection(); - } catch (final PeerConnectionNotInitialized e) { - Log.w( - EXTENDED_LOGGING_TAG, - "PeerConnection vanished before we could execute restart"); - return; - } - setIsReadyToReceiveIceCandidates(false); - peerConnection.restartIce(); - }); + void restartIceAsync() { + this.execute(this::restartIce); + } + + private void restartIce() { + final PeerConnection peerConnection; + try { + peerConnection = requirePeerConnection(); + } catch (final PeerConnectionNotInitialized e) { + Log.w(EXTENDED_LOGGING_TAG, "PeerConnection vanished before we could execute restart"); + return; + } + setIsReadyToReceiveIceCandidates(false); + peerConnection.restartIce(); } public void setIsReadyToReceiveIceCandidates(final boolean ready) { @@ -550,7 +565,7 @@ public class WebRTCWrapper { return false; } } else { - throw new IllegalStateException("Local audio track does not exist (yet)"); + return false; } } @@ -596,7 +611,9 @@ public class WebRTCWrapper { throw new IllegalStateException("Local video track does not exist"); } - synchronized ListenableFuture setLocalDescription() { + synchronized ListenableFuture setLocalDescription( + final boolean waitForCandidates) { + this.setIsReadyToReceiveIceCandidates(false); return Futures.transformAsync( getPeerConnectionFuture(), peerConnection -> { @@ -609,11 +626,20 @@ public class WebRTCWrapper { new SetSdpObserver() { @Override public void onSetSuccess() { - final SessionDescription description = - peerConnection.getLocalDescription(); - Log.d(EXTENDED_LOGGING_TAG, "set local description:"); - logDescription(description); - future.set(description); + if (waitForCandidates) { + final var delay = getIceGatheringCompleteOrTimeout(); + final var delayedSessionDescription = + Futures.transformAsync( + delay, + v -> { + iceCandidates.clear(); + return getLocalDescriptionFuture(); + }, + MoreExecutors.directExecutor()); + future.setFuture(delayedSessionDescription); + } else { + future.setFuture(getLocalDescriptionFuture()); + } } @Override @@ -627,6 +653,35 @@ public class WebRTCWrapper { MoreExecutors.directExecutor()); } + private ListenableFuture getIceGatheringCompleteOrTimeout() { + return Futures.catching( + Futures.withTimeout( + iceGatheringComplete, + 2, + TimeUnit.SECONDS, + JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE), + TimeoutException.class, + ex -> { + Log.d( + EXTENDED_LOGGING_TAG, + "timeout while waiting for ICE gathering to complete"); + return null; + }, + MoreExecutors.directExecutor()); + } + + private ListenableFuture getLocalDescriptionFuture() { + return Futures.submit( + () -> { + final SessionDescription description = + requirePeerConnection().getLocalDescription(); + Log.d(EXTENDED_LOGGING_TAG, "local description:"); + logDescription(description); + return description; + }, + localDescriptionExecutorService); + } + public static void logDescription(final SessionDescription sessionDescription) { for (final String line : sessionDescription.description.split( @@ -744,7 +799,7 @@ public class WebRTCWrapper { } void execute(final Runnable command) { - executorService.execute(command); + this.executorService.execute(command); } public void switchSpeakerPhonePreference(AppRTCAudioManager.SpeakerPhonePreference preference) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index 061cea752..0cca6527a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -6,6 +6,7 @@ import androidx.annotation.NonNull; import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; import java.util.Locale; import java.util.Set; @@ -147,6 +148,10 @@ public class Content extends Element { return BOTH; } + public static Set receiveOnly(final boolean initiator) { + return ImmutableSet.of(initiator ? RESPONDER : INITIATOR); + } + @Override @NonNull public String toString() { 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 432333090..ccaba56a6 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,17 +1,29 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; +import android.util.Log; + import androidx.annotation.NonNull; import com.google.common.base.Joiner; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.jingle.SessionDescription; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Hashtable; import java.util.LinkedHashMap; @@ -20,10 +32,6 @@ import java.util.Locale; import java.util.Map; import java.util.UUID; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.jingle.SessionDescription; - public class IceUdpTransportInfo extends GenericTransportInfo { public static final IceUdpTransportInfo STUB = new IceUdpTransportInfo(); @@ -59,15 +67,25 @@ public class IceUdpTransportInfo extends GenericTransportInfo { if (fingerprint != null) { iceUdpTransportInfo.addChild(fingerprint); } + for (final String iceOption : IceOption.of(media)) { + iceUdpTransportInfo.addChild(new IceOption(iceOption)); + } return iceUdpTransportInfo; } public static IceUdpTransportInfo of( - final Credentials credentials, final Setup setup, final String hash, final String fingerprint) { + final Credentials credentials, + final Collection iceOptions, + final Setup setup, + final String hash, + final String fingerprint) { final IceUdpTransportInfo iceUdpTransportInfo = new IceUdpTransportInfo(); iceUdpTransportInfo.addChild(Fingerprint.of(setup, hash, fingerprint)); iceUdpTransportInfo.setAttribute("ufrag", credentials.ufrag); iceUdpTransportInfo.setAttribute("pwd", credentials.password); + for (final String iceOption : iceOptions) { + iceUdpTransportInfo.addChild(new IceOption(iceOption)); + } return iceUdpTransportInfo; } @@ -76,12 +94,29 @@ public class IceUdpTransportInfo extends GenericTransportInfo { return fingerprint == null ? null : Fingerprint.upgrade(fingerprint); } + public List getIceOptions() { + final ImmutableList.Builder optionBuilder = new ImmutableList.Builder<>(); + for (final Element child : this.children) { + if (Namespace.JINGLE_TRANSPORT_ICE_OPTION.equals(child.getNamespace()) + && IceOption.WELL_KNOWN.contains(child.getName())) { + optionBuilder.add(child.getName()); + } + } + return optionBuilder.build(); + } + public Credentials getCredentials() { final String ufrag = this.getAttribute("ufrag"); final String password = this.getAttribute("pwd"); return new Credentials(ufrag, password); } + public boolean isStub() { + return Strings.isNullOrEmpty(this.getAttribute("ufrag")) + && Strings.isNullOrEmpty(this.getAttribute("pwd")) + && this.children.isEmpty(); + } + public List getCandidates() { final ImmutableList.Builder builder = new ImmutableList.Builder<>(); for (final Element child : getChildren()) { @@ -112,6 +147,19 @@ public class IceUdpTransportInfo extends GenericTransportInfo { transportInfo.addChild(fingerprint); } } + for (final String iceOption : this.getIceOptions()) { + transportInfo.addChild(new IceOption(iceOption)); + } + return transportInfo; + } + + public IceUdpTransportInfo withCandidates(ImmutableCollection candidates) { + final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); + transportInfo.setAttributes(new Hashtable<>(getAttributes())); + transportInfo.setChildren(this.getChildren()); + for(final Candidate candidate : candidates) { + transportInfo.addChild(candidate); + } return transportInfo; } @@ -165,41 +213,46 @@ public class IceUdpTransportInfo extends GenericTransportInfo { public static Candidate fromSdpAttribute(final String attribute, String currentUfrag) { final String[] pair = attribute.split(":", 2); if (pair.length == 2 && "candidate".equals(pair[0])) { - final String[] segments = pair[1].split(" "); - if (segments.length >= 6) { - final String id = UUID.randomUUID().toString(); - final String foundation = segments[0]; - final String component = segments[1]; - final String transport = segments[2].toLowerCase(Locale.ROOT); - final String priority = segments[3]; - final String connectionAddress = segments[4]; - final String port = segments[5]; - final HashMap additional = new HashMap<>(); - for (int i = 6; i < segments.length - 1; i = i + 2) { - additional.put(segments[i], segments[i + 1]); - } - final String ufrag = additional.get("ufrag"); - if (ufrag != null && !ufrag.equals(currentUfrag)) { - return null; - } - final Candidate candidate = new Candidate(); - candidate.setAttribute("component", component); - candidate.setAttribute("foundation", foundation); - candidate.setAttribute("generation", additional.get("generation")); - candidate.setAttribute("rel-addr", additional.get("raddr")); - candidate.setAttribute("rel-port", additional.get("rport")); - candidate.setAttribute("id", id); - candidate.setAttribute("ip", connectionAddress); - candidate.setAttribute("port", port); - candidate.setAttribute("priority", priority); - candidate.setAttribute("protocol", transport); - candidate.setAttribute("type", additional.get("typ")); - return candidate; - } + return fromSdpAttributeValue(pair[1], currentUfrag); } return null; } + public static Candidate fromSdpAttributeValue(final String value, final String currentUfrag) { + final String[] segments = value.split(" "); + if (segments.length < 6) { + return null; + } + final String id = UUID.randomUUID().toString(); + final String foundation = segments[0]; + final String component = segments[1]; + final String transport = segments[2].toLowerCase(Locale.ROOT); + final String priority = segments[3]; + final String connectionAddress = segments[4]; + final String port = segments[5]; + final HashMap additional = new HashMap<>(); + for (int i = 6; i < segments.length - 1; i = i + 2) { + additional.put(segments[i], segments[i + 1]); + } + final String ufrag = additional.get("ufrag"); + if (currentUfrag != null && ufrag != null && !ufrag.equals(currentUfrag)) { + return null; + } + final Candidate candidate = new Candidate(); + candidate.setAttribute("component", component); + candidate.setAttribute("foundation", foundation); + candidate.setAttribute("generation", additional.get("generation")); + candidate.setAttribute("rel-addr", additional.get("raddr")); + candidate.setAttribute("rel-port", additional.get("rport")); + candidate.setAttribute("id", id); + candidate.setAttribute("ip", connectionAddress); + candidate.setAttribute("port", port); + candidate.setAttribute("priority", priority); + candidate.setAttribute("protocol", transport); + candidate.setAttribute("type", additional.get("typ")); + return candidate; + } + public int getComponent() { return getAttributeAsInt("component"); } @@ -408,4 +461,29 @@ public class IceUdpTransportInfo extends GenericTransportInfo { throw new IllegalStateException(this.name() + " can not be flipped"); } } + + public static class IceOption extends Element { + + public static final List WELL_KNOWN = Arrays.asList("trickle", "renomination"); + + public IceOption(final String name) { + super(name, Namespace.JINGLE_TRANSPORT_ICE_OPTION); + } + + public static Collection of(SessionDescription.Media media) { + final String iceOptions = Iterables.getFirst(media.attributes.get("ice-options"), null); + if (Strings.isNullOrEmpty(iceOptions)) { + return Collections.emptyList(); + } + final ImmutableList.Builder optionBuilder = new ImmutableList.Builder<>(); + for (final String iceOption : Splitter.on(' ').split(iceOptions)) { + if (WELL_KNOWN.contains(iceOption)) { + optionBuilder.add(iceOption); + } else { + Log.w(Config.LOGTAG, "unrecognized ice option: " + iceOption); + } + } + return optionBuilder.build(); + } + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java b/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java index 71ba3d86a..24b429fd7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java +++ b/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java @@ -30,7 +30,7 @@ public class PublishOptions { options.putString("pubsub#persist_items", "true"); options.putString("pubsub#access_model", "whitelist"); options.putString("pubsub#send_last_published_item", "never"); - options.putString("pubsub#max_items", "max"); //YOLO! + options.putString("pubsub#max_items", "max"); options.putString("pubsub#notify_delete", "true"); options.putString("pubsub#notify_retract", "true"); //one could also set notify=true on the retract diff --git a/src/main/res/drawable-hdpi/ic_content_copy_black_24dp.png b/src/main/res/drawable-hdpi/ic_content_copy_black_24dp.png deleted file mode 100644 index 9a9e5706f..000000000 Binary files a/src/main/res/drawable-hdpi/ic_content_copy_black_24dp.png and /dev/null differ diff --git a/src/main/res/drawable-mdpi/ic_content_copy_black_24dp.png b/src/main/res/drawable-mdpi/ic_content_copy_black_24dp.png deleted file mode 100644 index c94cc28f1..000000000 Binary files a/src/main/res/drawable-mdpi/ic_content_copy_black_24dp.png and /dev/null differ diff --git a/src/main/res/drawable-v24/ic_launcher_background.xml b/src/main/res/drawable-v24/ic_launcher_background.xml new file mode 100644 index 000000000..ec3c58ad5 --- /dev/null +++ b/src/main/res/drawable-v24/ic_launcher_background.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + diff --git a/src/main/res/drawable-xhdpi/ic_content_copy_black_24dp.png b/src/main/res/drawable-xhdpi/ic_content_copy_black_24dp.png deleted file mode 100644 index 1cf76a960..000000000 Binary files a/src/main/res/drawable-xhdpi/ic_content_copy_black_24dp.png and /dev/null differ diff --git a/src/main/res/drawable-xxhdpi/ic_content_copy_black_24dp.png b/src/main/res/drawable-xxhdpi/ic_content_copy_black_24dp.png deleted file mode 100644 index 074ea8807..000000000 Binary files a/src/main/res/drawable-xxhdpi/ic_content_copy_black_24dp.png and /dev/null differ diff --git a/src/main/res/drawable-xxxhdpi/ic_content_copy_black_24dp.png b/src/main/res/drawable-xxxhdpi/ic_content_copy_black_24dp.png deleted file mode 100644 index 1f6af72d0..000000000 Binary files a/src/main/res/drawable-xxxhdpi/ic_content_copy_black_24dp.png and /dev/null differ diff --git a/src/main/res/drawable/ic_logout_white_24dp.xml b/src/main/res/drawable/ic_logout_white_24dp.xml new file mode 100644 index 000000000..5f818ab16 --- /dev/null +++ b/src/main/res/drawable/ic_logout_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/main/res/drawable/ic_play_lesson_black_24.xml b/src/main/res/drawable/ic_play_lesson_black_24.xml new file mode 100644 index 000000000..4c4a46ce7 --- /dev/null +++ b/src/main/res/drawable/ic_play_lesson_black_24.xml @@ -0,0 +1,6 @@ + + + + diff --git a/src/main/res/drawable/ic_play_lesson_white_48dp.xml b/src/main/res/drawable/ic_play_lesson_white_48dp.xml new file mode 100644 index 000000000..67fe7c696 --- /dev/null +++ b/src/main/res/drawable/ic_play_lesson_white_48dp.xml @@ -0,0 +1,6 @@ + + + + diff --git a/src/main/res/drawable/ic_qr_code_black_24dp.xml b/src/main/res/drawable/ic_qr_code_black_24dp.xml new file mode 100644 index 000000000..e33c1a622 --- /dev/null +++ b/src/main/res/drawable/ic_qr_code_black_24dp.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + diff --git a/src/main/res/drawable/ic_qr_code_white_24dp.xml b/src/main/res/drawable/ic_qr_code_white_24dp.xml new file mode 100644 index 000000000..d345816e0 --- /dev/null +++ b/src/main/res/drawable/ic_qr_code_white_24dp.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + diff --git a/src/main/res/layout/activity_contact_details.xml b/src/main/res/layout/activity_contact_details.xml index 2f21193b4..bf6377859 100644 --- a/src/main/res/layout/activity_contact_details.xml +++ b/src/main/res/layout/activity_contact_details.xml @@ -196,6 +196,19 @@ android:orientation="vertical" android:padding="@dimen/card_padding_list"/> + + + + + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> - + android:layout_marginBottom="@dimen/activity_vertical_margin"> + app:riv_corner_radius="8dp" /> + app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error" + app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint"> + android:textColor="?attr/edit_text_color" /> @@ -76,21 +77,21 @@ android:id="@+id/account_password_layout" android:layout_width="match_parent" android:layout_height="wrap_content" + app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error" + app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint" app:passwordToggleDrawable="@drawable/visibility_toggle_drawable" app:passwordToggleEnabled="true" - app:passwordToggleTint="?android:textColorSecondary" - app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint" - app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error"> + app:passwordToggleTint="?android:textColorSecondary"> + android:textColor="?attr/edit_text_color" /> + app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error" + app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint"> + android:inputType="textWebEmailAddress" /> @@ -135,16 +136,16 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/account_settings_port" - app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint" - app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error"> + app:errorTextAppearance="@style/TextAppearance.Conversations.Design.Error" + app:hintTextAppearance="@style/TextAppearance.Conversations.Design.Hint"> + android:maxLength="5" /> @@ -155,7 +156,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" - android:text="@string/register_account"/> + android:text="@string/register_account" /> @@ -164,10 +165,10 @@ android:id="@+id/os_optimization" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:layout_marginBottom="@dimen/activity_vertical_margin" android:layout_marginLeft="@dimen/activity_horizontal_margin" - android:layout_marginRight="@dimen/activity_horizontal_margin" android:layout_marginTop="@dimen/activity_vertical_margin" + android:layout_marginRight="@dimen/activity_horizontal_margin" + android:layout_marginBottom="@dimen/activity_vertical_margin" android:visibility="gone"> + android:textAppearance="@style/TextAppearance.Conversations.Title" /> + android:textAppearance="@style/TextAppearance.Conversations.Body1" /> + android:textColor="?colorAccent" /> @@ -222,10 +223,10 @@ android:id="@+id/stats" android:layout_width="fill_parent" android:layout_height="fill_parent" - android:layout_marginBottom="@dimen/activity_vertical_margin" android:layout_marginLeft="@dimen/activity_horizontal_margin" - android:layout_marginRight="@dimen/activity_horizontal_margin" android:layout_marginTop="@dimen/activity_vertical_margin" + android:layout_marginRight="@dimen/activity_horizontal_margin" + android:layout_marginBottom="@dimen/activity_vertical_margin" android:visibility="gone"> + android:textAppearance="@style/TextAppearance.Conversations.Body1" /> + android:textAppearance="@style/TextAppearance.Conversations.Body1" /> @@ -282,7 +283,7 @@ android:ellipsize="end" android:singleLine="true" android:text="@string/server_info_pep" - android:textAppearance="@style/TextAppearance.Conversations.Body1"/> + android:textAppearance="@style/TextAppearance.Conversations.Body1" /> + tools:ignore="RtlHardcoded" /> + android:textAppearance="@style/TextAppearance.Conversations.Body1" /> + tools:ignore="RtlHardcoded" /> + android:textAppearance="@style/TextAppearance.Conversations.Body1" /> + tools:ignore="RtlHardcoded" /> + android:textAppearance="@style/TextAppearance.Conversations.Body1" /> + tools:ignore="RtlHardcoded" /> + android:textAppearance="@style/TextAppearance.Conversations.Body1" /> + tools:ignore="RtlHardcoded" /> + android:textAppearance="@style/TextAppearance.Conversations.Body1" /> + tools:ignore="RtlHardcoded" /> + android:textAppearance="@style/TextAppearance.Conversations.Body1" /> + tools:ignore="RtlHardcoded" /> + android:textAppearance="@style/TextAppearance.Conversations.Body1" /> + tools:ignore="RtlHardcoded" /> + android:textAppearance="@style/TextAppearance.Conversations.Body1" /> + android:textAppearance="@style/TextAppearance.Conversations.Body1" /> + android:textAppearance="@style/TextAppearance.Conversations.Body1" /> + android:textAppearance="@style/TextAppearance.Conversations.Body1" /> @@ -512,14 +513,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/no_name_set_instructions" - android:textAppearance="@style/TextAppearance.Conversations.Body1.Tertiary"/> + android:textAppearance="@style/TextAppearance.Conversations.Body1.Tertiary" /> + android:textAppearance="@style/TextAppearance.Conversations.Caption" /> + android:visibility="visible" /> + android:textAppearance="@style/TextAppearance.Conversations.Fingerprint" /> + android:textAppearance="@style/TextAppearance.Conversations.Caption" /> + android:visibility="visible" /> + android:textAppearance="@style/TextAppearance.Conversations.Fingerprint" /> + android:textAppearance="@style/TextAppearance.Conversations.Caption" /> + android:src="?attr/icon_qr_code" + android:visibility="visible" /> + android:visibility="gone" /> @@ -642,41 +643,83 @@ android:id="@+id/other_device_keys_card" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:layout_marginBottom="@dimen/activity_vertical_margin" android:layout_marginLeft="@dimen/activity_horizontal_margin" - android:layout_marginRight="@dimen/activity_horizontal_margin" android:layout_marginTop="@dimen/activity_vertical_margin" + android:layout_marginRight="@dimen/activity_horizontal_margin" + android:layout_marginBottom="@dimen/activity_vertical_margin" android:visibility="gone"> - - + android:orientation="vertical"> + android:orientation="vertical" + android:padding="@dimen/card_padding_list"> -