diff --git a/.builds/debian-stable.yml b/.builds/debian-stable.yml index 7de4f7981c4c79f69888fd557f9e1db384469dab..cd29286df24ba5e573c691baaf69efb260c5b246 100644 --- a/.builds/debian-stable.yml +++ b/.builds/debian-stable.yml @@ -22,6 +22,11 @@ tasks: echo y | android/cmdline-tools/tools/bin/sdkmanager "build-tools;29.0.2" touch ~/.android/repositories.cfg yes | android/cmdline-tools/tools/bin/sdkmanager --licenses +- libwebrtc: | + cd cheogram-android + mkdir libs + cd libs + wget -qO libwebtrc.aar https://cloudflare-ipfs.com/ipfs/QmeqMiLxHi8AAjXobxr3QTfa1bSSLyAu86YviAqQnjxCjM/libwebrtc.aar - build: | cd cheogram-android ./gradlew assembleCheogramFreeSystemDebug diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 0000000000000000000000000000000000000000..badbbf5d765fcae525b9cb7b0db484516f85996d --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,38 @@ +name: Android CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'adopt' + - name: Download WebRTC + run: mkdir libs && wget -O libs/libwebrtc-m92.aar https://gultsch.de/files/libwebrtc-m92.aar + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build Quicksy (Compat) + run: ./gradlew assembleQuicksyFreeCompatDebug + - name: Build Quicksy (System) + run: ./gradlew assembleQuicksyFreeSystemDebug + - name: Build Conversations (Compat) + run: ./gradlew assembleConversationsFreeCompatDebug + - name: Build Conversations (System) + run: ./gradlew assembleConversationsFreeSystemDebug + - uses: actions/upload-artifact@v2 + with: + name: Conversations all-flavors (debug) + path: ./build/outputs/apk/**/debug/Conversations-*.apk + + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 664d3cf553231dc3793610b5b35e33e93286963d..0000000000000000000000000000000000000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: android -jdk: - - oraclejdk8 -android: - components: - - platform-tools - - tools - - build-tools-28.0.3 - - extra-google-google_play_services - licenses: - - '.+' -before_script: - - mkdir libs - - wget -O libs/libwebrtc-m90.aar https://gultsch.de/files/libwebrtc-m90.aar -script: - - ./gradlew assembleQuicksyFreeCompatDebug - - ./gradlew assembleQuicksyFreeSystemDebug - - ./gradlew assembleConversationsFreeCompatDebug - - ./gradlew assembleConversationsFreeSystemDebug - -before_install: - - yes | sdkmanager "platforms;android-28" diff --git a/CHANGELOG.md b/CHANGELOG.md index 9da1fec84bf42358dad7e9203e4ef3d174b61414..c2bb0a9381fab2381c0ad4377e808c6b82419da4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +### Version 2.10.2 + +* Fix crash when rendering some quotes +* Fix crash in welcome screen + +### Version 2.10.1 + +* Fix issue with some videos not being compressed +* Fix rare crash when opening notification + +### Version 2.10.0 + +* Show black bars when remote video does not match aspect ratio of screen +* Improve search performance +* Add setting to prevent screenshots + ### Version 2.9.13 * minor A/V improvements diff --git a/build.gradle b/build.gradle index 9264867e2b12a74b0c3ecfe3d1c82d13ef5390de..bd14098391f7362ffa4f2c1d4c54661269ee31bc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,3 @@ -import com.android.build.OutputFile - // Top-level build file where you can add configuration options common to all // sub-projects/modules. buildscript { @@ -8,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.2.1' + classpath 'com.android.tools.build:gradle:7.1.1' } } @@ -35,23 +33,23 @@ configurations { dependencies { implementation 'androidx.viewpager:viewpager:1.0.0' - playstoreImplementation('com.google.firebase:firebase-messaging:22.0.0') { + playstoreImplementation('com.google.firebase:firebase-messaging:23.0.0') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' } conversationsPlaystoreCompatImplementation("com.android.installreferrer:installreferrer:2.2") conversationsPlaystoreSystemImplementation("com.android.installreferrer:installreferrer:2.2") - quicksyPlaystoreCompatImplementation 'com.google.android.gms:play-services-auth-api-phone:17.5.0' - quicksyPlaystoreSystemImplementation 'com.google.android.gms:play-services-auth-api-phone:17.5.0' + quicksyPlaystoreCompatImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1' + quicksyPlaystoreSystemImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1' implementation 'org.sufficientlysecure:openpgp-api:10.0' implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'androidx.exifinterface:exifinterface:1.3.2' + implementation 'androidx.appcompat:appcompat:1.3.1' + implementation 'androidx.exifinterface:exifinterface:1.3.3' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.emoji:emoji:1.1.0' - implementation 'com.google.android.material:material:1.3.0' + implementation 'com.google.android.material:material:1.4.0' compatImplementation 'androidx.emoji:emoji-appcompat:1.1.0' conversationsFreeCompatImplementation 'androidx.emoji:emoji-bundled:1.1.0' quicksyFreeCompatImplementation 'androidx.emoji:emoji-bundled:1.1.0' @@ -64,21 +62,22 @@ dependencies { implementation 'org.whispersystems:signal-protocol-java:2.6.2' implementation 'com.makeramen:roundedimageview:2.3.0' implementation "com.wefika:flowlayout:0.4.1" - implementation 'net.ypresto.androidtranscoder:android-transcoder:0.3.0' - implementation 'org.jxmpp:jxmpp-jid:1.0.1' + implementation 'com.otaliastudios:transcoder:0.10.4' + + implementation 'org.jxmpp:jxmpp-jid:1.0.2' implementation 'org.osmdroid:osmdroid-android:6.1.10' implementation 'org.hsluv:hsluv:0.2' implementation 'org.conscrypt:conscrypt-android:2.5.2' implementation 'me.drakeet.support:toastcompat:1.1.0' - implementation "com.leinardi.android:speed-dial:2.0.1" + implementation "com.leinardi.android:speed-dial:3.2.0" implementation "com.squareup.retrofit2:retrofit:2.9.0" implementation "com.squareup.retrofit2:converter-gson:2.9.0" - implementation "com.squareup.okhttp3:okhttp:4.9.1" + implementation "com.squareup.okhttp3:okhttp:4.9.3" implementation 'com.google.guava:guava:30.1.1-android' - quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.18' - implementation 'org.webrtc:google-webrtc:1.0.32006' + quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.36' + implementation fileTree(include: ['libwebrtc.aar'], dir: 'libs') } ext { @@ -93,8 +92,8 @@ android { defaultConfig { minSdkVersion 24 targetSdkVersion 29 - versionCode 42015 - versionName "2.9.13" + versionCode 42024 + versionName "2.10.3-beta" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId @@ -105,19 +104,13 @@ android { configurations { - compile.exclude group: 'org.jetbrains' , module:'annotations' + implementation.exclude group: 'org.jetbrains' , module:'annotations' } dataBinding { enabled true } - dexOptions { - // Skip pre-dexing when running on Travis CI or when disabled via -Dpre-dex=false. - preDexLibraries = preDexEnabled && !travisBuild - jumboMode true - } - compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -292,10 +285,25 @@ android { } } - packagingOptions { - exclude 'META-INF/BCKEY.DSA' - exclude 'META-INF/BCKEY.SF' + resources { + excludes += ['META-INF/BCKEY.DSA', 'META-INF/BCKEY.SF'] + } } + lint { + disable 'MissingTranslation', 'InvalidPackage', 'AppCompatResource' + } + + + android.applicationVariants.all { variant -> + variant.outputs.each { output -> + def baseAbiVersionCode = project.ext.abiCodes.get(output.getFilter(com.android.build.OutputFile.ABI)) + if (baseAbiVersionCode != null) { + output.versionCodeOverride = (100 * project.android.defaultConfig.versionCode) + baseAbiVersionCode + } else { + output.versionCodeOverride = 100 * project.android.defaultConfig.versionCode + } + } + } } diff --git a/conversations.doap b/conversations.doap index 462d750da608bad3e0eb3149af22d22276430131..93a7df126d7c0db948baeb2ac397cda3815b4750 100644 --- a/conversations.doap +++ b/conversations.doap @@ -453,12 +453,19 @@ 0.2.0 + + + + complete + 0.1.0 + + - 2.5.8 - 2019-09-12 - + 2.9.13 + 2021-05-03 + diff --git a/fastlane/metadata/android/en-US/changelogs/42018.txt b/fastlane/metadata/android/en-US/changelogs/42018.txt new file mode 100644 index 0000000000000000000000000000000000000000..8f4d66caab71deced9f02d1258690ef06aecc2ca --- /dev/null +++ b/fastlane/metadata/android/en-US/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/en-US/changelogs/42022.txt b/fastlane/metadata/android/en-US/changelogs/42022.txt new file mode 100644 index 0000000000000000000000000000000000000000..eaaa190faabe6799d5fc22f80d7b6a5ccbd87805 --- /dev/null +++ b/fastlane/metadata/android/en-US/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/en-US/changelogs/42023.txt b/fastlane/metadata/android/en-US/changelogs/42023.txt new file mode 100644 index 0000000000000000000000000000000000000000..ed3c253807388b2b27f7716c86c88070232c3a28 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/42023.txt @@ -0,0 +1,2 @@ +* Fix crash when rendering some quotes +* Fix crash in welcome screen diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ea1e4b836718ba6cbc554edd76b03575ce9356a1..162dd9b7f25a2ebf7f029a7f7d681aeca4074879 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip diff --git a/proguard-rules.pro b/proguard-rules.pro index c8b4089c521d0fcfe7d73553998dfa1500f5bed0..7e4d7d31d6553d006e1d1008e5c4fd90a1004bb1 100644 --- a/proguard-rules.pro +++ b/proguard-rules.pro @@ -26,6 +26,15 @@ -dontwarn java.lang.** -dontwarn javax.lang.** +-dontwarn com.android.org.conscrypt.SSLParametersImpl +-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl +-dontwarn org.bouncycastle.jsse.BCSSLParameters +-dontwarn org.bouncycastle.jsse.BCSSLSocket +-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider +-dontwarn org.openjsse.javax.net.ssl.SSLParameters +-dontwarn org.openjsse.javax.net.ssl.SSLSocket +-dontwarn org.openjsse.net.ssl.OpenJSSE + -keepclassmembers class eu.siacs.conversations.http.services.** { !transient ; } diff --git a/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java b/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java index a1f4d8d11a37ce20132442579bcfcd8fdb50c60a..90c214ab44f8648a6d95ba24cbebc0a4240461bc 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java @@ -29,6 +29,7 @@ import eu.siacs.conversations.databinding.ActivityImportBackupBinding; 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.ThemeHelper; public class ImportBackupActivity extends ActionBarActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed { @@ -54,6 +55,12 @@ public class ImportBackupActivity extends ActionBarActivity implements ServiceCo this.binding.list.setAdapter(this.backupFileAdapter); this.backupFileAdapter.setOnItemClickedListener(this); } + + @Override + protected void onResume(){ + super.onResume(); + SettingsUtils.applyScreenshotPreventionSetting(this); + } @Override public boolean onCreateOptionsMenu(final Menu menu) { diff --git a/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java index c1ee451be7ac01ac8a25b3e7fd7cddedf6885750..6aecf4b26faaf601f37152dcd3fdfb48ecd50442 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -228,6 +228,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (grantResults.length > 0) { if (allGranted(grantResults)) { switch (requestCode) { diff --git a/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java b/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java index 8f652ce8e522bef41e4631c504aaacf3c22647cf..d61c64a9c38dc1deadd04016483efc503d3b5d7f 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java @@ -106,7 +106,8 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi } @Override - public void onNewIntent(Intent intent) { + public void onNewIntent(final Intent intent) { + super.onNewIntent(intent); if (intent != null) { setIntent(intent); } @@ -201,6 +202,7 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults); if (grantResults.length > 0) { if (allGranted(grantResults)) { diff --git a/src/conversations/java/eu/siacs/conversations/utils/PhoneNumberUtilWrapper.java b/src/conversations/java/eu/siacs/conversations/utils/PhoneNumberUtilWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..2f7963cf69d20ae746b3aff661a036606e86415f --- /dev/null +++ b/src/conversations/java/eu/siacs/conversations/utils/PhoneNumberUtilWrapper.java @@ -0,0 +1,11 @@ +package eu.siacs.conversations.utils; + +import android.content.Context; + +import eu.siacs.conversations.xmpp.Jid; + +public class PhoneNumberUtilWrapper { + public static String toFormattedPhoneNumber(Context context, Jid jid) { + throw new AssertionError("This method is not implemented in Conversations"); + } +} diff --git a/src/conversations/res/values-ar/strings.xml b/src/conversations/res/values-ar/strings.xml index 1e820a9ccff02fac4d7188668374088eede8f605..7d44818a72f347631896cbc0b4d223c58e499a20 100644 --- a/src/conversations/res/values-ar/strings.xml +++ b/src/conversations/res/values-ar/strings.xml @@ -3,4 +3,18 @@ اختر مزود خدمة XMPP الخاص بك استخدِم conversations.im أنشئ حسابًا جديدًا - \ No newline at end of file + هل تملك حساب XMPP؟؟ قد يكون ذلك ممكنا لو كنت تستعمل خدمة XMPP أخرى أو إستعملت تطبيق كونفرسايشنز سابقا. أو يمكنك صنع حساب XMPP جديد الآن. +ملاحظة: بعض خدمات البريد الإلكتروني تقدم حسابات XMPP. + XMPP هي خدمة مستقلة للتواصل بشبكة الرسائل المباشرة. يمكنك إستعمال هذه الخدمة مع أي خادم XMPP تختاره. +سعيا لراحتك جعلنا خلق حساب في كونفيرسايشنز سهلا مع مقدم خدمة خاص بالإستعمال مع كونفيرسايشنز. + لقد تمت دعوتك لـ %1$s. سيتم دلّك على طريقة صنع حساب. +عندما تختار %1$sكمقدّم خدمة سيصبح من الممكن لك التواصل مع مستعملين من أي خادم آخر عن طريق إعطائهم عنوانك الكامل على XMPP. + تمّت دعوتك إلى %1$s. تم إختيار إسم مستخدم خاص بك. سيتم قيادتك عبر طريقة صنع حساب. +سيمكنك التواصل مع مستخدمين من مزودين آخرين عبر إعطائهم كامل عنوانك XMPP. + سيرفر دعوتك + لم يتم التقاط الكود بطريقة جيّدة + إضغط على زر مشاركة لترسل إلى المتصل بك دعوة إلى %1$s. + إذا كان المتصل بك قريبا منك، يمكنه فحص الكود بالأسفل ليقبل دعوتك. + إنظم %1$s وتحدّث معي: %2$s + شارك إستدعاء مع... + \ No newline at end of file diff --git a/src/conversations/res/values-bg/strings.xml b/src/conversations/res/values-bg/strings.xml index 2981a6542b783e934c509c4b4c433ec53929541e..7ef32e0252a6fca28c58aafd24b9caf54b85e213 100644 --- a/src/conversations/res/values-bg/strings.xml +++ b/src/conversations/res/values-bg/strings.xml @@ -1,13 +1,13 @@ - Изберете вашият XMPP доставчик + Изберете своя XMPP доставчик Използвайте conversations.im Създаване не нов профил - Имате ли вече XMPP профил? Това може да се случи, ако вече използвате друг клиент на XMPP или сте използвали преди това Conversations. Ако не, можете да създадете нов XMPP профил в момента.\nСъвет: Някои доставчици на имейл също предоставят XMPP профили. + Имате ли вече XMPP профил? Може да имате, ако вече използвате друг клиент на XMPP или сте използвали Conversations и преди. Ако не, можете да създадете нов XMPP профил сега.\nСъвет: някои доставчици на е-поща също предоставят XMPP профили.   - XMPP е мрежа за общуване чрез мигновени съобщения, която не е обвързана с конкретен доставчик. Можете да използвате клиента с всеки сървър, който работи с XMPP.\nЗа Ваше удобство, ние предоставяме лесен начин да си създадете профил в conversations.im¹ — сървър, пригоден да работи добре с Conversations. - Бяхте поканен(а) в %1$s. Ще Ви преведем през процеса на създаване на акаунт.\nИзбирайки %1$s за доставчик, Вие ще можете да общувате и с потребители на други доставчици, като им предоставите своя пълен адрес за XMPP. - Бяхте поканен(а) в %1$s. Вече Ви избрахме потребителско име. Ще Ви преведем през процеса на създаване на акаунт.\nЩе можете да общувате и с потребители на други доставчици, като им предоставите своя пълен адрес за XMPP. + XMPP е мрежа за общуване чрез мигновени съобщения, която не е обвързана с конкретен доставчик. Можете да използвате клиента с всеки сървър, който работи с XMPP.\nЗа Ваше удобство, обаче, ние предоставяме лесен начин да си създадете профил в conversations.im¹ — сървър, пригоден да работи най-добре с Conversations. + Получихте покана за %1$s. Ще Ви преведем през процеса на създаване на профил.\nИзбирайки %1$s за доставчик, Вие ще можете да общувате и с потребители на други доставчици, като им предоставите своя пълен XMPP адрес. + Получихте покана за %1$s. Вече Ви избрахме потребителско име. Ще Ви преведем през процеса на създаване на профил.\nЩе можете да общувате и с потребители на други доставчици, като им предоставите своя пълен XMPP адрес. Вашата покана за сървъра Неправилно форматиран код за достъп Докоснете бутона за споделяне, за да изпратите на контакта си покана за %1$s. diff --git a/src/conversations/res/values-el/strings.xml b/src/conversations/res/values-el/strings.xml index 9ee6d96ae4fc53127a634e68087cf304cbd2a333..7c87e66a3eda03b9ec9a2e3de978b02d85a43eae 100644 --- a/src/conversations/res/values-el/strings.xml +++ b/src/conversations/res/values-el/strings.xml @@ -10,7 +10,7 @@ Η πρόσκλησή σας στον διακομιστή Λάθος μορφοποίηση κώδικα παροχής Πατήστε το πλήκτρο διαμοιρασμού για να στείλετε στην επαφή σας μια πρόσκληση στο %1$s. - Αν η επαφή σας βρίσκεται κοντά σας, μπορεί επίσης να σκανάρει τον κωδικό παρακάτω για να αποδεχτεί την πρόσκλησή σας. + Αν η επαφή σας βρίσκεται κοντά σας, μπορεί επίσης να σαρώσει τον κωδικό παρακάτω για να αποδεχτεί την πρόσκλησή σας. Μπείτε στο %1$s και συνομιλήστε μαζί μου: %2$s Διαμοιρασμός πρόσκλησης με... \ No newline at end of file diff --git a/src/conversations/res/values-fi/strings.xml b/src/conversations/res/values-fi/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..17c75a2977d6777910f2834fe9f874800b1462c2 --- /dev/null +++ b/src/conversations/res/values-fi/strings.xml @@ -0,0 +1,14 @@ + + + Valitse XMPP-palveluntarjoaja + Käytä conversations.im:ää + Luo uusi tili + Onko sinulla jo XMPP-tunnus? Jos käytät jo toista XMPP-sovellusta tai olet käyttänyt Conversationsia aiemmin, niin voi olla. Jos ei, voit tehdä uuden XMPP-tilin saman tien.\nVinkki: Jotkin sähköpostipalvelut tarjoavat myös XMPP-tilin. + XMPP on tietystä palveluntarjoasta riippumaton pikaviestiverkosto. Voit käyttää tätä asiakasohjelmaa minkä tahansa haluamasi XMPP-palvelimen kanssa.\nHelppouden nimissä olemme kuitenkin helpottaneet tilin luomista conversations.im:iin. + Sinut on kutsuttu %1$s:iin. Opastamme sinua tilin luomisen kanssa.\nValitessasi palvelimen %1$s palveluntarjoajaksesi voit jutella muiden palveluntajoajien käyttäjien kanssa kertomalla heille koko XMPP-osoitteesi. + Sinut on kutsuttu palvelimelle %1$s. Käyttäjänimesi on valittu valmiiksi puolestasi. Opastamme sinua tilin luomisen kanssa.\nVoit jutella muiden palveluntarjoajien käyttäjien kanssa kertomalle heille koko XMPP-osoitteesi. + Kutsusi palvelimelle + Virheellisesti muotoiltu koodi + Jos henkilö on lähellä, hän voi myös hyväksyä kutsun lukemalla allaolevan koodin. + Jaa kutsu sovelluksella... + \ No newline at end of file diff --git a/src/conversations/res/values-it/strings.xml b/src/conversations/res/values-it/strings.xml index 45800a7b8714a11fe2ae8f5a3e68c1d9a4940e85..6e68c5eaa11932b62970ee99d8018d8e8e8eab06 100644 --- a/src/conversations/res/values-it/strings.xml +++ b/src/conversations/res/values-it/strings.xml @@ -1,14 +1,14 @@ - Scegli il tuo provider XMPP + Scegli il tuo fornitore XMPP Usa conversations.im - Crea un nuovo account - Possiedi già un account XMPP? Questo succede se stai già usando un diverso client XMPP o hai già usato prima Conversations. In caso negativo puoi creare un account XMPP adesso. + Crea un nuovo profilo + Possiedi già un profilo XMPP? Questo succede se stai già usando un diverso client XMPP o hai già usato prima Conversations. In caso negativo puoi creare un profilo XMPP adesso. Suggerimento: alcuni provider di email forniscono anche un account XMPP. - XMPP è una rete di instant messaging indipendente dal provider. Puoi usare questo client con qualsiasi server XMPP. -In ogni caso per facilitare puoi creare facilmente un account su conversations.im, un provider pensato apposta per essere usato con Conversations. - Sei stato invitato su %1$s. Ti guideremo nel procedimento per creare un account.\nQuando scegli %1$s come fornitore sarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo. - Sei stato invitato su %1$s. È già stato scelto un nome utente per te. Ti guideremo nel procedimento per creare un account.\nSarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo. + XMPP è una rete di messaggistica istantanea indipendente dal fornitore. Puoi usare questo client con qualsiasi server XMPP. +In ogni caso per facilitare puoi creare facilmente un account su conversations.im, un fornitore pensato apposta per essere usato con Conversations. + Hai ricevuto un invito per %1$s. Ti guideremo nel procedimento per creare un profilo.\nQuando scegli %1$s come fornitore sarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo. + Hai ricevuto un invito per %1$s. È già stato scelto un nome utente per te. Ti guideremo nel procedimento per creare un profilo.\nSarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo. Il tuo invito al server Codice di approvvigionamento formattato male Tocca il pulsante condividi per inviare al contatto un invito per %1$s. diff --git a/src/conversations/res/values-ja/strings.xml b/src/conversations/res/values-ja/strings.xml index 2a9af807d2e8278dcedc50cc507ee81e575f780c..a36b4a119f154f07884c8dc1939214891f146484 100644 --- a/src/conversations/res/values-ja/strings.xml +++ b/src/conversations/res/values-ja/strings.xml @@ -1,12 +1,12 @@ - XMPPプロバイダーを選択してください - conversations.imを利用する - アカウントを作成 - XMPPアカウントをお持ちですか?既にほかのXMPPクライアントを利用しているか、Conversationsを利用したことがある場合はこちら。初めての方は、今すぐ新しいXMPPアカウントを作成できます。\nヒント: eメールのプロバイダーがXMPPアカウントも提供している場合があります。 - XMPPは、プロバイダーに依存しないインスタントメッセージのプロトコルです。XMPPサーバーならどこでも、このクライアントを使用することができます。\nよろしければ、Conversationsに最適化されたプロバイダーconversations.im¹で簡単にアカウントを作成することもできます。 - %1$sへ招待されました。アカウント作成手順をご案内します。 \n%1$sをプロバイダーに選択してほかのプロバイダーのユーザーと会話するには、XMPPのフルアドレスを相手にお知らせください。 - %1$sへ招待されました。ユーザーネームは既に選択されています。アカウント作成手順をご案内します。 \nほかのプロバイダーのユーザーと会話するには、XMPPのフルアドレスを相手にお知らせください。 + XMPP プロバイダーを選択してください + conversations.im を利用する + 新規アカウントを作成 + XMPP アカウントをお持ちですか?既にほかの XMPP クライアントを利用しているか、 Conversations を利用したことがある場合はこちら。初めての方は、今すぐ新規 XMPP アカウントを作成できます。\nヒント: e メールのプロバイダーが XMPP アカウントも提供している場合があります。 + XMPP は、プロバイダーに依存しないインスタントメッセージのプロトコルです。 XMPP サーバーならどこでも、このクライアントを使用することができます。\nよろしければ、 Conversations に最適化されたプロバイダー conversations.im¹ で簡単にアカウントを作成することもできます。 + %1$s へ招待されました。アカウント作成手順をご案内します。 \n%1$s をプロバイダーに選択してほかのプロバイダーのユーザーと会話するには、 XMPP のフルアドレスを相手にお知らせください。 + %1$s へ招待されました。ユーザー名は既に選択されています。アカウント作成手順をご案内します。 \nほかのプロバイダーのユーザーと会話するには、 XMPP のフルアドレスを相手にお知らせください。 サーバーの招待 仮コードの書式が不正です 共有ボタンを叩いて、連絡先の %1$s に招待を送信する。 diff --git a/src/conversations/res/values-sk/strings.xml b/src/conversations/res/values-sk/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..4897be11a6fe4adbb4b12b41c94d0dc87017bfcc --- /dev/null +++ b/src/conversations/res/values-sk/strings.xml @@ -0,0 +1,14 @@ + + + Vyberte si svojho XMPP poskytovateľa + Použiť conversations.im + Vytvoriť nové konto + Máte už svoje XMPP konto? Môže to tak byť v prípade, že už používate iného klienta XMPP alebo ste predtým používali Conversations. Ak nie, môžete si vytvoriť nové XMPP konto práve teraz.\nHint: Niektorí poskytovatelia emailu zároveň poskytujú aj XMPP kontá. + XMPP je sieť pre okamžité správy nezávislá od poskytovateľa. Tohto klienta môžete používať s akýmkoľvek XMPP serverom, ktorý si vyberiete..\nAvšak pre vaše pohodlie sme zjednodušili vytvorenie konta na conversations.im¹; poskytovateľ špeciálne vhodný na používanie s Conversations. + Boli ste pozvaný do %1$s. Prevedieme vás procesom vytvorenia konta..\nPo výbere %1$s ako poskytovateľa, budete môcť komunikovať s užívateľmi iných poskytovateľov tak, že im dáte vašu úplnú XMPP adresu. + Boli ste pozvaný do %1$s . Užívateľské meno vám už bolo vopred vybrané. Prevedieme vás procesom vytvorenia konta..\nBudete môcť komunikovať s užívateľmi iných poskytovateľov tak, že im dáte vašu úplnú XMPP adresu. + Ť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... + \ No newline at end of file diff --git a/src/conversations/res/values-sv/strings.xml b/src/conversations/res/values-sv/strings.xml index 9212ad109e6208468e8615436a48a240778c0803..a6185650eade287dfa1828f7d5d0436db8fc0bdb 100644 --- a/src/conversations/res/values-sv/strings.xml +++ b/src/conversations/res/values-sv/strings.xml @@ -1,5 +1,13 @@ + Välj din XMPP-leverantör Använd conversations.im - Skapa nytt konto - \ No newline at end of file + Skapa ett nytt konto + Har du redan ett XMPP-konto? Detta kan vara fallet om du redan använder en annan XMPP-klient eller om du har använt Conversations tidigare. Om inte, kan du skapa ett nytt XMPP-konto på en gång.\nTips: Vissa e-postleverantörer tillhandahåller även XMPP-konton. + Din serverinbjudan + Felaktigt formaterad provisioneringskod + Tryck på dela-knappen för att skicka en inbjudan till din kontakt till %1$s. + Om din kontakt är i närheten, kan de också skanna koden nedan för att acceptera din inbjudan. + Gå med %1$s och chatta med mig: %2$s + Dela inbjudan med... + \ No newline at end of file diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index f5db03b69cf197635283ed22d35094ad43ade33c..07923d9597ff9b169233f510042ede55abddd5ce 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -39,8 +39,6 @@ - - @@ -54,7 +52,8 @@ + + + + + + + + = Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { return Collections.emptyMap(); } - final String[] PROJECTION = new String[]{ContactsContract.Data._ID, - ContactsContract.Data.DISPLAY_NAME, - ContactsContract.Data.PHOTO_URI, - ContactsContract.Data.LOOKUP_KEY, - ContactsContract.CommonDataKinds.Im.DATA}; - - final String SELECTION = "(" + ContactsContract.Data.MIMETYPE + "=\"" - + ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE - + "\") AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL - + "=\"" + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER - + "\")"; - final Cursor cursor; - try { - cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, null, null); - } catch (Exception e) { - return Collections.emptyMap(); - } - final HashMap contacts = new HashMap<>(); - while (cursor != null && cursor.moveToNext()) { - try { - final JabberIdContact contact = new JabberIdContact(cursor); - final JabberIdContact preexisting = contacts.put(contact.getJid(), contact); - if (preexisting == null || preexisting.rating() < contact.rating()) { - contacts.put(contact.getJid(), contact); + try (final Cursor cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, SELECTION_ARGS, null)) { + if (cursor == null) { + return Collections.emptyMap(); + } + final HashMap contacts = new HashMap<>(); + while (cursor.moveToNext()) { + try { + final JabberIdContact contact = new JabberIdContact(cursor); + final JabberIdContact preexisting = contacts.put(contact.getJid(), contact); + if (preexisting == null || preexisting.rating() < contact.rating()) { + contacts.put(contact.getJid(), contact); + } + } catch (final IllegalArgumentException e) { + Log.d(Config.LOGTAG, "unable to create jabber id contact"); } - } catch (IllegalArgumentException e) { - Log.d(Config.LOGTAG,"unable to create jabber id contact"); } + return contacts; + } catch (final Exception e) { + Log.d(Config.LOGTAG, "unable to query", e); + return Collections.emptyMap(); } - if (cursor != null) { - cursor.close(); - } - return contacts; } } diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 0796a1c00b3778247d5612d79cf4510ed83a4721..37f8114e870ce6d90df196fe2e3a7400dabebbe0 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -5,6 +5,8 @@ import android.database.Cursor; import android.os.SystemClock; import android.util.Log; +import com.google.common.base.Strings; + import org.json.JSONException; import org.json.JSONObject; @@ -247,7 +249,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable } public String getHostname() { - return this.hostname == null ? "" : this.hostname; + return Strings.nullToEmpty(this.hostname); } public void setHostname(String hostname) { diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 20db15da23f2137b6fed0a75ea4ae7b688c84ab8..aeba1f14c7f4418d6123643f14f9e986435c3d6b 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -28,6 +28,7 @@ import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.utils.JidHelper; +import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.chatstate.ChatState; @@ -258,9 +259,22 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public Message findMessageWithFileAndUuid(final String uuid) { synchronized (this.messages) { for (final Message message : this.messages) { + final Transferable transferable = message.getTransferable(); + final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message); if (message.getUuid().equals(uuid) && message.getEncryption() != Message.ENCRYPTION_PGP - && (message.isFileOrImage() || message.treatAsDownloadable())) { + && (message.isFileOrImage() || message.treatAsDownloadable() || unInitiatedButKnownSize || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING))) { + return message; + } + } + } + return null; + } + + public Message findMessageWithUuid(final String uuid) { + synchronized (this.messages) { + for (final Message message : this.messages) { + if (message.getUuid().equals(uuid)) { return message; } } diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 6c53134aac3c007989b92dda771da7e3ad9f4947..aa197aa4449c3ca0216c5742d8baaab8af6e5da5 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -984,13 +984,28 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } if (conversation.getMode() == Conversation.MODE_MULTI) { final Jid nextCounterpart = conversation.getNextCounterpart(); - if (nextCounterpart != null) { - message.setCounterpart(nextCounterpart); - message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart)); - message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE); - return true; - } + return configurePrivateMessage(conversation, message, nextCounterpart, isFile); } return false; } + + public static boolean configurePrivateMessage(final Message message, final Jid counterpart) { + final Conversation conversation; + if (message.conversation instanceof Conversation) { + conversation = (Conversation) message.conversation; + } else { + return false; + } + return configurePrivateMessage(conversation, message, counterpart, false); + } + + private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) { + if (counterpart == null) { + return false; + } + message.setCounterpart(counterpart); + message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart)); + message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE); + return true; + } } diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index 349886115fb87513da3885df53ae0dc84d71aade..15dc6eac692a96dc51e5e618764accb3e0fb6b75 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -131,7 +131,6 @@ public class HttpDownloadConnection implements Transferable { } private void download(final boolean interactive) { - Log.d(Config.LOGTAG,"download()",new Exception()); EXECUTOR.execute(new FileDownloader(interactive)); } diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index ed6b80c5b7b4e02c3e5eb2459bbd3e099775b49b..de9bc0d2ce32e5135bdb7613ea8e24bd4c89b85d 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -11,6 +11,8 @@ import android.os.SystemClock; import android.util.Base64; import android.util.Log; +import com.google.common.base.Stopwatch; + import org.json.JSONException; import org.json.JSONObject; import org.whispersystems.libsignal.IdentityKey; @@ -62,7 +64,9 @@ import eu.siacs.conversations.xmpp.mam.MamReference; public class DatabaseBackend extends SQLiteOpenHelper { private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 48; + private static final int DATABASE_VERSION = 49; + + private static boolean requiresMessageIndexRebuild = false; private static DatabaseBackend instance = null; private static final String CREATE_CONTATCS_STATEMENT = "create table " + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " @@ -170,10 +174,11 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static final String CREATE_MESSAGE_RELATIVE_FILE_PATH_INDEX = "CREATE INDEX message_file_path_index ON " + Message.TABLENAME + "(" + Message.RELATIVE_FILE_PATH + ")"; private static final String CREATE_MESSAGE_TYPE_INDEX = "CREATE INDEX message_type_index ON " + Message.TABLENAME + "(" + Message.TYPE + ")"; - private static final String CREATE_MESSAGE_INDEX_TABLE = "CREATE VIRTUAL TABLE messages_index USING fts4 (uuid TEXT PRIMARY KEY, body TEXT, tokenize = 'unicode61')"; - private static final String CREATE_MESSAGE_INSERT_TRIGGER = "CREATE TRIGGER after_message_insert AFTER INSERT ON " + Message.TABLENAME + " BEGIN INSERT INTO messages_index (uuid,body) VALUES (new.uuid,new.body); END;"; - private static final String CREATE_MESSAGE_UPDATE_TRIGGER = "CREATE TRIGGER after_message_update UPDATE of uuid,body ON " + Message.TABLENAME + " BEGIN update messages_index set body=new.body,uuid=new.uuid WHERE uuid=old.uuid; END;"; - private static final String COPY_PREEXISTING_ENTRIES = "INSERT INTO messages_index(uuid,body) SELECT uuid,body FROM " + Message.TABLENAME + ";"; + private static final String CREATE_MESSAGE_INDEX_TABLE = "CREATE VIRTUAL TABLE messages_index USING fts4 (uuid,body,notindexed=\"uuid\",content=\"" + Message.TABLENAME + "\",tokenize='unicode61')"; + private static final String CREATE_MESSAGE_INSERT_TRIGGER = "CREATE TRIGGER after_message_insert AFTER INSERT ON " + Message.TABLENAME + " BEGIN INSERT INTO messages_index(rowid,uuid,body) VALUES(NEW.rowid,NEW.uuid,NEW.body); END;"; + private static final String CREATE_MESSAGE_UPDATE_TRIGGER = "CREATE TRIGGER after_message_update UPDATE OF uuid,body ON " + Message.TABLENAME + " BEGIN UPDATE messages_index SET body=NEW.body,uuid=NEW.uuid WHERE rowid=OLD.rowid; END;"; + private static final String CREATE_MESSAGE_DELETE_TRIGGER = "CREATE TRIGGER after_message_delete AFTER DELETE ON " + Message.TABLENAME + " BEGIN DELETE FROM messages_index WHERE rowid=OLD.rowid; END;"; + private static final String COPY_PREEXISTING_ENTRIES = "INSERT INTO messages_index(messages_index) VALUES('rebuild');"; private DatabaseBackend(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); @@ -186,6 +191,17 @@ public class DatabaseBackend extends SQLiteOpenHelper { return values; } + public static boolean requiresMessageIndexRebuild() { + return requiresMessageIndexRebuild; + } + + public void rebuildMessagesIndex() { + final SQLiteDatabase db = getWritableDatabase(); + final Stopwatch stopwatch = Stopwatch.createStarted(); + db.execSQL(COPY_PREEXISTING_ENTRIES); + Log.d(Config.LOGTAG,"rebuilt message index in "+ stopwatch.stop().toString()); + } + public static synchronized DatabaseBackend getInstance(Context context) { if (instance == null) { instance = new DatabaseBackend(context); @@ -262,6 +278,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL(CREATE_MESSAGE_INDEX_TABLE); db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER); db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER); + db.execSQL(CREATE_MESSAGE_DELETE_TRIGGER); } @Override @@ -518,16 +535,6 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL(CREATE_RESOLVER_RESULTS_TABLE); } - if (oldVersion < 41 && newVersion >= 41) { - db.execSQL(CREATE_MESSAGE_INDEX_TABLE); - db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER); - db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER); - db.execSQL(COPY_PREEXISTING_ENTRIES); - } - - if (oldVersion < 42 && newVersion >= 42) { - db.execSQL("DROP TRIGGER IF EXISTS after_message_delete"); - } if (QuickConversationsService.isQuicksy() && oldVersion < 43 && newVersion >= 43) { List accounts = getAccounts(db); for (Account account : accounts) { @@ -551,10 +558,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { if (oldVersion < 46 && newVersion >= 46) { final long start = SystemClock.elapsedRealtime(); db.rawQuery("PRAGMA secure_delete = FALSE", null).close(); - db.execSQL("update "+Message.TABLENAME+" set "+Message.EDITED+"=NULL"); + db.execSQL("update " + Message.TABLENAME + " set " + Message.EDITED + "=NULL"); db.rawQuery("PRAGMA secure_delete=ON", null).close(); final long diff = SystemClock.elapsedRealtime() - start; - Log.d(Config.LOGTAG,"deleted old edit information in "+diff+"ms"); + Log.d(Config.LOGTAG, "deleted old edit information in " + diff + "ms"); } if (oldVersion < 47 && newVersion >= 47) { db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.PRESENCE_NAME + " TEXT"); @@ -562,6 +569,26 @@ public class DatabaseBackend extends SQLiteOpenHelper { if (oldVersion < 48 && newVersion >= 48) { db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.RTP_CAPABILITY + " TEXT"); } + if (oldVersion < 49 && newVersion >= 49) { + db.beginTransaction(); + db.execSQL("DROP TRIGGER IF EXISTS after_message_insert;"); + db.execSQL("DROP TRIGGER IF EXISTS after_message_update;"); + db.execSQL("DROP TRIGGER IF EXISTS after_message_delete;"); + db.execSQL("DROP TABLE IF EXISTS messages_index;"); + // a hack that should not be necessary, but + // there was at least one occurence when SQLite failed at this + db.execSQL("DROP TABLE IF EXISTS messages_index_docsize;"); + db.execSQL("DROP TABLE IF EXISTS messages_index_segdir;"); + db.execSQL("DROP TABLE IF EXISTS messages_index_segments;"); + db.execSQL("DROP TABLE IF EXISTS messages_index_stat;"); + db.execSQL(CREATE_MESSAGE_INDEX_TABLE); + db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER); + db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER); + db.execSQL(CREATE_MESSAGE_DELETE_TRIGGER); + db.setTransactionSuccessful(); + db.endTransaction(); + requiresMessageIndexRebuild = true; + } } private void canonicalizeJids(SQLiteDatabase db) { @@ -776,7 +803,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { try { list.add(0, Message.fromCursor(cursor, conversation)); } catch (Exception e) { - Log.e(Config.LOGTAG,"unable to restore message"); + Log.e(Config.LOGTAG, "unable to restore message"); } } cursor.close(); @@ -787,12 +814,12 @@ public class DatabaseBackend extends SQLiteOpenHelper { final SQLiteDatabase db = this.getReadableDatabase(); final StringBuilder SQL = new StringBuilder(); final String[] selectionArgs; - SQL.append("SELECT " + Message.TABLENAME + ".*," + Conversation.TABLENAME + '.' + Conversation.CONTACTJID + ',' + Conversation.TABLENAME + '.' + Conversation.ACCOUNT + ',' + Conversation.TABLENAME + '.' + Conversation.MODE + " FROM " + Message.TABLENAME + " join " + Conversation.TABLENAME + " on " + Message.TABLENAME + '.' + Message.CONVERSATION + '=' + Conversation.TABLENAME + '.' + Conversation.UUID + " join messages_index ON messages_index.uuid=messages.uuid where " + Message.ENCRYPTION + " NOT IN(" + Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE + ',' + Message.ENCRYPTION_PGP + ',' + Message.ENCRYPTION_DECRYPTION_FAILED + ',' + Message.ENCRYPTION_AXOLOTL_FAILED + ") AND " + Message.TYPE + " IN(" + Message.TYPE_TEXT + ',' + Message.TYPE_PRIVATE + ") AND messages_index.body MATCH ?"); + SQL.append("SELECT " + Message.TABLENAME + ".*," + Conversation.TABLENAME + "." + Conversation.CONTACTJID + "," + Conversation.TABLENAME + "." + Conversation.ACCOUNT + "," + Conversation.TABLENAME + "." + Conversation.MODE + " FROM " + Message.TABLENAME + " JOIN " + Conversation.TABLENAME + " ON " + Message.TABLENAME + "." + Message.CONVERSATION + "=" + Conversation.TABLENAME + "." + Conversation.UUID + " JOIN messages_index ON messages_index.rowid=messages.rowid WHERE " + Message.ENCRYPTION + " NOT IN(" + Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE + "," + Message.ENCRYPTION_PGP + "," + Message.ENCRYPTION_DECRYPTION_FAILED + "," + Message.ENCRYPTION_AXOLOTL_FAILED + ") AND " + Message.TYPE + " IN(" + Message.TYPE_TEXT + "," + Message.TYPE_PRIVATE + ") AND messages_index.body MATCH ?"); if (uuid == null) { selectionArgs = new String[]{FtsUtils.toMatchString(term)}; } else { selectionArgs = new String[]{FtsUtils.toMatchString(term), uuid}; - SQL.append(" AND "+Conversation.TABLENAME+'.'+Conversation.UUID+"=?"); + SQL.append(" AND " + Conversation.TABLENAME + '.' + Conversation.UUID + "=?"); } SQL.append(" ORDER BY " + Message.TIME_SENT + " DESC limit " + Config.MAX_SEARCH_RESULTS); Log.d(Config.LOGTAG, "search term: " + FtsUtils.toMatchString(term)); @@ -856,7 +883,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { public List getFilePathInfo() { final SQLiteDatabase db = this.getReadableDatabase(); - final Cursor cursor = db.query(Message.TABLENAME, new String[]{Message.UUID, Message.RELATIVE_FILE_PATH, Message.DELETED}, "type in (1,2,5) and "+Message.RELATIVE_FILE_PATH+" is not null", null, null, null, null); + final Cursor cursor = db.query(Message.TABLENAME, new String[]{Message.UUID, Message.RELATIVE_FILE_PATH, Message.DELETED}, "type in (1,2,5) and " + Message.RELATIVE_FILE_PATH + " is not null", null, null, null, null); final List list = new ArrayList<>(); while (cursor != null && cursor.moveToNext()) { list.add(new FilePathInfo(cursor.getString(0), cursor.getString(1), cursor.getInt(2) > 0)); @@ -869,7 +896,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { public List getRelativeFilePaths(String account, Jid jid, int limit) { SQLiteDatabase db = this.getReadableDatabase(); - final String SQL = "select uuid,relativeFilePath from messages where type in (1,2,5) and deleted=0 and "+Message.RELATIVE_FILE_PATH+" is not null and conversationUuid=(select uuid from conversations where accountUuid=? and (contactJid=? or contactJid like ?)) order by timeSent desc"; + final String SQL = "select uuid,relativeFilePath from messages where type in (1,2,5) and deleted=0 and " + Message.RELATIVE_FILE_PATH + " is not null and conversationUuid=(select uuid from conversations where accountUuid=? and (contactJid=? or contactJid like ?)) order by timeSent desc"; final String[] args = {account, jid.toString(), jid.toString() + "/%"}; Cursor cursor = db.rawQuery(SQL + (limit > 0 ? " limit " + limit : ""), args); List filesPaths = new ArrayList<>(); @@ -894,7 +921,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { public boolean deleted; private FilePathInfo(String uuid, String path, boolean deleted) { - super(uuid,path); + super(uuid, path); this.deleted = deleted; } @@ -1038,8 +1065,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { long start = SystemClock.elapsedRealtime(); final SQLiteDatabase db = this.getWritableDatabase(); db.beginTransaction(); - String[] args = {conversation.getUuid()}; - db.delete("messages_index", "uuid in (select uuid from messages where conversationUuid=?)", args); + final String[] args = {conversation.getUuid()}; int num = db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args); db.setTransactionSuccessful(); db.endTransaction(); @@ -1050,7 +1076,6 @@ public class DatabaseBackend extends SQLiteOpenHelper { final String[] args = {String.valueOf(timestamp)}; SQLiteDatabase db = this.getReadableDatabase(); db.beginTransaction(); - db.delete("messages_index", "uuid in (select uuid from messages where timeSent 0) { - try { - os.write(buffer, 0, length); - } catch (IOException e) { - throw new FileWriterException(); - } + } catch (IOException e) { + throw new FileCopyException(R.string.error_unable_to_create_temporary_file); + } + try (final OutputStream os = new FileOutputStream(file); + final InputStream is = mXmppConnectionService.getContentResolver().openInputStream(uri)) { + if (is == null) { + throw new FileCopyException(R.string.error_file_not_found); + } + try { + ByteStreams.copy(is, os); + } catch (IOException e) { + throw new FileWriterException(); } try { os.flush(); @@ -648,16 +650,17 @@ public class FileBackend { throw new FileWriterException(); } } catch (final FileNotFoundException e) { + cleanup(file); throw new FileCopyException(R.string.error_file_not_found); } catch (final FileWriterException e) { + cleanup(file); throw new FileCopyException(R.string.error_unable_to_create_temporary_file); } catch (final SecurityException e) { + cleanup(file); throw new FileCopyException(R.string.error_security_exception); } catch (final IOException e) { + cleanup(file); throw new FileCopyException(R.string.error_io_exception); - } finally { - close(os); - close(is); } } @@ -708,7 +711,7 @@ public class FileBackend { private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException, ImageCompressionException { final File parent = file.getParentFile(); - if (parent.mkdirs()) { + if (parent != null && parent.mkdirs()) { Log.d(Config.LOGTAG, "created parent directory"); } InputStream is = null; @@ -743,23 +746,28 @@ public class FileBackend { final int imageMaxSize = mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize); while (!targetSizeReached) { os = new FileOutputStream(file); + Log.d(Config.LOGTAG, "compressing image with quality " + quality); boolean success = scaledBitmap.compress(Config.IMAGE_FORMAT, quality, os); if (!success) { throw new FileCopyException(R.string.error_compressing_image); } os.flush(); - targetSizeReached = file.length() <= imageMaxSize || quality <= 50; + final long fileSize = file.length(); + Log.d(Config.LOGTAG, "achieved file size of " + fileSize); + targetSizeReached = fileSize <= imageMaxSize || quality <= 50; quality -= 5; } scaledBitmap.recycle(); } catch (final FileNotFoundException e) { + cleanup(file); throw new FileCopyException(R.string.error_file_not_found); - } catch (IOException e) { - e.printStackTrace(); + } catch (final IOException e) { + cleanup(file); throw new FileCopyException(R.string.error_io_exception); } catch (SecurityException e) { + cleanup(file); throw new FileCopyException(R.string.error_security_exception_during_image_copy); - } catch (OutOfMemoryError e) { + } catch (final OutOfMemoryError e) { ++sampleSize; if (sampleSize <= 3) { copyImageToPrivateStorage(file, image, sampleSize); @@ -772,6 +780,14 @@ public class FileBackend { } } + private static void cleanup(final File file) { + try { + file.delete(); + } catch (Exception e) { + + } + } + public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException, ImageCompressionException { Log.d(Config.LOGTAG, "copy image (" + image.toString() + ") to private storage " + file.getAbsolutePath()); copyImageToPrivateStorage(file, image, 0); @@ -808,19 +824,34 @@ public class FileBackend { } } - private int getRotation(File file) { - return getRotation(Uri.parse("file://" + file.getAbsolutePath())); + private int getRotation(final File file) { + try (final InputStream inputStream = new FileInputStream(file)) { + return getRotation(inputStream); + } catch (Exception e) { + return 0; + } } - private int getRotation(Uri image) { - InputStream is = null; - try { - is = mXmppConnectionService.getContentResolver().openInputStream(image); - return ExifHelper.getOrientation(is); - } catch (FileNotFoundException e) { + private int getRotation(final Uri image) { + try (final InputStream is = mXmppConnectionService.getContentResolver().openInputStream(image)) { + return is == null ? 0 : getRotation(is); + } catch (final Exception e) { return 0; - } finally { - close(is); + } + } + + private static int getRotation(final InputStream inputStream) throws IOException { + final ExifInterface exif = new ExifInterface(inputStream); + final int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); + switch (orientation) { + case ExifInterface.ORIENTATION_ROTATE_180: + return 180; + case ExifInterface.ORIENTATION_ROTATE_90: + return 90; + case ExifInterface.ORIENTATION_ROTATE_270: + return 270; + default: + return 0; } } @@ -1468,7 +1499,8 @@ public class FileBackend { this.resId = resId; } - public @StringRes int getResId() { + public @StringRes + int getResId() { return resId; } } diff --git a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java b/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java index 5d39911edf6e4ae52557ae6cba2c0286fbd5708c..db879799d8835ffd5497c1b166dc64f96bbbee0d 100644 --- a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java +++ b/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java @@ -3,16 +3,19 @@ package eu.siacs.conversations.services; import android.content.Context; import android.content.SharedPreferences; import android.net.Uri; -import android.os.ParcelFileDescriptor; import android.preference.PreferenceManager; import android.util.Log; -import net.ypresto.androidtranscoder.MediaTranscoder; -import net.ypresto.androidtranscoder.format.MediaFormatStrategy; +import androidx.annotation.NonNull; + +import com.otaliastudios.transcoder.Transcoder; +import com.otaliastudios.transcoder.TranscoderListener; + +import org.jetbrains.annotations.NotNull; import java.io.File; -import java.io.FileDescriptor; import java.io.FileNotFoundException; +import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -23,161 +26,164 @@ import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.ui.UiCallback; -import eu.siacs.conversations.utils.Android360pFormatStrategy; -import eu.siacs.conversations.utils.Android720pFormatStrategy; import eu.siacs.conversations.utils.MimeUtils; - -public class AttachFileToConversationRunnable implements Runnable, MediaTranscoder.Listener { - - private final XmppConnectionService mXmppConnectionService; - private final Message message; - private final Uri uri; - private final String type; - private final UiCallback callback; - private final boolean isVideoMessage; - private final long originalFileSize; - private int currentProgress = -1; - - AttachFileToConversationRunnable(XmppConnectionService xmppConnectionService, Uri uri, String type, Message message, UiCallback callback) { - this.uri = uri; - this.type = type; - this.mXmppConnectionService = xmppConnectionService; - this.message = message; - this.callback = callback; - final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type); - final int autoAcceptFileSize = mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize); - this.originalFileSize = FileBackend.getFileSize(mXmppConnectionService,uri); - this.isVideoMessage = (mimeType != null && mimeType.startsWith("video/")) && originalFileSize > autoAcceptFileSize && !"uncompressed".equals(getVideoCompression()); - } - - boolean isVideoMessage() { - return this.isVideoMessage; - } - - private void processAsFile() { - final String path = mXmppConnectionService.getFileBackend().getOriginalPath(uri); - if (path != null && !FileBackend.isPathBlacklisted(path)) { - message.setRelativeFilePath(path); - mXmppConnectionService.getFileBackend().updateFileParams(message); - if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - mXmppConnectionService.getPgpEngine().encrypt(message, callback); - } else { - mXmppConnectionService.sendMessage(message); - callback.success(message); - } - } else { - try { - mXmppConnectionService.getFileBackend().copyFileToPrivateStorage(message, uri, type); - mXmppConnectionService.getFileBackend().updateFileParams(message); - if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - final PgpEngine pgpEngine = mXmppConnectionService.getPgpEngine(); - if (pgpEngine != null) { - pgpEngine.encrypt(message, callback); - } else if (callback != null) { - callback.error(R.string.unable_to_connect_to_keychain, null); - } - } else { - mXmppConnectionService.sendMessage(message); - callback.success(message); - } - } catch (FileBackend.FileCopyException e) { - callback.error(e.getResId(), message); - } - } - } - - private void processAsVideo() throws FileNotFoundException { - Log.d(Config.LOGTAG,"processing file as video"); - mXmppConnectionService.startForcingForegroundNotification(); - message.setRelativeFilePath(message.getUuid() + ".mp4"); - final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message); - final MediaFormatStrategy formatStrategy = "720".equals(getVideoCompression()) ? new Android720pFormatStrategy() : new Android360pFormatStrategy(); - file.getParentFile().mkdirs(); - final ParcelFileDescriptor parcelFileDescriptor = mXmppConnectionService.getContentResolver().openFileDescriptor(uri, "r"); - if (parcelFileDescriptor == null) { - throw new FileNotFoundException("Parcel File Descriptor was null"); - } - FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); - Future future = MediaTranscoder.getInstance().transcodeVideo(fileDescriptor, file.getAbsolutePath(), formatStrategy, this); - try { - future.get(); - } catch (InterruptedException e) { - throw new AssertionError(e); - } catch (ExecutionException e) { - if (e.getCause() instanceof Error) { - mXmppConnectionService.stopForcingForegroundNotification(); - processAsFile(); - } else { - Log.d(Config.LOGTAG, "ignoring execution exception. Should get handled by onTranscodeFiled() instead", e); - } - } - } - - @Override - public void onTranscodeProgress(double progress) { - final int p = (int) Math.round(progress * 100); - if (p > currentProgress) { - currentProgress = p; - mXmppConnectionService.getNotificationService().updateFileAddingNotification(p,message); - } - } - - @Override - public void onTranscodeCompleted() { - mXmppConnectionService.stopForcingForegroundNotification(); - final File file = mXmppConnectionService.getFileBackend().getFile(message); - long convertedFileSize = mXmppConnectionService.getFileBackend().getFile(message).getSize(); - Log.d(Config.LOGTAG,"originalFileSize="+originalFileSize+" convertedFileSize="+convertedFileSize); - if (originalFileSize != 0 && convertedFileSize >= originalFileSize) { - if (file.delete()) { - Log.d(Config.LOGTAG,"original file size was smaller. deleting and processing as file"); - processAsFile(); - return; - } else { - Log.d(Config.LOGTAG,"unable to delete converted file"); - } - } - mXmppConnectionService.getFileBackend().updateFileParams(message); - if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - mXmppConnectionService.getPgpEngine().encrypt(message, callback); - } else { - mXmppConnectionService.sendMessage(message); - callback.success(message); - } - } - - @Override - public void onTranscodeCanceled() { - mXmppConnectionService.stopForcingForegroundNotification(); - processAsFile(); - } - - @Override - public void onTranscodeFailed(Exception e) { - mXmppConnectionService.stopForcingForegroundNotification(); - Log.d(Config.LOGTAG,"video transcoding failed",e); - processAsFile(); - } - - @Override - public void run() { - if (this.isVideoMessage()) { - try { - processAsVideo(); - } catch (FileNotFoundException e) { - processAsFile(); - } - } else { - processAsFile(); - } - } - - private String getVideoCompression() { - return getVideoCompression(mXmppConnectionService); - } - - public static String getVideoCompression(final Context context) { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - return preferences.getString("video_compression", context.getResources().getString(R.string.video_compression)); - } +import eu.siacs.conversations.utils.TranscoderStrategies; + +public class AttachFileToConversationRunnable implements Runnable, TranscoderListener { + + private final XmppConnectionService mXmppConnectionService; + private final Message message; + private final Uri uri; + private final String type; + private final UiCallback callback; + private final boolean isVideoMessage; + private final long originalFileSize; + private int currentProgress = -1; + + AttachFileToConversationRunnable(XmppConnectionService xmppConnectionService, Uri uri, String type, Message message, UiCallback callback) { + this.uri = uri; + this.type = type; + this.mXmppConnectionService = xmppConnectionService; + this.message = message; + this.callback = callback; + final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type); + final int autoAcceptFileSize = mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize); + this.originalFileSize = FileBackend.getFileSize(mXmppConnectionService, uri); + this.isVideoMessage = (mimeType != null && mimeType.startsWith("video/")) && originalFileSize > autoAcceptFileSize && !"uncompressed".equals(getVideoCompression()); + } + + boolean isVideoMessage() { + return this.isVideoMessage; + } + + private void processAsFile() { + final String path = mXmppConnectionService.getFileBackend().getOriginalPath(uri); + if (path != null && !FileBackend.isPathBlacklisted(path)) { + message.setRelativeFilePath(path); + mXmppConnectionService.getFileBackend().updateFileParams(message); + if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + mXmppConnectionService.getPgpEngine().encrypt(message, callback); + } else { + mXmppConnectionService.sendMessage(message); + callback.success(message); + } + } else { + try { + mXmppConnectionService.getFileBackend().copyFileToPrivateStorage(message, uri, type); + mXmppConnectionService.getFileBackend().updateFileParams(message); + if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + final PgpEngine pgpEngine = mXmppConnectionService.getPgpEngine(); + if (pgpEngine != null) { + pgpEngine.encrypt(message, callback); + } else if (callback != null) { + callback.error(R.string.unable_to_connect_to_keychain, null); + } + } else { + mXmppConnectionService.sendMessage(message); + callback.success(message); + } + } catch (FileBackend.FileCopyException e) { + callback.error(e.getResId(), message); + } + } + } + + private void processAsVideo() throws FileNotFoundException { + Log.d(Config.LOGTAG, "processing file as video"); + mXmppConnectionService.startForcingForegroundNotification(); + message.setRelativeFilePath(message.getUuid() + ".mp4"); + final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message); + if (Objects.requireNonNull(file.getParentFile()).mkdirs()) { + Log.d(Config.LOGTAG, "created parent directory for video file"); + } + + final boolean highQuality = "720".equals(getVideoCompression()); + + final Future future = Transcoder.into(file.getAbsolutePath()). + addDataSource(mXmppConnectionService, uri) + .setVideoTrackStrategy(highQuality ? TranscoderStrategies.VIDEO_720P : TranscoderStrategies.VIDEO_360P) + .setAudioTrackStrategy(highQuality ? TranscoderStrategies.AUDIO_HQ : TranscoderStrategies.AUDIO_MQ) + .setListener(this) + .transcode(); + try { + future.get(); + } catch (InterruptedException e) { + throw new AssertionError(e); + } catch (ExecutionException e) { + if (e.getCause() instanceof Error) { + mXmppConnectionService.stopForcingForegroundNotification(); + processAsFile(); + } else { + Log.d(Config.LOGTAG, "ignoring execution exception. Should get handled by onTranscodeFiled() instead", e); + } + } + } + + @Override + public void onTranscodeProgress(double progress) { + final int p = (int) Math.round(progress * 100); + if (p > currentProgress) { + currentProgress = p; + mXmppConnectionService.getNotificationService().updateFileAddingNotification(p, message); + } + } + + @Override + public void onTranscodeCompleted(int successCode) { + mXmppConnectionService.stopForcingForegroundNotification(); + final File file = mXmppConnectionService.getFileBackend().getFile(message); + long convertedFileSize = mXmppConnectionService.getFileBackend().getFile(message).getSize(); + Log.d(Config.LOGTAG, "originalFileSize=" + originalFileSize + " convertedFileSize=" + convertedFileSize); + if (originalFileSize != 0 && convertedFileSize >= originalFileSize) { + if (file.delete()) { + Log.d(Config.LOGTAG, "original file size was smaller. deleting and processing as file"); + processAsFile(); + return; + } else { + Log.d(Config.LOGTAG, "unable to delete converted file"); + } + } + mXmppConnectionService.getFileBackend().updateFileParams(message); + if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + mXmppConnectionService.getPgpEngine().encrypt(message, callback); + } else { + mXmppConnectionService.sendMessage(message); + callback.success(message); + } + } + + @Override + public void onTranscodeCanceled() { + mXmppConnectionService.stopForcingForegroundNotification(); + processAsFile(); + } + + @Override + public void onTranscodeFailed(@NonNull @NotNull Throwable exception) { + mXmppConnectionService.stopForcingForegroundNotification(); + Log.d(Config.LOGTAG, "video transcoding failed", exception); + processAsFile(); + } + + @Override + public void run() { + if (this.isVideoMessage()) { + try { + processAsVideo(); + } catch (FileNotFoundException e) { + processAsFile(); + } + } else { + processAsFile(); + } + } + + private String getVideoCompression() { + return getVideoCompression(mXmppConnectionService); + } + + public static String getVideoCompression(final Context context) { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + return preferences.getString("video_compression", context.getResources().getString(R.string.video_compression)); + } } diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index bbde8e904a954b9dc82ce7ecbd52e85a5bb3318e..ca4499300ac6df8d7772211b74d8ef6649ea297d 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -35,6 +35,7 @@ import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.IconCompat; import com.google.common.base.Strings; +import com.google.common.collect.Iterables; import java.io.File; import java.io.IOException; @@ -407,7 +408,7 @@ public class NotificationService { currentInterruptionFilter = 1; //INTERRUPTION_FILTER_ALL } if (currentInterruptionFilter != 1) { - Log.d(Config.LOGTAG,"do not ring or vibrate because interruption filter has been set to "+currentInterruptionFilter); + Log.d(Config.LOGTAG, "do not ring or vibrate because interruption filter has been set to " + currentInterruptionFilter); return; } final ScheduledFuture currentVibrationFuture = this.vibrationFuture; @@ -424,13 +425,13 @@ public class NotificationService { final Resources resources = mXmppConnectionService.getResources(); final String ringtonePreference = preferences.getString("call_ringtone", resources.getString(R.string.incoming_call_ringtone)); if (Strings.isNullOrEmpty(ringtonePreference)) { - Log.d(Config.LOGTAG,"ringtone has been set to none"); + Log.d(Config.LOGTAG, "ringtone has been set to none"); return; } final Uri uri = Uri.parse(ringtonePreference); this.currentlyPlayingRingtone = RingtoneManager.getRingtone(mXmppConnectionService, uri); if (this.currentlyPlayingRingtone == null) { - Log.d(Config.LOGTAG,"unable to find ringtone for uri "+uri); + Log.d(Config.LOGTAG, "unable to find ringtone for uri " + uri); return; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { @@ -487,14 +488,23 @@ public class NotificationService { notify(INCOMING_CALL_NOTIFICATION_ID, notification); } - public Notification getOngoingCallNotification(final AbstractJingleConnection.Id id, final Set media) { + public Notification getOngoingCallNotification(final XmppConnectionService.OngoingCall ongoingCall) { + final AbstractJingleConnection.Id id = ongoingCall.id; final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, "ongoing_calls"); - if (media.contains(Media.VIDEO)) { + if (ongoingCall.media.contains(Media.VIDEO)) { builder.setSmallIcon(R.drawable.ic_videocam_white_24dp); - builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call)); + if (ongoingCall.reconnecting) { + builder.setContentTitle(mXmppConnectionService.getString(R.string.reconnecting_video_call)); + } else { + builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_video_call)); + } } else { builder.setSmallIcon(R.drawable.ic_call_white_24dp); - builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call)); + if (ongoingCall.reconnecting) { + builder.setContentTitle(mXmppConnectionService.getString(R.string.reconnecting_call)); + } else { + builder.setContentTitle(mXmppConnectionService.getString(R.string.ongoing_call)); + } } builder.setContentText(id.account.getRoster().getContact(id.with).getDisplayName()); builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); @@ -790,17 +800,18 @@ public class NotificationService { .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) .setShowsUserInterface(false) .build(); - String replyLabel = mXmppConnectionService.getString(R.string.reply); - NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder( + final String replyLabel = mXmppConnectionService.getString(R.string.reply); + final String lastMessageUuid = Iterables.getLast(messages).getUuid(); + final NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder( R.drawable.ic_send_text_offline, replyLabel, - createReplyIntent(conversation, false)) + createReplyIntent(conversation, lastMessageUuid, false)) .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) .setShowsUserInterface(false) .addRemoteInput(remoteInput).build(); - NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply, + final NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply, replyLabel, - createReplyIntent(conversation, true)).addRemoteInput(remoteInput).build(); + createReplyIntent(conversation, lastMessageUuid, true)).addRemoteInput(remoteInput).build(); mBuilder.extend(new NotificationCompat.WearableExtender().addAction(wearReplyAction)); int addedActionsCount = 1; mBuilder.addAction(markReadAction); @@ -1066,13 +1077,14 @@ public class NotificationService { return PendingIntent.getService(mXmppConnectionService, 0, intent, 0); } - private PendingIntent createReplyIntent(Conversation conversation, boolean dismissAfterReply) { + private PendingIntent createReplyIntent(final Conversation conversation, final String lastMessageUuid, final boolean dismissAfterReply) { final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION); intent.putExtra("uuid", conversation.getUuid()); intent.putExtra("dismiss_notification", dismissAfterReply); + intent.putExtra("last_message_uuid", lastMessageUuid); final int id = generateRequestCode(conversation, dismissAfterReply ? 12 : 14); - return PendingIntent.getService(mXmppConnectionService, id, intent, 0); + return PendingIntent.getService(mXmppConnectionService, id, intent, PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createReadPendingIntent(Conversation conversation) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 759a0d727d1d9b8617e3693e2eb652b1ceac8464..42b699e46e806cf95b0048f0fd320930a94cb967 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -75,6 +75,8 @@ import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -184,8 +186,9 @@ public class XmppConnectionService extends Service { private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp"; public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1); - private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor("FileAdding"); - private final SerialSingleThreadExecutor mVideoCompressionExecutor = new SerialSingleThreadExecutor("VideoCompression"); + private final static Executor FILE_OBSERVER_EXECUTOR = Executors.newSingleThreadExecutor(); + private final static Executor FILE_ATTACHMENT_EXECUTOR = Executors.newSingleThreadExecutor(); + private final static SerialSingleThreadExecutor VIDEO_COMPRESSION_EXECUTOR = new SerialSingleThreadExecutor("VideoCompression"); private final SerialSingleThreadExecutor mDatabaseWriterExecutor = new SerialSingleThreadExecutor("DatabaseWriter"); private final SerialSingleThreadExecutor mDatabaseReaderExecutor = new SerialSingleThreadExecutor("DatabaseReader"); private final SerialSingleThreadExecutor mNotificationExecutor = new SerialSingleThreadExecutor("NotificationExecutor"); @@ -471,7 +474,6 @@ public class XmppConnectionService extends Service { private OpenPgpServiceConnection pgpServiceConnection; private PgpEngine mPgpEngine = null; private WakeLock wakeLock; - private PowerManager pm; private LruCache mBitmapCache; private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver(); private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver(); @@ -564,14 +566,14 @@ public class XmppConnectionService extends Service { Log.d(Config.LOGTAG, "counterpart=" + message.getCounterpart()); final AttachFileToConversationRunnable runnable = new AttachFileToConversationRunnable(this, uri, type, message, callback); if (runnable.isVideoMessage()) { - mVideoCompressionExecutor.execute(runnable); + VIDEO_COMPRESSION_EXECUTOR.execute(runnable); } else { - mFileAddingExecutor.execute(runnable); + FILE_ATTACHMENT_EXECUTOR.execute(runnable); } } - public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback callback) { - final String mimeType = MimeUtils.guessMimeTypeFromUri(this, uri); + public void attachImageToConversation(final Conversation conversation, final Uri uri, final String type, final UiCallback callback) { + final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(this, uri, type); final String compressPictures = getCompressPicturesPreference(); if ("never".equals(compressPictures) @@ -593,7 +595,7 @@ public class XmppConnectionService extends Service { message.setType(Message.TYPE_IMAGE); } Log.d(Config.LOGTAG, "attachImage: type=" + message.getType()); - mFileAddingExecutor.execute(() -> { + FILE_ATTACHMENT_EXECUTOR.execute(() -> { try { getFileBackend().copyImageToPrivateStorage(message, uri); } catch (FileBackend.ImageCompressionException e) { @@ -724,6 +726,7 @@ public class XmppConnectionService extends Service { } 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; } @@ -732,7 +735,7 @@ public class XmppConnectionService extends Service { restoredFromDatabaseLatch.await(); final Conversation c = findConversationByUuid(uuid); if (c != null) { - directReply(c, body.toString(), dismissNotification); + directReply(c, body.toString(), lastMessageUuid, dismissNotification); } } catch (InterruptedException e) { Log.d(Config.LOGTAG, "unable to process direct reply"); @@ -930,8 +933,12 @@ public class XmppConnectionService extends Service { } } - private void directReply(Conversation conversation, String body, final boolean dismissAfterReply) { - Message message = new Message(conversation, body, conversation.getNextEncryption()); + private void directReply(final Conversation conversation, final String body, final String lastMessageUuid, final boolean dismissAfterReply) { + final Message inReplyTo = lastMessageUuid == null ? null : conversation.findMessageWithUuid(lastMessageUuid); + final Message message = new Message(conversation, body, conversation.getNextEncryption()); + if (inReplyTo != null && inReplyTo.isPrivateMessage()) { + Message.configurePrivateMessage(message, inReplyTo.getCounterpart()); + } message.markUnread(); if (message.getEncryption() == Message.ENCRYPTION_PGP) { getPgpEngine().encrypt(message, new UiCallback() { @@ -1147,11 +1154,11 @@ public class XmppConnectionService extends Service { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) { startContactObserver(); } - mFileAddingExecutor.execute(fileBackend::deleteHistoricAvatarPath); + FILE_OBSERVER_EXECUTOR.execute(fileBackend::deleteHistoricAvatarPath); if (Compatibility.hasStoragePermission(this)) { Log.d(Config.LOGTAG, "starting file observer"); - mFileAddingExecutor.execute(this.fileObserver::startWatching); - mFileAddingExecutor.execute(this::checkForDeletedFiles); + FILE_OBSERVER_EXECUTOR.execute(this.fileObserver::startWatching); + FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles); } if (Config.supportOpenPgp()) { this.pgpServiceConnection = new OpenPgpServiceConnection(this, "org.sufficientlysecure.keychain", new OpenPgpServiceConnection.OnBound() { @@ -1172,7 +1179,7 @@ public class XmppConnectionService extends Service { this.pgpServiceConnection.bindToService(); } - this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + final PowerManager pm = ContextCompat.getSystemService(this, PowerManager.class); this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Conversations:Service"); toggleForegroundService(); @@ -1267,8 +1274,8 @@ public class XmppConnectionService extends Service { public void restartFileObserver() { Log.d(Config.LOGTAG, "restarting file observer"); - mFileAddingExecutor.execute(this.fileObserver::restartWatching); - mFileAddingExecutor.execute(this::checkForDeletedFiles); + FILE_OBSERVER_EXECUTOR.execute(this.fileObserver::restartWatching); + FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles); } public void toggleScreenEventReceiver() { @@ -1291,8 +1298,8 @@ public class XmppConnectionService extends Service { toggleForegroundService(false); } - public void setOngoingCall(AbstractJingleConnection.Id id, Set media) { - ongoingCall.set(new OngoingCall(id, media)); + public void setOngoingCall(AbstractJingleConnection.Id id, Set media, final boolean reconnecting) { + ongoingCall.set(new OngoingCall(id, media, reconnecting)); toggleForegroundService(false); } @@ -1308,7 +1315,7 @@ public class XmppConnectionService extends Service { final Notification notification; final int id; if (ongoing != null) { - notification = this.mNotificationService.getOngoingCallNotification(ongoing.id, ongoing.media); + notification = this.mNotificationService.getOngoingCallNotification(ongoing); id = NotificationService.ONGOING_CALL_NOTIFICATION_ID; startForeground(id, notification); mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID); @@ -1885,7 +1892,10 @@ public class XmppConnectionService extends Service { long diffConversationsRestore = SystemClock.elapsedRealtime() - startTimeConversationsRestore; Log.d(Config.LOGTAG, "finished restoring conversations in " + diffConversationsRestore + "ms"); Runnable runnable = () -> { - long deletionDate = getAutomaticMessageDeletionDate(); + if (DatabaseBackend.requiresMessageIndexRebuild()) { + DatabaseBackend.getInstance(this).rebuildMessagesIndex(); + } + final long deletionDate = getAutomaticMessageDeletionDate(); mLastExpiryRun.set(SystemClock.elapsedRealtime()); if (deletionDate > 0) { Log.d(Config.LOGTAG, "deleting messages that are older than " + AbstractGenerator.getTimestamp(deletionDate)); @@ -1925,7 +1935,7 @@ public class XmppConnectionService extends Service { private void restoreMessages(Conversation conversation) { conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE)); conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING)); - conversation.findUnreadMessages(message -> mNotificationService.pushFromBacklog(message)); + conversation.findUnreadMessages(mNotificationService::pushFromBacklog); } public void loadPhoneContacts() { @@ -3337,35 +3347,26 @@ public class XmppConnectionService extends Service { public void changeAffiliationInConference(final Conversation conference, Jid user, final MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) { final Jid jid = user.asBareJid(); - IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString()); - sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - conference.getMucOptions().changeAffiliation(jid, affiliation); - getAvatarService().clear(conference); + final IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString()); + sendIqPacket(conference.getAccount(), request, (account, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + conference.getMucOptions().changeAffiliation(jid, affiliation); + getAvatarService().clear(conference); + if (callback != null) { callback.onAffiliationChangedSuccessful(jid); } else { - callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation); + Log.d(Config.LOGTAG, "changed affiliation of " + user + " to " + affiliation); } + } else if (callback != null) { + callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation); + } else { + Log.d(Config.LOGTAG, "unable to change affiliation"); } }); } - public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) { - List jids = new ArrayList<>(); - for (MucOptions.User user : conference.getMucOptions().getUsers()) { - if (user.getAffiliation() == before && user.getRealJid() != null) { - jids.add(user.getRealJid()); - } - } - IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString()); - sendIqPacket(conference.getAccount(), request, mDefaultIqHandler); - } - public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role) { IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString()); - Log.d(Config.LOGTAG, request.toString()); sendIqPacket(conference.getAccount(), request, (account, packet) -> { if (packet.getType() != IqPacket.TYPE.RESULT) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to change role of " + nick); @@ -3928,9 +3929,13 @@ public class XmppConnectionService extends Service { new Thread(() -> reconnectAccount(account, false, true)).start(); } - public void invite(Conversation conversation, Jid contact) { + public void invite(final Conversation conversation, final Jid contact) { Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": inviting " + contact + " to " + conversation.getJid().asBareJid()); - MessagePacket packet = mMessageGenerator.invite(conversation, contact); + final MucOptions.User user = conversation.getMucOptions().findUserByRealJid(contact.asBareJid()); + if (user == null || user.getAffiliation() == MucOptions.Affiliation.OUTCAST) { + changeAffiliationInConference(conversation, contact, MucOptions.Affiliation.NONE, null); + } + final MessagePacket packet = mMessageGenerator.invite(conversation, contact); sendMessagePacket(conversation.getAccount(), packet); } @@ -4864,12 +4869,14 @@ public class XmppConnectionService extends Service { } public static class OngoingCall { - private final AbstractJingleConnection.Id id; - private final Set media; + public final AbstractJingleConnection.Id id; + public final Set media; + public final boolean reconnecting; - public OngoingCall(AbstractJingleConnection.Id id, Set media) { + public OngoingCall(AbstractJingleConnection.Id id, Set media, final boolean reconnecting) { this.id = id; this.media = media; + this.reconnecting = reconnecting; } @Override @@ -4877,12 +4884,12 @@ public class XmppConnectionService extends Service { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; OngoingCall that = (OngoingCall) o; - return Objects.equal(id, that.id); + return reconnecting == that.reconnecting && Objects.equal(id, that.id) && Objects.equal(media, that.media); } @Override public int hashCode() { - return Objects.hashCode(id); + return Objects.hashCode(id, media, reconnecting); } } } diff --git a/src/main/java/eu/siacs/conversations/ui/AboutActivity.java b/src/main/java/eu/siacs/conversations/ui/AboutActivity.java index 3c705e639fa23e932fcc18fbb13721cad6b0e7ca..917512a029c26348c8283bbbfabe27d4c65f4cde 100644 --- a/src/main/java/eu/siacs/conversations/ui/AboutActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/AboutActivity.java @@ -5,12 +5,19 @@ import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; import eu.siacs.conversations.R; +import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.utils.ThemeHelper; import static eu.siacs.conversations.ui.XmppActivity.configureActionBar; public class AboutActivity extends AppCompatActivity { + @Override + protected void onResume(){ + super.onResume(); + SettingsUtils.applyScreenshotPreventionSetting(this); + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 3d29bb89a8f5f32b91d5d2f3b30512b60346bcee..d35d4808c29c34182fadda123e6afc3b5aaa7e73 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.ui; +import android.app.Activity; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; @@ -7,6 +8,7 @@ import android.os.Bundle; import android.text.Editable; import android.text.SpannableStringBuilder; import android.text.TextWatcher; +import android.text.method.LinkMovementMethod; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -86,6 +88,13 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers } }; + public static void open(final Activity activity, final Conversation conversation) { + Intent intent = new Intent(activity, ConferenceDetailsActivity.class); + intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); + intent.putExtra("uuid", conversation.getUuid()); + activity.startActivity(intent); + } + private final OnClickListener mNotifyStatusClickListener = new OnClickListener() { @Override public void onClick(View v) { @@ -481,6 +490,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers this.binding.mucSubject.setTextAppearance(this, subject.length() > (hasTitle ? 128 : 196) ? R.style.TextAppearance_Conversations_Body1_Linkified : R.style.TextAppearance_Conversations_Subhead); this.binding.mucSubject.setAutoLinkMask(0); this.binding.mucSubject.setVisibility(View.VISIBLE); + this.binding.mucSubject.setMovementMethod(LinkMovementMethod.getInstance()); } else { this.binding.mucSubject.setVisibility(View.GONE); } diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 74f9e7068d491764427967334428eb8f6deecaaf..7c1d08643cb8f52808a56d9464854d2b1b5dde1a 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -45,6 +45,7 @@ import eu.siacs.conversations.databinding.ActivityContactDetailsBinding; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.ListItem; +import eu.siacs.conversations.services.AbstractQuickConversationsService; import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; import eu.siacs.conversations.ui.adapter.MediaAdapter; @@ -58,6 +59,7 @@ import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.Emoticons; import eu.siacs.conversations.utils.IrregularUnicodeDetector; +import eu.siacs.conversations.utils.PhoneNumberUtilWrapper; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xml.Namespace; @@ -131,15 +133,31 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp } private void showAddToPhoneBookDialog() { + final Jid jid = contact.getJid(); + final boolean quicksyContact = AbstractQuickConversationsService.isQuicksy() + && Config.QUICKSY_DOMAIN.equals(jid.getDomain()) + && jid.getLocal() != null; + final String value; + if (quicksyContact) { + value = PhoneNumberUtilWrapper.toFormattedPhoneNumber(this, jid); + } else { + value = jid.toEscapedString(); + } final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(getString(R.string.action_add_phone_book)); - builder.setMessage(getString(R.string.add_phone_book_text, contact.getJid().toEscapedString())); + builder.setMessage(getString(R.string.add_phone_book_text, value)); builder.setNegativeButton(getString(R.string.cancel), null); builder.setPositiveButton(getString(R.string.add), (dialog, which) -> { final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); intent.setType(Contacts.CONTENT_ITEM_TYPE); - intent.putExtra(Intents.Insert.IM_HANDLE, contact.getJid().toEscapedString()); - intent.putExtra(Intents.Insert.IM_PROTOCOL, CommonDataKinds.Im.PROTOCOL_JABBER); + if (quicksyContact) { + intent.putExtra(Intents.Insert.PHONE, value); + } else { + intent.putExtra(Intents.Insert.IM_HANDLE, value); + intent.putExtra(Intents.Insert.IM_PROTOCOL, CommonDataKinds.Im.PROTOCOL_JABBER); + //TODO for modern use we want PROTOCOL_CUSTOM and an extra field with a value of 'XMPP' + // however we don’t have such a field and thus have to use the legacy PROTOCOL_JABBER + } intent.putExtra("finishActivityOnSaveCompleted", true); try { startActivityForResult(intent, 0); @@ -233,6 +251,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp @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) { if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index 8e23da5bc0e4f9ecebd7738830db2d9894922ccc..2d9fe91ae1c5301bc0f60159b1fddb123a0aa7a0 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -6,6 +6,8 @@ import android.os.Bundle; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import eu.siacs.conversations.ui.util.SettingsUtils; + public class ConversationActivity extends AppCompatActivity { @Override @@ -14,4 +16,10 @@ public class ConversationActivity extends AppCompatActivity { startActivity(new Intent(this, ConversationsActivity.class)); finish(); } + + @Override + protected void onResume(){ + super.onResume(); + SettingsUtils.applyScreenshotPreventionSetting(this); + } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 7bbd4c8e556317650dcc92e28a72748908164cfe..771c99e9cc28d7e32df863612283925f339881f9 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -135,6 +135,8 @@ import static eu.siacs.conversations.utils.PermissionUtils.allGranted; import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied; import static eu.siacs.conversations.utils.PermissionUtils.writeGranted; +import org.jetbrains.annotations.NotNull; + public class ConversationFragment extends XmppFragment implements EditMessage.KeyboardListener, MessageAdapter.OnContactPictureLongClicked, MessageAdapter.OnContactPictureClicked { @@ -186,10 +188,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke @Override public void onClick(View v) { - Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class); - intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); - intent.putExtra("uuid", conversation.getUuid()); - startActivity(intent); + ConferenceDetailsActivity.open(getActivity(), conversation); } }; private final OnClickListener leaveMuc = new OnClickListener() { @@ -689,14 +688,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke toggleInputMethod(); } - private void attachImageToConversation(Conversation conversation, Uri uri) { + private void attachImageToConversation(Conversation conversation, Uri uri, String type) { if (conversation == null) { return; } final Toast prepareFileToast = Toast.makeText(getActivity(), getText(R.string.preparing_image), Toast.LENGTH_LONG); prepareFileToast.show(); activity.delegateUriPermissionsToService(uri); - activity.xmppConnectionService.attachImageToConversation(conversation, uri, + activity.xmppConnectionService.attachImageToConversation(conversation, uri, type, new UiCallback() { @Override @@ -857,9 +856,15 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke toggleInputMethod(); break; case ATTACHMENT_CHOICE_LOCATION: - double latitude = data.getDoubleExtra("latitude", 0); - double longitude = data.getDoubleExtra("longitude", 0); - Uri geo = Uri.parse("geo:" + latitude + "," + longitude); + final double latitude = data.getDoubleExtra("latitude", 0); + final double longitude = data.getDoubleExtra("longitude", 0); + final int accuracy = data.getIntExtra("accuracy", 0); + final Uri geo; + if (accuracy > 0) { + geo = Uri.parse(String.format("geo:%s,%s;u=%s", latitude, longitude, accuracy)); + } else { + geo = Uri.parse(String.format("geo:%s,%s", latitude, longitude)); + } mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), geo, Attachment.Type.LOCATION)); toggleInputMethod(); break; @@ -890,7 +895,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke attachLocationToConversation(conversation, attachment.getUri()); } else if (attachment.getType() == Attachment.Type.IMAGE) { Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE"); - attachImageToConversation(conversation, attachment.getUri()); + attachImageToConversation(conversation, attachment.getUri(), attachment.getMime()); } else { Log.d(Config.LOGTAG, "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO"); attachFileToConversation(conversation, attachment.getUri(), attachment.getMime()); @@ -1272,10 +1277,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke activity.switchToContactDetails(conversation.getContact()); break; case R.id.action_muc_details: - Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class); - intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); - intent.putExtra("uuid", conversation.getUuid()); - startActivity(intent); + ConferenceDetailsActivity.open(getActivity(), conversation); break; case R.id.action_invite: startActivityForResult(ChooseContactActivity.create(activity, conversation), REQUEST_INVITE_TO_CONVERSATION); @@ -1586,6 +1588,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } if (writeGranted(grantResults, permissions)) { if (activity != null && activity.xmppConnectionService != null) { + activity.xmppConnectionService.getBitmapCache().evictAll(); activity.xmppConnectionService.restartFileObserver(); } refresh(); @@ -1624,9 +1627,9 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke @SuppressLint("InflateParams") protected void clearHistoryDialog(final Conversation conversation) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); builder.setTitle(getString(R.string.clear_conversation_history)); - final View dialogView = getActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null); + final View dialogView = requireActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null); final CheckBox endConversationCheckBox = dialogView.findViewById(R.id.end_conversation_checkbox); builder.setView(dialogView); builder.setNegativeButton(getString(R.string.cancel), null); @@ -1644,7 +1647,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } protected void muteConversationDialog(final Conversation conversation) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); builder.setTitle(R.string.disable_notifications); final int[] durations = getResources().getIntArray(R.array.mute_options_durations); final CharSequence[] labels = new CharSequence[durations.length]; @@ -1660,13 +1663,13 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke if (durations[which] == -1) { till = Long.MAX_VALUE; } else { - till = System.currentTimeMillis() + (durations[which] * 1000); + till = System.currentTimeMillis() + (durations[which] * 1000L); } conversation.setMutedTill(till); activity.xmppConnectionService.updateConversation(conversation); activity.onConversationsListItemUpdated(); refresh(); - getActivity().invalidateOptionsMenu(); + requireActivity().invalidateOptionsMenu(); }); builder.create().show(); } @@ -1698,7 +1701,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke this.activity.xmppConnectionService.updateConversation(conversation); this.activity.onConversationsListItemUpdated(); refresh(); - getActivity().invalidateOptionsMenu(); + requireActivity().invalidateOptionsMenu(); } @@ -1708,9 +1711,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke switch (attachmentChoice) { case ATTACHMENT_CHOICE_CHOOSE_IMAGE: intent.setAction(Intent.ACTION_GET_CONTENT); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); - } + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); intent.setType("image/*"); chooser = true; break; @@ -1728,9 +1729,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke case ATTACHMENT_CHOICE_CHOOSE_FILE: chooser = true; intent.setType("*/*"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); - } + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setAction(Intent.ACTION_GET_CONTENT); break; @@ -1813,7 +1812,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } private void showErrorMessage(final Message message) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); builder.setTitle(R.string.error_message); final String errorMessage = message.getErrorMessage(); final String[] errorMessageParts = errorMessage == null ? new String[0] : errorMessage.split("\\u001f"); @@ -1834,7 +1833,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private void deleteFile(final Message message) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); builder.setNegativeButton(R.string.cancel, null); builder.setTitle(R.string.delete_file_dialog); builder.setMessage(R.string.delete_file_dialog_msg); @@ -1968,7 +1967,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(@NotNull Bundle outState) { super.onSaveInstanceState(outState); if (conversation != null) { outState.putString(STATE_CONVERSATION_UUID, conversation.getUuid()); @@ -2193,13 +2192,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke final boolean asQuote = extras.getBoolean(ConversationsActivity.EXTRA_AS_QUOTE); final boolean pm = extras.getBoolean(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, false); final boolean doNotAppend = extras.getBoolean(ConversationsActivity.EXTRA_DO_NOT_APPEND, false); + final String type = extras.getString(ConversationsActivity.EXTRA_TYPE); final List uris = extractUris(extras); if (uris != null && uris.size() > 0) { if (uris.size() == 1 && "geo".equals(uris.get(0).getScheme())) { mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uris.get(0), Attachment.Type.LOCATION)); } else { final List cleanedUris = cleanUris(new ArrayList<>(uris)); - mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris)); + mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), cleanedUris, type)); } toggleInputMethod(); return; @@ -3058,4 +3058,12 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } activity.switchToAccount(message.getConversation().getAccount(), fingerprint); } + + private Activity requireActivity() { + final Activity activity = getActivity(); + if (activity == null) { + throw new IllegalStateException("Activity not attached"); + } + return activity; + } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index e3f52aa2116d5619c8a4534e8dd9cf7bda264cc7..cc46ed33f8f3813acb7f8e1497a852c45bbd6e2a 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -30,6 +30,8 @@ package eu.siacs.conversations.ui; +import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP; + import android.annotation.SuppressLint; import android.app.Activity; import android.app.Fragment; @@ -65,13 +67,16 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.OmemoSetting; import eu.siacs.conversations.databinding.ActivityConversationsBinding; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.interfaces.OnBackendConnected; import eu.siacs.conversations.ui.interfaces.OnConversationArchived; import eu.siacs.conversations.ui.interfaces.OnConversationRead; import eu.siacs.conversations.ui.interfaces.OnConversationSelected; import eu.siacs.conversations.ui.interfaces.OnConversationsListItemUpdated; +import eu.siacs.conversations.ui.util.ActionBarUtil; import eu.siacs.conversations.ui.util.ActivityResult; import eu.siacs.conversations.ui.util.ConversationMenuConfigurator; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; @@ -83,8 +88,6 @@ import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; -import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP; - public class ConversationsActivity extends XmppActivity implements OnConversationSelected, OnConversationArchived, OnConversationsListItemUpdated, OnConversationRead, XmppConnectionService.OnAccountUpdate, XmppConnectionService.OnConversationUpdate, XmppConnectionService.OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnAffiliationChanged { public static final String ACTION_VIEW_CONVERSATION = "eu.siacs.conversations.action.VIEW"; @@ -96,6 +99,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio public static final String EXTRA_DO_NOT_APPEND = "do_not_append"; public static final String EXTRA_POST_INIT_ACTION = "post_init_action"; public static final String POST_ACTION_RECORD_VOICE = "record_voice"; + public static final String EXTRA_TYPE = "type"; private static final List VIEW_AND_SHARE_ACTIONS = Arrays.asList( ACTION_VIEW_CONVERSATION, @@ -278,6 +282,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults); if (grantResults.length > 0) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { @@ -425,16 +430,18 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio } private void openConversation(Conversation conversation, Bundle extras) { - ConversationFragment conversationFragment = (ConversationFragment) getFragmentManager().findFragmentById(R.id.secondary_fragment); + final FragmentManager fragmentManager = getFragmentManager(); + executePendingTransactions(fragmentManager); + ConversationFragment conversationFragment = (ConversationFragment) fragmentManager.findFragmentById(R.id.secondary_fragment); final boolean mainNeedsRefresh; if (conversationFragment == null) { mainNeedsRefresh = false; - Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment); + final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment); if (mainFragment instanceof ConversationFragment) { conversationFragment = (ConversationFragment) mainFragment; } else { conversationFragment = new ConversationFragment(); - FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction(); + FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); fragmentTransaction.replace(R.id.main_fragment, conversationFragment); fragmentTransaction.addToBackStack(null); try { @@ -456,6 +463,14 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio } } + private static void executePendingTransactions(final FragmentManager fragmentManager) { + try { + fragmentManager.executePendingTransactions(); + } catch (final Exception e) { + Log.e(Config.LOGTAG,"unable to execute pending fragment transactions"); + } + } + public boolean onXmppUriClicked(Uri uri) { XmppUri xmppUri = new XmppUri(uri); if (xmppUri.isValidJid() && !xmppUri.hasFingerprints()) { @@ -524,6 +539,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio @Override protected void onStart() { + super.onStart(); final int theme = findTheme(); if (this.mTheme != theme) { this.mSkipBackgroundBinding = true; @@ -532,7 +548,6 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio this.mSkipBackgroundBinding = false; } mRedirectInProcess.set(false); - super.onStart(); } @Override @@ -562,17 +577,18 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio } private void initializeFragments() { - FragmentTransaction transaction = getFragmentManager().beginTransaction(); - Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment); - Fragment secondaryFragment = getFragmentManager().findFragmentById(R.id.secondary_fragment); + final FragmentManager fragmentManager = getFragmentManager(); + FragmentTransaction transaction = fragmentManager.beginTransaction(); + final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment); + final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment); if (mainFragment != null) { if (binding.secondaryFragment != null) { if (mainFragment instanceof ConversationFragment) { getFragmentManager().popBackStack(); transaction.remove(mainFragment); transaction.commit(); - getFragmentManager().executePendingTransactions(); - transaction = getFragmentManager().beginTransaction(); + fragmentManager.executePendingTransactions(); + transaction = fragmentManager.beginTransaction(); transaction.replace(R.id.secondary_fragment, mainFragment); transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment()); transaction.commit(); @@ -583,7 +599,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio transaction.remove(secondaryFragment); transaction.commit(); getFragmentManager().executePendingTransactions(); - transaction = getFragmentManager().beginTransaction(); + transaction = fragmentManager.beginTransaction(); transaction.replace(R.id.main_fragment, secondaryFragment); transaction.addToBackStack(null); transaction.commit(); @@ -601,18 +617,38 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio private void invalidateActionBarTitle() { final ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment); - if (mainFragment instanceof ConversationFragment) { - final Conversation conversation = ((ConversationFragment) mainFragment).getConversation(); - if (conversation != null) { - actionBar.setTitle(EmojiWrapper.transform(conversation.getName())); - actionBar.setDisplayHomeAsUpEnabled(true); - return; - } + if (actionBar == null) { + return; + } + final FragmentManager fragmentManager = getFragmentManager(); + final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment); + if (mainFragment instanceof ConversationFragment) { + final Conversation conversation = ((ConversationFragment) mainFragment).getConversation(); + if (conversation != null) { + actionBar.setTitle(EmojiWrapper.transform(conversation.getName())); + actionBar.setDisplayHomeAsUpEnabled(true); + ActionBarUtil.setActionBarOnClickListener( + binding.toolbar, + (v) -> openConversationDetails(conversation) + ); + return; + } + } + actionBar.setTitle(R.string.app_name); + actionBar.setDisplayHomeAsUpEnabled(false); + ActionBarUtil.resetActionBarOnClickListeners(binding.toolbar); + } + + private void openConversationDetails(final Conversation conversation) { + if (conversation.getMode() == Conversational.MODE_MULTI) { + ConferenceDetailsActivity.open(this, conversation); + } else { + final Contact contact = conversation.getContact(); + if (contact.isSelf()) { + switchToAccount(conversation.getAccount()); + } else { + switchToContactDetails(contact); } - actionBar.setTitle(R.string.app_name); - actionBar.setDisplayHomeAsUpEnabled(false); } } @@ -621,17 +657,18 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio if (performRedirectIfNecessary(conversation, false)) { return; } - Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment); + final FragmentManager fragmentManager = getFragmentManager(); + final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment); if (mainFragment instanceof ConversationFragment) { try { - getFragmentManager().popBackStack(); - } catch (IllegalStateException e) { + fragmentManager.popBackStack(); + } catch (final IllegalStateException e) { Log.w(Config.LOGTAG, "state loss while popping back state after archiving conversation", e); //this usually means activity is no longer active; meaning on the next open we will run through this again } return; } - Fragment secondaryFragment = getFragmentManager().findFragmentById(R.id.secondary_fragment); + final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment); if (secondaryFragment instanceof ConversationFragment) { if (((ConversationFragment) secondaryFragment).getConversation() == conversation) { Conversation suggestion = ConversationsOverviewFragment.getSuggestion(this, conversation); diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 25d3a0738b314763e9d54d229d6c5c5c28dda13f..921756a70884c8a630651fdde9965c7475958095 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -23,11 +23,14 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; +import android.widget.CheckBox; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.EditText; import android.widget.ImageView; +import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog.Builder; @@ -693,12 +696,18 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } catch (final IllegalArgumentException | NullPointerException ignored) { this.jidToEdit = null; } - if (jidToEdit != null && intent.getData() != null && intent.getBooleanExtra("scanned", false)) { - final XmppUri uri = new XmppUri(intent.getData()); - if (xmppConnectionServiceBound) { - processFingerprintVerification(uri, false); + final Uri data = intent.getData(); + final XmppUri xmppUri = data == null ? null : new XmppUri(data); + final boolean scanned = intent.getBooleanExtra("scanned", false); + if (jidToEdit != null && xmppUri != null && xmppUri.hasFingerprints()) { + if (scanned) { + if (xmppConnectionServiceBound) { + processFingerprintVerification(xmppUri, false); + } else { + this.pendingUri = xmppUri; + } } else { - this.pendingUri = uri; + displayVerificationWarningDialog(xmppUri); } } boolean init = intent.getBooleanExtra("init", false); @@ -735,6 +744,28 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } } + private void displayVerificationWarningDialog(final XmppUri xmppUri) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.verify_omemo_keys); + View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null); + final CheckBox isTrustedSource = view.findViewById(R.id.trusted_source); + TextView warning = view.findViewById(R.id.warning); + warning.setText(R.string.verifying_omemo_keys_trusted_source_account); + builder.setView(view); + builder.setPositiveButton(R.string.continue_btn, (dialog, which) -> { + if (isTrustedSource.isChecked()) { + processFingerprintVerification(xmppUri, false); + } else { + finish(); + } + }); + builder.setNegativeButton(R.string.cancel, (dialog, which) -> finish()); + AlertDialog dialog = builder.create(); + dialog.setCanceledOnTouchOutside(false); + dialog.setOnCancelListener(d -> finish()); + dialog.show(); + } + @Override public void onNewIntent(final Intent intent) { super.onNewIntent(intent); @@ -749,7 +780,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } @Override - public void onSaveInstanceState(final Bundle savedInstanceState) { + public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) { if (mAccount != null) { savedInstanceState.putString("account", mAccount.getJid().asBareJid().toEscapedString()); savedInstanceState.putBoolean("initMode", mInitMode); diff --git a/src/main/java/eu/siacs/conversations/ui/LocationActivity.java b/src/main/java/eu/siacs/conversations/ui/LocationActivity.java index ed08fa0d24aee40a9f9e9dd80c1372cd056d5a0f..2627e0e59ea44cc4ad3be7b4434d88af67691fad 100644 --- a/src/main/java/eu/siacs/conversations/ui/LocationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/LocationActivity.java @@ -39,6 +39,7 @@ import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.ui.util.LocationHelper; import eu.siacs.conversations.ui.widget.Marker; import eu.siacs.conversations.ui.widget.MyLocation; +import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.utils.ThemeHelper; public abstract class LocationActivity extends ActionBarActivity implements LocationListener { @@ -68,6 +69,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca } } + protected void updateLocationMarkers() { clearMarkers(); } @@ -222,6 +224,7 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca @Override protected void onResume() { super.onResume(); + SettingsUtils.applyScreenshotPreventionSetting(this); Configuration.getInstance().load(this, getPreferences()); map.onResume(); this.setMyLoc(null); diff --git a/src/main/java/eu/siacs/conversations/ui/MemorizingActivity.java b/src/main/java/eu/siacs/conversations/ui/MemorizingActivity.java index 7f8a55a727d527bd74cff977ed51ff61e613168e..23f3c82d8b0585bd62c88e3e0be0d15c961b9a6c 100644 --- a/src/main/java/eu/siacs/conversations/ui/MemorizingActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/MemorizingActivity.java @@ -39,6 +39,7 @@ import java.util.logging.Logger; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.MTMDecision; import eu.siacs.conversations.services.MemorizingTrustManager; +import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.utils.ThemeHelper; public class MemorizingActivity extends AppCompatActivity implements OnClickListener, OnCancelListener { @@ -61,6 +62,8 @@ public class MemorizingActivity extends AppCompatActivity implements OnClickList @Override public void onResume() { super.onResume(); + SettingsUtils.applyScreenshotPreventionSetting(this); + Intent i = getIntent(); decisionId = i.getIntExtra(MemorizingTrustManager.DECISION_INTENT_ID, MTMDecision.DECISION_INVALID); int titleId = i.getIntExtra(MemorizingTrustManager.DECISION_TITLE_ID, R.string.mtm_accept_cert); diff --git a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java index 5e4c900487fcc5f9152c8652931fc07d23cd0288..6146c4ae72a1131793ff649b45551062203b6fb8 100644 --- a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java @@ -28,6 +28,7 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityRecordingBinding; import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.utils.ThemeHelper; import eu.siacs.conversations.utils.TimeFrameUtils; @@ -66,6 +67,12 @@ public class RecordingActivity extends Activity implements View.OnClickListener getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } + @Override + protected void onResume(){ + super.onResume(); + SettingsUtils.applyScreenshotPreventionSetting(this); + } + @Override protected void onStart() { super.onStart(); diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 7df969fae3d615f7880456692e017ed95c3fa3a7..8a2b87b47ed5d2499521b6fa367cca69d960a6f9 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -1,5 +1,8 @@ package eu.siacs.conversations.ui; +import static java.util.Arrays.asList; +import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied; + import android.Manifest; import android.annotation.SuppressLint; import android.app.PictureInPictureParams; @@ -57,6 +60,7 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.widget.DialpadView; import eu.siacs.conversations.ui.util.AvatarWorkerTask; 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; @@ -67,10 +71,7 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; -import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied; -import static java.util.Arrays.asList; - -public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate { +public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate, eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged { public static final String EXTRA_WITH = "with"; public static final String EXTRA_SESSION_ID = "session_id"; @@ -97,7 +98,17 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe ); private static final List STATES_SHOWING_SWITCH_TO_CHAT = Arrays.asList( RtpEndUserState.CONNECTING, - RtpEndUserState.CONNECTED + RtpEndUserState.CONNECTED, + RtpEndUserState.RECONNECTING + ); + private static final List STATES_CONSIDERED_CONNECTED = Arrays.asList( + RtpEndUserState.CONNECTED, + RtpEndUserState.RECONNECTING + ); + private static final List STATES_SHOWING_PIP_PLACEHOLDER = Arrays.asList( + RtpEndUserState.ACCEPTING_CALL, + RtpEndUserState.CONNECTING, + RtpEndUserState.RECONNECTING ); private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session"; private static final int REQUEST_ACCEPT_CALL = 0x1111; @@ -141,13 +152,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session); setSupportActionBar(binding.toolbar); - ((DialpadView)findViewById(R.id.dialpad)).setClickConsumer(tag -> { + binding.dialpad.setClickConsumer(tag -> { requireRtpConnection().applyDtmfTone(tag); }); if (savedInstanceState != null) { - int dialpad_visibility = savedInstanceState.getInt("dialpad_visibility"); - findViewById(R.id.dialpad).setVisibility(dialpad_visibility); + boolean dialpadVisible = savedInstanceState.getBoolean("dialpad_visible"); + binding.dialpad.setVisibility(dialpadVisible ? View.VISIBLE : View.GONE); } } @@ -197,9 +208,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private boolean isAudioOnlyConversation() { final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; - return connection != null && - connection.getEndUserState() == RtpEndUserState.CONNECTED && - !connection.isVideoEnabled(); + + return connection != null && !connection.getMedia().contains(Media.VIDEO); } private void switchToConversation() { @@ -482,12 +492,14 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe public void onStart() { super.onStart(); mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL); + this.binding.remoteVideo.setOnAspectRatioChanged(this); } @Override public void onStop() { mHandler.removeCallbacks(mTickExecutor); binding.remoteVideo.release(); + binding.remoteVideo.setOnAspectRatioChanged(null); binding.localVideo.release(); final WeakReference weakReference = this.rtpConnectionReference; final JingleRtpConnection jingleRtpConnection = weakReference == null ? null : weakReference.get(); @@ -535,7 +547,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe private boolean isConnected() { final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; - return connection != null && connection.getEndUserState() == RtpEndUserState.CONNECTED; + return connection != null && STATES_CONSIDERED_CONNECTED.contains(connection.getEndUserState()); } private boolean switchToPictureInPicture() { @@ -551,9 +563,12 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @RequiresApi(api = Build.VERSION_CODES.O) private void startPictureInPicture() { try { + final Rational rational = this.binding.remoteVideo.getAspectRatio(); + final Rational clippedRational = Rationals.clip(rational); + Log.d(Config.LOGTAG, "suggested rational " + rational + ". clipped to " + clippedRational); enterPictureInPictureMode( new PictureInPictureParams.Builder() - .setAspectRatio(new Rational(10, 16)) + .setAspectRatio(clippedRational) .build() ); } catch (final IllegalStateException e) { @@ -562,6 +577,17 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } + @Override + public void onAspectRatioChanged(final Rational rational) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPictureInPicture()) { + final Rational clippedRational = Rationals.clip(rational); + Log.d(Config.LOGTAG, "suggested rational after aspect ratio change " + rational + ". clipped to " + clippedRational); + setPictureInPictureParams(new PictureInPictureParams.Builder() + .setAspectRatio(clippedRational) + .build()); + } + } + private boolean deviceSupportsPictureInPicture() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE); @@ -656,8 +682,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe surfaceViewRenderer.setVisibility(View.VISIBLE); try { surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null); - } catch (IllegalStateException e) { - Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized"); + } catch (final IllegalStateException e) { + //Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized"); } surfaceViewRenderer.setEnableHardwareScaler(true); } @@ -682,6 +708,9 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe 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; @@ -824,7 +853,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @SuppressLint("RestrictedApi") private void updateInCallButtonConfiguration(final RtpEndUserState state, final Set media) { - if (state == RtpEndUserState.CONNECTED && !isPictureInPicture()) { + if (STATES_CONSIDERED_CONNECTED.contains(state) && !isPictureInPicture()) { Preconditions.checkArgument(media.size() > 0, "Media must not be empty"); if (media.contains(Media.VIDEO)) { final JingleRtpConnection rtpConnection = requireRtpConnection(); @@ -952,14 +981,11 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe this.binding.duration.setVisibility(View.GONE); return; } - final long rtpConnectionStarted = connection.getRtpConnectionStarted(); - final long rtpConnectionEnded = connection.getRtpConnectionEnded(); - if (rtpConnectionStarted != 0) { - final long ended = rtpConnectionEnded == 0 ? SystemClock.elapsedRealtime() : rtpConnectionEnded; - this.binding.duration.setText(TimeFrameUtils.formatTimePassed(rtpConnectionStarted, ended, false)); - this.binding.duration.setVisibility(View.VISIBLE); - } else { + if (connection.zeroDuration()) { this.binding.duration.setVisibility(View.GONE); + } else { + this.binding.duration.setText(TimeFrameUtils.formatElapsedTime(connection.getCallDuration(), false)); + this.binding.duration.setVisibility(View.VISIBLE); } } @@ -991,7 +1017,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); return; } - if (isPictureInPicture() && (state == RtpEndUserState.CONNECTING || state == RtpEndUserState.ACCEPTING_CALL)) { + if (isPictureInPicture() && STATES_SHOWING_PIP_PLACEHOLDER.contains(state)) { binding.localVideo.setVisibility(View.GONE); binding.remoteVideoWrapper.setVisibility(View.GONE); binding.appBarLayout.setVisibility(View.GONE); @@ -1024,6 +1050,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); binding.remoteVideoWrapper.setVisibility(View.VISIBLE); } else { + binding.appBarLayout.setVisibility(View.VISIBLE); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); binding.remoteVideoWrapper.setVisibility(View.GONE); } @@ -1200,8 +1227,7 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override protected void onSaveInstanceState(@NonNull @NotNull Bundle outState) { super.onSaveInstanceState(outState); - int visibility = findViewById(R.id.dialpad).getVisibility(); - outState.putInt("dialpad_visibility", visibility); + outState.putBoolean("dialpad_visible", binding.dialpad.getVisibility() == View.VISIBLE); } private void updateRtpSessionProposalState(final Account account, final Jid with, final RtpEndUserState state) { diff --git a/src/main/java/eu/siacs/conversations/ui/ScanActivity.java b/src/main/java/eu/siacs/conversations/ui/ScanActivity.java index cebd19bc3dd068a3545fa5b623ef486ade9c2af7..95505647d2dc5240c2a5932667ed0eb833830a6c 100644 --- a/src/main/java/eu/siacs/conversations/ui/ScanActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ScanActivity.java @@ -60,6 +60,7 @@ import java.util.Map; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.ui.service.CameraManager; +import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.ui.widget.ScannerView; /** @@ -181,6 +182,7 @@ public final class ScanActivity extends Activity implements SurfaceTextureListen @Override protected void onResume() { super.onResume(); + SettingsUtils.applyScreenshotPreventionSetting(this); maybeOpenCamera(); } diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index 68cc429216591b845751ffeaac786df6755331b3..7f4e59d1a2e31f565361fcfb0fafaa259b9785e0 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -40,6 +40,7 @@ import eu.siacs.conversations.services.MemorizingTrustManager; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.ui.util.StyledAttributes; import eu.siacs.conversations.utils.GeoHelper; +import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.utils.TimeFrameUtils; import eu.siacs.conversations.xmpp.Jid; @@ -57,8 +58,10 @@ public class SettingsActivity extends XmppActivity implements public static final String THEME = "theme"; public static final String SHOW_DYNAMIC_TAGS = "show_dynamic_tags"; public static final String OMEMO_SETTING = "omemo"; + public static final String PREVENT_SCREENSHOTS = "prevent_screenshots"; public static final int REQUEST_CREATE_BACKUP = 0xbf8701; + private SettingsFragment mSettingsFragment; @Override @@ -393,8 +396,15 @@ public class SettingsActivity extends XmppActivity implements if (this.mTheme != theme) { recreate(); } + } else if(name.equals(PREVENT_SCREENSHOTS)){ + SettingsUtils.applyScreenshotPreventionSetting(this); } + } + @Override + public void onResume(){ + super.onResume(); + SettingsUtils.applyScreenshotPreventionSetting(this); } @Override diff --git a/src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java index 641a01e5c1e952bb48cd5413c034e65fe5b20026..7e53fe89792b107f039bfa54a0c3532a61f1c4e9 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java @@ -13,10 +13,13 @@ import androidx.annotation.NonNull; import androidx.databinding.DataBindingUtil; import com.google.android.material.snackbar.Snackbar; +import com.google.common.math.DoubleMath; import org.osmdroid.api.IGeoPoint; import org.osmdroid.util.GeoPoint; +import java.math.RoundingMode; + import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityShareLocationBinding; @@ -28,213 +31,213 @@ import eu.siacs.conversations.utils.ThemeHelper; public class ShareLocationActivity extends LocationActivity implements LocationListener { - private Snackbar snackBar; - private ActivityShareLocationBinding binding; - private boolean marker_fixed_to_loc = false; - private static final String KEY_FIXED_TO_LOC = "fixed_to_loc"; - private Boolean noAskAgain = false; - - @Override - protected void onSaveInstanceState(@NonNull final Bundle outState) { - super.onSaveInstanceState(outState); - - outState.putBoolean(KEY_FIXED_TO_LOC, marker_fixed_to_loc); - } - - @Override - protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - - if (savedInstanceState.containsKey(KEY_FIXED_TO_LOC)) { - this.marker_fixed_to_loc = savedInstanceState.getBoolean(KEY_FIXED_TO_LOC); - } - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - this.binding = DataBindingUtil.setContentView(this,R.layout.activity_share_location); - setSupportActionBar(binding.toolbar); - configureActionBar(getSupportActionBar()); - setupMapView(binding.map, LocationProvider.getGeoPoint(this)); - - this.binding.cancelButton.setOnClickListener(view -> { - setResult(RESULT_CANCELED); - finish(); - }); - - this.snackBar = Snackbar.make(this.binding.snackbarCoordinator, R.string.location_disabled, Snackbar.LENGTH_INDEFINITE); - this.snackBar.setAction(R.string.enable, view -> { - if (isLocationEnabledAndAllowed()) { - updateUi(); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasLocationPermissions()) { - requestPermissions(REQUEST_CODE_SNACKBAR_PRESSED); - } else if (!isLocationEnabled()) { - startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); - } - }); - ThemeHelper.fix(this.snackBar); - - this.binding.shareButton.setOnClickListener(view -> { - final Intent result = new Intent(); - - if (marker_fixed_to_loc && myLoc != null) { - result.putExtra("latitude", myLoc.getLatitude()); - result.putExtra("longitude", myLoc.getLongitude()); - result.putExtra("altitude", myLoc.getAltitude()); - result.putExtra("accuracy", (int) myLoc.getAccuracy()); - } else { - final IGeoPoint markerPoint = this.binding.map.getMapCenter(); - result.putExtra("latitude", markerPoint.getLatitude()); - result.putExtra("longitude", markerPoint.getLongitude()); - } - - setResult(RESULT_OK, result); - finish(); - }); - - this.marker_fixed_to_loc = isLocationEnabledAndAllowed(); - - this.binding.fab.setOnClickListener(view -> { - if (!marker_fixed_to_loc) { - if (!isLocationEnabled()) { - startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(REQUEST_CODE_FAB_PRESSED); - } - } - toggleFixedLocation(); - }); - } - - @Override - public void onRequestPermissionsResult(final int requestCode, - @NonNull final String[] permissions, - @NonNull final int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - - if (grantResults.length > 0 && - grantResults[0] != PackageManager.PERMISSION_GRANTED && - Build.VERSION.SDK_INT >= 23 && - permissions.length > 0 && - ( - Manifest.permission.LOCATION_HARDWARE.equals(permissions[0]) || - Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[0]) || - Manifest.permission.ACCESS_COARSE_LOCATION.equals(permissions[0]) - ) && - !shouldShowRequestPermissionRationale(permissions[0])) { - noAskAgain = true; - } - - if (!noAskAgain && requestCode == REQUEST_CODE_SNACKBAR_PRESSED && !isLocationEnabled() && hasLocationPermissions()) { - startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); - } - updateUi(); - } - - @Override - protected void gotoLoc(final boolean setZoomLevel) { - if (this.myLoc != null && mapController != null) { - if (setZoomLevel) { - mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL); - } - mapController.animateTo(new GeoPoint(this.myLoc)); - } - } - - @Override - protected void setMyLoc(final Location location) { - this.myLoc = location; - } - - @Override - protected void onPause() { - super.onPause(); - } - - @Override - protected void updateLocationMarkers() { - super.updateLocationMarkers(); - if (this.myLoc != null) { - this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc)); - if (this.marker_fixed_to_loc) { - this.binding.map.getOverlays().add(new Marker(marker_icon, new GeoPoint(this.myLoc))); - } else { - this.binding.map.getOverlays().add(new Marker(marker_icon)); - } - } else { - this.binding.map.getOverlays().add(new Marker(marker_icon)); - } - } - - @Override - public void onLocationChanged(final Location location) { - if (this.myLoc == null) { - this.marker_fixed_to_loc = true; - } - updateUi(); - if (LocationHelper.isBetterLocation(location, this.myLoc)) { - final Location oldLoc = this.myLoc; - this.myLoc = location; - - // Don't jump back to the users location if they're not moving (more or less). - if (oldLoc == null || (this.marker_fixed_to_loc && this.myLoc.distanceTo(oldLoc) > 1)) { - gotoLoc(); - } - - updateLocationMarkers(); - } - } - - @Override - public void onStatusChanged(final String provider, final int status, final Bundle extras) { - - } - - @Override - public void onProviderEnabled(final String provider) { - - } - - @Override - public void onProviderDisabled(final String provider) { - - } - - private boolean isLocationEnabledAndAllowed() { - return this.hasLocationFeature && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || this.hasLocationPermissions()) && this.isLocationEnabled(); - } - - private void toggleFixedLocation() { - this.marker_fixed_to_loc = isLocationEnabledAndAllowed() && !this.marker_fixed_to_loc; - if (this.marker_fixed_to_loc) { - gotoLoc(false); - } - updateLocationMarkers(); - updateUi(); - } - - @Override - protected void updateUi() { - if (!hasLocationFeature || noAskAgain || isLocationEnabledAndAllowed()) { - this.snackBar.dismiss(); - } else { - this.snackBar.show(); - } - - if (isLocationEnabledAndAllowed()) { - this.binding.fab.setVisibility(View.VISIBLE); - runOnUiThread(() -> { - this.binding.fab.setImageResource(marker_fixed_to_loc ? R.drawable.ic_gps_fixed_white_24dp : - R.drawable.ic_gps_not_fixed_white_24dp); - this.binding.fab.setContentDescription(getResources().getString( - marker_fixed_to_loc ? R.string.action_unfix_from_location : R.string.action_fix_to_location - )); - this.binding.fab.invalidate(); - }); - } else { - this.binding.fab.setVisibility(View.GONE); - } - } + private Snackbar snackBar; + private ActivityShareLocationBinding binding; + private boolean marker_fixed_to_loc = false; + private static final String KEY_FIXED_TO_LOC = "fixed_to_loc"; + private Boolean noAskAgain = false; + + @Override + protected void onSaveInstanceState(@NonNull final Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putBoolean(KEY_FIXED_TO_LOC, marker_fixed_to_loc); + } + + @Override + protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + + if (savedInstanceState.containsKey(KEY_FIXED_TO_LOC)) { + this.marker_fixed_to_loc = savedInstanceState.getBoolean(KEY_FIXED_TO_LOC); + } + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + this.binding = DataBindingUtil.setContentView(this, R.layout.activity_share_location); + setSupportActionBar(binding.toolbar); + configureActionBar(getSupportActionBar()); + setupMapView(binding.map, LocationProvider.getGeoPoint(this)); + + this.binding.cancelButton.setOnClickListener(view -> { + setResult(RESULT_CANCELED); + finish(); + }); + + this.snackBar = Snackbar.make(this.binding.snackbarCoordinator, R.string.location_disabled, Snackbar.LENGTH_INDEFINITE); + this.snackBar.setAction(R.string.enable, view -> { + if (isLocationEnabledAndAllowed()) { + updateUi(); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasLocationPermissions()) { + requestPermissions(REQUEST_CODE_SNACKBAR_PRESSED); + } else if (!isLocationEnabled()) { + startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); + } + }); + ThemeHelper.fix(this.snackBar); + + this.binding.shareButton.setOnClickListener(this::shareLocation); + + this.marker_fixed_to_loc = isLocationEnabledAndAllowed(); + + this.binding.fab.setOnClickListener(view -> { + if (!marker_fixed_to_loc) { + if (!isLocationEnabled()) { + startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + requestPermissions(REQUEST_CODE_FAB_PRESSED); + } + } + toggleFixedLocation(); + }); + } + + private void shareLocation(final View view) { + final Intent result = new Intent(); + if (marker_fixed_to_loc && myLoc != null) { + result.putExtra("latitude", myLoc.getLatitude()); + result.putExtra("longitude", myLoc.getLongitude()); + result.putExtra("altitude", myLoc.getAltitude()); + result.putExtra("accuracy", DoubleMath.roundToInt(myLoc.getAccuracy(), RoundingMode.HALF_UP)); + } else { + final IGeoPoint markerPoint = this.binding.map.getMapCenter(); + result.putExtra("latitude", markerPoint.getLatitude()); + result.putExtra("longitude", markerPoint.getLongitude()); + } + setResult(RESULT_OK, result); + finish(); + } + + @Override + public void onRequestPermissionsResult(final int requestCode, + @NonNull final String[] permissions, + @NonNull final int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + if (grantResults.length > 0 && + grantResults[0] != PackageManager.PERMISSION_GRANTED && + Build.VERSION.SDK_INT >= 23 && + permissions.length > 0 && + ( + Manifest.permission.LOCATION_HARDWARE.equals(permissions[0]) || + Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[0]) || + Manifest.permission.ACCESS_COARSE_LOCATION.equals(permissions[0]) + ) && + !shouldShowRequestPermissionRationale(permissions[0])) { + noAskAgain = true; + } + + if (!noAskAgain && requestCode == REQUEST_CODE_SNACKBAR_PRESSED && !isLocationEnabled() && hasLocationPermissions()) { + startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); + } + updateUi(); + } + + @Override + protected void gotoLoc(final boolean setZoomLevel) { + if (this.myLoc != null && mapController != null) { + if (setZoomLevel) { + mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL); + } + mapController.animateTo(new GeoPoint(this.myLoc)); + } + } + + @Override + protected void setMyLoc(final Location location) { + this.myLoc = location; + } + + @Override + protected void onPause() { + super.onPause(); + } + + @Override + protected void updateLocationMarkers() { + super.updateLocationMarkers(); + if (this.myLoc != null) { + this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc)); + if (this.marker_fixed_to_loc) { + this.binding.map.getOverlays().add(new Marker(marker_icon, new GeoPoint(this.myLoc))); + } else { + this.binding.map.getOverlays().add(new Marker(marker_icon)); + } + } else { + this.binding.map.getOverlays().add(new Marker(marker_icon)); + } + } + + @Override + public void onLocationChanged(final Location location) { + if (this.myLoc == null) { + this.marker_fixed_to_loc = true; + } + updateUi(); + if (LocationHelper.isBetterLocation(location, this.myLoc)) { + final Location oldLoc = this.myLoc; + this.myLoc = location; + + // Don't jump back to the users location if they're not moving (more or less). + if (oldLoc == null || (this.marker_fixed_to_loc && this.myLoc.distanceTo(oldLoc) > 1)) { + gotoLoc(); + } + + updateLocationMarkers(); + } + } + + @Override + public void onStatusChanged(final String provider, final int status, final Bundle extras) { + + } + + @Override + public void onProviderEnabled(final String provider) { + + } + + @Override + public void onProviderDisabled(final String provider) { + + } + + private boolean isLocationEnabledAndAllowed() { + return this.hasLocationFeature && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || this.hasLocationPermissions()) && this.isLocationEnabled(); + } + + private void toggleFixedLocation() { + this.marker_fixed_to_loc = isLocationEnabledAndAllowed() && !this.marker_fixed_to_loc; + if (this.marker_fixed_to_loc) { + gotoLoc(false); + } + updateLocationMarkers(); + updateUi(); + } + + @Override + protected void updateUi() { + if (!hasLocationFeature || noAskAgain || isLocationEnabledAndAllowed()) { + this.snackBar.dismiss(); + } else { + this.snackBar.show(); + } + + if (isLocationEnabledAndAllowed()) { + this.binding.fab.setVisibility(View.VISIBLE); + runOnUiThread(() -> { + this.binding.fab.setImageResource(marker_fixed_to_loc ? R.drawable.ic_gps_fixed_white_24dp : + R.drawable.ic_gps_not_fixed_white_24dp); + this.binding.fab.setContentDescription(getResources().getString( + marker_fixed_to_loc ? R.string.action_unfix_from_location : R.string.action_fix_to_location + )); + this.binding.fab.invalidate(); + }); + } else { + this.binding.fab.setVisibility(View.GONE); + } + } } \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java index cb698691e538881c4d74c6f07da085074f8fbaea..d03928c8cb422ce608ffc69b8f3fd71d76780acd 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java @@ -33,7 +33,8 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer refreshUi(); } - private class Share { + private static class Share { + public String type; ArrayList uris = new ArrayList<>(); public String account; public String contact; @@ -65,6 +66,7 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (grantResults.length > 0) if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (requestCode == REQUEST_STORAGE_PERMISSION) { @@ -139,6 +141,7 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer } else if (type != null && uri != null) { this.share.uris.clear(); this.share.uris.add(uri); + this.share.type = type; } else { this.share.text = text; this.share.asQuote = asQuote; @@ -193,6 +196,9 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer intent.setAction(Intent.ACTION_SEND_MULTIPLE); intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, share.uris); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + if (share.type != null) { + intent.putExtra(ConversationsActivity.EXTRA_TYPE, share.type); + } } else if (share.text != null) { intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION); intent.putExtra(Intent.EXTRA_TEXT, share.text); diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index e6d3ebf6ee524f854171f93f95b150bddb7364d1..99479777963a8aca1551b4717f79d1f804bec0ef 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -37,11 +37,14 @@ import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.MenuRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.PopupMenu; +import androidx.core.content.ContextCompat; import androidx.databinding.DataBindingUtil; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; @@ -51,6 +54,8 @@ import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import com.google.android.material.textfield.TextInputLayout; +import com.leinardi.android.speeddial.SpeedDialActionItem; +import com.leinardi.android.speeddial.SpeedDialView; import java.util.ArrayList; import java.util.Collections; @@ -266,8 +271,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne setSupportActionBar(binding.toolbar); configureActionBar(getSupportActionBar()); - binding.speedDial.inflate(R.menu.start_conversation_fab_submenu); - + inflateFab(binding.speedDial, R.menu.start_conversation_fab_submenu); binding.tabLayout.setupWithViewPager(binding.startConversationViewPager); binding.startConversationViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { @Override @@ -338,6 +342,21 @@ public class StartConversationActivity extends XmppActivity implements XmppConne }); } + private void inflateFab(final SpeedDialView speedDialView, final @MenuRes int menuRes) { + speedDialView.clearActionItems(); + final PopupMenu popupMenu = new PopupMenu(this, new View(this)); + popupMenu.inflate(menuRes); + final Menu menu = popupMenu.getMenu(); + for (int i = 0; i < menu.size(); i++) { + final MenuItem menuItem = menu.getItem(i); + 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)) + .create(); + speedDialView.addActionItem(actionItem); + } + } + public static boolean isValidJid(String input) { try { Jid jid = Jid.ofEscaped(input); diff --git a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java index e549c9d20ec8097698fbe579f0518c72e482faf2..a3f062814ba47e26580a3d1dd0cee4bc62f56c58 100644 --- a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java @@ -7,24 +7,39 @@ import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.util.Log; +import android.view.View; import android.widget.Toast; +import androidx.annotation.StringRes; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; +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; +import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.utils.ProvisioningUtils; import eu.siacs.conversations.utils.SignupUtils; import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xmpp.Jid; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; public class UriHandlerActivity extends AppCompatActivity { @@ -34,7 +49,9 @@ public class UriHandlerActivity extends AppCompatActivity { private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN = 0x6789; private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION = 0x6790; private static final Pattern V_CARD_XMPP_PATTERN = Pattern.compile("\nIMPP([^:]*):(xmpp:.+)\n"); - private boolean handled = false; + private static final Pattern LINK_HEADER_PATTERN = Pattern.compile("<(.*?)>"); + private ActivityUriHandlerBinding binding; + private Call call; public static void scan(final Activity activity) { scan(activity, false); @@ -77,9 +94,7 @@ public class UriHandlerActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - this.handled = savedInstanceState != null && savedInstanceState.getBoolean("handled", false); - getLayoutInflater().inflate(R.layout.toolbar, findViewById(android.R.id.content)); - setSupportActionBar(findViewById(R.id.toolbar)); + this.binding = DataBindingUtil.setContentView(this, R.layout.activity_uri_handler); } @Override @@ -88,23 +103,17 @@ public class UriHandlerActivity extends AppCompatActivity { handleIntent(getIntent()); } - @Override - public void onSaveInstanceState(Bundle savedInstanceState) { - savedInstanceState.putBoolean("handled", this.handled); - super.onSaveInstanceState(savedInstanceState); - } - @Override public void onNewIntent(final Intent intent) { super.onNewIntent(intent); handleIntent(intent); } - private void handleUri(Uri uri) { - handleUri(uri, false); + private boolean handleUri(final Uri uri) { + return handleUri(uri, false); } - private void handleUri(Uri uri, final boolean scanned) { + 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); @@ -114,19 +123,22 @@ public class UriHandlerActivity extends AppCompatActivity { final Jid jid = xmppUri.getJid(); if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) { if (jid.getEscapedLocal() != null && accounts.contains(jid.asBareJid())) { - Toast.makeText(this, R.string.account_already_exists, Toast.LENGTH_LONG).show(); - return; + showError(R.string.account_already_exists); + return false; } intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth); startActivity(intent); - return; + return true; } if (accounts.size() == 0 && xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) { intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth); intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString()); startActivity(intent); - return; + return true; } + } else if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) { + showError(R.string.account_registrations_are_not_supported); + return false; } if (accounts.size() == 0) { @@ -134,26 +146,19 @@ public class UriHandlerActivity extends AppCompatActivity { intent = SignupUtils.getSignUpIntent(this); intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString()); startActivity(intent); + return true; } else { - Toast.makeText(this, R.string.invalid_jid, Toast.LENGTH_SHORT).show(); + showError(R.string.invalid_jid); + return false; } - - return; } if (xmppUri.isAction(XmppUri.ACTION_MESSAGE)) { - final Jid jid = xmppUri.getJid(); final String body = xmppUri.getBody(); if (jid != null) { - Class clazz; - try { - clazz = Class.forName("eu.siacs.conversations.ui.ShareViaAccountActivity"); - } catch (ClassNotFoundException e) { - clazz = null; - - } + final Class clazz = findShareViaAccountClass(); if (clazz != null) { intent = new Intent(this, clazz); intent.putExtra("contact", jid.toEscapedString()); @@ -164,7 +169,6 @@ public class UriHandlerActivity extends AppCompatActivity { intent.setData(uri); intent.putExtra("account", accounts.get(0).toEscapedString()); } - } else { intent = new Intent(this, ShareWithActivity.class); intent.setAction(Intent.ACTION_SEND); @@ -184,38 +188,96 @@ public class UriHandlerActivity extends AppCompatActivity { intent.putExtra("scanned", scanned); intent.setData(uri); } else { - Toast.makeText(this, R.string.invalid_jid, Toast.LENGTH_SHORT).show(); - return; + showError(R.string.invalid_jid); + return false; } - startActivity(intent); + return true; } - private void handleIntent(Intent data) { - if (handled) { - return; - } - if (data == null || data.getAction() == null) { - finish(); - return; + 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; + } + } + showError(R.string.no_xmpp_adddress_found); + } + }); + + } + + private boolean processLinkHeader(final String header) { + final Matcher matcher = LINK_HEADER_PATTERN.matcher(header); + if (matcher.find()) { + final String group = matcher.group(); + final String link = group.substring(1, group.length() - 1); + if (handleUri(Uri.parse(link))) { + finish(); + return true; + } } + return false; + } + + private void showError(@StringRes int error) { + this.binding.progress.setVisibility(View.INVISIBLE); + this.binding.error.setText(error); + this.binding.error.setVisibility(View.VISIBLE); + } - handled = true; + private static Class findShareViaAccountClass() { + try { + return Class.forName("eu.siacs.conversations.ui.ShareViaAccountActivity"); + } catch (final ClassNotFoundException e) { + return null; + } + } - switch (data.getAction()) { + private void handleIntent(final Intent data) { + final String action = data == null ? null : data.getAction(); + if (action == null) { + return; + } + switch (action) { + case Intent.ACTION_MAIN: + binding.progress.setVisibility(call != null && !call.isCanceled() ? View.VISIBLE : View.INVISIBLE); + break; case Intent.ACTION_VIEW: case Intent.ACTION_SENDTO: case Intent.ACTION_DIAL: case Intent.ACTION_CALL: - handleUri(data.getData()); + if (handleUri(data.getData())) { + finish(); + } break; case ACTION_SCAN_QR_CODE: - Intent intent = new Intent(this, ScanActivity.class); - startActivityForResult(intent, REQUEST_SCAN_QR_CODE); - return; + Log.d(Config.LOGTAG, "scan. allow=" + allowProvisioning()); + setIntent(createMainIntent()); + startActivityForResult(new Intent(this, ScanActivity.class), REQUEST_SCAN_QR_CODE); + break; } + } - finish(); + private Intent createMainIntent() { + final Intent intent = new Intent(Intent.ACTION_MAIN); + intent.putExtra(EXTRA_ALLOW_PROVISIONING, allowProvisioning()); + return intent; } private boolean allowProvisioning() { @@ -227,6 +289,7 @@ public class UriHandlerActivity extends AppCompatActivity { public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) { super.onActivityResult(requestCode, requestCode, intent); if (requestCode == REQUEST_SCAN_QR_CODE && resultCode == RESULT_OK) { + final boolean allowProvisioning = allowProvisioning(); final String result = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT); if (Strings.isNullOrEmpty(result)) { finish(); @@ -235,22 +298,38 @@ public class UriHandlerActivity extends AppCompatActivity { if (result.startsWith("BEGIN:VCARD\n")) { final Matcher matcher = V_CARD_XMPP_PATTERN.matcher(result); if (matcher.find()) { - handleUri(Uri.parse(matcher.group(2)), true); + if (handleUri(Uri.parse(matcher.group(2)), true)) { + finish(); + } + } else { + showError(R.string.no_xmpp_adddress_found); } - finish(); return; - } else if (QuickConversationsService.isConversations() && looksLikeJsonObject(result) && allowProvisioning()) { + } else if (QuickConversationsService.isConversations() && looksLikeJsonObject(result) && allowProvisioning) { ProvisioningUtils.provision(this, result); finish(); return; } - handleUri(Uri.parse(result), true); + final Uri uri = Uri.parse(result.trim()); + if (allowProvisioning && "https".equalsIgnoreCase(uri.getScheme()) && !XmppUri.INVITE_DOMAIN.equalsIgnoreCase(uri.getHost())) { + final HttpUrl httpUrl = HttpUrl.parse(uri.toString()); + if (httpUrl != null) { + checkForLinkHeader(httpUrl); + } else { + finish(); + } + } else if (handleUri(uri, true)) { + finish(); + } else { + setIntent(new Intent(Intent.ACTION_VIEW, uri)); + } + } else { + finish(); } - finish(); } private static boolean looksLikeJsonObject(final String input) { - final String trimmed = Strings.emptyToNull(input).trim(); + final String trimmed = Strings.nullToEmpty(input).trim(); return trimmed.charAt(0) == '{' && trimmed.charAt(trimmed.length() - 1) == '}'; } -} \ No newline at end of file +} diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 021ec4a5e34f6c636143f6e19b612d2e42456e7c..4b5382b444cf70a1781386916fd99b6b42a99800 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -80,6 +80,7 @@ import eu.siacs.conversations.ui.util.PresenceSelector; import eu.siacs.conversations.ui.util.SoftKeyboardUtils; import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.ExceptionHelper; +import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.utils.ThemeHelper; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; @@ -819,8 +820,9 @@ public abstract class XmppActivity extends ActionBarActivity { } @Override - public void onResume() { + protected void onResume(){ super.onResume(); + SettingsUtils.applyScreenshotPreventionSetting(this); } protected int findTheme() { diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java index 73f24fe1541208333a8b8c2a3b62591b6ec1627a..44a3835e0e881bc7feeb36c74a26032f72b9a9a4 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java @@ -75,8 +75,10 @@ public class MediaPreviewAdapter extends RecyclerView.Adapter { */ private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) { boolean startsWithQuote = false; - char previous = '\n'; - int lineStart = -1; - int lineTextStart = -1; - int quoteStart = -1; - for (int i = 0; i <= body.length(); i++) { - char current = body.length() > i ? body.charAt(i) : '\n'; - if (lineStart == -1) { - if (previous == '\n') { - if ((current == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(body, i)) - || current == '\u00bb' && !UIHelper.isPositionFollowedByQuote(body, i)) { - // Line start with quote - lineStart = i; - if (quoteStart == -1) quoteStart = i; - if (i == 0) startsWithQuote = true; - } else if (quoteStart >= 0) { - // Line start without quote, apply spans there - applyQuoteSpan(body, quoteStart, i - 1, darkBackground); - quoteStart = -1; + int quoteDepth = 1; + while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) { + char previous = '\n'; + int lineStart = -1; + int lineTextStart = -1; + int quoteStart = -1; + for (int i = 0; i <= body.length(); i++) { + char current = body.length() > i ? body.charAt(i) : '\n'; + if (lineStart == -1) { + if (previous == '\n') { + if (i < body.length() && QuoteHelper.isPositionQuoteStart(body, i)) { + // Line start with quote + lineStart = i; + if (quoteStart == -1) quoteStart = i; + if (i == 0) startsWithQuote = true; + } else if (quoteStart >= 0) { + // Line start without quote, apply spans there + applyQuoteSpan(body, quoteStart, i - 1, darkBackground); + quoteStart = -1; + } } - } - } else { - // Remove extra spaces between > and first character in the line - // > character will be removed too - if (current != ' ' && lineTextStart == -1) { - lineTextStart = i; - } - if (current == '\n') { - body.delete(lineStart, lineTextStart); - i -= lineTextStart - lineStart; - if (i == lineStart) { - // Avoid empty lines because span over empty line can be hidden - body.insert(i++, " "); + } else { + // Remove extra spaces between > and first character in the line + // > character will be removed too + if (current != ' ' && lineTextStart == -1) { + lineTextStart = i; + } + if (current == '\n') { + body.delete(lineStart, lineTextStart); + i -= lineTextStart - lineStart; + if (i == lineStart) { + // Avoid empty lines because span over empty line can be hidden + body.insert(i++, " "); + } + lineStart = -1; + lineTextStart = -1; } - lineStart = -1; - lineTextStart = -1; } + previous = current; } - previous = current; - } - if (quoteStart >= 0) { - // Apply spans to finishing open quote - applyQuoteSpan(body, quoteStart, body.length(), darkBackground); + if (quoteStart >= 0) { + // Apply spans to finishing open quote + applyQuoteSpan(body, quoteStart, body.length(), darkBackground); + } + quoteDepth++; } return startsWithQuote; } @@ -800,12 +804,12 @@ public class MessageAdapter extends ArrayAdapter { } else if (message.treatAsDownloadable()) { try { final URI uri = new URI(message.getBody()); - displayDownloadableMessage(viewHolder, - message, - activity.getString(R.string.check_x_filesize_on_host, - UIHelper.getFileDescriptionString(activity, message), - uri.getHost()), - darkBackground); + displayDownloadableMessage(viewHolder, + message, + activity.getString(R.string.check_x_filesize_on_host, + UIHelper.getFileDescriptionString(activity, message), + uri.getHost()), + darkBackground); } catch (Exception e) { displayDownloadableMessage(viewHolder, message, diff --git a/src/main/java/eu/siacs/conversations/ui/util/ActionBarUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ActionBarUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..80f0ae93eaad04a4a92410b0c56711bc6da65f12 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/util/ActionBarUtil.java @@ -0,0 +1,88 @@ +package eu.siacs.conversations.ui.util; + +import android.content.Context; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.reflect.Field; + +public class ActionBarUtil { + + public static void resetActionBarOnClickListeners(@NonNull View view) { + final View title = findActionBarTitle(view); + final View subtitle = findActionBarSubTitle(view); + if (title != null) { + title.setOnClickListener(null); + } + if (subtitle != null) { + subtitle.setOnClickListener(null); + } + } + + public static void setActionBarOnClickListener(@NonNull View view, + @NonNull final View.OnClickListener onClickListener) { + final View title = findActionBarTitle(view); + final View subtitle = findActionBarSubTitle(view); + if (title != null) { + title.setOnClickListener(onClickListener); + } + if (subtitle != null) { + subtitle.setOnClickListener(onClickListener); + } + } + + private static @Nullable View findActionBarTitle(@NonNull View root) { + return findActionBarItem(root, "action_bar_title", "mTitleTextView"); + } + + private static @Nullable + View findActionBarSubTitle(@NonNull View root) { + return findActionBarItem(root, "action_bar_subtitle", "mSubtitleTextView"); + } + + private static @Nullable View findActionBarItem(@NonNull View root, + @NonNull String resourceName, + @NonNull String toolbarFieldName) { + View result = findViewSupportOrAndroid(root, resourceName); + + if (result == null) { + View actionBar = findViewSupportOrAndroid(root, "action_bar"); + if (actionBar != null) { + result = reflectiveRead(actionBar, toolbarFieldName); + } + } + if (result == null && root.getClass().getName().endsWith("widget.Toolbar")) { + result = reflectiveRead(root, toolbarFieldName); + } + return result; + } + + @SuppressWarnings("ConstantConditions") + private static @Nullable View findViewSupportOrAndroid(@NonNull View root, + @NonNull String resourceName) { + Context context = root.getContext(); + View result = null; + if (result == null) { + int supportID = context.getResources().getIdentifier(resourceName, "id", context.getPackageName()); + result = root.findViewById(supportID); + } + if (result == null) { + int androidID = context.getResources().getIdentifier(resourceName, "id", "android"); + result = root.findViewById(androidID); + } + return result; + } + + @SuppressWarnings("unchecked") + private static T reflectiveRead(@NonNull Object object, @NonNull String fieldName) { + try { + Field field = object.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return (T) field.get(object); + } catch (final Exception ex) { + return null; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java index 4083d5b0450a156f96a65cb7e43edd3de7a96a01..b539c70efca6aafc6de8b998f323abbf055b06a1 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java +++ b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java @@ -136,10 +136,10 @@ public class Attachment implements Parcelable { return Collections.singletonList(new Attachment(uri, type, mime)); } - public static List of(final Context context, List uris) { - List attachments = new ArrayList<>(); - for (Uri uri : uris) { - final String mime = MimeUtils.guessMimeTypeFromUri(context, uri); + public static List of(final Context context, List uris, final String type) { + final List attachments = new ArrayList<>(); + for (final Uri uri : uris) { + final String mime = MimeUtils.guessMimeTypeFromUriAndMime(context, uri, type); attachments.add(new Attachment(uri, mime != null && isImage(mime) ? Type.IMAGE : Type.FILE, mime)); } return attachments; diff --git a/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java b/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..cf49be76703dcefb04958c979c4fc53c15b51b2e --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java @@ -0,0 +1,106 @@ +package eu.siacs.conversations.ui.util; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.utils.UIHelper; + +public class QuoteHelper { + + + public static final char QUOTE_CHAR = '>'; + public static final char QUOTE_END_CHAR = '<'; // used for one check, not for actual quoting + public static final char QUOTE_ALT_CHAR = '»'; + public static final char QUOTE_ALT_END_CHAR = '«'; + + public static boolean isPositionQuoteCharacter(CharSequence body, int pos) { + // second part of logical check actually goes against the logic indicated in the method name, since it also checks for context + // but it's very useful + return body.charAt(pos) == QUOTE_CHAR || isPositionAltQuoteStart(body, pos); + } + + public static boolean isPositionQuoteEndCharacter(CharSequence body, int pos) { + return body.charAt(pos) == QUOTE_END_CHAR; + } + + public static boolean isPositionAltQuoteCharacter(CharSequence body, int pos) { + return body.charAt(pos) == QUOTE_ALT_CHAR; + } + + public static boolean isPositionAltQuoteEndCharacter(CharSequence body, int pos) { + return body.charAt(pos) == QUOTE_ALT_END_CHAR; + } + + public static boolean isPositionAltQuoteStart(CharSequence body, int pos) { + return isPositionAltQuoteCharacter(body, pos) + && isPositionPrecededByPreQuote(body, pos) + && !isPositionFollowedByAltQuoteEnd(body, pos); + } + + public static boolean isPositionFollowedByQuoteChar(CharSequence body, int pos) { + return body.length() > pos + 1 && isPositionQuoteCharacter(body, pos + 1); + } + + /** + * 'Prequote' means anything we require or can accept in front of a QuoteChar. + */ + public static boolean isPositionPrecededByPreQuote(CharSequence body, int pos) { + return UIHelper.isPositionPrecededByLineStart(body, pos); + } + + public static boolean isPositionQuoteStart(CharSequence body, int pos) { + return (isPositionQuoteCharacter(body, pos) + && isPositionPrecededByPreQuote(body, pos) + && (UIHelper.isPositionFollowedByQuoteableCharacter(body, pos) + || isPositionFollowedByQuoteChar(body, pos))); + } + + public static boolean bodyContainsQuoteStart(CharSequence body) { + for (int i = 0; i < body.length(); i++) { + if (isPositionQuoteStart(body, i)) { + return true; + } + } + return false; + } + + public static boolean isPositionFollowedByAltQuoteEnd(CharSequence body, int pos) { + if (body.length() <= pos + 1 || Character.isWhitespace(body.charAt(pos + 1))) { + return false; + } + boolean previousWasWhitespace = false; + for (int i = pos + 1; i < body.length(); i++) { + char c = body.charAt(i); + if (c == '\n' || isPositionAltQuoteCharacter(body, i)) { + return false; + } else if (isPositionAltQuoteEndCharacter(body, i) && !previousWasWhitespace) { + return true; + } else { + previousWasWhitespace = Character.isWhitespace(c); + } + } + return false; + } + + public static boolean isNestedTooDeeply(CharSequence line) { + if (isPositionQuoteStart(line, 0)) { + int nestingDepth = 1; + for (int i = 1; i < line.length(); i++) { + if (isPositionQuoteStart(line, i)) { + nestingDepth++; + } + if (nestingDepth > (Config.QUOTING_MAX_DEPTH - 1)) { + return true; + } + } + } + return false; + } + + public static String replaceAltQuoteCharsInText(String text) { + for (int i = 0; i < text.length(); i++) { + if (isPositionAltQuoteStart(text, i)) { + text = text.substring(0, i) + QUOTE_CHAR + text.substring(i + 1); + } + } + return text; + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/Rationals.java b/src/main/java/eu/siacs/conversations/ui/util/Rationals.java new file mode 100644 index 0000000000000000000000000000000000000000..31155cd6ee8d038e4866f1e9d1374ec118aaa635 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/util/Rationals.java @@ -0,0 +1,26 @@ +package eu.siacs.conversations.ui.util; + +import android.util.Rational; + +public final class Rationals { + + //between 2.39:1 and 1:2.39 (inclusive). + private static final Rational MIN = new Rational(100,239); + private static final Rational MAX = new Rational(239,100); + + private Rationals() { + + } + + + public static Rational clip(final Rational input) { + if (input.compareTo(MIN) < 0) { + return MIN; + } + if (input.compareTo(MAX) > 0) { + return MAX; + } + return input; + } + +} diff --git a/src/main/java/eu/siacs/conversations/ui/util/SettingsUtils.java b/src/main/java/eu/siacs/conversations/ui/util/SettingsUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..ae99e094335798a7e3d7af52558e660dc0ce7b37 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/util/SettingsUtils.java @@ -0,0 +1,20 @@ +package eu.siacs.conversations.ui.util; + +import android.app.Activity; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.view.Window; +import android.view.WindowManager; + +public class SettingsUtils { + public static void applyScreenshotPreventionSetting(Activity activity){ + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); + boolean preventScreenshots = preferences.getBoolean("prevent_screenshots", false); + Window activityWindow = activity.getWindow(); + if(preventScreenshots){ + activityWindow.addFlags(WindowManager.LayoutParams.FLAG_SECURE); + } else { + activityWindow.clearFlags(WindowManager.LayoutParams.FLAG_SECURE); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/widget/DialpadView.java b/src/main/java/eu/siacs/conversations/ui/widget/DialpadView.java index 8f89064c60691d71a4557616670d64e9239f12e4..ccf91cad4bfc76b7a533a2ded7a29041b84f29bc 100644 --- a/src/main/java/eu/siacs/conversations/ui/widget/DialpadView.java +++ b/src/main/java/eu/siacs/conversations/ui/widget/DialpadView.java @@ -19,15 +19,17 @@ package eu.siacs.conversations.ui.widget; import android.content.Context; import android.util.AttributeSet; +import android.view.LayoutInflater; import android.view.View; import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.databinding.DataBindingUtil; +import eu.siacs.conversations.databinding.DialpadBinding; import eu.siacs.conversations.R; -import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; public class DialpadView extends ConstraintLayout implements View.OnClickListener { - protected java.util.function.Consumer clickConsumer = null; + protected Consumer clickConsumer = null; public DialpadView(Context context) { super(context); @@ -44,28 +46,18 @@ public class DialpadView extends ConstraintLayout implements View.OnClickListene init(); } - public void setClickConsumer(java.util.function.Consumer clickConsumer) { + public void setClickConsumer(Consumer clickConsumer) { this.clickConsumer = clickConsumer; } private void init() { - inflate(getContext(), R.layout.dialpad, this); - initViews(); - } - - private void initViews() { - findViewById(R.id.dialpad_1_holder).setOnClickListener(this); - findViewById(R.id.dialpad_2_holder).setOnClickListener(this); - findViewById(R.id.dialpad_3_holder).setOnClickListener(this); - findViewById(R.id.dialpad_4_holder).setOnClickListener(this); - findViewById(R.id.dialpad_5_holder).setOnClickListener(this); - findViewById(R.id.dialpad_6_holder).setOnClickListener(this); - findViewById(R.id.dialpad_7_holder).setOnClickListener(this); - findViewById(R.id.dialpad_8_holder).setOnClickListener(this); - findViewById(R.id.dialpad_9_holder).setOnClickListener(this); - findViewById(R.id.dialpad_0_holder).setOnClickListener(this); - findViewById(R.id.dialpad_asterisk_holder).setOnClickListener(this); - findViewById(R.id.dialpad_pound_holder).setOnClickListener(this); + DialpadBinding binding = DataBindingUtil.inflate( + LayoutInflater.from(getContext()), + R.layout.dialpad, + this, + true + ); + binding.setDialpadView(this); } @Override @@ -73,4 +65,8 @@ public class DialpadView extends ConstraintLayout implements View.OnClickListene clickConsumer.accept(v.getTag().toString()); } + // Based on java.util.function.Consumer to avoid Android 24 dependency + public interface Consumer { + void accept(T t); + } } diff --git a/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java b/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java index eadd562a709c0fc532f4140de7e92313c5d8ea0f..eba833c9bd7014db4cccb5bf27178e9c8010718b 100644 --- a/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java +++ b/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java @@ -24,6 +24,7 @@ import java.util.concurrent.Executors; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.ui.util.QuoteHelper; public class EditMessage extends EmojiWrapperEditText { @@ -142,7 +143,8 @@ public class EditMessage extends EmojiWrapperEditText { } public void insertAsQuote(String text) { - text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)", "$1> ").replaceAll("\n$", ""); + text = QuoteHelper.replaceAltQuoteCharsInText(text); + text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)(" + QuoteHelper.QUOTE_CHAR + ")", "$1$2$2").replaceAll("(^|\n)([^" + QuoteHelper.QUOTE_CHAR + "])", "$1> $2").replaceAll("\n$", ""); Editable editable = getEditableText(); int position = getSelectionEnd(); if (position == -1) position = editable.length(); diff --git a/src/main/java/eu/siacs/conversations/ui/widget/SurfaceViewRenderer.java b/src/main/java/eu/siacs/conversations/ui/widget/SurfaceViewRenderer.java new file mode 100644 index 0000000000000000000000000000000000000000..06f6040764a68300c9d26350d86e54334aef1c6e --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/widget/SurfaceViewRenderer.java @@ -0,0 +1,48 @@ +package eu.siacs.conversations.ui.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Rational; + +import eu.siacs.conversations.Config; + +public class SurfaceViewRenderer extends org.webrtc.SurfaceViewRenderer { + + private Rational aspectRatio = new Rational(1,1); + + private OnAspectRatioChanged onAspectRatioChanged; + + public SurfaceViewRenderer(Context context) { + super(context); + } + + public SurfaceViewRenderer(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void onFrameResolutionChanged(int videoWidth, int videoHeight, int rotation) { + super.onFrameResolutionChanged(videoWidth, videoHeight, rotation); + final int rotatedWidth = rotation != 0 && rotation != 180 ? videoHeight : videoWidth; + final int rotatedHeight = rotation != 0 && rotation != 180 ? videoWidth : videoHeight; + final Rational currentRational = this.aspectRatio; + this.aspectRatio = new Rational(rotatedWidth, rotatedHeight); + Log.d(Config.LOGTAG,"onFrameResolutionChanged("+rotatedWidth+","+rotatedHeight+","+aspectRatio+")"); + if (currentRational.equals(this.aspectRatio) || onAspectRatioChanged == null) { + return; + } + onAspectRatioChanged.onAspectRatioChanged(this.aspectRatio); + } + + public void setOnAspectRatioChanged(final OnAspectRatioChanged onAspectRatioChanged) { + this.onAspectRatioChanged = onAspectRatioChanged; + } + + public Rational getAspectRatio() { + return this.aspectRatio; + } + + public interface OnAspectRatioChanged { + void onAspectRatioChanged(final Rational rational); + } +} diff --git a/src/main/java/eu/siacs/conversations/utils/Android360pFormatStrategy.java b/src/main/java/eu/siacs/conversations/utils/Android360pFormatStrategy.java deleted file mode 100644 index a692cc6dca5aba382a64b026bffba0812235fe7d..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/utils/Android360pFormatStrategy.java +++ /dev/null @@ -1,76 +0,0 @@ -package eu.siacs.conversations.utils; - -import android.media.MediaCodecInfo; -import android.media.MediaFormat; -import android.os.Build; -import android.util.Log; - -import androidx.annotation.RequiresApi; - -import net.ypresto.androidtranscoder.format.MediaFormatExtraConstants; -import net.ypresto.androidtranscoder.format.MediaFormatStrategy; -import net.ypresto.androidtranscoder.format.OutputFormatUnavailableException; - -import eu.siacs.conversations.Config; - -public class Android360pFormatStrategy implements MediaFormatStrategy { - - private static final int LONGER_LENGTH = 640; - private static final int SHORTER_LENGTH = 360; - private static final int DEFAULT_VIDEO_BITRATE = 1000 * 1000; - private static final int DEFAULT_AUDIO_BITRATE = 128 * 1000; - private final int mVideoBitrate; - private final int mAudioBitrate; - private final int mAudioChannels; - - public Android360pFormatStrategy() { - mVideoBitrate = DEFAULT_VIDEO_BITRATE; - mAudioBitrate = DEFAULT_AUDIO_BITRATE; - mAudioChannels = 2; - } - - @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) - @Override - public MediaFormat createVideoOutputFormat(MediaFormat inputFormat) { - int width = inputFormat.getInteger(MediaFormat.KEY_WIDTH); - int height = inputFormat.getInteger(MediaFormat.KEY_HEIGHT); - int longer, shorter, outWidth, outHeight; - if (width >= height) { - longer = width; - shorter = height; - outWidth = LONGER_LENGTH; - outHeight = SHORTER_LENGTH; - } else { - shorter = width; - longer = height; - outWidth = SHORTER_LENGTH; - outHeight = LONGER_LENGTH; - } - if (longer * 9 != shorter * 16) { - throw new OutputFormatUnavailableException("This video is not 16:9, and is not able to transcode. (" + width + "x" + height + ")"); - } - if (shorter <= SHORTER_LENGTH) { - Log.d(Config.LOGTAG, "This video is less or equal to 360p, pass-through. (" + width + "x" + height + ")"); - return null; - } - MediaFormat format = MediaFormat.createVideoFormat("video/avc", outWidth, outHeight); - format.setInteger(MediaFormat.KEY_BIT_RATE, mVideoBitrate); - format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); - format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 3); - format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - format.setInteger(MediaFormat.KEY_PROFILE ,MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline); - format.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel13); - } - return format; - } - - @Override - public MediaFormat createAudioOutputFormat(MediaFormat inputFormat) { - final MediaFormat format = MediaFormat.createAudioFormat(MediaFormatExtraConstants.MIMETYPE_AUDIO_AAC, inputFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE), mAudioChannels); - format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); - format.setInteger(MediaFormat.KEY_BIT_RATE, mAudioBitrate); - return format; - } - -} diff --git a/src/main/java/eu/siacs/conversations/utils/Android720pFormatStrategy.java b/src/main/java/eu/siacs/conversations/utils/Android720pFormatStrategy.java deleted file mode 100644 index 274ebb76f12d356b72f7aa95a02289d0c2f103b5..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/utils/Android720pFormatStrategy.java +++ /dev/null @@ -1,76 +0,0 @@ -package eu.siacs.conversations.utils; - -import android.media.MediaCodecInfo; -import android.media.MediaFormat; -import android.os.Build; -import android.util.Log; - -import androidx.annotation.RequiresApi; - -import net.ypresto.androidtranscoder.format.MediaFormatExtraConstants; -import net.ypresto.androidtranscoder.format.MediaFormatStrategy; -import net.ypresto.androidtranscoder.format.OutputFormatUnavailableException; - -import eu.siacs.conversations.Config; - -public class Android720pFormatStrategy implements MediaFormatStrategy { - - private static final int LONGER_LENGTH = 1280; - private static final int SHORTER_LENGTH = 720; - private static final int DEFAULT_VIDEO_BITRATE = 2000 * 1000; - private static final int DEFAULT_AUDIO_BITRATE = 192 * 1000; - private final int mVideoBitrate; - private final int mAudioBitrate; - private final int mAudioChannels; - - public Android720pFormatStrategy() { - mVideoBitrate = DEFAULT_VIDEO_BITRATE; - mAudioBitrate = DEFAULT_AUDIO_BITRATE; - mAudioChannels = 2; - } - - @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) - @Override - public MediaFormat createVideoOutputFormat(MediaFormat inputFormat) { - int width = inputFormat.getInteger(MediaFormat.KEY_WIDTH); - int height = inputFormat.getInteger(MediaFormat.KEY_HEIGHT); - int longer, shorter, outWidth, outHeight; - if (width >= height) { - longer = width; - shorter = height; - outWidth = LONGER_LENGTH; - outHeight = SHORTER_LENGTH; - } else { - shorter = width; - longer = height; - outWidth = SHORTER_LENGTH; - outHeight = LONGER_LENGTH; - } - if (longer * 9 != shorter * 16) { - throw new OutputFormatUnavailableException("This video is not 16:9, and is not able to transcode. (" + width + "x" + height + ")"); - } - if (shorter <= SHORTER_LENGTH) { - Log.d(Config.LOGTAG, "This video is less or equal to 720p, pass-through. (" + width + "x" + height + ")"); - return null; - } - MediaFormat format = MediaFormat.createVideoFormat("video/avc", outWidth, outHeight); - format.setInteger(MediaFormat.KEY_BIT_RATE, mVideoBitrate); - format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); - format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 3); - format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - format.setInteger(MediaFormat.KEY_PROFILE ,MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline); - format.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel13); - } - return format; - } - - @Override - public MediaFormat createAudioOutputFormat(MediaFormat inputFormat) { - final MediaFormat format = MediaFormat.createAudioFormat(MediaFormatExtraConstants.MIMETYPE_AUDIO_AAC, inputFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE), mAudioChannels); - format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); - format.setInteger(MediaFormat.KEY_BIT_RATE, mAudioBitrate); - return format; - } - -} diff --git a/src/main/java/eu/siacs/conversations/utils/ExifHelper.java b/src/main/java/eu/siacs/conversations/utils/ExifHelper.java deleted file mode 100644 index ceda729331050141da1ca51392a6949729bfb5ba..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/utils/ExifHelper.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package eu.siacs.conversations.utils; - -import android.util.Log; - -import java.io.IOException; -import java.io.InputStream; - -public class ExifHelper { - private static final String TAG = "CameraExif"; - - public static int getOrientation(InputStream is) { - if (is == null) { - return 0; - } - - byte[] buf = new byte[8]; - int length = 0; - - // ISO/IEC 10918-1:1993(E) - while (read(is, buf, 2) && (buf[0] & 0xFF) == 0xFF) { - int marker = buf[1] & 0xFF; - - // Check if the marker is a padding. - if (marker == 0xFF) { - continue; - } - - // Check if the marker is SOI or TEM. - if (marker == 0xD8 || marker == 0x01) { - continue; - } - // Check if the marker is EOI or SOS. - if (marker == 0xD9 || marker == 0xDA) { - return 0; - } - - // Get the length and check if it is reasonable. - if (!read(is, buf, 2)) { - return 0; - } - length = pack(buf, 0, 2, false); - if (length < 2) { - Log.e(TAG, "Invalid length"); - return 0; - } - length -= 2; - - // Break if the marker is EXIF in APP1. - if (marker == 0xE1 && length >= 6) { - if (!read(is, buf, 6)) return 0; - length -= 6; - if (pack(buf, 0, 4, false) == 0x45786966 && - pack(buf, 4, 2, false) == 0) { - break; - } - } - - // Skip other markers. - try { - is.skip(length); - } catch (IOException ex) { - return 0; - } - length = 0; - } - - // JEITA CP-3451 Exif Version 2.2 - if (length > 8) { - int offset = 0; - byte[] jpeg = new byte[length]; - if (!read(is, jpeg, length)) { - return 0; - } - - // Identify the byte order. - int tag = pack(jpeg, offset, 4, false); - if (tag != 0x49492A00 && tag != 0x4D4D002A) { - Log.e(TAG, "Invalid byte order"); - return 0; - } - boolean littleEndian = (tag == 0x49492A00); - - // Get the offset and check if it is reasonable. - int count = pack(jpeg, offset + 4, 4, littleEndian) + 2; - if (count < 10 || count > length) { - Log.e(TAG, "Invalid offset"); - return 0; - } - offset += count; - length -= count; - - // Get the count and go through all the elements. - count = pack(jpeg, offset - 2, 2, littleEndian); - while (count-- > 0 && length >= 12) { - // Get the tag and check if it is orientation. - tag = pack(jpeg, offset, 2, littleEndian); - if (tag == 0x0112) { - // We do not really care about type and count, do we? - int orientation = pack(jpeg, offset + 8, 2, littleEndian); - switch (orientation) { - case 1: - return 0; - case 3: - return 180; - case 6: - return 90; - case 8: - return 270; - } - Log.i(TAG, "Unsupported orientation"); - return 0; - } - offset += 12; - length -= 12; - } - } - - Log.i(TAG, "Orientation not found"); - return 0; - } - - private static int pack(byte[] bytes, int offset, int length, - boolean littleEndian) { - int step = 1; - if (littleEndian) { - offset += length - 1; - step = -1; - } - - int value = 0; - while (length-- > 0) { - value = (value << 8) | (bytes[offset] & 0xFF); - offset += step; - } - return value; - } - - private static boolean read(InputStream is, byte[] buf, int length) { - try { - return is.read(buf, 0, length) == length; - } catch (IOException ex) { - return false; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/utils/LocationProvider.java b/src/main/java/eu/siacs/conversations/utils/LocationProvider.java index afb39a0089030c238ce1566aaaccf182b7ef459e..3eb786e39a18957a44920b79070384c4500c4098 100644 --- a/src/main/java/eu/siacs/conversations/utils/LocationProvider.java +++ b/src/main/java/eu/siacs/conversations/utils/LocationProvider.java @@ -4,6 +4,8 @@ import android.content.Context; import android.telephony.TelephonyManager; import android.util.Log; +import androidx.core.content.ContextCompat; + import org.osmdroid.util.GeoPoint; import java.io.BufferedReader; @@ -16,11 +18,14 @@ import eu.siacs.conversations.R; public class LocationProvider { - public static final GeoPoint FALLBACK = new GeoPoint(0.0,0.0); + public static final GeoPoint FALLBACK = new GeoPoint(0.0, 0.0); - public static String getUserCountry(Context context) { + public static String getUserCountry(final Context context) { try { - final TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + final TelephonyManager tm = ContextCompat.getSystemService(context, TelephonyManager.class); + if (tm == null) { + return getUserCountryFallback(); + } final String simCountry = tm.getSimCountryIso(); if (simCountry != null && simCountry.length() == 2) { // SIM country code is available return simCountry.toUpperCase(Locale.US); @@ -30,40 +35,41 @@ public class LocationProvider { return networkCountry.toUpperCase(Locale.US); } } - } catch (Exception e) { - // fallthrough + return getUserCountryFallback(); + } catch (final Exception e) { + return getUserCountryFallback(); } - Locale locale = Locale.getDefault(); + } + + private static String getUserCountryFallback() { + final Locale locale = Locale.getDefault(); return locale.getCountry(); } - public static GeoPoint getGeoPoint(Context context) { + public static GeoPoint getGeoPoint(final Context context) { return getGeoPoint(context, getUserCountry(context)); } - public static synchronized GeoPoint getGeoPoint(Context context, String country) { - try { - BufferedReader reader = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(R.raw.countries))); + public static synchronized GeoPoint getGeoPoint(final Context context, final String country) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(R.raw.countries)))) { String line; - while((line = reader.readLine()) != null) { - String[] parts = line.split("\\s+",4); + while ((line = reader.readLine()) != null) { + final String[] parts = line.split("\\s+", 4); if (parts.length == 4) { if (country.equalsIgnoreCase(parts[0])) { try { return new GeoPoint(Double.parseDouble(parts[1]), Double.parseDouble(parts[2])); - } catch (NumberFormatException e) { + } catch (final NumberFormatException e) { return FALLBACK; } } - } else { - Log.d(Config.LOGTAG,"unable to parse line="+line); } } - } catch (IOException e) { - Log.d(Config.LOGTAG,e.getMessage()); + } catch (final IOException e) { + Log.d(Config.LOGTAG, "unable to parse country->geo map", e); } return FALLBACK; } -} +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java index 3cb6338dbadfaa0ed0832737d7dae053adaa7a41..0a11cd720412476334f94f8ae127b89a11f4ddfd 100644 --- a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java @@ -39,6 +39,7 @@ import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.http.AesGcmURL; import eu.siacs.conversations.http.URL; +import eu.siacs.conversations.ui.util.QuoteHelper; public class MessageUtils { @@ -69,8 +70,7 @@ public class MessageUtils { continue; } final char c = line.charAt(0); - if (c == '>' && UIHelper.isPositionFollowedByQuoteableCharacter(line, 0) - || (c == '\u00bb' && !UIHelper.isPositionFollowedByQuote(line, 0))) { + if (QuoteHelper.isNestedTooDeeply(line)) { continue; } if (builder.length() != 0) { diff --git a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java index 53e2e1917f5f3eb9565eafb5dfdd19904c51ac4e..c0a6f4cfd17491a87f616f003e796836d7b37542 100644 --- a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java @@ -21,6 +21,8 @@ import android.net.Uri; import android.provider.OpenableColumns; import android.util.Log; +import com.google.common.base.Strings; + import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -247,6 +249,7 @@ public final class MimeUtils { add("audio/mpeg", "m4a"); add("audio/mpegurl", "m3u"); add("audio/ogg", "oga"); + add("audio/opus", "opus"); add("audio/prs.sid", "sid"); add("audio/x-aiff", "aif"); add("audio/x-aiff", "aiff"); @@ -273,6 +276,8 @@ public final class MimeUtils { add("image/ico", "ico"); add("image/ief", "ief"); add("image/heic", "heic"); + add("image/heif", "heif"); + add("image/avif", "avif"); // add ".jpg" first so it will be the default for guessExtensionFromMimeType add("image/jpeg", "jpg"); add("image/jpeg", "jpeg"); @@ -567,6 +572,8 @@ public final class MimeUtils { if (cursor != null && cursor.moveToFirst()) { return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); } + } catch (Exception e) { + return null; } return null; } @@ -584,22 +591,33 @@ public final class MimeUtils { } public static String extractRelevantExtension(final String path, final boolean ignoreCryptoExtension) { - if (path == null || path.isEmpty()) { + if (Strings.isNullOrEmpty(path)) { return null; } - String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase(); - int dotPosition = filename.lastIndexOf("."); + final String filenameQueryAnchor = path.substring(path.lastIndexOf('/') + 1); + final String filenameQuery = cutBefore(filenameQueryAnchor, '#'); + final String filename = cutBefore(filenameQuery, '?'); + final int dotPosition = filename.lastIndexOf('.'); - if (dotPosition != -1) { - String extension = filename.substring(dotPosition + 1); - // we want the real file extension, not the crypto one - if (ignoreCryptoExtension && Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) { - return extractRelevantExtension(filename.substring(0, dotPosition)); - } else { - return extension; - } + if (dotPosition == -1) { + return null; + } + final String extension = filename.substring(dotPosition + 1); + // we want the real file extension, not the crypto one + if (ignoreCryptoExtension && Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) { + return extractRelevantExtension(filename.substring(0, dotPosition)); + } else { + return extension; + } + } + + private static String cutBefore(final String input, final char c) { + final int position = input.indexOf(c); + if (position > 0) { + return input.substring(0, position); + } else { + return input; } - return null; } } diff --git a/src/main/java/eu/siacs/conversations/utils/Patterns.java b/src/main/java/eu/siacs/conversations/utils/Patterns.java index fae13aaea79f811be0a1f4c64fda72178e1d65b2..026951b22eed95f2943f46c8a44266e1dabe8058 100644 --- a/src/main/java/eu/siacs/conversations/utils/Patterns.java +++ b/src/main/java/eu/siacs/conversations/utils/Patterns.java @@ -254,6 +254,39 @@ public class Patterns { + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]" + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" + "|[1-9][0-9]|[0-9]))"); + + /** + * IPv6 address matcher for + * IPv6 addresses + * zero compressed IPv6 addresses (section 2.2 of rfc5952) + * link-local IPv6 addresses with zone index (section 11 of rfc4007) + * IPv4-Embedded IPv6 Address (section 2 of rfc6052) + * IPv4-mapped IPv6 addresses (section 2.1 of rfc2765) + * IPv4-translated addresses (section 2.1 of rfc2765) + * + * Taken from https://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses/17871737#17871737 + */ + public static final Pattern IP6_ADDRESS + = Pattern.compile( + "\\[" + + "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|" + + "([0-9a-fA-F]{1,4}:){1,7}:|" + + "([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|" + + "([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|" + + "([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|" + + "([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|" + + "([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|" + + "[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|" + + ":((:[0-9a-fA-F]{1,4}){1,7}|:)|" + + "fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|" + + "::(ffff(:0{1,4}){0,1}:){0,1}" + + "((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}" + + "(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|" + + "([0-9a-fA-F]{1,4}:){1,4}:" + + "((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}" + + "(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))" + + "\\]" + ); /** * Valid UCS characters defined in RFC 3987. Excludes space characters. */ @@ -296,7 +329,7 @@ public class Patterns { private static final String TLD = "(" + PUNYCODE_TLD + "|" + "[" + TLD_CHAR + "]{2,63}" +")"; private static final String HOST_NAME = "(" + IRI_LABEL + "\\.)+" + TLD; public static final Pattern DOMAIN_NAME - = Pattern.compile("(" + HOST_NAME + "|" + IP_ADDRESS + ")"); + = Pattern.compile("(" + HOST_NAME + "|" + IP6_ADDRESS + "|" + IP_ADDRESS +")"); private static final String PROTOCOL = "(?i:http|https|rtsp):\\/\\/"; /* A word boundary or end of input. This is to stop foo.sure from matching as foo.su */ private static final String WORD_BOUNDARY = "(?:\\b|$|^)"; @@ -306,7 +339,7 @@ public class Patterns { private static final String PORT_NUMBER = "\\:\\d{1,5}"; private static final String PATH_AND_QUERY = "\\/(?:(?:[" + LABEL_CHAR + "\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus optional query params - + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*"; + + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_\\$])|(?:\\%[a-fA-F0-9]{2}))*"; /** * Regular expression pattern to match most part of RFC 3987 * Internationalized URLs, aka IRIs. @@ -335,12 +368,12 @@ public class Patterns { * {@link #IP_ADDRESS} */ private static final Pattern STRICT_DOMAIN_NAME - = Pattern.compile("(?:" + STRICT_HOST_NAME + "|" + IP_ADDRESS + ")"); + = Pattern.compile("(?:" + STRICT_HOST_NAME + "|" + IP_ADDRESS + "|" + IP6_ADDRESS + ")"); /** * Regular expression that matches domain names without a TLD */ private static final String RELAXED_DOMAIN_NAME = - "(?:" + "(?:" + IRI_LABEL + "(?:\\.(?=\\S))" +"?)+" + "|" + IP_ADDRESS + ")"; + "(?:" + "(?:" + IRI_LABEL + "(?:\\.(?=\\S))" +"?)+" + "|" + IP_ADDRESS + "|" + IP6_ADDRESS + ")"; /** * Regular expression to match strings that do not start with a supported protocol. The TLDs * are expected to be one of the known TLDs. @@ -477,4 +510,4 @@ public class Patterns { * Do not create this static utility class. */ private Patterns() {} -} \ No newline at end of file +} diff --git a/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java b/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java index 0ef6ef55ea1a8d66330ae17ad1d119666343e957..2b9d42d7ad1c53215604d752ef24953555344555 100644 --- a/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java +++ b/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.utils; +import com.google.common.io.ByteStreams; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -12,76 +14,108 @@ import eu.siacs.conversations.Config; public class SocksSocketFactory { - private static final byte[] LOCALHOST = new byte[]{127,0,0,1}; + private static final byte[] LOCALHOST = new byte[]{127, 0, 0, 1}; + + public static void createSocksConnection(final Socket socket, final String destination, final int port) throws IOException { + //TODO use different Socks Addr Type if destination is IP or IPv6 + final InputStream proxyIs = socket.getInputStream(); + final OutputStream proxyOs = socket.getOutputStream(); + proxyOs.write(new byte[]{0x05, 0x01, 0x00}); + proxyOs.flush(); + final byte[] handshake = new byte[2]; + ByteStreams.readFully(proxyIs, handshake); + if (handshake[0] != 0x05 || handshake[1] != 0x00) { + throw new SocksConnectionException("Socks 5 handshake failed"); + } + final byte[] dest = destination.getBytes(); + final ByteBuffer request = ByteBuffer.allocate(7 + dest.length); + request.put(new byte[]{0x05, 0x01, 0x00, 0x03}); + request.put((byte) dest.length); + request.put(dest); + request.putShort((short) port); + proxyOs.write(request.array()); + proxyOs.flush(); + final byte[] response = new byte[4]; + ByteStreams.readFully(proxyIs, response); + final byte ver = response[0]; + if (ver != 0x05) { + throw new IOException(String.format("Unknown Socks version %02X ", ver)); + } + final byte status = response[1]; + final byte bndAddrType = response[3]; + final byte[] bndDestination = readDestination(bndAddrType, proxyIs); + final byte[] bndPort = new byte[2]; + if (bndAddrType == 0x03) { + final String receivedDestination = new String(bndDestination); + if (!receivedDestination.equalsIgnoreCase(destination)) { + throw new IOException(String.format("Destination mismatch. Received %s Expected %s", receivedDestination, destination)); + } + } + ByteStreams.readFully(proxyIs, bndPort); + if (status != 0x00) { + if (status == 0x04) { + throw new HostNotFoundException("Host unreachable"); + } + if (status == 0x05) { + throw new HostNotFoundException("Connection refused"); + } + throw new IOException(String.format("Unknown status code %02X ", status)); + } + } - public static void createSocksConnection(final Socket socket, final String destination, final int port) throws IOException { - final InputStream proxyIs = socket.getInputStream(); - final OutputStream proxyOs = socket.getOutputStream(); - proxyOs.write(new byte[]{0x05, 0x01, 0x00}); - proxyOs.flush(); - final byte[] handshake = new byte[2]; - proxyIs.read(handshake); - if (handshake[0] != 0x05 || handshake[1] != 0x00) { - throw new SocksConnectionException("Socks 5 handshake failed"); - } - final byte[] dest = destination.getBytes(); - final ByteBuffer request = ByteBuffer.allocate(7 + dest.length); - request.put(new byte[]{0x05, 0x01, 0x00, 0x03}); - request.put((byte) dest.length); - request.put(dest); - request.putShort((short) port); - proxyOs.write(request.array()); - proxyOs.flush(); - final byte[] response = new byte[7 + dest.length]; - proxyIs.read(response); - if (response[1] != 0x00) { - if (response[1] == 0x04) { - throw new HostNotFoundException("Host unreachable"); - } - if (response[1] == 0x05) { - throw new HostNotFoundException("Connection refused"); - } - throw new SocksConnectionException("Unable to connect to destination "+(int) (response[1])); - } - } + private static byte[] readDestination(final byte type, final InputStream inputStream) throws IOException { + final byte[] bndDestination; + if (type == 0x01) { + bndDestination = new byte[4]; + } else if (type == 0x03) { + final int length = inputStream.read(); + bndDestination = new byte[length]; + } else if (type == 0x04) { + bndDestination = new byte[16]; + } else { + throw new IOException(String.format("Unknown Socks address type %02X ", type)); + } + ByteStreams.readFully(inputStream, bndDestination); + return bndDestination; + } - public static boolean contains(byte needle, byte[] haystack) { - for(byte hay : haystack) { - if (hay == needle) { - return true; - } - } - return false; - } + public static boolean contains(byte needle, byte[] haystack) { + for (byte hay : haystack) { + if (hay == needle) { + return true; + } + } + return false; + } - private static Socket createSocket(InetSocketAddress address, String destination, int port) throws IOException { - Socket socket = new Socket(); - try { - socket.connect(address, Config.CONNECT_TIMEOUT * 1000); - } catch (IOException e) { - throw new SocksProxyNotFoundException(); - } - createSocksConnection(socket, destination, port); - return socket; - } + private static Socket createSocket(InetSocketAddress address, String destination, int port) throws IOException { + Socket socket = new Socket(); + try { + socket.connect(address, Config.CONNECT_TIMEOUT * 1000); + } catch (IOException e) { + throw new SocksProxyNotFoundException(); + } + createSocksConnection(socket, destination, port); + return socket; + } - public static Socket createSocketOverTor(String destination, int port) throws IOException { - return createSocket(new InetSocketAddress(InetAddress.getByAddress(LOCALHOST), 9050), destination, port); - } + public static Socket createSocketOverTor(String destination, int port) throws IOException { + return createSocket(new InetSocketAddress(InetAddress.getByAddress(LOCALHOST), 9050), destination, port); + } - private static class SocksConnectionException extends IOException { - SocksConnectionException(String message) { - super(message); - } - } + private static class SocksConnectionException extends IOException { + SocksConnectionException(String message) { + super(message); + } + } - public static class SocksProxyNotFoundException extends IOException { + public static class SocksProxyNotFoundException extends IOException { - } + } - public static class HostNotFoundException extends SocksConnectionException { - HostNotFoundException(String message) { - super(message); - } - } + public static class HostNotFoundException extends SocksConnectionException { + HostNotFoundException(String message) { + super(message); + } + } } diff --git a/src/main/java/eu/siacs/conversations/utils/TimeFrameUtils.java b/src/main/java/eu/siacs/conversations/utils/TimeFrameUtils.java index 1cb78db0cfe7b01ebf5174dfbf1bb5b595b70e69..9e7946d570b459cf465a5265f8f2b297c143b4eb 100644 --- a/src/main/java/eu/siacs/conversations/utils/TimeFrameUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/TimeFrameUtils.java @@ -71,10 +71,14 @@ public class TimeFrameUtils { public static String formatTimePassed(final long since, final long to, final boolean withMilliseconds) { final long passed = (since < 0) ? 0 : (to - since); - final int hours = (int) (passed / 3600000); - final int minutes = (int) (passed / 60000) % 60; - final int seconds = (int) (passed / 1000) % 60; - final int milliseconds = (int) (passed / 100) % 10; + return formatElapsedTime(passed, withMilliseconds); + } + + public static String formatElapsedTime(final long elapsed, final boolean withMilliseconds) { + final int hours = (int) (elapsed / 3600000); + final int minutes = (int) (elapsed / 60000) % 60; + final int seconds = (int) (elapsed / 1000) % 60; + final int milliseconds = (int) (elapsed / 100) % 10; if (hours > 0) { return String.format(Locale.ENGLISH, "%d:%02d:%02d", hours, minutes, seconds); } else if (withMilliseconds) { diff --git a/src/main/java/eu/siacs/conversations/utils/TranscoderStrategies.java b/src/main/java/eu/siacs/conversations/utils/TranscoderStrategies.java new file mode 100644 index 0000000000000000000000000000000000000000..0fb0766d1f1ff14c8c9f5809772c5251c1d717cb --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/TranscoderStrategies.java @@ -0,0 +1,41 @@ +package eu.siacs.conversations.utils; + +import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy; +import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy; + +public final class TranscoderStrategies { + + public static final DefaultVideoStrategy VIDEO_720P = DefaultVideoStrategy.atMost(720) + .bitRate(2L * 1000 * 1000) + .frameRate(30) + .keyFrameInterval(3F) + .build(); + + public static final DefaultVideoStrategy VIDEO_360P = DefaultVideoStrategy.atMost(360) + .bitRate(1000 * 1000) + .frameRate(30) + .keyFrameInterval(3F) + .build(); + + //TODO do we want to add 240p (@500kbs) and 1080p (@4mbs?) ? + // see suggested bit rates on https://www.videoproc.com/media-converter/bitrate-setting-for-h264.htm + + public static final DefaultAudioStrategy AUDIO_HQ = DefaultAudioStrategy.builder() + .bitRate(192 * 1000) + .channels(2) + .sampleRate(DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT) + .build(); + + public static final DefaultAudioStrategy AUDIO_MQ = DefaultAudioStrategy.builder() + .bitRate(128 * 1000) + .channels(2) + .sampleRate(DefaultAudioStrategy.SAMPLE_RATE_AS_INPUT) + .build(); + + //TODO if we add 144p we definitely want to add a lower audio bit rate as well + + private TranscoderStrategies() { + throw new IllegalStateException("Do not instantiate me"); + } + +} diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 6dcf517a0eb2de935097fc9368c9ebb325b2b8bb..26732b501e45733b3738f2fa20edb72ab3a64fb0 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -32,6 +32,7 @@ import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.services.ExportBackupService; +import eu.siacs.conversations.ui.util.QuoteHelper; import eu.siacs.conversations.xmpp.Jid; public class UIHelper { @@ -328,7 +329,7 @@ public class UIHelper { continue; } char first = l.charAt(0); - if ((first != '>' || !isPositionFollowedByQuoteableCharacter(l, 0)) && first != '\u00bb') { + if ((!QuoteHelper.isPositionQuoteStart(l, 0))) { CharSequence line = CharSequenceUtils.trim(l); if (line.length() == 0) { continue; @@ -372,6 +373,23 @@ public class UIHelper { return input.length() > 256 ? StylingHelper.subSequence(input, 0, 256) : input; } + public static boolean isPositionPrecededByBodyStart(CharSequence body, int pos){ + // true if not a single linebreak before current position + for (int i = pos - 1; i >= 0; i--){ + if (body.charAt(i) != ' '){ + return false; + } + } + return true; + } + + public static boolean isPositionPrecededByLineStart(CharSequence body, int pos){ + if (isPositionPrecededByBodyStart(body, pos)){ + return true; + } + return body.charAt(pos - 1) == '\n'; + } + public static boolean isPositionFollowedByQuoteableCharacter(CharSequence body, int pos) { return !isPositionFollowedByNumber(body, pos) && !isPositionFollowedByEmoticon(body, pos) @@ -404,6 +422,7 @@ public class UIHelper { final char first = body.charAt(pos + 1); return first == ';' || first == ':' + || first == '.' // do not quote >.< (but >>.<) || closingBeforeWhitespace(body, pos + 1); } } @@ -413,31 +432,13 @@ public class UIHelper { final char c = body.charAt(i); if (Character.isWhitespace(c)) { return false; - } else if (c == '<' || c == '>') { + } else if (QuoteHelper.isPositionQuoteCharacter(body, pos) || QuoteHelper.isPositionQuoteEndCharacter(body, pos)) { return body.length() == i + 1 || Character.isWhitespace(body.charAt(i + 1)); } } return false; } - public static boolean isPositionFollowedByQuote(CharSequence body, int pos) { - if (body.length() <= pos + 1 || Character.isWhitespace(body.charAt(pos + 1))) { - return false; - } - boolean previousWasWhitespace = false; - for (int i = pos + 1; i < body.length(); i++) { - char c = body.charAt(i); - if (c == '\n' || c == '»') { - return false; - } else if (c == '«' && !previousWasWhitespace) { - return true; - } else { - previousWasWhitespace = Character.isWhitespace(c); - } - } - return false; - } - public static String getDisplayName(MucOptions.User user) { Contact contact = user.getContact(); if (contact != null) { diff --git a/src/main/java/eu/siacs/conversations/utils/XmppUri.java b/src/main/java/eu/siacs/conversations/utils/XmppUri.java index 0343af4c90c83aaa4dbb4c365952f46c9a622771..59f8cb822448297641d99443349fcf6d682693f3 100644 --- a/src/main/java/eu/siacs/conversations/utils/XmppUri.java +++ b/src/main/java/eu/siacs/conversations/utils/XmppUri.java @@ -4,12 +4,15 @@ import android.net.Uri; import androidx.annotation.NonNull; +import com.google.common.base.CharMatcher; +import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -32,6 +35,8 @@ public class XmppUri { private Map parameters = Collections.emptyMap(); private boolean safeSource = true; + public static final String INVITE_DOMAIN = "conversations.im"; + public XmppUri(final String uri) { try { parse(Uri.parse(uri)); @@ -133,10 +138,10 @@ public class XmppUri { return; } this.uri = uri; - String scheme = uri.getScheme(); - String host = uri.getHost(); + final String scheme = uri.getScheme(); + final String host = uri.getHost(); List segments = uri.getPathSegments(); - if ("https".equalsIgnoreCase(scheme) && "conversations.im".equalsIgnoreCase(host)) { + if ("https".equalsIgnoreCase(scheme) && INVITE_DOMAIN.equalsIgnoreCase(host)) { if (segments.size() >= 2 && segments.get(1).contains("@")) { // sample : https://conversations.im/i/foo@bar.com try { @@ -167,7 +172,7 @@ public class XmppUri { } } this.fingerprints = parseFingerprints(parameters); - } else if ("imto".equalsIgnoreCase(scheme)) { + } else if ("imto".equalsIgnoreCase(scheme) && Arrays.asList("xmpp", "jabber").contains(uri.getHost())) { // sample: imto://xmpp/foo@bar.com try { jid = URLDecoder.decode(uri.getEncodedPath(), "UTF-8").split("/")[1].trim(); @@ -191,7 +196,10 @@ public class XmppUri { } public boolean isAction(final String action) { - return parameters.containsKey(action); + return Collections2.transform( + parameters.keySet(), + s -> CharMatcher.inRange('a', 'z').or(CharMatcher.inRange('A', 'Z')).retainFrom(s) + ).contains(action); } public Jid getJid() { diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index c0ece7f4cecd3634c59812ee7e3d9c343e49196e..4d53a17b723f3b179bf8446d82829b7ebc0b3348 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.xml; +import org.jetbrains.annotations.NotNull; + import java.util.ArrayList; import java.util.Hashtable; import java.util.List; @@ -165,8 +167,9 @@ public class Element { return this.attributes; } + @NotNull public String toString() { - StringBuilder elementOutput = new StringBuilder(); + final StringBuilder elementOutput = new StringBuilder(); if ((content == null) && (children.size() == 0)) { Tag emptyTag = Tag.empty(name); emptyTag.setAtttributes(this.attributes); diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index b0c4fe85c4e54dbe5d5522bf9c1ec2c08cf41020..09bbda4cdcb4dbbe7a08352f452f931bf7ea4866 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -28,6 +28,7 @@ public final class Namespace { public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0"; public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0"; public static final String JINGLE = "urn:xmpp:jingle:1"; + public static final String JINGLE_ERRORS = "urn:xmpp:jingle:errors:1"; public static final String JINGLE_MESSAGE = "urn:xmpp:jingle-message:0"; public static final String JINGLE_ENCRYPTED_TRANSPORT = "urn:xmpp:jingle:jet:0"; public static final String JINGLE_ENCRYPTED_TRANSPORT_OMEMO = "urn:xmpp:jingle:jet-omemo:0"; diff --git a/src/main/java/eu/siacs/conversations/xml/TagWriter.java b/src/main/java/eu/siacs/conversations/xml/TagWriter.java index 0e03fc1e8f19fb04321aed93a5d0e1a0440fddbd..2c2b8ac2cc1f1f5c3dc286760aef5ea514083f33 100644 --- a/src/main/java/eu/siacs/conversations/xml/TagWriter.java +++ b/src/main/java/eu/siacs/conversations/xml/TagWriter.java @@ -8,120 +8,129 @@ import java.io.OutputStreamWriter; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import eu.siacs.conversations.Config; import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; public class TagWriter { - private OutputStreamWriter outputStream; - private boolean finished = false; - private final LinkedBlockingQueue writeQueue = new LinkedBlockingQueue(); - private CountDownLatch stanzaWriterCountDownLatch = null; - - private final Thread asyncStanzaWriter = new Thread() { - - @Override - public void run() { - stanzaWriterCountDownLatch = new CountDownLatch(1); - while (!isInterrupted()) { - if (finished && writeQueue.size() == 0) { - break; - } - try { - AbstractStanza output = writeQueue.take(); - outputStream.write(output.toString()); - if (writeQueue.size() == 0) { - outputStream.flush(); - } - } catch (Exception e) { - break; - } - } - stanzaWriterCountDownLatch.countDown(); - } - - }; - - public TagWriter() { - } - - public synchronized void setOutputStream(OutputStream out) throws IOException { - if (out == null) { - throw new IOException(); - } - this.outputStream = new OutputStreamWriter(out); - } - - public TagWriter beginDocument() throws IOException { - if (outputStream == null) { - throw new IOException("output stream was null"); - } - outputStream.write(""); - outputStream.flush(); - return this; - } - - public synchronized TagWriter writeTag(Tag tag) throws IOException { - if (outputStream == null) { - throw new IOException("output stream was null"); - } - outputStream.write(tag.toString()); - outputStream.flush(); - return this; - } - - public synchronized TagWriter writeElement(Element element) throws IOException { - if (outputStream == null) { - throw new IOException("output stream was null"); - } - outputStream.write(element.toString()); - outputStream.flush(); - return this; - } - - public TagWriter writeStanzaAsync(AbstractStanza stanza) { - if (finished) { - Log.d(Config.LOGTAG,"attempting to write stanza to finished TagWriter"); - return this; - } else { - if (!asyncStanzaWriter.isAlive()) { - try { - asyncStanzaWriter.start(); - } catch (IllegalThreadStateException e) { - // already started - } - } - writeQueue.add(stanza); - return this; - } - } - - public void finish() { - this.finished = true; - } - - public boolean await(long timeout, TimeUnit timeunit) throws InterruptedException { - if (stanzaWriterCountDownLatch == null) { - return true; - } else { - return stanzaWriterCountDownLatch.await(timeout, timeunit); - } - } - - public boolean isActive() { - return outputStream != null; - } - - public synchronized void forceClose() { - asyncStanzaWriter.interrupt(); - if (outputStream != null) { - try { - outputStream.close(); - } catch (IOException e) { - //ignoring - } - } - outputStream = null; - } + private static final int FLUSH_DELAY = 400; + + private OutputStreamWriter outputStream; + private boolean finished = false; + private final LinkedBlockingQueue writeQueue = new LinkedBlockingQueue(); + private CountDownLatch stanzaWriterCountDownLatch = null; + + private final Thread asyncStanzaWriter = new Thread() { + + private final AtomicInteger batchStanzaCount = new AtomicInteger(0); + + @Override + public void run() { + stanzaWriterCountDownLatch = new CountDownLatch(1); + while (!isInterrupted()) { + if (finished && writeQueue.size() == 0) { + break; + } + try { + final AbstractStanza stanza = writeQueue.poll(FLUSH_DELAY, TimeUnit.MILLISECONDS); + if (stanza != null) { + batchStanzaCount.incrementAndGet(); + outputStream.write(stanza.toString()); + } else { + final int batch = batchStanzaCount.getAndSet(0); + if (batch > 1) { + Log.d(Config.LOGTAG, "flushing " + batch + " stanzas"); + } + outputStream.flush(); + final AbstractStanza nextStanza = writeQueue.take(); + batchStanzaCount.incrementAndGet(); + outputStream.write(nextStanza.toString()); + } + } catch (final Exception e) { + break; + } + } + stanzaWriterCountDownLatch.countDown(); + } + + }; + + public TagWriter() { + } + + public synchronized void setOutputStream(OutputStream out) throws IOException { + if (out == null) { + throw new IOException(); + } + this.outputStream = new OutputStreamWriter(out); + } + + public void beginDocument() throws IOException { + if (outputStream == null) { + throw new IOException("output stream was null"); + } + outputStream.write(""); + outputStream.flush(); + } + + public synchronized void writeTag(Tag tag) throws IOException { + if (outputStream == null) { + throw new IOException("output stream was null"); + } + outputStream.write(tag.toString()); + outputStream.flush(); + } + + public synchronized void writeElement(Element element) throws IOException { + if (outputStream == null) { + throw new IOException("output stream was null"); + } + outputStream.write(element.toString()); + outputStream.flush(); + } + + public void writeStanzaAsync(AbstractStanza stanza) { + if (finished) { + Log.d(Config.LOGTAG, "attempting to write stanza to finished TagWriter"); + } else { + if (!asyncStanzaWriter.isAlive()) { + try { + asyncStanzaWriter.start(); + } catch (IllegalThreadStateException e) { + // already started + } + } + writeQueue.add(stanza); + } + } + + public void finish() { + this.finished = true; + } + + public boolean await(long timeout, TimeUnit timeunit) throws InterruptedException { + if (stanzaWriterCountDownLatch == null) { + return true; + } else { + return stanzaWriterCountDownLatch.await(timeout, timeunit); + } + } + + public boolean isActive() { + return outputStream != null; + } + + public synchronized void forceClose() { + asyncStanzaWriter.interrupt(); + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + //ignoring + } + } + outputStream = null; + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index abe5d161f55c89a477470d1e9f230f51870d1b35..c3a3b153261acbaac29354b7493d76d7d8943df1 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -54,7 +54,6 @@ import javax.net.ssl.X509TrustManager; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.DomainHostnameVerifier; import eu.siacs.conversations.crypto.XmppDomainVerifier; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.sasl.Anonymous; 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 6b94f1f4dad24a540764315ef342cc9a7ae940c1..cbf4b85fd253a2519e94acef650b4b6c0f1dcadc 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -206,7 +206,7 @@ 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, "urn:xmpp:jingle:errors:1"); + error.addChild(jingleCondition, Namespace.JINGLE_ERRORS); account.getXmppConnection().sendIqPacket(response, null); } 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 ac499afc45787d52bc3a84f345ff7dd2e2070bea..46d89ec3f84e073f9704549ef8aeab915f6ef8fb 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.xmpp.jingle; -import android.os.SystemClock; import android.util.Log; import androidx.annotation.NonNull; @@ -8,6 +7,7 @@ import androidx.annotation.Nullable; import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.collect.Collections2; @@ -26,13 +26,15 @@ import org.webrtc.IceCandidate; import org.webrtc.PeerConnection; import org.webrtc.VideoTrack; -import java.util.ArrayDeque; 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; @@ -140,7 +142,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private final WebRTCWrapper webRTCWrapper = new WebRTCWrapper(this); - private final ArrayDeque>> pendingIceCandidates = new ArrayDeque<>(); + private final Queue> pendingIceCandidates = new LinkedList<>(); private final OmemoVerification omemoVerification = new OmemoVerification(); private final Message message; private State state = State.NULL; @@ -148,8 +150,9 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private Set proposedMedia; private RtpContentMap initiatorRtpContentMap; private RtpContentMap responderRtpContentMap; - private long rtpConnectionStarted = 0; //time of 'connected' - private long rtpConnectionEnded = 0; + private IceUdpTransportInfo.Setup peerDtlsSetup; + private final Stopwatch sessionDuration = Stopwatch.createUnstarted(); + private final Queue stateHistory = new LinkedList<>(); private ScheduledFuture ringingTimeoutFuture; JingleRtpConnection(JingleConnectionManager jingleConnectionManager, Id id, Jid initiator) { @@ -191,7 +194,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web @Override synchronized void deliverPacket(final JinglePacket jinglePacket) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": packet delivered to JingleRtpConnection"); switch (jinglePacket.getAction()) { case SESSION_INITIATE: receiveSessionInitiate(jinglePacket); @@ -230,12 +232,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - //FIXME: possible implementation public boolean applyDtmfTone(String tone) { return webRTCWrapper.applyDtmfTone(tone); } - private void receiveSessionTerminate(final JinglePacket jinglePacket) { respondOk(jinglePacket); final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason(); @@ -258,24 +258,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void receiveTransportInfo(final JinglePacket jinglePacket) { //Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to INITIALIZED only after transport-info has been received if (isInState(State.NULL, State.PROCEED, State.SESSION_INITIALIZED, State.SESSION_INITIALIZED_PRE_APPROVED, State.SESSION_ACCEPTED)) { - respondOk(jinglePacket); final RtpContentMap contentMap; try { contentMap = RtpContentMap.of(jinglePacket); - } catch (IllegalArgumentException | NullPointerException e) { + } catch (final IllegalArgumentException | NullPointerException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents; ignoring", e); + respondOk(jinglePacket); return; } - final Set> candidates = contentMap.contents.entrySet(); - if (this.state == State.SESSION_ACCEPTED) { - 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 { - pendingIceCandidates.push(candidates); - } + receiveTransportInfo(jinglePacket, contentMap); } else { if (isTerminated()) { respondOk(jinglePacket); @@ -287,41 +278,170 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - private void processCandidates(final Set> contents) { - final RtpContentMap rtpContentMap = isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap; - final Group originalGroup = rtpContentMap.group; - final List identificationTags = originalGroup == null ? rtpContentMap.getNames() : originalGroup.getIdentificationTags(); - if (identificationTags.size() == 0) { - Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices"); + private void receiveTransportInfo(final JinglePacket jinglePacket, final RtpContentMap contentMap) { + final Set> candidates = contentMap.contents.entrySet(); + if (this.state == State.SESSION_ACCEPTED) { + //zero candidates + modified credentials are an ICE restart offer + 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 { + respondOk(jinglePacket); + pendingIceCandidates.addAll(candidates); } - processCandidates(identificationTags, contents); } - private void processCandidates(final List indices, final Set> contents) { + private boolean checkForIceRestart(final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) { + final RtpContentMap existing = getRemoteContentMap(); + final IceUdpTransportInfo.Credentials existingCredentials; + final IceUdpTransportInfo.Credentials newCredentials; + try { + existingCredentials = existing.getCredentials(); + newCredentials = rtpContentMap.getCredentials(); + } catch (final IllegalStateException e) { + Log.d(Config.LOGTAG, "unable to gather credentials for comparison", e); + return false; + } + if (existingCredentials.equals(newCredentials)) { + return false; + } + //TODO an alternative approach is to check if we already got an iq result to our ICE-restart + // and if that's the case we are seeing an answer. + // This might be more spec compliant but also more error prone potentially + final boolean isOffer = rtpContentMap.emptyCandidates(); + final RtpContentMap restartContentMap; + try { + if (isOffer) { + Log.d(Config.LOGTAG, "received offer to restart ICE " + newCredentials); + restartContentMap = existing.modifiedCredentials(newCredentials, IceUdpTransportInfo.Setup.ACTPASS); + } else { + final IceUdpTransportInfo.Setup setup = getPeerDtlsSetup(); + Log.d(Config.LOGTAG, "received confirmation of ICE restart" + newCredentials + " peer_setup=" + setup); + // DTLS setup attribute needs to be rewritten to reflect current peer state + // https://groups.google.com/g/discuss-webrtc/c/DfpIMwvUfeM + restartContentMap = existing.modifiedCredentials(newCredentials, setup); + } + if (applyIceRestart(jinglePacket, restartContentMap, isOffer)) { + return isOffer; + } else { + Log.d(Config.LOGTAG, "ignoring ICE restart. sending tie-break"); + respondWithTieBreak(jinglePacket); + return true; + } + } catch (final Exception exception) { + respondOk(jinglePacket); + final Throwable rootCause = Throwables.getRootCause(exception); + if (rootCause instanceof WebRTCWrapper.PeerConnectionNotInitialized) { + //If this happens a termination is already in progress + Log.d(Config.LOGTAG, "ignoring PeerConnectionNotInitialized on ICE restart"); + return true; + } + Log.d(Config.LOGTAG, "failure to apply ICE restart", rootCause); + webRTCWrapper.close(); + sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage()); + return true; + } + } + + private IceUdpTransportInfo.Setup getPeerDtlsSetup() { + final IceUdpTransportInfo.Setup peerSetup = this.peerDtlsSetup; + if (peerSetup == null || peerSetup == IceUdpTransportInfo.Setup.ACTPASS) { + throw new IllegalStateException("Invalid peer setup"); + } + return peerSetup; + } + + private void storePeerDtlsSetup(final IceUdpTransportInfo.Setup setup) { + if (setup == null || setup == IceUdpTransportInfo.Setup.ACTPASS) { + throw new IllegalArgumentException("Trying to store invalid peer dtls setup"); + } + this.peerDtlsSetup = setup; + } + + private boolean applyIceRestart(final JinglePacket jinglePacket, final RtpContentMap restartContentMap, final boolean isOffer) throws ExecutionException, InterruptedException { + final SessionDescription sessionDescription = SessionDescription.of(restartContentMap); + final org.webrtc.SessionDescription.Type type = isOffer ? org.webrtc.SessionDescription.Type.OFFER : org.webrtc.SessionDescription.Type.ANSWER; + org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription(type, sessionDescription.toString()); + if (isOffer && webRTCWrapper.getSignalingState() != PeerConnection.SignalingState.STABLE) { + if (isInitiator()) { + //We ignore the offer and respond with tie-break. This will clause the responder not to apply the content map + return false; + } + } + webRTCWrapper.setRemoteDescription(sdp).get(); + setRemoteContentMap(restartContentMap); + if (isOffer) { + webRTCWrapper.setIsReadyToReceiveIceCandidates(false); + final SessionDescription localSessionDescription = setLocalSessionDescription(); + setLocalContentMap(RtpContentMap.of(localSessionDescription)); + //We need to respond OK before sending any candidates + respondOk(jinglePacket); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); + } else { + storePeerDtlsSetup(restartContentMap.getDtlsSetup()); + } + return true; + } + + private void processCandidates(final Set> contents) { for (final Map.Entry content : contents) { - final String ufrag = content.getValue().transport.getAttribute("ufrag"); - for (final IceUdpTransportInfo.Candidate candidate : content.getValue().transport.getCandidates()) { - final String sdp; - try { - sdp = candidate.toSdpAttribute(ufrag); - } catch (IllegalArgumentException e) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage()); - continue; - } - final String sdpMid = content.getKey(); - final int mLineIndex = indices.indexOf(sdpMid); - if (mLineIndex < 0) { - Log.w(Config.LOGTAG, "mLineIndex not found for " + sdpMid + ". available indices " + indices); - } - final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp); - Log.d(Config.LOGTAG, "received candidate: " + iceCandidate); - this.webRTCWrapper.addIceCandidate(iceCandidate); + processCandidate(content); + } + } + + private void processCandidate(final Map.Entry content) { + final RtpContentMap rtpContentMap = getRemoteContentMap(); + final List indices = toIdentificationTags(rtpContentMap); + final String sdpMid = content.getKey(); //aka content name + final IceUdpTransportInfo transport = content.getValue().transport; + final IceUdpTransportInfo.Credentials credentials = transport.getCredentials(); + + //TODO check that credentials remained the same + + for (final IceUdpTransportInfo.Candidate candidate : transport.getCandidates()) { + final String sdp; + try { + sdp = candidate.toSdpAttribute(credentials.ufrag); + } catch (final IllegalArgumentException e) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring invalid ICE candidate " + e.getMessage()); + continue; + } + final int mLineIndex = indices.indexOf(sdpMid); + if (mLineIndex < 0) { + Log.w(Config.LOGTAG, "mLineIndex not found for " + sdpMid + ". available indices " + indices); } + final IceCandidate iceCandidate = new IceCandidate(sdpMid, mLineIndex, sdp); + Log.d(Config.LOGTAG, "received candidate: " + iceCandidate); + this.webRTCWrapper.addIceCandidate(iceCandidate); + } + } + + private RtpContentMap getRemoteContentMap() { + return isInitiator() ? this.responderRtpContentMap : this.initiatorRtpContentMap; + } + + private List toIdentificationTags(final RtpContentMap rtpContentMap) { + final Group originalGroup = rtpContentMap.group; + final List identificationTags = originalGroup == null ? rtpContentMap.getNames() : originalGroup.getIdentificationTags(); + if (identificationTags.size() == 0) { + Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": no identification tags found in initial offer. we won't be able to calculate mLineIndices"); } + return identificationTags; } private ListenableFuture receiveRtpContentMap(final JinglePacket jinglePacket, final boolean expectVerification) { - final RtpContentMap receivedContentMap = RtpContentMap.of(jinglePacket); + final RtpContentMap receivedContentMap; + try { + receivedContentMap = RtpContentMap.of(jinglePacket); + } catch (final Exception e) { + return Futures.immediateFailedFuture(e); + } if (receivedContentMap instanceof OmemoVerifiedRtpContentMap) { final ListenableFuture> future = id.account.getAxolotlService().decrypt((OmemoVerifiedRtpContentMap) receivedContentMap, id.with); return Futures.transform(future, omemoVerifiedPayload -> { @@ -372,7 +492,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void receiveSessionInitiate(final JinglePacket jinglePacket, final RtpContentMap contentMap) { try { contentMap.requireContentDescriptions(); - contentMap.requireDTLSFingerprint(); + contentMap.requireDTLSFingerprint(true); } catch (final RuntimeException e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e)); respondOk(jinglePacket); @@ -400,11 +520,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } if (transition(target, () -> this.initiatorRtpContentMap = contentMap)) { respondOk(jinglePacket); - - final Set> candidates = contentMap.contents.entrySet(); - if (candidates.size() > 0) { - pendingIceCandidates.push(candidates); - } + pendingIceCandidates.addAll(contentMap.contents.entrySet()); if (target == State.SESSION_INITIALIZED_PRE_APPROVED) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": automatically accepting session-initiate"); sendSessionAccept(); @@ -473,6 +589,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private void receiveSessionAccept(final RtpContentMap contentMap) { this.responderRtpContentMap = contentMap; + this.storePeerDtlsSetup(contentMap.getDtlsSetup()); final SessionDescription sessionDescription; try { sessionDescription = SessionDescription.of(contentMap); @@ -491,11 +608,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } catch (final Exception e) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": unable to set remote description after receiving session-accept", Throwables.getRootCause(e)); webRTCWrapper.close(); - sendSessionTerminate(Reason.FAILED_APPLICATION); + sendSessionTerminate(Reason.FAILED_APPLICATION, Throwables.getRootCause(e).getMessage()); return; } - final List identificationTags = contentMap.group == null ? contentMap.getNames() : contentMap.group.getIdentificationTags(); - processCandidates(identificationTags, contentMap.contents.entrySet()); + processCandidates(contentMap.contents.entrySet()); } private void sendSessionAccept() { @@ -539,7 +655,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web try { this.webRTCWrapper.setRemoteDescription(sdp).get(); addIceCandidatesFromBlackLog(); - org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createAnswer().get(); + org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.setLocalDescription().get(); prepareSessionAccept(webRTCSessionDescription); } catch (final Exception e) { failureToAcceptSession(e); @@ -550,15 +666,17 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web if (isTerminated()) { return; } - Log.d(Config.LOGTAG, "unable to send session accept", Throwables.getRootCause(throwable)); + final Throwable rootCause = Throwables.getRootCause(throwable); + Log.d(Config.LOGTAG, "unable to send session accept", rootCause); webRTCWrapper.close(); - sendSessionTerminate(Reason.ofThrowable(throwable)); + sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage()); } private void addIceCandidatesFromBlackLog() { - while (!this.pendingIceCandidates.isEmpty()) { - processCandidates(this.pendingIceCandidates.poll()); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added candidates from back log"); + Map.Entry foo; + while ((foo = this.pendingIceCandidates.poll()) != null) { + processCandidate(foo); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": added candidate from back log"); } } @@ -566,12 +684,14 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap respondingRtpContentMap = RtpContentMap.of(sessionDescription); this.responderRtpContentMap = respondingRtpContentMap; + storePeerDtlsSetup(respondingRtpContentMap.getDtlsSetup().flip()); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); final ListenableFuture outgoingContentMapFuture = prepareOutgoingContentMap(respondingRtpContentMap); Futures.addCallback(outgoingContentMapFuture, new FutureCallback() { @Override public void onSuccess(final RtpContentMap outgoingContentMap) { - sendSessionAccept(outgoingContentMap, webRTCSessionDescription); + sendSessionAccept(outgoingContentMap); } @Override @@ -583,7 +703,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web ); } - private void sendSessionAccept(final RtpContentMap rtpContentMap, final org.webrtc.SessionDescription webRTCSessionDescription) { + private void sendSessionAccept(final RtpContentMap rtpContentMap) { if (isTerminated()) { Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session accept was too slow. already terminated. nothing to do."); return; @@ -591,11 +711,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web transitionOrThrow(State.SESSION_ACCEPTED); final JinglePacket sessionAccept = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId); send(sessionAccept); - try { - webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); - } catch (Exception e) { - failureToAcceptSession(e); - } } private ListenableFuture prepareOutgoingContentMap(final RtpContentMap rtpContentMap) { @@ -843,9 +958,10 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return; } try { - org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.createOffer().get(); + org.webrtc.SessionDescription webRTCSessionDescription = this.webRTCWrapper.setLocalDescription().get(); prepareSessionInitiate(webRTCSessionDescription, targetState); } catch (final Exception e) { + //TODO sending the error text is worthwhile as well. Especially for FailureToSet exceptions failureToInitiateSession(e, targetState); } } @@ -875,11 +991,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web final SessionDescription sessionDescription = SessionDescription.parse(webRTCSessionDescription.description); final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); this.initiatorRtpContentMap = rtpContentMap; + this.webRTCWrapper.setIsReadyToReceiveIceCandidates(true); final ListenableFuture outgoingContentMapFuture = encryptSessionInitiate(rtpContentMap); Futures.addCallback(outgoingContentMapFuture, new FutureCallback() { @Override public void onSuccess(final RtpContentMap outgoingContentMap) { - sendSessionInitiate(outgoingContentMap, webRTCSessionDescription, targetState); + sendSessionInitiate(outgoingContentMap, targetState); } @Override @@ -889,7 +1006,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web }, MoreExecutors.directExecutor()); } - private void sendSessionInitiate(final RtpContentMap rtpContentMap, final org.webrtc.SessionDescription webRTCSessionDescription, final State targetState) { + private void sendSessionInitiate(final RtpContentMap rtpContentMap, final State targetState) { if (isTerminated()) { Log.w(Config.LOGTAG, id.account.getJid().asBareJid() + ": preparing session was too slow. already terminated. nothing to do."); return; @@ -897,11 +1014,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.transitionOrThrow(targetState); final JinglePacket sessionInitiate = rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); send(sessionInitiate); - try { - this.webRTCWrapper.setLocalDescription(webRTCSessionDescription).get(); - } catch (Exception e) { - failureToInitiateSession(e, targetState); - } } private ListenableFuture encryptSessionInitiate(final RtpContentMap rtpContentMap) { @@ -967,36 +1079,48 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web private synchronized void handleIqResponse(final Account account, final IqPacket response) { if (response.getType() == IqPacket.TYPE.ERROR) { - final String errorCondition = response.getErrorCondition(); - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ-error from " + response.getFrom() + " in RTP session. " + errorCondition); - if (isTerminated()) { - Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); - return; - } - this.webRTCWrapper.close(); - final State target; - if (Arrays.asList( - "service-unavailable", - "recipient-unavailable", - "remote-server-not-found", - "remote-server-timeout" - ).contains(errorCondition)) { - target = State.TERMINATED_CONNECTIVITY_ERROR; - } else { - target = State.TERMINATED_APPLICATION_FAILURE; - } - transitionOrThrow(target); - this.finish(); - } else if (response.getType() == IqPacket.TYPE.TIMEOUT) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ timeout in RTP session with " + id.with + ". terminating with connectivity error"); - if (isTerminated()) { - Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); - return; - } - this.webRTCWrapper.close(); - transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR); - this.finish(); + handleIqErrorResponse(response); + return; + } + if (response.getType() == IqPacket.TYPE.TIMEOUT) { + handleIqTimeoutResponse(response); + } + } + + private void handleIqErrorResponse(final IqPacket response) { + Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR); + final String errorCondition = response.getErrorCondition(); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ-error from " + response.getFrom() + " in RTP session. " + errorCondition); + if (isTerminated()) { + Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); + return; + } + this.webRTCWrapper.close(); + final State target; + if (Arrays.asList( + "service-unavailable", + "recipient-unavailable", + "remote-server-not-found", + "remote-server-timeout" + ).contains(errorCondition)) { + target = State.TERMINATED_CONNECTIVITY_ERROR; + } else { + target = State.TERMINATED_APPLICATION_FAILURE; + } + transitionOrThrow(target); + this.finish(); + } + + private void handleIqTimeoutResponse(final IqPacket response) { + Preconditions.checkArgument(response.getType() == IqPacket.TYPE.TIMEOUT); + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": received IQ timeout in RTP session with " + id.with + ". terminating with connectivity error"); + if (isTerminated()) { + Log.i(Config.LOGTAG, id.account.getJid().asBareJid() + ": ignoring error because session was already terminated"); + return; } + this.webRTCWrapper.close(); + transitionOrThrow(State.TERMINATED_CONNECTIVITY_ERROR); + this.finish(); } private void terminateWithOutOfOrder(final JinglePacket jinglePacket) { @@ -1007,8 +1131,16 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web this.finish(); } + private void respondWithTieBreak(final JinglePacket jinglePacket) { + respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel"); + } + private void respondWithOutOfOrder(final JinglePacket jinglePacket) { - jingleConnectionManager.respondWithJingleError(id.account, jinglePacket, "out-of-order", "unexpected-request", "wait"); + respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait"); + } + + void respondWithJingleError(final IqPacket original, String jingleCondition, String condition, String conditionType) { + jingleConnectionManager.respondWithJingleError(id.account, original, jingleCondition, condition, conditionType); } private void respondOk(final JinglePacket jinglePacket) { @@ -1045,23 +1177,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return RtpEndUserState.CONNECTING; } case SESSION_ACCEPTED: - final PeerConnection.PeerConnectionState state; - try { - state = webRTCWrapper.getState(); - } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) { - //We usually close the WebRTCWrapper *before* transitioning so we might still - //be in SESSION_ACCEPTED even though the peerConnection has been torn down - return RtpEndUserState.ENDING_CALL; - } - if (state == PeerConnection.PeerConnectionState.CONNECTED) { - return RtpEndUserState.CONNECTED; - } else if (state == PeerConnection.PeerConnectionState.NEW || state == PeerConnection.PeerConnectionState.CONNECTING) { - return RtpEndUserState.CONNECTING; - } else if (state == PeerConnection.PeerConnectionState.CLOSED) { - return RtpEndUserState.ENDING_CALL; - } else { - return rtpConnectionStarted == 0 ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR; - } + return getPeerConnectionStateAsEndUserState(); case REJECTED: case REJECTED_RACED: case TERMINATED_DECLINED_OR_BUSY: @@ -1082,7 +1198,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return RtpEndUserState.RETRACTED; } case TERMINATED_CONNECTIVITY_ERROR: - return rtpConnectionStarted == 0 ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR; + return zeroDuration() ? RtpEndUserState.CONNECTIVITY_ERROR : RtpEndUserState.CONNECTIVITY_LOST_ERROR; case TERMINATED_APPLICATION_FAILURE: return RtpEndUserState.APPLICATION_ERROR; case TERMINATED_SECURITY_ERROR: @@ -1091,6 +1207,29 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web throw new IllegalStateException(String.format("%s has no equivalent EndUserState", this.state)); } + + private RtpEndUserState getPeerConnectionStateAsEndUserState() { + final PeerConnection.PeerConnectionState state; + try { + state = webRTCWrapper.getState(); + } catch (final WebRTCWrapper.PeerConnectionNotInitialized e) { + //We usually close the WebRTCWrapper *before* transitioning so we might still + //be in SESSION_ACCEPTED even though the peerConnection has been torn down + 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; + } + } + public Set getMedia() { final State current = getState(); if (current == State.NULL) { @@ -1333,7 +1472,13 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web @Override public void onIceCandidate(final IceCandidate iceCandidate) { - final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp); + final RtpContentMap rtpContentMap = isInitiator() ? this.initiatorRtpContentMap : this.responderRtpContentMap; + final String ufrag = rtpContentMap.getCredentials().ufrag; + final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttribute(iceCandidate.sdp, ufrag); + if (candidate == null) { + Log.d(Config.LOGTAG, "ignoring (not sending) candidate: " + iceCandidate.toString()); + return; + } Log.d(Config.LOGTAG, "sending candidate: " + iceCandidate.toString()); sendTransportInfo(iceCandidate.sdpMid, candidate); } @@ -1341,26 +1486,97 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web @Override public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": PeerConnectionState changed to " + newState); - if (newState == PeerConnection.PeerConnectionState.CONNECTED && this.rtpConnectionStarted == 0) { - this.rtpConnectionStarted = SystemClock.elapsedRealtime(); + this.stateHistory.add(newState); + if (newState == PeerConnection.PeerConnectionState.CONNECTED) { + this.sessionDuration.start(); + updateOngoingCallNotification(); + } else if (this.sessionDuration.isRunning()) { + this.sessionDuration.stop(); + updateOngoingCallNotification(); } - if (newState == PeerConnection.PeerConnectionState.CLOSED && this.rtpConnectionEnded == 0) { - this.rtpConnectionEnded = SystemClock.elapsedRealtime(); + + final boolean neverConnected = !this.stateHistory.contains(PeerConnection.PeerConnectionState.CONNECTED); + + if (newState == PeerConnection.PeerConnectionState.FAILED) { + if (neverConnected) { + if (isTerminated()) { + Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); + return; + } + webRTCWrapper.execute(this::closeWebRTCSessionAfterFailedConnection); + return; + } else { + webRTCWrapper.restartIce(); + } } - //TODO 'DISCONNECTED' might be an opportunity to renew the offer and send a transport-replace - //TODO exact syntax is yet to be determined but transport-replace sounds like the most reasonable - //as there is no content-replace - if (Arrays.asList(PeerConnection.PeerConnectionState.FAILED, PeerConnection.PeerConnectionState.DISCONNECTED).contains(newState)) { - if (isTerminated()) { - Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": not sending session-terminate after connectivity error because session is already in state " + this.state); + updateEndUserState(); + } + + @Override + public void onRenegotiationNeeded() { + this.webRTCWrapper.execute(this::initiateIceRestart); + } + + private void initiateIceRestart() { + //TODO discover new TURN/STUN credentials + this.stateHistory.clear(); + this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false); + final SessionDescription sessionDescription; + try { + sessionDescription = setLocalSessionDescription(); + } catch (final Exception e) { + final Throwable cause = Throwables.getRootCause(e); + Log.d(Config.LOGTAG, "failed to renegotiate", cause); + sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage()); + return; + } + final RtpContentMap rtpContentMap = RtpContentMap.of(sessionDescription); + final RtpContentMap transportInfo = rtpContentMap.transportInfo(); + final JinglePacket jinglePacket = transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); + Log.d(Config.LOGTAG, "initiating ice restart: " + jinglePacket); + jinglePacket.setTo(id.with); + xmppConnectionService.sendIqPacket(id.account, jinglePacket, (account, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + Log.d(Config.LOGTAG, "received success to our ice restart"); + setLocalContentMap(rtpContentMap); + webRTCWrapper.setIsReadyToReceiveIceCandidates(true); return; } - new Thread(this::closeWebRTCSessionAfterFailedConnection).start(); + if (response.getType() == IqPacket.TYPE.ERROR) { + final Element error = response.findChild("error"); + if (error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS)) { + Log.d(Config.LOGTAG, "received tie-break as result of ice restart"); + return; + } + handleIqErrorResponse(response); + } + if (response.getType() == IqPacket.TYPE.TIMEOUT) { + handleIqTimeoutResponse(response); + } + }); + } + + private void setLocalContentMap(final RtpContentMap rtpContentMap) { + if (isInitiator()) { + this.initiatorRtpContentMap = rtpContentMap; } else { - updateEndUserState(); + this.responderRtpContentMap = rtpContentMap; } } + private void setRemoteContentMap(final RtpContentMap rtpContentMap) { + if (isInitiator()) { + this.responderRtpContentMap = rtpContentMap; + } else { + this.initiatorRtpContentMap = rtpContentMap; + } + } + + private SessionDescription setLocalSessionDescription() throws ExecutionException, InterruptedException { + final org.webrtc.SessionDescription sessionDescription = this.webRTCWrapper.setLocalDescription().get(); + return SessionDescription.parse(sessionDescription.description); + } + private void closeWebRTCSessionAfterFailedConnection() { this.webRTCWrapper.close(); synchronized (this) { @@ -1372,12 +1588,12 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } } - public long getRtpConnectionStarted() { - return this.rtpConnectionStarted; + public boolean zeroDuration() { + return this.sessionDuration.elapsed(TimeUnit.NANOSECONDS) <= 0; } - public long getRtpConnectionEnded() { - return this.rtpConnectionEnded; + public long getCallDuration() { + return this.sessionDuration.elapsed(TimeUnit.MILLISECONDS); } public AppRTCAudioManager getAudioManager() { @@ -1424,8 +1640,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void updateOngoingCallNotification() { - if (STATES_SHOWING_ONGOING_CALL.contains(this.state)) { - xmppConnectionService.setOngoingCall(id, getMedia()); + final State state = this.state; + if (STATES_SHOWING_ONGOING_CALL.contains(state)) { + final boolean reconnecting; + if (state == State.SESSION_ACCEPTED) { + reconnecting = getPeerConnectionStateAsEndUserState() == RtpEndUserState.RECONNECTING; + } else { + reconnecting = false; + } + xmppConnectionService.setOngoingCall(id, getMedia(), reconnecting); } else { xmppConnectionService.removeOngoingCall(); } @@ -1504,8 +1727,7 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web } private void writeLogMessage(final State state) { - final long started = this.rtpConnectionStarted; - long duration = started <= 0 ? 0 : SystemClock.elapsedRealtime() - started; + final long duration = getCallDuration(); if (state == State.TERMINATED_SUCCESS || (state == State.TERMINATED_CONNECTIVITY_ERROR && duration > 0)) { writeLogMessageSuccess(duration); } else { @@ -1550,7 +1772,6 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web return webRTCWrapper.getRemoteVideoTrack(); } - public EglBase.Context getEglBaseContext() { return webRTCWrapper.getEglBaseContext(); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java index 6b9a6429ba3adaa85584f5223df731c003b67525..a57f4927ff8d65108d4563e14af7882eda8d5cc9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -3,6 +3,8 @@ package eu.siacs.conversations.xmpp.jingle; import android.os.PowerManager; import android.util.Log; +import com.google.common.io.ByteStreams; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -58,22 +60,12 @@ public class JingleSocks5Transport extends JingleTransport { } else { destBuilder.append(this.connection.getTransportId()); } - if (candidate.getType() == JingleCandidate.TYPE_PROXY) { - if (candidate.isOurs()) { - destBuilder.append(this.account.getJid()); - destBuilder.append(this.connection.getId().with); - } else { - destBuilder.append(this.connection.getId().with); - destBuilder.append(this.account.getJid()); - } + if (candidate.isOurs()) { + destBuilder.append(this.account.getJid()); + destBuilder.append(this.connection.getId().with); } else { - if (connection.isInitiator()) { - destBuilder.append(this.account.getJid()); - destBuilder.append(this.connection.getId().with); - } else { - destBuilder.append(this.connection.getId().with); - destBuilder.append(this.account.getJid()); - } + destBuilder.append(this.connection.getId().with); + destBuilder.append(this.account.getJid()); } messageDigest.reset(); this.destination = CryptoHelper.bytesToHex(messageDigest.digest(destBuilder.toString().getBytes())); @@ -114,26 +106,26 @@ public class JingleSocks5Transport extends JingleTransport { final byte[] authBegin = new byte[2]; final InputStream inputStream = socket.getInputStream(); final OutputStream outputStream = socket.getOutputStream(); - inputStream.read(authBegin); + ByteStreams.readFully(inputStream, authBegin); if (authBegin[0] != 0x5) { socket.close(); } final short methodCount = authBegin[1]; final byte[] methods = new byte[methodCount]; - inputStream.read(methods); + ByteStreams.readFully(inputStream, methods); if (SocksSocketFactory.contains((byte) 0x00, methods)) { outputStream.write(new byte[]{0x05, 0x00}); } else { outputStream.write(new byte[]{0x05, (byte) 0xff}); } - byte[] connectCommand = new byte[4]; - inputStream.read(connectCommand); + final byte[] connectCommand = new byte[4]; + ByteStreams.readFully(inputStream, connectCommand); if (connectCommand[0] == 0x05 && connectCommand[1] == 0x01 && connectCommand[3] == 0x03) { int destinationCount = inputStream.read(); final byte[] destination = new byte[destinationCount]; - inputStream.read(destination); + ByteStreams.readFully(inputStream, destination); final byte[] port = new byte[2]; - inputStream.read(port); + ByteStreams.readFully(inputStream, port); final String receivedDestination = new String(destination); final ByteBuffer response = ByteBuffer.allocate(7 + destination.length); final byte[] responseHeader; @@ -187,7 +179,7 @@ public class JingleSocks5Transport extends JingleTransport { socket.setSoTimeout(0); isEstablished = true; callback.established(); - } catch (IOException e) { + } catch (final IOException e) { callback.failed(); } }).start(); 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 3e02cc29b77d7ba1a5b0085b4de2bef285066117..21684a1657b242ce9db39553d18c27aa4c1f82f0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -1,13 +1,12 @@ package eu.siacs.conversations.xmpp.jingle; -import android.util.Log; - import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.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.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Sets; @@ -17,9 +16,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableDecl; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; -import eu.siacs.conversations.Config; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; @@ -97,6 +96,10 @@ public class RtpContentMap { } void requireDTLSFingerprint() { + requireDTLSFingerprint(false); + } + + void requireDTLSFingerprint(final boolean requireActPass) { if (this.contents.size() == 0) { throw new IllegalStateException("No contents available"); } @@ -106,9 +109,13 @@ public class RtpContentMap { if (fingerprint == null || Strings.isNullOrEmpty(fingerprint.getContent()) || Strings.isNullOrEmpty(fingerprint.getHash())) { throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s", entry.getKey())); } - if (Strings.isNullOrEmpty(fingerprint.getSetup())) { + final IceUdpTransportInfo.Setup setup = fingerprint.getSetup(); + if (setup == null) { throw new SecurityException(String.format("Use of DTLS-SRTP (XEP-0320) is required for content %s but missing setup attribute", entry.getKey())); } + if (requireActPass && setup != IceUdpTransportInfo.Setup.ACTPASS) { + throw new SecurityException("Initiator needs to offer ACTPASS as setup for DTLS-SRTP (XEP-0320)"); + } } } @@ -137,7 +144,56 @@ public class RtpContentMap { final IceUdpTransportInfo newTransportInfo = transportInfo.cloneWrapper(); newTransportInfo.addChild(candidate); return new RtpContentMap(null, ImmutableMap.of(contentName, new DescriptionTransport(null, newTransportInfo))); + } + + RtpContentMap transportInfo() { + return new RtpContentMap( + null, + Maps.transformValues(contents, dt -> new DescriptionTransport(null, dt.transport.cloneWrapper())) + ); + } + + public IceUdpTransportInfo.Credentials getCredentials() { + final Set allCredentials = ImmutableSet.copyOf(Collections2.transform( + contents.values(), + dt -> dt.transport.getCredentials() + )); + final IceUdpTransportInfo.Credentials credentials = Iterables.getFirst(allCredentials, null); + if (allCredentials.size() == 1 && credentials != null) { + return credentials; + } + throw new IllegalStateException("Content map does not have distinct credentials"); + } + public IceUdpTransportInfo.Setup getDtlsSetup() { + final Set setups = ImmutableSet.copyOf(Collections2.transform( + contents.values(), + dt -> dt.transport.getFingerprint().getSetup() + )); + final IceUdpTransportInfo.Setup setup = Iterables.getFirst(setups, null); + if (setups.size() == 1 && setup != null) { + return setup; + } + throw new IllegalStateException("Content map doesn't have distinct DTLS setup"); + } + + public boolean emptyCandidates() { + int count = 0; + for (DescriptionTransport descriptionTransport : contents.values()) { + count += descriptionTransport.transport.getCandidates().size(); + } + return count == 0; + } + + public RtpContentMap modifiedCredentials(IceUdpTransportInfo.Credentials credentials, final IceUdpTransportInfo.Setup setup) { + final ImmutableMap.Builder contentMapBuilder = new ImmutableMap.Builder<>(); + for (final Map.Entry content : contents.entrySet()) { + final RtpDescription rtpDescription = content.getValue().description; + IceUdpTransportInfo transportInfo = content.getValue().transport; + final IceUdpTransportInfo modifiedTransportInfo = transportInfo.modifyCredentials(credentials, setup); + contentMapBuilder.put(content.getKey(), new DescriptionTransport(rtpDescription, modifiedTransportInfo)); + } + return new RtpContentMap(this.group, contentMapBuilder.build()); } public static class DescriptionTransport { @@ -159,7 +215,6 @@ public class RtpContentMap { } else if (description instanceof RtpDescription) { rtpDescription = (RtpDescription) description; } else { - Log.d(Config.LOGTAG, "description was " + description); throw new UnsupportedApplicationException("Content does not contain rtp description"); } if (transportInfo instanceof IceUdpTransportInfo) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java index 61536bb7c19b849cb2cf140c8560447a27117f45..9a431bc011c7bb6758d3916ffcd240b6c0acb958 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpEndUserState.java @@ -4,6 +4,7 @@ public enum RtpEndUserState { INCOMING_CALL, //received a 'propose' message CONNECTING, //session-initiate or session-accepted but no webrtc peer connection yet CONNECTED, //session-accepted and webrtc peer connection is connected + RECONNECTING, //session-accepted and webrtc peer connection was connected once but is currently disconnected or failed FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet RINGING, //'propose' has been sent out and it has been 184 acked ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received 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 39031c4a9c2b5bc183cc3b8e7320785488cc2a03..e113146b135ed5beabbd61cd49b1a91babca520b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -156,7 +156,10 @@ public class SessionDescription { final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); if (fingerprint != null) { mediaAttributes.put("fingerprint", fingerprint.getHash() + " " + fingerprint.getContent()); - mediaAttributes.put("setup", fingerprint.getSetup()); + final IceUdpTransportInfo.Setup setup = fingerprint.getSetup(); + if (setup != null) { + mediaAttributes.put("setup", setup.toString().toLowerCase(Locale.ROOT)); + } } final ImmutableList.Builder formatBuilder = new ImmutableList.Builder<>(); for (RtpDescription.PayloadType payloadType : description.getPayloadTypes()) { 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 e50f67e2cb5cc731abcc443fb40075b6fda4d8b9..5c49b34e1934f99a60e9913a64139297e9503c30 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java @@ -51,7 +51,7 @@ class ToneManager { return ToneState.ENDING_CALL; } } - if (state == RtpEndUserState.CONNECTED) { + if (state == RtpEndUserState.CONNECTED || state == RtpEndUserState.RECONNECTING) { if (media.contains(Media.VIDEO)) { return ToneState.NULL; } else { 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 b561ee1af14fde5a7ae9031241a498d425f1dc7e..a579ccd77bb219f7a8ba8e2d5a6579656085482e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -19,7 +19,6 @@ import com.google.common.util.concurrent.SettableFuture; import org.webrtc.AudioSource; import org.webrtc.AudioTrack; -import org.webrtc.Camera1Enumerator; import org.webrtc.Camera2Enumerator; import org.webrtc.CameraEnumerationAndroid; import org.webrtc.CameraEnumerator; @@ -48,9 +47,14 @@ import org.webrtc.voiceengine.WebRtcAudioEffects; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedList; import java.util.List; +import java.util.Queue; import java.util.Map; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -63,7 +67,8 @@ public class WebRTCWrapper { private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName(); - //we should probably keep this in sync with: https://github.com/signalapp/Signal-Android/blob/master/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java#L296 + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + private static final Set HARDWARE_AEC_BLACKLIST = new ImmutableSet.Builder() .add("Pixel") .add("Pixel XL") @@ -77,6 +82,9 @@ public class WebRTCWrapper { .add("Redmi Note 5") .add("FP2") // Fairphone FP2 .add("MI 5") + .add("GT-I9515") // Samsung Galaxy S4 Value Edition (jfvelte) + .add("GT-I9515L") // Samsung Galaxy S4 Value Edition (jfvelte) + .add("GT-I9505") // Samsung Galaxy S4 (jfltexx) .build(); private static final int TONE_DURATION = 200; @@ -102,6 +110,8 @@ public class WebRTCWrapper { private static final int CAPTURING_MAX_FRAME_RATE = 30; private final EventCallback eventCallback; + private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false); + private final Queue iceCandidates = new LinkedList<>(); private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = new AppRTCAudioManager.AudioManagerEvents() { @Override public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { @@ -121,13 +131,13 @@ public class WebRTCWrapper { } @Override - public void onConnectionChange(PeerConnection.PeerConnectionState newState) { + public void onConnectionChange(final PeerConnection.PeerConnectionState newState) { eventCallback.onConnectionChange(newState); } @Override public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { - + Log.d(EXTENDED_LOGGING_TAG, "onIceConnectionChange(" + iceConnectionState + ")"); } @Override @@ -148,7 +158,11 @@ public class WebRTCWrapper { @Override public void onIceCandidate(IceCandidate iceCandidate) { - eventCallback.onIceCandidate(iceCandidate); + if (readyToReceivedIceCandidates.get()) { + eventCallback.onIceCandidate(iceCandidate); + } else { + iceCandidates.add(iceCandidate); + } } @Override @@ -173,7 +187,11 @@ public class WebRTCWrapper { @Override public void onRenegotiationNeeded() { - + Log.d(EXTENDED_LOGGING_TAG, "onRenegotiationNeeded()"); + final PeerConnection.PeerConnectionState currentState = peerConnection == null ? null : peerConnection.connectionState(); + if (currentState != null && currentState != PeerConnection.PeerConnectionState.NEW) { + eventCallback.onRenegotiationNeeded(); + } } @Override @@ -274,10 +292,7 @@ public class WebRTCWrapper { .createPeerConnectionFactory(); - 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; - rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN; + final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers); final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, peerConnectionObserver); if (peerConnection == null) { throw new InitializationException("Unable to create PeerConnection"); @@ -311,6 +326,31 @@ public class WebRTCWrapper { this.peerConnection = peerConnection; } + private static PeerConnection.RTCConfiguration buildConfiguration(final List iceServers) { + 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; + 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 restartIce() { + executorService.execute(() -> requirePeerConnection().restartIce()); + } + + public void setIsReadyToReceiveIceCandidates(final boolean ready) { + readyToReceivedIceCandidates.set(ready); + while (ready && iceCandidates.peek() != null) { + eventCallback.onIceCandidate(iceCandidates.poll()); + } + } + synchronized void close() { final PeerConnection peerConnection = this.peerConnection; final CapturerChoice capturerChoice = this.capturerChoice; @@ -425,70 +465,36 @@ public class WebRTCWrapper { videoTrack.setEnabled(enabled); } - ListenableFuture createOffer() { - return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { - final SettableFuture future = SettableFuture.create(); - peerConnection.createOffer(new CreateSdpObserver() { - @Override - public void onCreateSuccess(SessionDescription sessionDescription) { - future.set(sessionDescription); - } - - @Override - public void onCreateFailure(String s) { - future.setException(new IllegalStateException("Unable to create offer: " + s)); - } - }, new MediaConstraints()); - return future; - }, MoreExecutors.directExecutor()); - } - - ListenableFuture createAnswer() { + synchronized ListenableFuture setLocalDescription() { return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { final SettableFuture future = SettableFuture.create(); - peerConnection.createAnswer(new CreateSdpObserver() { + peerConnection.setLocalDescription(new SetSdpObserver() { @Override - public void onCreateSuccess(SessionDescription sessionDescription) { - future.set(sessionDescription); + public void onSetSuccess() { + final SessionDescription description = peerConnection.getLocalDescription(); + Log.d(EXTENDED_LOGGING_TAG, "set local description:"); + logDescription(description); + future.set(description); } @Override - public void onCreateFailure(String s) { - future.setException(new IllegalStateException("Unable to create answer: " + s)); + public void onSetFailure(final String message) { + future.setException(new FailureToSetDescriptionException(message)); } - }, new MediaConstraints()); + }); return future; }, MoreExecutors.directExecutor()); } - ListenableFuture setLocalDescription(final SessionDescription sessionDescription) { - Log.d(EXTENDED_LOGGING_TAG, "setting local description:"); + private static void logDescription(final SessionDescription sessionDescription) { for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { Log.d(EXTENDED_LOGGING_TAG, line); } - return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { - final SettableFuture future = SettableFuture.create(); - peerConnection.setLocalDescription(new SetSdpObserver() { - @Override - public void onSetSuccess() { - future.set(null); - } - - @Override - public void onSetFailure(final String s) { - future.setException(new IllegalArgumentException("unable to set local session description: " + s)); - - } - }, sessionDescription); - return future; - }, MoreExecutors.directExecutor()); } - ListenableFuture setRemoteDescription(final SessionDescription sessionDescription) { + synchronized ListenableFuture setRemoteDescription(final SessionDescription sessionDescription) { Log.d(EXTENDED_LOGGING_TAG, "setting remote description:"); - for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) { - Log.d(EXTENDED_LOGGING_TAG, line); - } + logDescription(sessionDescription); return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> { final SettableFuture future = SettableFuture.create(); peerConnection.setRemoteDescription(new SetSdpObserver() { @@ -498,9 +504,8 @@ public class WebRTCWrapper { } @Override - public void onSetFailure(String s) { - future.setException(new IllegalArgumentException("unable to set remote session description: " + s)); - + public void onSetFailure(final String message) { + future.setException(new FailureToSetDescriptionException(message)); } }, sessionDescription); return future; @@ -511,15 +516,18 @@ public class WebRTCWrapper { private ListenableFuture getPeerConnectionFuture() { final PeerConnection peerConnection = this.peerConnection; if (peerConnection == null) { - return Futures.immediateFailedFuture(new IllegalStateException("initialize PeerConnection first")); + return Futures.immediateFailedFuture(new PeerConnectionNotInitialized()); } else { return Futures.immediateFuture(peerConnection); } } - //TODO: remove - hack to test dtmfSending - public DtmfSender getDtmfSender() { - return peerConnection.getSenders().get(0).dtmf(); + private PeerConnection requirePeerConnection() { + final PeerConnection peerConnection = this.peerConnection; + if (peerConnection == null) { + throw new PeerConnectionNotInitialized(); + } + return peerConnection; } public boolean applyDtmfTone(String tone) { @@ -535,16 +543,8 @@ public class WebRTCWrapper { requirePeerConnection().addIceCandidate(iceCandidate); } - private CameraEnumerator getCameraEnumerator() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - return new Camera2Enumerator(requireContext()); - } else { - return new Camera1Enumerator(); - } - } - private Optional getVideoCapturer() { - final CameraEnumerator enumerator = getCameraEnumerator(); + final CameraEnumerator enumerator = new Camera2Enumerator(requireContext()); final Set deviceNames = ImmutableSet.copyOf(enumerator.getDeviceNames()); for (final String deviceName : deviceNames) { if (isFrontFacing(enumerator, deviceName)) { @@ -563,10 +563,15 @@ public class WebRTCWrapper { } } - public PeerConnection.PeerConnectionState getState() { + PeerConnection.PeerConnectionState getState() { return requirePeerConnection().connectionState(); } + public PeerConnection.SignalingState getSignalingState() { + return requirePeerConnection().signalingState(); + } + + EglBase.Context getEglBaseContext() { return this.eglBase.getEglBaseContext(); } @@ -579,14 +584,6 @@ public class WebRTCWrapper { return Optional.fromNullable(this.remoteVideoTrack); } - private PeerConnection requirePeerConnection() { - final PeerConnection peerConnection = this.peerConnection; - if (peerConnection == null) { - throw new PeerConnectionNotInitialized(); - } - return peerConnection; - } - private Context requireContext() { final Context context = this.context; if (context == null) { @@ -599,12 +596,18 @@ public class WebRTCWrapper { return appRTCAudioManager; } + void execute(final Runnable command) { + executorService.execute(command); + } + public interface EventCallback { void onIceCandidate(IceCandidate iceCandidate); void onConnectionChange(PeerConnection.PeerConnectionState newState); void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices); + + void onRenegotiationNeeded(); } private static abstract class SetSdpObserver implements SdpObserver { @@ -655,6 +658,12 @@ public class WebRTCWrapper { } + private static class FailureToSetDescriptionException extends IllegalArgumentException { + public FailureToSetDescriptionException(String message) { + super(message); + } + } + private static class CapturerChoice { private final CameraVideoCapturer cameraVideoCapturer; private final CameraEnumerationAndroid.CaptureFormat captureFormat; 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 022c4d2dd309c24e7c3216265a0956fc402dd535..45260cafb44a4f9ee89ada72933499ee585b25d4 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -1,6 +1,8 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; 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.Strings; import com.google.common.collect.ArrayListMultimap; @@ -8,6 +10,8 @@ import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.Hashtable; import java.util.LinkedHashMap; @@ -58,6 +62,12 @@ public class IceUdpTransportInfo extends GenericTransportInfo { return fingerprint == null ? null : Fingerprint.upgrade(fingerprint); } + public Credentials getCredentials() { + final String ufrag = this.getAttribute("ufrag"); + final String password = this.getAttribute("pwd"); + return new Credentials(ufrag, password); + } + public List getCandidates() { final ImmutableList.Builder builder = new ImmutableList.Builder<>(); for (final Element child : getChildren()) { @@ -74,6 +84,53 @@ public class IceUdpTransportInfo extends GenericTransportInfo { return transportInfo; } + public IceUdpTransportInfo modifyCredentials(final Credentials credentials, final Setup setup) { + final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); + transportInfo.setAttribute("ufrag", credentials.ufrag); + transportInfo.setAttribute("pwd", credentials.password); + for (final Element child : getChildren()) { + if (child.getName().equals("fingerprint") && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) { + final Fingerprint fingerprint = new Fingerprint(); + fingerprint.setAttributes(new Hashtable<>(child.getAttributes())); + fingerprint.setContent(child.getContent()); + fingerprint.setAttribute("setup", setup.toString().toLowerCase(Locale.ROOT)); + transportInfo.addChild(fingerprint); + } + } + return transportInfo; + } + + public static class Credentials { + public final String ufrag; + public final String password; + + public Credentials(String ufrag, String password) { + this.ufrag = ufrag; + this.password = password; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Credentials that = (Credentials) o; + return Objects.equal(ufrag, that.ufrag) && Objects.equal(password, that.password); + } + + @Override + public int hashCode() { + return Objects.hashCode(ufrag, password); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("ufrag", ufrag) + .add("password", password) + .toString(); + } + } + public static class Candidate extends Element { private Candidate() { @@ -89,7 +146,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo { } // https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-39#section-5.1 - public static Candidate fromSdpAttribute(final String attribute) { + 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(" "); @@ -105,6 +162,10 @@ public class IceUdpTransportInfo extends GenericTransportInfo { 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); @@ -285,8 +346,31 @@ public class IceUdpTransportInfo extends GenericTransportInfo { return this.getAttribute("hash"); } - public String getSetup() { - return this.getAttribute("setup"); + public Setup getSetup() { + final String setup = this.getAttribute("setup"); + return setup == null ? null : Setup.of(setup); + } + } + + public enum Setup { + ACTPASS, PASSIVE, ACTIVE; + + public static Setup of(String setup) { + try { + return valueOf(setup.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + return null; + } + } + + public Setup flip() { + if (this == PASSIVE) { + return ACTIVE; + } + if (this == ACTIVE) { + return PASSIVE; + } + throw new IllegalStateException(this.name()+" can not be flipped"); } } } diff --git a/src/main/res/layout/activity_rtp_session.xml b/src/main/res/layout/activity_rtp_session.xml index c171d739e56e2e92ac97c9393bd37bb4538f84a0..18beba4548637a0ac421775f4a9ffb2a79e65f82 100644 --- a/src/main/res/layout/activity_rtp_session.xml +++ b/src/main/res/layout/activity_rtp_session.xml @@ -119,13 +119,13 @@ android:gravity="center" android:visibility="gone"> - - + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/dialpad.xml b/src/main/res/layout/dialpad.xml index 2a2208c945ed3b3febe4b4ae03e8dd743d990c12..4a18363fe67aa16381b38bfbac7b56856bf4c356 100644 --- a/src/main/res/layout/dialpad.xml +++ b/src/main/res/layout/dialpad.xml @@ -1,368 +1,385 @@ - - - - - - - - + + + + + - - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - + android:background="?attr/selectableItemBackgroundBorderless" + app:layout_constraintBottom_toBottomOf="@+id/dialpad_0_holder" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/dialpad_0_holder" + app:layout_constraintTop_toTopOf="@+id/dialpad_0_holder"> + + + + + diff --git a/src/main/res/menu/activity_rtp_session.xml b/src/main/res/menu/activity_rtp_session.xml index 92c9540cf3c7ca0b2fba0bc21c240eb79ca24bfe..420896b091fa75c48dab19dd78b00a54263cc532 100644 --- a/src/main/res/menu/activity_rtp_session.xml +++ b/src/main/res/menu/activity_rtp_session.xml @@ -18,6 +18,4 @@ android:icon="?attr/icon_goto_chat" android:title="@string/switch_to_conversation" app:showAsAction="ifRoom" /> - - - \ No newline at end of file + diff --git a/src/main/res/menu/start_conversation_fab_submenu.xml b/src/main/res/menu/start_conversation_fab_submenu.xml index bfaca0727684c50016c5558f75a0d0e0282657fd..2cf545d681c821da29bfbba7b9aba202a5e0dcb2 100644 --- a/src/main/res/menu/start_conversation_fab_submenu.xml +++ b/src/main/res/menu/start_conversation_fab_submenu.xml @@ -2,22 +2,22 @@ + android:icon="@drawable/ic_search_white_24dp" + android:title="@string/discover_channels" /> + android:icon="@drawable/ic_input_white_24dp" + android:title="@string/join_public_channel" /> + android:icon="@drawable/ic_public_white_24dp" + android:title="@string/create_public_channel" /> + android:icon="@drawable/ic_group_white_24dp" + android:title="@string/create_private_group_chat" /> + android:icon="@drawable/ic_person_white_48dp" + android:title="@string/add_contact" /> \ No newline at end of file diff --git a/src/main/res/values-ar/strings.xml b/src/main/res/values-ar/strings.xml index 4dd5d52482e389c51363109adcfd6f8f0c4bc113..197b23a2657801549a9ca3190332e0c6244cb3b5 100644 --- a/src/main/res/values-ar/strings.xml +++ b/src/main/res/values-ar/strings.xml @@ -4,6 +4,7 @@ محادثة جديدة إدارة الحسابات إدارة الحساب + أغلق المحادثة بيانات جهة الإتصال تفاصيل مجموعة المحادثة تفاصيل القناة @@ -27,7 +28,7 @@ قائمة المحجوبين الآن منذ 1 دقيقة - دقائق %d منذ + منذ %d دقائق ارسال حل شيفرة الرسالة. الرجاء الإنتظار ... رسالة مشفرة عبر OpenPGP @@ -38,12 +39,14 @@ مشرف مشترك زائر + هل تريد حذف %sمن قائمة إتصالك؟ المحادثات مع هذا الشخص لن تحذف. هل ترغب في حجب %s من ارسال الرسائل لك? هل ترغب في انهاء حجب %s والسماح له بمراسلتك? هل تريد حجب جميع جهات الإتصال من %s? الغاء حجب جميع جهات الإتصال من %s? جهة الاتصال محجوبه محجوب + هل تريد حذف %sمن قائمة المفضلة؟ المحادثات مع هذا المفضل لن تحذف. تسجيل حساب جديد في سيرفر تغيير كلمة المرور في سيرفر مشاركة مع @@ -61,11 +64,16 @@ الغاء حجب حفظ موافق + %1$sتعطّل ارسال الآن لا تسألني ثانية + لا يمكن الإتصال بالحساب + لايمكن الإتصال بحسابات متعددة ارفاق ملف اضافة جهة اتصال فشل التسليم + الإستعداد لإرسال الصورة + الإستعداد لإرسال الصور جاري إرسال الملفات. الرجاء الإنتظار ... حذف سجل المحفوظات حذف سجل المحفوظات للمحادثة @@ -77,6 +85,7 @@ إرسال رسالة مشفرة عبر OMEMO إبعث رسالة مشفَّرة بـ أومي مو OMEMO إرسال رسالة مشفرة عبر OpenPGP + إسم مستخدم جديد تحت الإستعمال إرسال بدون تشفير فشل فك التشفير. ربما ليس لديك المفتاح الخاص الصحيح. OpenKeychain @@ -96,6 +105,7 @@ إهتز عند وصول رسالة جديدة إشعار ضوئي التنبيه الصوتي + تنبيه صوتي فترة السماح متقدم لا ترسل تقارير أخطاء @@ -112,6 +122,7 @@ اختيار صورة التقاط صورة الملف الذي حددته ليس صورة + لا يمكن تحويل ملف الصورة الملف غير موجود غير معروف معطلٌ موقتاً @@ -145,6 +156,7 @@ احجب عنوان XMPP username@example.com كلمة السر + الذاكرة مليئة، صورة كبيرة جدا هل تود إضافة %s إلى سجل عناوينك ؟ معلومات عن المضيف XEP-0313: إدارة أرشيف الرسائل @@ -159,6 +171,7 @@ متاح غير متاح آخر ظهور الآن + آخر مشاهدة منذ دقيقة آخر ظهور منذ %d دقيقة آخر ظهور منذ %d ساعة آخر ظهور منذ %d يوم @@ -642,4 +655,5 @@ افتح النسخة الاحتياطية يرجى إدخال الكلمة السرية للحساب مشغول + خيارات أخرى diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml index d27eda1f8d3500851a1822a222741bdba6ee0e2e..ee4687dfa13d9d9e38e55294d4fb48dc86470cad 100644 --- a/src/main/res/values-bg/strings.xml +++ b/src/main/res/values-bg/strings.xml @@ -29,10 +29,17 @@ току-що преди 1 минута преди %d минути + + %d непрочетен разговор + + + %d непрочетени разговора + + изпращане… Дешифроване на съобщението. Моля, изчакайте… Съобщение, шифр. чрез OpenPGP - Псевдонимът вече се използва + Псевдонимът е зает Неправилен псевдоним Администратор Собственик @@ -52,6 +59,7 @@ Споделяне с… Започване на разговор Поканете контакт + Поканете Контакти Контакт Отказ @@ -63,14 +71,23 @@ Деблокиране Запазване Добре + %1$s се срина + Използването на Вашия профил в XMPP за изпращане на проследявания на стека помага на разработката на %1$s. Изпращане сега Не ме питайте повече + Свързването с профила е невъзможно + Свързването с няколко профила е невъзможно + Докоснете за управление на профилите си Прикачане на файл + Добавяне на този липсващ контакт към списъка с контакти? Добавяне на контакт доставянето се провали + Изображението се подготвя за изпращане + Изображенията се подготвят за изпращане Споделяне на файлове. Моля, изчакайте… Изчистване на историята Изчистване на историята на разговорите + Наистина ли искате да изтриете всички съобщения в този разговор?\n\nВнимание: Това няма да изтрие съобщенията, които се съхраняват на други устройства или сървъри. Изтриване на файла Наистина ли искате да изтриете този файл?\n\nВнимание: Това няма да изтрие копията на файла, които се съхраняват на други устройства или сървъри. Затваряне на този разговор след това @@ -81,16 +98,20 @@ Изпр. на съобщение, шифр. чрез OMEMO Изпр. на съобщение, шифр. чрез v\\OMEMO Изпр. на съобщение, шифр. чрез OpenPGP + Новият псевдоним е зает Изпращане нешифровано Неуспешно дешифроване. Възможно е да нямате правилния частен ключ. OpenKeychain + OpenKeychain, за да шифрова и дешифрова съобщенията и да управлява публичните Ви ключове.

OpenKeychain е под лиценза GPLv3+ и може се свали от F-Droid и Google Play.

(Моля, рестартирайте %1$s след това.)]]>
Рестартиране Инсталиране Моля, инсталирайте OpenKeychain предлагане… изчакване… Не е открит OpenPGP ключ + Съобщението Ви не може да се шифрова, тъй като контактът Ви не е обявил публичния си ключ.\n\nМоля, помолете го да инсталира и настрои OpenPGP. Не са открити OpenPGP ключове + Съобщението Ви не може да се шифрова, тъй като контактите Ви не са обявили публичните си ключове.\n\nМоля, помолете ги да инсталират и настроят OpenPGP. Общи Приемане на файлове Автоматично приемане на файлове с размер, по-малък от… @@ -101,12 +122,20 @@ Известие чрез светодиода Мигане на индикаторния светодиод при получаване на ново съобщение Тон на звънене + Звук за известията + Звук за известията при получаване на нови съобщения + Звук за входящи обаждания Период на пренебрегване + Продължителност на времето, през което известията се заглушават, след като бъде забележена дейност на някое от другите Ви устройства. Разширени Никога да не се изпращат доклади за сривове + Изпращайки проследявания на стека, Вие помагате на разработката Потвърждаване на съобщенията Така контактите Ви ще разбират, че сте получили и прочели съобщенията им + Забраняване на снимките на екрана + Скриване на съдържанието на приложенията от превключвателя на приложения и блокиране на снимките на екрана Потр. интерфейс + Възникна грешка в OpenKeychain. Неправилен ключ за шифроване. Приемане Възникна грешка @@ -119,8 +148,11 @@ Заснемане Предварително позволяване на абониране при заявка Избраният файл не е изображение + Файлът с изображението не може да бъде преобразуван Файлът не е открит Обща В/И грешка. Може би нямате достатъчно свободно място? + Приложението, което използвахте, за да изберете това изображение, не предоставя нужните права за прочитането му.\n\nИзползвайте друг файлов мениджър, за да изберете изображение. + Приложението, което използвахте, за да споделите този файл, не предоставя нужните правомощия. Непознат Временно деактивиран На линия @@ -132,7 +164,10 @@ Неуспешна регистрация Потребителското име е заето Регистрацията е завършена + Регистрацията не се поддържа от сървъра + Неправилен регистрационен идентификатор Договарянето чрез TLS беше неуспешно + Домейнът не може да се провери Нарушение на политиката Несъвместим сървър Поточна грешка @@ -147,13 +182,17 @@ Публикуване на публичния OpenPGP ключ Премахване на публичния OpenPGP ключ Наистина ли искате да премахнете своя публичен OpenPGP ключ от известяването си за присъствие?\nКонтактите Ви вече няма да могат да Ви изпращат съобщение, шифровани чрез OpenPGP. + Публичният OpenPGP ключ е публикуван. Активиране на профила - Сигурни ли сте? + Наистина ли искате това? + Изтриването на профила Ви ще изтрие и цялата история на разговорите Ви Запис на глас XMPP адрес + Блокиране на XMPP адрес username@example.com Парола - Това не е валиден XMPP адрес + Това не е правилен XMPP адрес + Няма достатъчно памет. Изображението е твърде голямо. Искате ли да добавите %s към адресния си указател? Инф. за сървъра XEP-0313: Управление на архива на съобщенията @@ -162,6 +201,7 @@ XEP-0191: Команда за блокиране XEP-0237: Поддържане на версия на списъка XEP-0198: Управление на потоците + XEP-0215: Откриване на външни услуги XEP-0163: PEP (Аватари / OMEMO) XEP-0363: Качване на файл през HTTP XEP-0357: Изпращане @@ -169,17 +209,25 @@ не е налично Липсват обявления за публичен ключ последно видян току-що + последно видян преди една минута последно видян преди %d минути + последно видян преди час последно видян преди %d часа + последно видян преди ден последно видян преди %d дни + Шифровано съобщение. Моля, инсталирайте OpenKeychain, за да го дешифровате. + Открити са нови съобщения, шифровани чрез OpenPGP Ид. на OpenPGP ключа Отпечатък OMEMO Отпечатък v\\OMEMO + Отпечатък OMEMO (източник на съобщението) + v\\Отпечатък OMEMO (източник на съобщението) Други устройства Доверяване на отпечатъци OMEMO Изтегляне на ключове… Готово Дешифроване + Отметки Търсене Въведете контакт Изтриване на контакта @@ -190,11 +238,16 @@ Избиране Контактът вече съществува Присъединяване + канал@беседа.сървър.com/псевдоним + канал@беседа.сървър.com Запазване като отметка Изтриване на отметка Унищожаване на груповия разговор + Унищожаване на канала Наистина ли искате да унищожите този групов разговор?\n\nВнимание: Груповият разговор ще бъде премахнат от сървъра. + Наистина ли искате да унищожите този групов канал?\n\nВнимание: Груповият канал ще бъде напълно премахнат от сървъра. Груповият разговор не може да бъде унищожен + Каналът не може да бъде унищожен Редактиране на темата на груповия разговор Тема Присъединяване в групов разговор… @@ -203,25 +256,32 @@ Добавяне обратно %s е прочел до тук %s човека са прочели до тук + %1$s и още %2$d човека са прочели до тук Всички са прочели до тук Публикуване + Докоснете аватара, за да изберете снимка от галерията Публикуване… Сървърът отказа Вашето публикуване - Неуспешно запазване на аватара на диска + Снимката Ви не може да бъде преобразувана + Аватарът не може да бъде запазен на диска (Или задръжте, за да върнете началното) + Сървърът Ви не поддържа публикуване на аватари прошепна на %s Изпращане на лично съобщение до %s Свързване Този профил вече съществува Следващо + Установена сесия Пропускане Изключване на известията Включване Груповият разговор изисква парола Въведете парола + Моля, първо помолете контакта за актуализации на присъствието му.\n\nТова ще бъде използвано, за да се провери какво приложение използва контактът. Поискване сега Пренебрегване + Внимание: Изпращането на това без съвместни актуализации на присъствието може да доведе до неочаквани проблеми.\n\nПогледнете подробностите за контакта, за да проверите дали сте абониран за актуализации на присъствието. Сигурност Позволяване на поправянето на съобщения Позволяване на контактите да редактират съобщенията си след като са ги изпратили. @@ -235,6 +295,8 @@ Известията ще бъдат заглушени по време на тихите часове Други Синхронизиране с отметките + Автоматично присъединяване към групови разговори, ако такава е настройката на отметката + Отпечатъкът OMEMO е копиран Достъпът Ви до този групов разговор е забранен Този групов разговор е само за членове Ограничение на ресурса @@ -253,6 +315,7 @@ Повторно изпращане Адрес на файла Копиране на адреса + XMPP-адресът е копиран Съобщението за грешка е копирано уеб адрес Сканиране на 2-измерен баркод @@ -263,6 +326,14 @@ Повторен опит Услуга на преден план Предотвратява прекъсването на връзката Ви от операционната система + Създаване на резервно копие + Резервните копия ще се пазят в %s + Създаване на резервни копия + Резервното копие е създадено + Файловете на резервното копие бяха запазени в %s + Възстановяване от резервно копие + Възстановяването от резервно копие е завършено + Не забравяйте да включите профила. Изберете файл Получаване на %1$s (%2$d%% завършено) Сваляне на %s @@ -270,25 +341,37 @@ файл Отваряне на %s изпращане (%1$d%% завършено) + Файлът се подготвя за споделяне %s е предложен за сваляне Отказ на прехвърлянето + файлът не може да бъде споделен + изпращането на файла е отменено + Файлът е изтрит + Няма намерено приложение за отваряне на файла + Няма намерено приложение за отваряне на връзката + Няма намерено приложение за преглед на контакта Динамични етикети Показване на етикети, предназначени само за четене под контактите Включване на известията Не е открит сървър за груповия разговор + Груповият разговор не може да бъде създаден Аватар на профила Копиране на отпечатъка OMEMO Повторно създаване на ключа OMEMO Премахване на устройствата + Наистина ли искате да премахнете всички останали устройства от обявлението OMEMO? Следващия път, когато устройствата Ви се свържат, те ще обявят себе си отново, но може да не получат съобщенията, изпратени междувременно. + Няма ключове, които могат да бъдат използвани за този контакт.\nОт сървъра не могат да бъдат изтеглени нови ключове. Възможно е да има проблем със сървъра на контакта Ви. + Няма ключове, които могат да бъдат използвани за този контакт.\nУверете се, че и двамата имате абонамент за присъствието. Нещо се обърка Получаване на историята от сървъра Няма повече история на сървъра Актуализиране… Паролата е променена! - Неуспешна промяна на паролата + Паролата не може да бъде променена Промяна на паролата Текуща парола Нова парола + Паролата не може да е празна Активиране на всички профили Деактивиране на всички профили Изпълнение на действието с @@ -297,20 +380,28 @@ Отхвърлен Член Разширен режим - Дай членски привилегии - Премахни членски привилегии - Даване на администраторски права + Даване на правомощия на член + Премахване на правомощията на член + Даване на правомощия на администратор Отмяна на администраторските права - Дай права на собственик + Даване на правомощия на собственик + Премахване на правомощията на собственик Премахване от груповия разговор - Неуспешна промяна на принадлежността на %s + Премахване от канала + Принадлежността на %s не може да бъде променена Забраняване на достъпа до груповия разговор + Забраняване на достъпа до канала + Опитвате се да премахнете%s от публичен канал. Единственият начин да направите това е да блокирате завинаги потребителя. Забраняване на достъпа сега - Неуспешна промяна на ролята на %s + Ролята на %s не може да бъде променена + Настройка на частен групов разговор + Настройка на публичен групов разговор Частно, само за членове + Нека XMPP адресите бъдат видими за всички + Нека каналът да се модерира Вие не участвате Настройките на груповия разговор бяха променени! - Неуспешна промяна на настройките на груповия разговор + Настройките на груповия разговор не могат да бъдат променени Никога До отмяна Отлагане @@ -318,11 +409,13 @@ Отбелязване като прочетено Въвеждане Enter изпраща + Използвайте клавиша Enter, за да изпратите съобщение. Винаги може да използвате Ctrl+Enter за изпращане на съобщение, дори тази настройка да е изключена. Показване на клавиша Enter Смяна на клавиша за емотикони с клавиша Enter аудио видео изображение + векторна графика PDF документ Приложение за Андроид Контакт @@ -338,8 +431,11 @@ Така контактите Ви ще разбират, когато им пишете съобщения Изпращане на местоположението Показване на местоположението + Няма намерено приложение за показване на местоположението Местоположение Conversation се затвори + Напуснахте частния групов разговор + Напуснахте публичния канал Да не се вярва на системните сертификати Всички сертификати трябва да бъдат одобрени на ръка Премахване на сертификатите @@ -352,12 +448,15 @@ %d сертификат е изтрит %d сертификата са изтрити + Замяна на бутона „Изпращане“ с бързо действие Бързо действие Нищо Използвани наскоро Изберете бързо действие Търсене в контактите + Търсене в отметките Изпращане на лично съобщение + %1$s напусна груповия разговор Потребителско име Потребителско име Това не е правилно потребителско име @@ -367,16 +466,28 @@ Неуспешно сваляне: Файлът не може да бъде записан Мрежата на Тор е недостъпна Грешка при свързване + Сървърът не отговаря за този домейн Повредено Присъствие + Отсъстващ при заключено устройство + Показване като „отсъстващ“, когато устройството е заключено + Зает в тих режим + Показване като „зает“, когато устройството е в тих режим Тих режим при режим на вибриране + Показване като „зает“, когато устройството е на вибрация Разширени настройки за връзката Показване на настройките за сървър и порт при установка на профил xmpp.example.com + Влизане със сертификат + Сертификатът не може да бъде прочетен Настройки за архивирането Настройки за архивирането на сървъра Получаване на настройките за архивирането. Моля, изчакайте… + Настройките за архивирането не могат да бъдат получени + Проверката е задължителна Въведете текста от горното изображение + Недоверен верижен сертификат + XMPP адресът не съответства на сертификата Подновяване на сертификата Грешка при получаването на ключа за OMEMO! Ключът за OMEMO беше потвърден със сертификат! @@ -386,6 +497,7 @@ Всички връзки да минават през мрежата на Тор. Изисква Орбот Име на сървър Порт + Адрес на сървър или .onion Това не е правилен номер на порт Това не е правилно име на сървър %1$d от %2$d свързани профила @@ -394,23 +506,41 @@ %d съобщения Зареждане на още съобщения + Файлът е споделен с %s + Изображението е споделено с %s + Изображенията са споделени с %s + Текстът е споделен с %s + Дайте на %1$s разрешение за достъп до външната памет + Дайте на %1$s разрешение за достъп до камерата Синхронизиране с контактите + %1$s иска разрешение за достъп до адресната Ви книга, за да потърси съвпадения със списъка от контакти в XMPP.\nТова ще покаже пълните имена и аватари на контактите Ви.\n\n%1$s само ще прочете адресната книга и ще потърси съвпадения на това устройство – нищо няма да се качва на сървъра Ви.
Ние няма да пазим копия на тези телефонни номера.\n\nЗа повече информация, прочетете декларацията ни за поверителност.

Сега ще Ви помолим да дадете достъп до контактите си.]]>
Известяване за всички съобщения Известяване само при споменаване Известията са изключени Известията са спрени временно Компресия на изображенията + Съвет: използвайте „Изберете файл“ вместо „Изберете снимка“, за да изпращате снимките некомпресирани, независимо от тази настройка. Винаги + Само за големи изображения Оптимизациите за използв. на батерията са вкл. + Устройството Ви прилага сериозни оптимизации за използването на батерията върху %1$s, които може да доведат до забавени известия и дори пропуснати съобщения.\nПрепоръчително е да ги изключите. + Устройството Ви прилага сериозни оптимизации за използването на батерията върху %1$s, които може да доведат до забавени известия и дори пропуснати съобщения.\nСега ще бъдете помолен(а) да ги изключите. Изключване Избраната област е твърде голяма (Няма активирани профили) Това поле е задължително Поправяне на съобщението Изпращане на поправеното съобщение + Вече сте потвърдили доверието си в този човек, чрез защитена проверка на отпечатъка му. Ако изберете „Готово“, ще потвърдите само това, че %s е част от този групов разговор. Вие сте деактивирали този профил + Грешка в сигурността: неправилен достъп до файл! + Няма намерено приложение за споделяне на адреса Споделяне на адреса с… +
Трябва да се регистрирате чрез телефонния си номер, след което Quicksy автоматично ще претърси телефонните номера в указателя Ви и ще Ви предложи контакти в приложението.

Регистрирайки се, Вие се съгласявате с нашата декларация за поверителност.]]>
+ Съгласяване и продължаване + На conversations.im има ръководство за създаване на профил.\nИзбирайки conversations.im за доставчик, Вие ще можете да общувате и с потребители на други доставчици, като им предоставите своя пълен адрес за XMPP. + Пълният Ви XMPP адрес ще бъде: %s Създаване на профил Използване на собствен доставчик Изберете потребителското си име @@ -433,11 +563,17 @@ Кратко Средно Дълго + Информиране за използването + Позволява на контактите Ви да знаят кога използвате Conversations Поверителност Тема Изберете цветовата схема + Автоматично + Светла + Тъмна Зелен фон Получените съобщения ще бъдат на зелен фон + Свързването с OpenKeychain е невъзможно Това устройство вече не се използва Компютър Мобилен телефон @@ -445,20 +581,29 @@ Браузър Конзола Изисква се плащане + Дайте разрешение за достъп до Интернет Аз Контакт моли за абонамент за присъствието Позволяване Няма позволение за достъп до %s Отдалеченият сървър не е намерен Времето за изчакване на отдалечения сървър изтече + Профилът не може да бъде обновен + Докладване този XMPP адрес за спам. Изтриване на идентификаторите OMEMO + Пресъздайте своите ключове OMEMO. Всички Ваши контакти ще трябва да Ви потвърдят отново. Използвайте това само в краен случай. Изтриване на избраните ключове. Трябва да бъдете свързан(а), за да публикувате аватара си. Показване на грешка Съобщение за грешка - Съхранението на данни е включено + Пестенето на данни е включено + Операционната Ви система не позволява на %1$s да се свързва с Интернет когато работи на заден фон. За да получавате известия за новите съобщения, трябва да дадете на %1$s неограничен достъп когато пестенето на данни е включено.\n%1$s ще продължи да се опитва да записва данните когато е възможно. + Устройството Ви не поддържа изключването на пестенето на данни за %1$s. + Не може да се създаде временен файл Това устройство е потвърдено Копиране на отпечатъка + Потвърдили сте всички ключове OMEMO, които притежавате + Баркодът не съдържа отпечатъци за този разговор. Потвърдени отпечатъци Използвайте камерата, за да сканирате баркода на контакт Моля, изчакайте получаването на ключовете @@ -466,8 +611,11 @@ Споделяне като адрес на XMPP Споделяне като връзка в Интернет Доверяване на сляпо преди потвърждение + Нови устройства на непотвърдени контакти автоматично получават доверие, но нови устройства на потвърдени контакти изискват ръчно потвърждаване. + Доверени на сляпо ключове OMEMO, което означава, че това може да е някой друг, или че някой може да е получил неправомерен достъп. Неприети Грешен 2-измерен баркод + Изчистване на папката с кеша (използвана от камерата) Изчистване на кеша Изчистване на личното място за съхранение Изчистване на мястото, където се съхраняват личните файлове. (Те могат да бъдат повторно изтеглени от сървъра.) @@ -477,6 +625,7 @@ Показване на неактивните Скриване на неактивните Сваляне на доверието + Наистина ли искате да заличите потвърждението на това устройство?\nТова устройство и съобщенията от него ще бъдат отбелязани като „недоверени“. %d секунда %d секунди @@ -509,6 +658,7 @@ Съответстващите разговори са затворени. Контактът е блокиран. Известия от непознати + Известяване за съобщения и обаждания от непознати. Получено е съобщение от непознат Блокиране на непознатия Блокиране на целия домейн @@ -518,11 +668,14 @@ Механизмът на SASL е понижен Сървърът изисква регистриране чрез уеб сайт Отваряне на уеб сайта + Няма намерено приложение за отваряне на уеб сайта Изскачащи известия + Показване на изскачащи известия Днес Вчера Проверка на името на сървъра чрез DNSSEC Сървърните сертификати, които съдържат проверено име на сървъра, се смятат за потвърдени + Сертификатът не съдържа XMPP адрес частично Запис на видео Копиране в буфера @@ -544,6 +697,9 @@ Редактиране на съобщението за състоянието Редактиране на съобщението за състоянието Изключване на шифроването + %1$s не може да изпраща шифровани съобщения до %2$s. Възможно е Вашият контакт да използва остарял сървър или клиент, който не може да работи с OMEMO. + Неуспешно получаване на списъка с устройства + Неуспешно получаване на ключове за шифроване Съвет: В някои случаи това може да се оправи, ако се добавите един друг в списъците си с контакти. Наистина ли искате да изключите шифроването чрез OMEMO за този разговор?\nТова ще позволи на администратора Ви да чете съобщенията Ви, но пък най-вероятно е единственият начин за общуване с хората, използващи стари клиенти. Изключване сега @@ -572,13 +728,16 @@ Споделяне на местоположението Показване на местоположението Споделяне + Записът не може да започне Моля, изчакайте… + Дайте на %1$s разрешение за достъп до микрофона Търсене в съобщенията GIF Преглед на разговора Разширение за споделяне на местоположението Използване на разширението за споделяне на местоположението вместо вградената карта Копиране на уеб адрес + Копиране на XMPP адрес Споделяне на файлове през HTTP за S3 Директно търсене На екрана за „Започване на разговор“ да се отваря клавиатурата и да се поставя курсорът в полето за търсене @@ -591,17 +750,26 @@ Въвеждането на име не е задължитално Име на груповия разговор Този групов разговор е унищожен + Записът не може да бъде запазен Услуга на преден план + Тази категория известия се използва за показване на постоянно известие, което показва, че %1$s работи. Информация за състоянието Проблеми с връзката Тази категория известия се използва за показване на известие, в случай че има проблем със свързването с профил. Съобщения + Обаждания Съобщения + Входящи обаждания + Изходящи обаждания Тихи съобщения Тази категория известия се използва за показване на известия, които не бива да изпълняват звук. Това може да се използва, например, докато използвате друго устройство (по време на Период на пренебрегване). + Неуспешни доставяния + Настройки на известията за съобщения + Настройки на известията за обаждания Важност, звук, вибрация Компресия на видеото Преглед на медийното съдържание + Участници Разглеждане на медийното съдържание Файлът е пропуснат поради нарушение на сигурността. Качество на видеото @@ -639,6 +807,9 @@ Кодът, който Ви изпратихме, е с изтекла давност. Неизвестна мрежова грешка. Непознат отговор от сървъра. + Свързването със сървъра е невъзможно. + Установяването на защитена връзка е невъзможно. + Сървърът не може да бъде намерен. Нещо се обърка при обработването на заявката Ви. Неправилно въведени данни Временно недостъпно. Опитайте отново по-късно. @@ -657,6 +828,7 @@ Инсталиране на Orbot Пускане на Orbot Няма инсталирано приложение за инсталиране на приложения. + Този канал ще направи Вашия XMPP-адрес публичен е-книга Оригинално (некомпресирано) Отваряне с… @@ -666,9 +838,132 @@ Възстановяване Въведете паролата си за профила %s, за да направите възстановяване от резервно копие. Не използвайте възможността за възстановяване от резервно копие, за да клонирате (да изпълнявате едновременно) инсталацията. Възстановяването от резервно копие е предназначено за мигриране или в случай, че сте загубили устройството си. + Не може да се извърши възстановяване от резервно копие. + Резервното копие не може да бъде дешифрирано. Правилна ли е паролата? + Резервни копия и възстановяване + Въведете XMPP адрес Създаване на групов разговор + Присъединяване към публичен канал + Създаване на частен групов разговор + Създаване на публичен канал + Име на канала XMPP адрес + Моля, задайте име за канала + Моля, задайте XMPP адрес + Това е XMPP адрес. Моля, задайте име. + Създаване на публичен канал… + Този канал вече съществува Присъединихте се към съществуващ канал - Добави съществуващ профил + Настройката на канала не може да бъде запазена + Нека всеки може да редактира темата + Нека всеки може да кани други хора + Всеки може да редактира темата. + Собствениците могат да редактират темата. + Администраторите могат да редактират темата. + Собствениците могат да канят други хора. + Всеки може да кани други хора. + XMPP адресите са видими за администраторите. + XMPP адресите са видими за всички. + В този публичен канал няма никакви участници. Поканете контактите си или използвайте бутона за споделяне, за да разпространите XMPP-адреса на канала. + В този частен групов разговор няма никакви участници. + Управление на правомощията + Търсене на участници + Файлът е твърде голям + Прикачане + Откриване на канали + Търсене на канали + Възможно нарушаване на декларацията за поверителност! + search.jabber.network.

Ако използвате тази функционалност, Вашият IP адрес и въведеният текст за търсене ще бъдат изпратени до сървъра на тази услуга. Разгледайте нейната Декларация за поверителност за повече информация.]]>
+ Вече имам профил + Добавяне на съществуващ профил + Регистриране на нов профил + Това прилича на адрес на домейн + Добавяне въпреки това + Това прилича на адрес на канал + Споделяне на файловете с резервни копия + Резервно копие от Conversations + Събитие + Отваряне на резервно копие + Избраният файл не е резервно копие от Conversations + Този профил вече е настроен + Въведете паролата за този профил + Това действие не може да бъде извършено + Присъединяване към публичен канал… + Приложението за споделяне не даде разрешение за достъп до този файл. + + jabber.network + Локален сървър + Повечето потребители трябва да изберат „jabber.network“ за по-добри предложения от цялата публична екосистема на XMPP. + Метод за откриване на канали + Резервно копие + Относно + Моля, активирайте профил + Направете обаждане + Входящо обаждане + Входящо видео-обаждане + Свързване + Установена връзка + Приемане на обаждане + Приключване на обаждане + Отговор + Отхвърляне + Откриване на устройства + Позвъняване Зает - + Свързването с разговора е невъзможно + Връзката беше прекъсната + Върнат разговор + Грешка в приложението + Проблем с потвърждението + Затваряне + Текущо обаждане + Текущо видео-обаждане + Изключете Tor, за да правите обаждания + Входящо обаждане + Входящо обаждане · %s + Пропуснато обаждане · %s + Изходящо обаждане + Изходящо обаждане · %s + Пропуснато обаждане + Гласово обаждане + Видео обаждане + Помо + Превключване към разговор + Микрофонът не е наличен + Не може да има повече от едно обаждане едновременно. + Обратно към текущия разговор + Закачане горе + Откачане от горе + Съобщението не може да бъде поправено + Всички разговори + Този разговор + Вашият аватар + Аватар за %s + Шифровано с OMEMO + Шифровано с OpenPGP + Нешифровано + Изход + Запис на гласова поща + Възпроизвеждане на звука + Пауза на звука + Добавете контакт, създайте или се присъединете към групов разговор, или разгледайте каналите + + Преглед на %1$d член + Преглед на %1$d членове + + + Едно съобщение не може да бъде доставено + Някои съобщения не могат да бъдат доставени + + Неуспешни доставяния + Още настройки + Няма намерено приложение + Канене в Conversations + Поканата не може да бъде анализирана + Сървърът не поддържа създаването на покани + Нито един от активните профили не поддържа тази функционалност + Създаването на резервно копие е стартирано. Ще получите известие, когато приключи. + Видеото не може да бъде включено. + Обикновен текстов документ + + diff --git a/src/main/res/values-bn-rIN/strings.xml b/src/main/res/values-bn-rIN/strings.xml index cd8dbf1871f41f7bc586482ee370578fe58f996c..bd426b737a968a48aa4ec5f7391761337bbeb759 100644 --- a/src/main/res/values-bn-rIN/strings.xml +++ b/src/main/res/values-bn-rIN/strings.xml @@ -46,6 +46,9 @@ নির্ধারক অংশগ্রহণকারী অতিথি + আপনি কি আপনার পরিচিতি তালিকা থেকে %s-কে অপসারণ করতে চান? এই যোগাযোগের সাথে কথোপকথনগুলি সরানো হবে না। + %s-কে বার্তা পাঠানো থেকে ব্লক করতে চান? + আপনি কি %s-কে আনব্লক করতে চান এবং তাদের আপনাকে বার্তা পাঠানোর অনুমতি দিতে চান? ব্যক্তিটিকে ব্লক্ করা হয়েছে ব্লক্ করা আছে সার্ভারে একটি নতুন অ্যকাউন্ট খোলা যাক @@ -90,8 +93,26 @@ OMEMO সাঙ্কেতিক বার্তা পাঠানো হোক v\\OMEMO সাঙ্কেতিক বার্তা পাঠানো হোক OpenPGP সাঙ্কেতিক বার্তা পাঠানো হোক + নতুন নাম ব্যবহার করা হচ্ছে + এনক্রিপ্ট না করেই পাঠানো হোক + ডিক্রিপ্ট করা যায়নি। হয়তো আপনার কাছে সঠিক Private Key নেই। + রিস্টার্ট্ + ইনস্টল্ + OpenKeychain ইনস্টল্ করতে হবে + প্রস্তাব দেওয়া হচ্ছে... + অপেক্ষা করা হচ্ছে... + কোনো OpenPGP Key খুঁজে পাওয়া যায়নি + বুকমার্ক করা যেগুলি + খোঁজা যাক এই ব্যক্তিকে ব্লক্ করা যাক ব্লকটা সরিয়ে ফেলা যাক পরিচিত ব্যক্তি না, থাক। + পরিচিত ব্যক্তিদের মধ্যে খোঁজা যাক + বার্তাগুলির মধ্যে খোঁজা যাক + সরাসরিভাবেই খোঁজা যাক + পাবলিক চ্যানেলে যোগ দেওয়া যাক + ব্যক্তিগত গ্রুপ চ্যাট তৈরি করুন + পাবলিক চ্যানেল তৈরি করা যাক + বর্তমান চ্যানেলগুলির মধ্যে থেকে খোঁজা যাক diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml index 684632474eb62d9794c919e1e11194a7700a523a..d0ddfa60471dde0e8ef8d4fa309e95b6aeb5ea6d 100644 --- a/src/main/res/values-cs/strings.xml +++ b/src/main/res/values-cs/strings.xml @@ -156,6 +156,7 @@ Soubor nenalezen Obecná I/O chyba. Že by již nebylo volné místo? Aplikace, kterou jste použil(a) k výběru obrázku, neposkytla dostatečná oprávnění ke čtení souboru.\n\nPoužijte jiného správce souborů k výběru obrázku. + Aplikace kterou jste použili pro nasdílení tohoto souboru nemá dostatečná oprávnění. Neznámý Dočasně vypnuto Online @@ -170,6 +171,7 @@ Registrace není podporována serverem Chybný registrační token Vyjednávání TLS selhalo + Doménu nelze ověřit Porušení podmínek Nekompatibilní server Chyba přenosu @@ -406,6 +408,7 @@ Nebylo možné změnit nastavení skupinového chatu Nikdy Než opět změním + Posunout Odpovědět Označit jako přečtené Vstup @@ -434,6 +437,8 @@ Nebyla nalezena aplikace pro zobrazení pozice Pozice Conversation zavřena + Opustil(a) soukromý skupinový chat + Opustil(a) veřejný kanál Nedůvěřovat systémovým CA Všechny certifikáty musí být schváleny ručně Odstranit certifikáty @@ -466,6 +471,7 @@ Stažení selhalo: Nelze zapsat soubor Tor síť není dostupná Bind chyba + Server není zodpovědný za tuto doménu Rozbité Dostupnost Pryč při uzamčení zařízení @@ -495,6 +501,7 @@ Vedení všech připojení po Tor síti vyžaduje aplikaci Orbot Hostname Port + Server nebo .onion adresa Toto není platné číslo portu Toto není platné hostname %1$d z %2$d účtů připojeno @@ -533,6 +540,7 @@ Odeslat opravenou zprávu Tento osobní otisk byl již bezpečně ověřen. Ťuknutím na \"Hotovo\" pouze potvrzujete, že %s je členem tohoto skupinového chatu. Tento účet byl vypnut + Bezpečnostní chyba: Neplatný přístup k souboru Nebyla nalezena aplikace umožňující sdílení URI Sdílet URI s…
Po zadání Vašeho telefonního čísla Vám Quicksy automaticky—na základě čísel ve Vašem telefonním seznamu—navrhne možné kontakty.

Přihlášením se do služby potvrzujete souhlas s našimi zásadami pro ochranu osobních údajů.]]>
@@ -744,6 +752,7 @@ Použít Plugin pro sdílení pozice namísto interní mapy Kopírovat webovou adresu Kopírovat XMPP adresu + HTTP sdílení souborů pro S3 Přímé vyhledávání Na úvodní obrazovce otevřít klávesnici a umístit kurzor do vyhledávacího pole Avatar skupinového chatu @@ -883,6 +892,7 @@ Toto vypadá jako adresa kanálu Sdílet soubory zálohy Záloha Conversations + Událost Otevřít zálohu Soubor, který jste zvolili, není soubor zálohy Conversations Tento účet byl již nastaven @@ -932,6 +942,7 @@ Nebylo možné přepnout kameru Připnout nahoru Odepnout shora + GPX trasa Nebylo možné opravit zprávu Všechny konverzace Tato konverzace @@ -961,7 +972,9 @@ Více možností Nenalezena žádná aplikace Pozvat do Conversations + Nelze načíst pozvánku Server nepodporuje vytváření pozvánek Žádný z aktivních účtů tuto funkci nepodporuje Zálohování zahájeno. Budete upozorněni, jakmile bude záloha hotova. + Nelze povolit video. diff --git a/src/main/res/values-da-rDK/strings.xml b/src/main/res/values-da-rDK/strings.xml index 9129da103004f4cef8f296a155f24a80fa950b5e..8c06e3f505ebb330eacb50de53220675cb66ed99 100644 --- a/src/main/res/values-da-rDK/strings.xml +++ b/src/main/res/values-da-rDK/strings.xml @@ -132,6 +132,8 @@ Ved at indsende \"stack traces\" hjælper du udviklingen Bekræft beskeder Lad dine kontakter vide når du har modtaget og læst deres beskeder + Forbyd skærmbillede + Skjul app indhold i app-skifteren og bloker skærmbilleder UI OpenKeychain producerede en fejl Dårlig nøgle til kryptering @@ -150,6 +152,7 @@ Fil ikke fundet General I/O fejl. Måske er du kørt tør for lagerplads? Appen du brugte til at vælge dette billede havde ikke tilstrækkelig tilladelse til at læse filen.\n\nBrug en anden filmanager til at vælge et billede. + Appen du brugte til at dele denne fil har ikke givet nok tilladelser. Ukendt Midlertidigt deaktiveret Online @@ -164,6 +167,7 @@ Registrering er ikke understøttet af server Ugyldig registreringstoken TLS forhandling mislykkedes + Domæne kan ikke verificeres Brud på retningslinjer Inkompatibel server Strømfejl @@ -412,6 +416,7 @@ lyd video billede + vektorgrafik PDF dokument Android App Kontakt @@ -910,6 +915,7 @@ Forbindelsen tabt Tilbagetrukket opkald App fejl + Bekræftelsesproblem Læg på Udgående opkald Igangværende videoopkald @@ -960,4 +966,7 @@ Server understøtter ikke generering af invitationer Ingen aktive konti understøtter denne funktion Sikkerhedskopieringen er startet. Du får en notifikation, når den er afsluttet. - + Kunne ikke aktivere video. + Ren tekstdokument + + diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index fe9d6d6428ec304da65be9a4594feb9dbd38ec7c..787025a67e1f09f5c004fcbb9b179a43b6a80478 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -76,7 +76,7 @@ Jetzt abschicken Nie mehr nachfragen Verbindung zum Konto konnte nicht hergestellt werden - Verbindung zu mehreren Konto konnte nicht hergestellt werden + Verbindung zu mehreren Konten konnte nicht hergestellt werden Antippen, um deine Konten zu verwalten Datei auswählen Diesen fehlenden Kontakt zu deiner Kontaktliste hinzufügen? @@ -94,7 +94,7 @@ Gerät auswählen Unverschlüsselt schreiben… Nachricht senden - Sende Nachricht an %s + Nachricht an %s senden OMEMO-verschlüsselt schreiben… v\\OMEMO-verschlüsselte Nachricht senden OpenPGP-verschlüsselt schreiben… @@ -112,7 +112,7 @@ Deine Nachricht konnte nicht verschlüsselt werden, weil dein Kontakt seinen öffentlichen Schlüssel nicht bekannt gibt.\n\nBitte sage deinem Kontakt, er möge OpenPGP einrichten. Keine OpenPGP-Schlüssel gefunden Deine Nachrichten konnten nicht verschlüsselt werden, weil deine Kontakte ihre öffentlichen Schlüssel nicht bekannt geben.\n\nBitte sage ihnen, sie mögen OpenPGP einrichten. - Allgemeines + Allgemein Dateien annehmen Dateien automatisch annehmen, die kleiner sind als … Anhänge @@ -132,6 +132,8 @@ Mit dem Einsenden von Absturzberichten hilfst du bei der Weiterentwicklung Lese- und Empfangsbestätigung senden Informiere deine Kontakte, wenn du eine Nachricht empfangen und gelesen hast + Screenshots verhindern + Ausblenden von App-Inhalten im App-Switcher und Blockieren von Screenshots Benutzeroberfläche OpenKeychain verursachte einen Fehler. Fehlerhafter Schlüssel für die Verschlüsselung. @@ -149,7 +151,7 @@ Bilddatei konnte nicht konvertiert werden Datei nicht gefunden Allgemeiner Fehler. Vielleicht hast du keinen Speicherplatz mehr? - Die App, mit der du das Bild ausgesucht hast, hat keine Rechte eingeräumt, um das Bild zu betrachten.\n\nBenutze einen anderen Dateimanager, um ein Bild auszuwählen. + Die App, mit der du das Bild ausgesucht hast, hat nicht die erforderlichen Berechtigungen, um das Bild zu betrachten.\n\nBenutze einen anderen Dateimanager, um ein Bild auszuwählen. Die App, die du zum Teilen dieser Datei verwendet hast, hat nicht die erforderlichen Berechtigungen bereitgestellt. Unbekannt Vorübergehend abgeschaltet @@ -168,7 +170,7 @@ Domain nicht überprüfbar Verstoß gegen die Richtlinien Inkompatibler Server - Stream Fehler + Stream-Fehler Fehler beim Öffnen des Streams Unverschlüsselt OTR @@ -180,7 +182,7 @@ Öffentlichen OpenPGP-Schlüssel veröffentlichen Öffentlichen OpenPGP-Schlüssel verwerfen Bist du sicher, dass du deinen öffentlichen OpenPGP-Schlüssel aus deiner Anwesenheitsmitteilung entfernen möchtest?\nDeine Kontakte können dir dann keine OpenPGP-verschlüsselten Nachrichten senden. - Öffentlicher OpenPGP-Schlüssel veröffentlicht + Öffentlicher OpenPGP-Schlüssel veröffentlicht. Konto aktivieren Bist du dir sicher? Die Löschung deines Kontos löscht deinen gesamten Gesprächsverlauf @@ -250,25 +252,25 @@ Thema Gruppenchat wird beigetreten… Verlassen - Der Kontakt hat dich zur Kontaktliste hinzugefügt + Kontakt hat dich zur Kontaktliste hinzugefügt Auch hinzufügen %s hat bis zu diesem Punkt gelesen %s haben bis zu diesem Punkt gelesen %1$s +%2$d andere haben bis zu diesem Punkt gelesen Alle haben bis zu diesem Punkt gelesen Veröffentlichen - Avatar antippen, um ein Bild aus Galerie auszuwählen + Avatar antippen, um ein Bild aus der Galerie auszuwählen Veröffentliche… Der Server hat die Veröffentlichung des Avatars abgelehnt. Bild konnte nicht konvertiert werden Avatar kann nicht gespeichert werden - (Oder klicke lange, um Standard wiederherzustellen) + (Oder klicke lange, um den Standard wiederherzustellen) Dein Server unterstützt die Veröffentlichung von Avataren nicht private Nachricht: an %s Private Nachricht an %s senden Verbinden - Das Konto existiert bereits + Dieses Konto existiert bereits Weiter Sitzung wiederhergestellt Überspringen @@ -304,7 +306,7 @@ verwende Konto %s gehostet bei %s %s auf HTTP-Host wird überprüft - Nicht verbunden, bitte später versuchen + Du bist nicht verbunden. Bitte versuche es später noch einmal %s-Größe prüfen %1$s-Größe auf %2$s prüfen Nachrichtenoptionen @@ -324,7 +326,7 @@ Bestätigen Erneut versuchen Vordergrunddienst - Verhindert, dass Android Conversations beendet und die Verbindung unterbricht + Verhindert, dass das Betriebssystem deine Verbindung unterbricht Sicherung erstellen Sicherungsdateien werden gespeichert in %s Erstelle Sicherungsdateien @@ -362,7 +364,7 @@ Für diesen Kontakt sind keine nutzbaren Schlüssel verfügbar.\nEs konnten keine neuen Schlüssel vom Server abgerufen werden. Gibt es vielleicht ein Problem mit dem Server deines Kontaktes? Für diesen Kontakt sind keine benutzbaren Schlüssel verfügbar.\nStelle sicher, dass ihre beide gegenseitig den Online-Status aktiviert habt. Etwas ist schief gelaufen - Lade Chatverlauf… + Lade Chatverlauf vom Server Keine weiteren Nachrichten vorhanden Aktualisieren… Passwort geändert! @@ -371,7 +373,7 @@ Aktuelles Passwort Neues Passwort Passwort kann nicht leer sein - Alle Konten aktiveren + Alle Konten aktivieren Alle Konten abschalten Aktion durchführen mit Keine Zugehörigkeit @@ -414,6 +416,7 @@ Audio Video Bild + Vektorgrafik PDF-Dokument Android App Kontakt @@ -554,7 +557,7 @@ Dein Gerät unterstützt kein Ausschalten der Akkuoptimierung Registrierung fehlgeschlagen: Bitte später versuchen Registrierung fehlgeschlagen: Passwort zu schwach - Mitglieder wählen + Teilnehmer wählen Erstelle Gruppenchat… Erneut einladen Deaktivieren @@ -603,7 +606,7 @@ Du hast alle in deinem Besitz befindlichen OMEMO-Schlüssel überprüft Der Barcode enthält keine Fingerabdrücke für diese Unterhaltung. Überprüfte Fingerabdrücke - Nutze Kamera, um Barcodes deiner Kontakte zu scannen + Nutze die Kamera, um Barcodes deiner Kontakte zu scannen Bitte warten, bis die Schlüssel abgerufen werden Als Barcode teilen Als XMPP URI teilen @@ -789,7 +792,7 @@ Überprüfe %s %s geschickt.]]> Wir haben dir eine weitere SMS mit einem 6-stelligen Code geschickt. - Gib bitte den 6-stellige PIN unten ein. + Gib bitte die 6-stellige PIN unten ein. SMS erneut versenden SMS erneut versenden (%s) Bitte warten (%s) @@ -893,7 +896,7 @@ Lokaler Server Die meisten Benutzer sollten hier ‘jabber.network’ auswählen, um bessere Vorschläge aus dem gesamten, öffentlichen XMPP-Ökosystem zu bekommen. Channelsuchmethode - Sicherungskopie + Sicherung Über Bitte aktiviere ein Konto Anrufen @@ -912,6 +915,7 @@ Verbindung unterbrochen Anruf zurückgenommen App-Fehler + Verifikationsproblem Auflegen Laufender Anruf Laufender Videoanruf @@ -961,6 +965,8 @@ Einladung kann nicht gelesen werden Server unterstützt keine Generierung von Einladungen Keine aktiven Konten unterstützen diese Funktion - Das Backup wurde gestartet. Du bekommst eine Benachrichtigung sobald es fertig ist. + Die Sicherung wurde gestartet. Du bekommst eine Benachrichtigung, sobald sie fertig ist. Video kann nicht aktiviert werden. + Textdokument + diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml index 57527772c66d585389550f14504895fd83261c0f..16a2a8742540ffc61d74c4a7377cbd7baadd6a4c 100644 --- a/src/main/res/values-el/strings.xml +++ b/src/main/res/values-el/strings.xml @@ -53,7 +53,7 @@ Άρση αποκλεισμού όλων των επαφών από το %s; Η επαφή αποκλείστηκε Αποκλεισμένος - Θέλετε να αφαιρέσετε το %s ως σελιδοδείκτη; Οι συζητήσεις που σχετίζονται με αυτόν τον σελιδοδείκτη δεν θα αφαιρεθούν. + Θέλετε να αφαιρέσετε το %s από σελιδοδείκτη; Οι συζητήσεις που σχετίζονται με αυτόν τον σελιδοδείκτη δεν θα αφαιρεθούν. Εγγραφή νέου λογαριασμού στον διακομιστή Αλλαγή συνθηματικού στον διακομιστή Διαμοιρασμός με... @@ -89,7 +89,7 @@ Καθαρισμός ιστορικού Συζήτησης Θέλετε να διαγράψετε όλα τα μηνύματα αυτής της συζήτησης;\n\nΠροσοχή: Αυτή η ενέργεια δεν θα επηρεάσει μηνύματα που είναι αποθηκευμένα σε άλλες συσκευές ή εξυπηρετητές. Διαγραφή αρχείου - Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το αρχείο;\n\nΠροσοχή Αυτή η ενέργεια δεν θα διαγράψει αντίγραφα αυτού του αρχείου που είναι αποθηκευμένα σε άλλες συσκευές ή εξυπηρετητές. + Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το αρχείο;\n\nΠροσοχή: Αυτή η ενέργεια δεν θα διαγράψει αντίγραφα αυτού του αρχείου που είναι αποθηκευμένα σε άλλες συσκευές ή εξυπηρετητές. Κλείσιμο της συζήτησης αμέσως μετά Επιλογή συσκευής Αποστολή μη κρυπτογραφημένου μηνύματος @@ -114,7 +114,7 @@ Δεν ήταν δυνατή η κρυπτογράφηση του μηνύματός σας γιατί οι επαφές σας δεν ανακοινώνουν το δημόσιο κλειδί τους.\n\nΠαρακαλώ ζητήστε από τις επαφές σας να εγκαταστήσουν το OpenPGP. Γενικά Αποδοχή αρχείων - Αυτόματη αποδοχή αρχείων μικρότερα από... + Αυτόματη αποδοχή αρχείων μικρότερων από... Συνημμένα Ειδοποίηση Δόνηση @@ -124,6 +124,7 @@ Κουδούνισμα Ήχος ειδοποίησης Ήχος ειδοποίησης για νέα μηνύματα + Ήχος κουδουνίσματος για εισερχόμενες κλήσεις Περίοδος Χάριτος Ο χρόνος σίγασης ειδοποιήσεων αφότου ανιχνευθεί δραστηριότητα σε μια από τις άλλες συσκευές σας. Για προχωρημένους @@ -133,7 +134,7 @@ Επιτρέψτε στις επαφές σας να γνωρίζουν όταν έχετε λάβει και διαβάσει τα μηνύματά τους Διεπαφή χρήστη Το OpenKeychain ανέφερε κάποιο σφάλμα. - Σφάλμα στο κλειδί κρυπτογράφησης + Σφάλμα στο κλειδί κρυπτογράφησης. Αποδοχή Έχει συμβεί κάποιο σφάλμα Σφάλμα @@ -149,6 +150,7 @@ Το αρχείο δεν βρέθηκε Γενικό σφάλμα εισόδου/εξόδου. Ίσως δεν έχετε ελεύθερο χώρο αποθήκευσης; Η εφαρμογή που χρησιμοποιήσατε για να επιλέξετε αυτή την εικόνα δεν παραχώρησε αρκετά δικαιώματα για την ανάγνωση του αρχείου.\n\nΧρησιμοποιήστε διαφορετικό διαχειριστή αρχείων για να επιλέξετε μια εικόνα + Η εφαρμογή που χρησιμοποιήσατε για να διαμοιραστείτε αυτό το αρχείο δεν παρείχε αρκετά δικαιώματα. Άγνωστο Προσωρινά απενεργοποιημένο Σε σύνδεση @@ -163,6 +165,7 @@ Ο διακομιστής δεν υποστηρίζει εγγραφή Άκυρο κουπόνι εγγραφής Αποτυχία διαπραγμάτευσης TLS + Ο τομέας δεν είναι επαληθεύσιμος Παραβίαση κανονισμού Μη συμβατός διακομιστής Σφάλμα μετάδοσης @@ -204,11 +207,11 @@ μη διαθέσιμος Ελλειπείς ανακοινώσεις δημοσίων κλειδιών συνδέθηκε τελευταία φορά μόλις τώρα - τελευταία σύνδεση πριν από 1 λεπτό + τελευταία σύνδεση πριν από ένα λεπτό τελευταία σύνδεση πριν από %d λεπτά - τελευταία σύνδεση πριν από 1 ώρα + τελευταία σύνδεση πριν από μία ώρα τελευταία σύνδεση πριν από %d ώρες - τελευταία σύνδεση πριν από 1 μέρα + τελευταία σύνδεση πριν από μία μέρα τελευταία σύνδεση πριν από %d μέρες Κρυπτογραφημένο μήνυμα. Παρακαλώ εγκαταστήστε το OpenKeychain για αποκρυπτογράφηση. Βρέθηκαν νέα μηνύματα κρυπτογραφημένα με OpenPGP @@ -217,7 +220,7 @@ v\\Αποτύπωμα OMEMO Αποτύπωμα OMEMO (πηγή μηνύματος) v\\Αποτύπωμα OMEMO (πηγή μηνύματος) - \'Αλλες συσκευές + Άλλες συσκευές Επαλήθευση των αποτυπωμάτων OMEMO Μεταφόρτωση κλειδιών... Έγινε @@ -327,7 +330,7 @@ Δημιουργία αντιγράφων ασφαλείας Το αντίγραφο ασφαλείας σας έχει δημιουργηθεί Τα αρχεία του αντιγράφου ασφαλείας έχουν αποθηκευτεί στο %s - Επαναφορά αντιγράφου ασφαλείας + Γίνεται επαναφορά αντιγράφου ασφαλείας Έχει γίνει επαναφορά του αντιγράφου ασφαλείας σας Μην παραλείψετε να ενεργοποιήσετε τον λογαριασμό. Επιλογή αρχείου @@ -338,7 +341,7 @@ Άνοιγμα του %s αποστολή (ολοκλήρωση %1$d%%) Προετοιμασία του αρχείου για διαμοιρασμό - %s προσφέρθηκε για μεταφόρτωση + Το %s προσφέρθηκε για μεταφόρτωση Ακύρωση μετάδοσης ο διαμοιρασμός του αρχείου απέτυχε η μεταφορά αρχείου ακυρώθηκε @@ -418,10 +421,10 @@ Αποστολή του %s Προσφορά του %s Απόκρυψη των εκτός σύνδεσης - Η επαφή %s πληκτρολογεί... - Η επαφή %s σταμάτησε να πληκτρολογεί - Οι επαφές %s πληκτρολογούν... - Η επαφές %s σταμάτησαν να πληκτρολογούν + Ο/Η %s πληκτρολογεί... + Ο/Η %s σταμάτησε να πληκτρολογεί + Οι %s πληκτρολογούν... + Οι %s σταμάτησαν να πληκτρολογούν Ειδοποιήσεις πληκτρολόγησης Επιτρέψτε στις επαφές σας να γνωρίζουν πότε γράφετε μηνύματα προς αυτές Αποστολή τοποθεσίας @@ -451,7 +454,7 @@ Αναζήτηση επαφών Αναζήτηση σελιδοδεικτών Αποστολή ιδιωτικού μηνύματος - Η επαφή %1$s αποχώρησε από την ομαδική συζήτηση + Ο/Η %1$s αποχώρησε από την ομαδική συζήτηση Όνομα χρήστη Όνομα χρήστη Αυτό δεν είναι έγκυρο όνομα χρήστη @@ -464,6 +467,8 @@ Ο διακομιστής δεν είναι υπεύθυνος για αυτόν τον τομέα Χαλασμένος Διαθεσιμότητα + Εκτός χρήσης όταν η οθόνη είναι κλειδωμένη + Εμφάνιση παρουσίας ως εκτός χρήσης όταν η συσκευή κλειδώνεται Απασχολημένος/η όταν βρίσκεται σε σιωπηρή λειτουργία Σημειώνει την παρουσία σας ως Απασχολημένος/η όταν η συσκευή είναι σε κατάσταση σιωπής Χρήση της κατάστασης δόνησης ως σιωπηρή κατάσταση @@ -501,7 +506,7 @@ Φόρτωση περισσότερων μηνυμάτων Το αρχείο διαμοιράστηκε με την επαφή %s Η εικόνα διαμοιράστηκε με την επαφή %s - Η εικόνες διαμοιράστηκαν με την επαφή %s + Οι εικόνες διαμοιράστηκαν με την επαφή %s Το κείμενο διαμοιράστηκε με την επαφή %s Απόδοση δικαιώματος στο %1$s για πρόσβαση στον εξωτερικό αποθηκευτικό χώρο Απόδοση δικαιώματος στο %1$s για πρόσβαση στην φωτογραφική μηχανή @@ -577,7 +582,7 @@ Απόδοση δικαιώματος χρήσης Internet Εγώ Η επαφή ζητά συνδρομή σε υπηρεσία παρουσίας - Επιτρέπω + Να επιτραπεί Δεν υπάρχει δικαίωμα για πρόσβαση στο %s Δεν βρέθηκε ο απομακρυσμένος διακομιστής Λήξη χρόνου για τον απομακρυσμένο διακομιστή @@ -833,7 +838,7 @@ Μην χρησιμοποιείτε τη λειτουργία επαναφοράς αντιγράφων ασφαλείας για να κλωνοποιήσετε (ταυτόχρονη εκτέλεση) μια εγκατάσταση. Η επαναφορά αντιγράφου ασφαλείας προσφέρεται μόνο για μεταφορές ή σε περίπτωση που έχετε χάσει την αρχική συσκευή. Αδυναμία επαναφοράς αντιγράφου ασφαλείας. Αδυναμία αποκρυπτογράφησης του αντιγράφου ασφαλείας. Είναι ο κωδικός σωστός; - Δημιουργία & Επαναφορά αντιγράφων ασφαλείας + Δημιουργία & Επαναφορά Εισάγετε τη διεύθυνση XMPP Δημιουργία ομαδικής συζήτησης Είσοδος σε δημόσιο κανάλι @@ -893,8 +898,8 @@ Παρακαλώ ενεργοποιήστε έναν λογαριασμό Νέα κλήση Εισερχόμενη κλήση - Εισερχόμενη κλήση βίντεο - Σύνδεση + Εισερχόμενη βιντεοκλήση + Γίνεται σύνδεση Συνδέθηκε Αποδοχή κλήσης Τερματισμός κλήσης @@ -909,7 +914,7 @@ Αποτυχία εφαρμογής Τερματισμός κλήσης Κλήση σε εξέλιξη - Κλήση βίντεο σε εξέλιξη + Βιντεοκλήση σε εξέλιξη Απενεργοποίηστε το Tor για να κάνετε κλήσεις Εισερχόμενη κλήση Εισερχόμενη κλήση · %s @@ -918,7 +923,7 @@ Εξερχόμενη κλήση · %s Αναπάντηση κλήση Κλήση ήχου - Κλήση βίντεο + Βιντεοκλήση Βοήθεια Εναλλαγή στη συζήτηση Το μικρόφωνο δεν είναι διαθέσιμο @@ -949,11 +954,13 @@ Κάποιο μήνυμα δεν ήταν δυνατό να παραδοθεί Κάποια μηνύματα δεν ήταν δυνατό να παραδοθούν
- Αποτυχημένες διανομές + Αποτυχημένες παραδόσεις Περισσότερες επιλογές Δεν βρέθηκε εφαρμογή Πρόσκληση στο Conversations Αδυναμία ανάγνωσης πρόσκλησης Ο διακομιστής δεν υποστηρίζει την δημιουργία προσκλήσεων Κανένας από τους ενεργούς λογαριασμούς δεν υποστηρίζει αυτό το χαρακτηριστικό + Το αντίγραφο ασφαλείας δημιουργείται. Θα λάβετε ειδοποίηση όταν ολοκληρωθεί. + Αδυναμία ενεργοποίησης βίντεο. diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 5be2d0e909ef4a456a671e325dec5e505c677382..535209db8edc6c543a7783046b61163e1d1245f1 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -124,6 +124,7 @@ Tono de llamada Sonido de notificación Sonido de notificación para nuevos mensajes + Tono para las nuevas llamadas Periodo de gracia El periodo de tiempo en el que las notificaciones están silenciadas tras detectar actividad en otro de tus dispositivos. Avanzado @@ -131,6 +132,8 @@ Al enviar las trazas de error estás ayudando en el desarrollo Confirmar mensajes Permitir a tus contactos saber cuando has recibido y leído sus mensajes + Impedir capturas de pantalla + Ocultar el contenido de la aplicación en el selector de aplicaciones y bloquear las capturas de pantalla Pantalla OpenKeychain causó un error. Clave errónea para el cifrado. @@ -149,6 +152,7 @@ Archivo no encontrado Error general. ¿Es posible que no tengas espacio en disco? La aplicación usaste para seleccionar esta imagen no proporcionó suficientes permisos para leer el archivo.\n\nUtiliza un explorador de archivos diferente para seleccionar la imagen + La aplicación que has utilizado para compartir este archivo no presentó permisos suficientes Desconocido Deshabilitado temporalmente Conectado @@ -163,6 +167,7 @@ El servidor no soporta registros Token de registro inválido Error de negociación TLS + Dominio no verificable Policy violation Servidor incompatible Error de flujo @@ -215,6 +220,8 @@ OpenPGP Key ID Huella digital OMEMO Huella digital v\\OMEMO + Huella digital OMEMO (origen del mensaje) + Huella digital v\\OMEMO (origen del mensaje) Otros dispositivos Huellas digitales OMEMO de confianza Buscando claves... @@ -409,6 +416,7 @@ audio vídeo imagen + gráfico de vectores documento PDF Android App Contacto @@ -462,6 +470,8 @@ El servidor no es responsable de este dominio Error Disponibilidad + Ausente cuando el dispositivo esté bloqueado + Mostrar como Ausente cuando el dispositivo esté bloqueado Ocupado en modo silencio Mostrar como Ocupado cuando el dispositivo esté en modo silencio Modo vibración como modo silencio @@ -905,6 +915,7 @@ Conexión perdida Llamada rechazada Fallo en la aplicación + Problema de verificación Colgar Llamada saliente Video llamada saliente @@ -954,4 +965,8 @@ No se ha podido leer la invitación El servidor no soporta la creación de invitaciones Ninguna cuenta activa soporta esta característica - + La copia de seguridad ha empezado. Recibirás una notificación cuando se haya completado. + No se ha podido habilitar el vídeo. + Documento de texto plano + + diff --git a/src/main/res/values-fi/strings.xml b/src/main/res/values-fi/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..3fa9ad89f6f06952bf70e84ab7f59ab16322f909 --- /dev/null +++ b/src/main/res/values-fi/strings.xml @@ -0,0 +1,918 @@ + + + Asetukset + Uusi keskustelu + Hallitse tilejä + Hallinnoi tiliä + Päätä keskustelu + Yhteystiedot + Ryhmäkeskustelun tiedot + Kanavan tiedot + Lisää tili + Muokkaa nimeä + Lisää yhteystietoihin + Poista yhteystietolistasta + Estä yhteystieto + Peru yhteystiedon esto + Estä verkkotunnus + Peru verkkotunnuksen esto + Estä osallistuja + Peru osallistujan esto + Hallitse tilejä + Asetukset + Aloita keskustelu + Valitse yhteystieto + Valitse yhteystiedot + Jaa tilillä + Estolista + äskettäin + minuutti sitten + %d minuuttia sitten + + %d lukematon keskustelu + + + %d lukematonta keskustelua + + + lähettää... + Puretaan viestin salausta. Odota hetki... + OpenPGP-salattu viesti + Nimimerkki on jo käytössä + Nimimerkki on virheellinen + Ylläpitäjä + Omistaja + Moderaattori + Osallistuja + Vierailija + Poistetaanko %s yhteystiedoistasi? Keskustelujasi hänen kanssaan ei poisteta. + Estetäänkö %s lähettämästä viestejä sinulle? + Perutaanko %s:n esto lähettää viestejä sinulle? + Estetäänkö kaikki yhteydet verkkotunnuksesta %s? + Perutaanko kaikkien verkkotunnuksen %s käyttäjien esto? + Yhteystieto estetty + Estetty + Poistetaanko %s kirjanmerkeistä? Mitään keskustelujasi sen kanssa ei poisteta. + Rekisteröi uusi tili palvelimella + Vaihda salasanaa palvelimelle + Aloita keskustelu + Kutsu yhteystieto + Kutsu + Yhteystiedot + Yhteystieto + Peruuta + Aseta + Lisää + Muokkaa + Poista + Estä + Peruuta esto + Tallenna + OK + %1$s kaatui + Virheenkorjaustietojen lähettäminen XMPP-tililläsi helpottaa %1$s:n kehitystyötä. + Lähetä nyt + Älä koskaan pyydä lähettämään + Tiliin ei saatu yhteyttä + Useisiin tileihin ei saatu yhteyttä + Napauta hallinnoidaksesi tilejä + Liitä tiedosto + Lisätäänkö tämä puuttuva yhteystieto listaasi? + Lisää yhteystieto + toimitus epäonnistui + Valmistaudutaan lähettämään kuva + Valmistaudutaan lähettämään kuvat + Jaetaan tiedostoja. Odota hetki... + Pyyhi historia + Pyyhi keskusteluhistoria + Poistetaanko kaikki keskustelun viestit?\n\nVaroitus: Muilla laitteilla tai palvelimilla säilytettyjä kopioita ei poisteta. + Poista tiedosto + Haluatko varmasti poistaa tämän tiedoston?\n\nVaroitus: Muilla laitteilla tai palvelimilla olevia kopioita ei poisteta. + Päätä keskustelu myös + Valitse laite + Lähetä salaamaton viesti + Lähetä viesti + Lähetä viesti henkilölle %s + Lähetä OMEMO-salattu viesti + Lähetä v\\OMEMO-salattu viesti + Lähetä OpenPGP-salattu viesti + Uusi nimimerkki on jo varattu + Lähetä salaamaton + Salauksen purku epäonnistui. Sinulle ei varmaan ole oikeaa salaista avainta. + OpenKeychain + OpenKeychainia viestien salaamiseeen ja salauksen purkamiseen, sekä julkisten avaintesi hallinointiin.

Se on GPLv3+-lisensoitu ja saatavilla F-Droidista sekä Google Playsta.

(Käynnistä %1$s uudelleen asennettuasi sovelluksen.)]]>
+ Käynnistä uudelleen + Asenna + Asenna OpenKeychain + tarjotaan... + odotetaan... + OpenPGP-avainta ei löydy + Viestin salaaminen ei onnistu koska vastaanottaja ei mainosta julkista avaintaan.\n\nPyydä kontaktiasi ottamaan OpenPGP käyttöön. + OpenPGP-avaimia ei löydy + Viestin salaaminen ei onnistu koska kontaktisi eivät mainosta julkisia avaimiaan.\n\nPyydä heitä ottamaan OpenPGP käyttöön. + Yleinen + Lataa tiedostot + Lataa automaattisesti tiedostot jotka ovat pienempiä kuin... + Liitteet + Ilmoitus + Värinä + Värise uuden viestin saapuessa + LED-ilmoitus + Vilkuta ilmoitusvaloa vastaanotettuasi uuden viestin + Soittoääni + Ilmoitusääni + Uusien viestien ilmoitusääni + Soittoääni saamillesi puheluille + Rauhanaika + Kuinka pitkäksi aikaa ilmoitukset hiljennetään kun jollain toisella laitteillasi tehdään jotain. + Edistyneet + Älä koskaan lähetä vikailmoituksia + Vikailmoituksia lähettämällä autat kehitystyötä + Lukukuittaus + Ilmoita lähettäjälle kun olet vastaanottanut ja lukenut viestin + Estä kuvankaappaukset + Piilota sovelluksen sisältö sovellusvaihtajassa ja estä ruutukaappaukset + Käyttöliittymä + OpenKeychain-virhe + Avain ei kelpaa salaamiseen. + Hyväksy + Virhe tapahtui + Virhe + Tilisi + Lähetä tilapäivityksiä + Vastaanota tilapäivityksiä + Pyydä tilapäivityksiä + Valitse kuva + Ota kuva + Alustavasti hyväksy liittymispyynnöt + Valitsemasi tiedosto ei ole kuva + Kuvatiedoston pakkaaminen epäonnistui + Tiedostoa ei löytynyt + Yleinen I/O-virhe. Ehkä vapaa tallennustila loppui kesken? + Kuvan valitsemiseen käyttämäsi sovellus ei myöntänyt riittävästi oikeuksia kuvan lukemiseen.\n\nKäytä toista tiedostonhallintaohjelmaa kuvan valitsemiseen. + Jakamiseen käyttämäsi sovellus ei myöntänyt riittävästi oikeuksia. + Tuntematon + Väliaikaisesti poistettu käytöstä + Paikalla + Yhdistäää\u2026 + Poissa + Ei sallittu + Palvelinta ei löydy + Ei yhteyttä + Rekisteröinti epäonnistui + Käyttäjänimi on varattu + Rekisteröinti valmis + Palvelin ei tue rekisteröintiä + Viallinen rekisteröintitunnus + TLS-kättely epäonnistui + Verkkotunnuksen varmentaminen epäonnistui + Yhteensopimaton palvelin + Salaamaton + OTR + OpenPGP + OMEMO + Poista tili + Poista käytöstä väliaikaisesti + Julkaise proofilikuva + Julkaise OpenPGP julkinen avain + Poista OpenPGP julkinen avain + Haluatko varmasti poistaa OpenPGP-avaimesi tilamainostuksistasi?\nYhteystietosi eivät voi enää lähettää sinulle OpenPGP-salattuja viestejä. + OpenPGP julkinen avain julkaistu. + Ota tunnus käyttöön + Oletko varma? + Tilin poistaminen pyyhkii koko keskusteluhistoriasi + Nauhoita ääntä + XMPP-osoite + Estä XMPP-osoite + käyttäjä@esimerkki.fi + Salasana + Tämä ei ole kunnollinen XMPP-osoite + Muisti loppui. Kuva on liian suuri. + Lisätäänkö %s osoitekirjaan? + Tietoa palvelimesta + XEP-0313: MAM + XEP-0280: Viestin kopiot + XEP-0352: Asiakkaan tilan vihjaus + XEP-0191: Estokomennot + XEP-0237: Yhteystietolistan versiointi + XEP-0198: Virran hallinta + XEP-0215: Ulkoisten palveluiden löytäminen + XEP-0163: PEP (Profiilikuvat / OMEMO) + XEP-0363: Tiedoston lähetys HTTP:llä + XEP-0357: Työntö + käytössä + ei käytössä + Julkisten avainten mainostus puuttuu + nähty juuri äsken + nähty minuutti sitten + nähty %d minuuttia sitten + nähty tunti sitten + nähty %d tuntia sitten + nähty päivä sitten + nähty %d päivää sitten + Salattu viesti. Asenna OpenKeychain purkaaksesi salauksen. + Löytyi uusi OpenPGP-salattu viesti + OpenPGP-avaimen tunniste + OMEMO-sormenjälki + v\\OMEMO-sormenjälki + OMEMO-sormenjälki (viestin lähettäjä) + v\\OMEMO-sormenjälki (viestin lähettäjä) + Muut laitteet + Luota OMEMO-sormenjälkiin + Haetaan avaimia... + Valmis + Pura salaus + Kirjanmerkit + Haku + Syötä yhteystieto + Poista yhteystieto + Näytä yhteystieto + Estä yhteystieto + Peruuta yhteystiedon esto + Luo + Valitse + Yhteystieto on jo olemassa + Liity + kanava@kokous.esimerkki.fi/nimimerkki + kanava@kokous.esimerkki.fi + Tallenna kirjanmerkkinä + Poista kirjanmerkki + Tuhoa ryhmäkeskustelu + Tuhoa kanava + Haluatko varmasti tuhota tämän ryhmäkeskustelun?\n\nVaroitus: Ryhmäkeskustelu poistetaan lopullisesti palvelimelta. + Haluatko varmasti tuhota tämän julkisen kanavan?\n\nVaroitus: Kanava poistetaan lopullisesti palvelimelta. + Ryhmäkeskustelun tuhomainen epäonnistui + Kanavan tuhoaminen epäonnistui + Muokkaa ryhmäkeskustelun aihetta + Aihe + Liitytään ryhmäkeskusteluun... + Poistu + Yhteystieto lisätty luetteloon + Lisää takaisin + %s on lukenut tähän asti + %s ovat lukeneet tähän asti + %1$s ja %2$d muuta on lukenut tähän asti + Kaikki ovat lukeneet tähän asti + Julkaise + Napauta profiilikuvaa valitaksesi kuvan galleriasta + Julkaistaan... + Palvelin hylkäsi julkaisusi + Kuvan muuntaminen epäonnistui + Profiilikuvan tallentaminen levylle epäonnistui + (Tai paina pitkään palauttaaksesi oletuksen) + Palvelin ei tue profiilikuvien julkaisua + kuiskasi + %s:lle + Lähetä yksityisviesti %s:lle + Yhdistä + Tili on jo olemassa + Seuraava + Istunto aloitettu + Ohita + Poista ilmoitukset käytöstä + Ota käyttöön + Ryhmäkeskustelu vaatii salasanan + Kirjoita salasana + Pyydä yhteystietoa ensin lähettämään tilapäivityksiä.\n\nTätä käytetään sen tunnistamiseen mitä sovellusta tämä käyttää. + Pyydä nyt + Ohita + Varoitus: Tämän lähettäminen ilman molemminpuolisia tilapäivityksiä voi aiheuttaa odottamattomia ongelmia.\n\nMene \"Yhteystiedon tietoihin\" tarkistaaksesi tilapäivitysten tilauksesi. + Turvallisuus + Salli viestien korjaaminen + Mahdollistaa muiden muokata sinulle lähettämiään viestejä jälkikäteen + Edistyneet asetukset + Ole varovainen näiden kanssa + Tietoa %s + Hiljaisuus + Alku + Loppu + Ota käyttöön hiljaisuus + Ilmoitukset vaimennetaan hiljaisuuden aikana + Muut + Synkronoi kirjanmerkkien kanssa + Liity ryhmään automaattisesti jos se on kirjanmerkeissäsi + OMEMO-sormenjälki kopioitu leikepöydälle + Sinut on estetty tästä ryhmäkeskustelusta + Tämä ryhmäkeskustelu on vain jäsenille + Resurssin rajallisuus + Sinut on poistettu tästä ryhmästä + Ryhmäkeskustelu on suljettu + Et ole enää tässä ryhmäkeskustelussa + käytetään tiliä %s + palvelimella %s + Tarkistetaan %s HTTP-palvelimella + Et ole yhteydessä. Yritä myöhemmin uudelleen + Tarkista %s:n koko + Tarkista %1$s:n koko palvelimella %2$s + Toiminnot + Lainaa + Liitä lainauksena + Kopio alkuperäinen URL + Lähetä uudestaan + Tiedoston URL + URL kopioitu leikepöydälle + XMPP-osoite kopioitu leikepöydälle + Vikailmoitus kopioitu leikepöydälle + web-osoite + Lue 2D-viivakoodi + Näytä 2D-viivakoodi + Näytä estolista + Tilitiedot + Vahvista + Yritä uudelleen + Palvelu etualalla + Estää käyttöjärjestelmää katkaisemasta yhteyttäsi + Tee varmuuskopio + Varmuuskopioiden säilytyspaikka: %s + Tehdään varmuuskopiota + Varmuuskopiosi on luotu + Varmuuskopiotiedostot tallennettu kansioon %s + Palautetaan varmuuskopiota + Varmuuskopiosi on palautettu + Älä unohda ottaa tiliä käyttöön. + Valitse tiedosto + Vastaanotetaan %1$s (%2$d%% valmis) + Lataa %s + Poista %s + tiedosto + Avaa %s + Lähetetään (%1$d%% valmis) + Valmistellaan tiedoston lähettämistä + %s tarjottu ladattavaksi + Peru siirto + tiedoston jako epäonnistui + tiedoston siirto peruttu + Tiedosto poistettu + Tiedoston avaamiseen sopivaa sovellusta ei löytynyt + Linkin avaamiseen sopivaa sovellusta ei löytynyt + Yhteystiedon näyttämiseen sopivaa sovellusta ei löytynyt + Dynaamiset tunnisteet + Näytä tunnisteet yhteystiedon alla (vain luku) + Näytä ilmoitukset + Ryhmäkeskustelupalvelinta ei löytynyt + Ryhmäkeskustelun luominen epäonnistui + Tilin profiilikuva + Kopioi OMEMO-sormenjälki leikepöydälle + Uusi OMEMO-avain + Poista laitteet + Haluatko poistaa kaikki muut laitteet OMEMO-mainoksistasi? Kun laite seuraavan kerran yhdistää, se lisää itsensä, mutta ennen sitä lähetettyjä viestejä et välttämättä saa. + Jokin meni pieleen + Haetaan historiaa palvelimelta + Palvelimella ei ollut enempää historiaa + Päivitetään... + Salasana vaihdettu! + Salasanan vaihto epäonnistui + Vaihda salasana + Nykyinen salasana + Uusi salasana + Salasana ei voi olla tyhjä + Kaikki tilit käyttöön + Kaikki tilit pois käytöstä + Poissa + Hylkiö + Jäsen + Edistynyt tila + Myönnä jäsenyys + Peru jäsenyys + Myönnä ylläpitäjän oikeudet + Peru ylläpitäjän oikeudet + Myönnä omistajuus + Peru omistajuus + Poista ryhmäkeskustelusta + Poista kanavalta + Estä ryhmäkeskustelusta + Estä kanavalta + Olet poistamassa %s:n julkiselta kanavalta. Ainoa tapa tehdä se on estää kyseinen käyttäjä ikuisesti. + Estä nyt + %s:n roolin muuttaminen epäonnistui + Yksityisen ryhmäkeskustelun asetukset + Julkisen kanavan asetukset + Yksityinen, vain jäsenille + Tee XMPP-osoitteista kaikille näkyvät + Tee kanavasta moderoitu + Et osallistu + Ryhmäkeskustelun asetuksia muutettu! + Ryhmäkeskustelun asetuksia ei voitu muuttaa + Ei koskaan + Kunnes toisin mainitaan + Torkku + Vastaa + Merkitse luetuksi + Syöttö + Enter lähettää + Käytä Enteriä viestin lähetämiseen. Ctrl+Enter lähettää viestin joka tapauksessa. + Näytä enter-nappi + Vaihtaa emojinapin enteriksi + ääni + video + kuva + vektorigrafiikka + PDF-asiakirja + Android-sovellus + Yhteystieto + Profiilikuva julkaistu! + Lähetetään %s + Tarjotaan %s + Piilota poissaolevat + %s kirjoittaa... + %s lopetti kirjoittamisen + %s kirjoittavat... + %s lopettivat kirjoittamisen + Kirjoitusilmoitukset + Näyttää ytheystiedoillesi kun kirjoitat heille viestiä + Lähetä sijainti + Näytä sijainti + Sijainnin näyttämiseen sopivaa sovellusta ei löytynyt + Sijainti + Keskustelu suljettu + Poistuit yksityisestä ryhmäkeskustelusta + Poistuit julkisesta kanavasta + Älä luota varmenteiden myöntäjiin + Kaikki varmenteet täytyy hyväksyä käsin + Poista varmenteet + Poista käsin hyväksytyt varmenteet + Ei käsin hyväksyttyjä varmenteita + Poista varmenteet + Peruuta + + %d varmenne poistettiin + %d varmennetta poistettiin + + Korvaa \"Lähetä\"-nappi pikatoiminnolla + Pikatoiminto + Ei mikään + Viimeksi käytetty + Valitse pikatoiminto + Lähetä yksityisviesti + %1$s poistui ryhmäkeskustelusta + Käyttäjänimi + Käyttäjänimi + Lataus epäonnistui: Palvelinta ei löydy + Lataus epäonnistui: Tiedostoa ei löytynyt + Lataus epöonnistui: Isäntään ei saatu yhteyttä + Lataus epäonnistui: Tiedoston tallennus epäonnistui + Tor-verkkoa ei saavutettu + Palvelin ei vastaa tästä verkkotunnuksesta + Rikki + Saatavuus + Poissa kun laite on lukittu + Näytä minut poissaolevana kun näyttö on lukittu + Kiireinen kun laite on äänetön + Näytä minut kiireisenä kun laite on äänettömänäq + Kohtele vain värinä -tilaa äänettömän lailla + Näytä minut kiireisenä kun laite on vain värinä -tilassa + Laajemmat yhteysasetukset + Näytä isäntänimen ja portin valinta tiliä lisätessä + xmpp.esimerkki.fi + Kirjaudu varmenteella + Varmenteen jäsennys epäonnistui + Arkistointiasetukset + Palvelimen arkitsointiasetukset + Kysytään arkistointiasetuksia. Odota hetki... + Arkistointiasetusten haku epäonnistui + CAPTCHA vaaditaan + Kirjoita ylläolevassa kuvassa näkyvä teksti + Varmenneketju ei ole luotettu + XMPP-osoite on eri kuin varmenteessa + Uusi varmenne + OMEMO-avaimen lataus epäonnistui! + OMEMO-avain vahvistettu varmenteella! + Laitteesi ei tue käyttäjän varmenteen valitsemista! + Yhteys + Käytä Tor-verkkoa + Tunneloi kaikki yhteydet Tor-verkon kautta. Vaatii Orbot-sovelluksen + Isäntänimi + Portti + Palvelin- tai .onion-osoite + Porttinumero on virheellinen + Isäntänimi on virheellinen + %1$d tunnusta %2$d:sta yhdistetty + + %d viesti + %d viestiä + + Lataa lisää viestejä + Tiedosto jaettu %s:n kanssa + Kuva jaettu %s:n kanssa + Kuvat jaettu %s:n kanssa + Salli %1$s:n käyttää ulkoista tallennustilaa + Salli %1$s:n käyttää kameraa + Synkronoi yhteystietojen kanssa + %1$s haluaa pääsyn osoitekirjaasi yhdistääkseen sen XMPP-yhteystietojesi kanssa.\nTämä näyttää yhteystietojesi koko nimen ja kuvan.\n\n%1$s pelkästään lukee osoitekirjaasi ja vertailee niitä paikallisesti, lähettämättä mitään palvelimelle. + Ilmoita kaikista uusista viesteistä + Ilmoita vain kun minut mainitaan + Ilmoitukset pois käytöstä + Ilmoitukset keskeytetty + Kuvan pakkaus + Vinkki: Käytä \'Lähetä tiedosto\':a \'Lähetä kuva\':n sijaan lähettääksesi kuvan pakkaamattomana sillä kertaa tästä asetuksesta huolimatta. + Aina + Vain isot kuvat + Akun käytön optimointi käytössä + Poista käytöstä + Valittu alue on liian suuri + (Ei aktivoituja tilejä) + Tämä kenttä on pakollinen + Korjaa viestiä + Lähetä korjattu viesti + Olet jo varmistanut tämän henkilön OMEMO-sormenjäljen turvallisesti luottamuksen varmistamikseksi. Hyväksymällä varmistat vain että %s on osa tätä ryhmäkeskustelua. + Olet poistanut tämän tilin käytöstä + URI:n jakamiseen sopivaa sovellusta ei löytynyt + Jaa URI sovelluksella... + Hyväksy ja jatka + XMPP-osoitteesi tulee olemaan kokonaisuudessaan: %s + Luo tunnus + Käytän itse valitsemaani palveluntarjoajaa + Valitse käyttäjänimi + Hallitse saatavuutta käsin + Valitse saatavuutesi itse muokatessasi tilaviestiäsi. + Tila + Vapaa juttelemaan + Paikalla + Poissa + Ei saatavilla + Kiireellinen + Turvallinen salasana on luotu + Laitteesi ei tue akun kulutuksen optimoinnin ohittamista + Rekisteröinti epäonnistui: Yritä myöhemmin uudelleen + Rekisteröinti epäonnistui: Salasana on liian heikko + Valitse osallistujat + Luodaan ryhmää... + Kutsu uudestaan + Poista käytöstä + Lyhyt + Keskipitkä + Pitkä + Kertoo yhteystiedoillesi milloin käytät Conversationsia + Yksityisyys + Teema + Valitse väripaletti + Automaattinen + Vaalea + Tumma + Vihreä tausta + Näytä vastaanotetut viestit vihreällä taustalla + OpenKeychainiin ei saatu yhteyttä + Laite ei ole enää käytössä + Tietokone + Puhelin + Tabletti + Selain + Pääte + Maksu vaaditaan + Salli internetin käyttö + Minä + Salli + Käyttöoikeus %s puuttuu + Etäpalvelinta ei löytynyt + Etäpalvelimen aikakatkaisu + Tiliä ei saatu muutettua + Ilmoita tämä XMPP-osoite roskapostista. + Poista OMEMO-identiteetit + Tee uudet OMEMO-avaimet. Kaikkien yhteystietojesi pitää varmistaa sinut uudestaan. Käytä tätä ainoastaan viimeisenä oljenkortena. + Poista valitut avaimet + Yhteys vaaditaan profiilikuvan julkaisemista varten. + Näytä virheilmoitus + Virheilmoitus + Datansäästö käytössä + Käyttöjärjestelmäsi estää %1$s:tä käyttämästä nettiä ollessaan taustalla. Vastaanottaaksesi ilmoitukset uusista viesteistä, salli %1$s:n käyttää esteettä verkkoa datansäästön ollessa käytössä. %1$s tekee silti parhaansa käyttääkseen mahdollisimman vähän dataa. + Laitteesi ei tue datansäästön poistamista käytöstä sovellukselle %1$s. + Väliaikaisen tiedoston luominen epäonnistui + Laite on varmennettu + Kopioi sormenjälki + Olet varmentanut kaikki hallussasi olevat OMEMO-avaimet + Viivakoodi ei sisällä tähän keskusteluun liittyviä sormenjälkiä. + Varmennetut sormenjäljet + Käytä kameraa yhteystietosi viivakoodin lukemiseen + Odota hetki, avaimia ladataan + Jaa viivakoodina + Jaa XMPP-osoitteena + Jaa HTTP-linkkinä + Sokea luottamus ennen varmistusta + Luota uusiin laitteisiin varmistamattomilta yhteystiedoilta, mutta vaadi varmistettujen yhteystietojen uusien laitteiden manuaalinen hyväksyminen. + OMEMO-avaimiin luotetaan sokeasti, eli ne voivat olla jonkun muun tai joku voi salakuunnella. + Ei luotettu + Viallinen 2D-viivakoodi + Tyhjennä välimuisti (kamerasovelluksen käyttämä) + Tyhjennä välimuisti + Siivoa yksityinen tallennustila + Tyhjennä tallennustila jossa tiedostoja säilytetään (Ne voi ladata uudestaan palvelimelta) + Seurasin tätä linkkiä luotettavasta lähteestä + Olet varmistamassa yhteystiedon %1$s OMEMO-avaimia linkin avaamalla. Tämä on turvallista ainoastaan jos löysit linkin luotettavasta paikasta jossa vain %2$s on sen kyennyt julkaisemaan. + Varmista OMEMO-avaimet + + %d sekunti + %d sekuntia + + + %d minuutti + %d minuuttia + + + %d tunti + %d tuntia + + + %d päivä + %d päivää + + + %d viikko + %d viikkoa + + + %d kuukausi + %d kuukautta + + Viestien automaattinen poisto + Poista tältä laitteelta automaattisesti valittua vanhemmat viestit. + Salataan viestiä + Pakataan videota + Yhteystieto estettiin. + Ilmoitukset tuntemattomila + Ilmoita tuntemattomien henkilöiden viesteistä ja puheluista. + Sait viestin tuntemattomalta henkilöltä + Estä tuntematon + Estä koko verkkotunnus + paikalla juuri nyt + Yritä salauksen purkua uudestaan + Istuntovirhe + Alennettu SASL-mekanismi + Palvelin vaatii tekemään rekisteröinnin verkkosivulla + Avaa verkkosivu + Verkkosivun avaamiseen sopivaa sovellusta ei löytynyt + Tänään + Eilen + Varmenna verkkotunnus DNSSEC:llä + Palvelinvarmenteet jotka sisältävät varmennetun verkkotunnukset hyväksytään + Varmenne ei sisällä XMPP-osoitetta + osittain + Nauhoita video + Kopioi leikepöydälle + Viesti kopioitu leikepöydälle + Viesti + Yksityisviestit on poistettu käytöstä + Suojatut sovellukset + Saadaksesi ilmoituksia silloinkin kun näyttö on sammutettu, Conversations pitää lisätä suojattujen sovellusten luetteloon. + Hyväksytäänkö tuntematon varmenne? + Palvelimen varmenne ei ole luotetun myöntäjän allekirjoittama. + Hyväksytäänkö eriävä palvelimen nimi? + Palvelin ei voinut tunnistautua olevansa verkkotunnusta \"%s\". Varmenne sisältää vain seuraavat verkkotunnukset: + Haluatko yhdistää joka tapauksessa? + Varmenteen tiedot: + Kerran + QR-koodilukija tarvitsee luvan käyttää kameraa + Vieritä alas asti + Vieritä alas veistin lähetyksen jälkeen + Vaihda tila + Muokkaa tilaa + Poista salaus käytöstä + %1$s ei voi lähettää salattuja viestejä %2$s:lle. Se voi johtua siitä että vastaanottajan palvelin on vanhentunut tai hänen käyttämänsä sovellus ei tue OMEMO:a. + Laiteluettelon lataus epäonnistui + Salausavainten lataus epäonnistui + Vinkki: Jossain tapauksissa tämä ratkeaa kun molemmat osapuolet lisäävät toisena yhteystietoihinsa. + Haluatko varmasti poistaa OMEMO-salauksen käytöstä tässä keskustelussa?\nPalvelimen ylläpitäjä voi tällöin lukea viestinne, mutta se voi olla ainoa mahdollinen tapa keskustella vanhentunutta sovellusta käyttävän henkilön kanssa. + Poista käytöstä nyt + Luonnos: + OMEMO-salaus + OMEMO:a ei koskaan käytetä oletuksena uusissa keskusteluissa. + Luo pikakuvake + Kirjasinkoko + Kirjasimen suhteellinen koko sovelluksen sisällä. + Käytössä oletuksena + Oletuksena pois käytöstä + Pieni + Keksikokoinen + Suuri + Viestiä ei salattu tälle laitteelle + OMEMO-salatun viestin purku epäonnistui + peru + Sijainnin jakaminen on pois käytöstä + Kopioi sijainti + Jaa sijainti + Reittiohjeet + Jaa sijainti + Näytä sijainti + Jaa + Odota hetki... + Salli %1$s:n käyttää mikrofonia + GIF + Näytä keskustelu + Sijainnin jako -lisäosa + Käytä lisäosaa sisäänrakennetun kartan sijaan + Kopioi web-osoite + Kopioi XMPP-osoite + Tiedostonjako HTTP:llä S3:een + \'Aloita keskustelu\' -näytöllä avaa näppäimistö ja siirrä kursori hakukenttään + Ryhmän kuvake + Isäntäpalvelin ei tue ryhmäkeskustelun kuvakkeita + Vain omistaja voi vaihtaa kuvakkeen + Yhteystiedon nimi + Nimimerkki + Nimi + Nimen antaminen on vapaaehtoista + Ryhmäkeskustelun nimi + Ryhmäkeskustelu on tuhottu + Tallennetta ei voitu tallentaa + Edustapalvelu + Tilatieto + Yhteysongelmat + Viestit + Puhelut + Viestit + Saapuvat puhelut + Toimitusvirheet + Tärkeys, ääni, värinä + Videonpakkaus + Näytä media + Osallistujat + Mediaselain + Videon laatu + Heikompi laatu tarkoittaa pienempiä tiedostoja + Keski (360p) + Korkea (720p) + peruutettu + Olet jo aloittanut viestin luonnostelun + Ominaisuutta ei ole toteutettu + Virheellinen maakoodi + Valitse maa + puhelinnumero + Vahvista puhelinnumerosi + Quicksy lähettää viestin sinulle varmistaaksesi puhelinnumerosi. Operaattorisi saattaa laskuttaa siitä. Syötä maakoodi ja puhelinnumero: +
%s

Onko se oikein, vai haluaisitko muuttaa numeroa?]]>
+ %s ei ole kelvollinen puhelinnumero. + Syötä puhelinnumerosi. + Varmista %s + %s.]]> + Lähetimme sinulle uuden viestin 6-numeroisella koodilla. + Kirjoita 6-numeroinen PIN-koodi alapuolelle. + Lähetä uusi tekstiviesti + Lähetä uusi tekstivieti (%s) + Odota (%s) + takaisin + Mahdollinen PIN-koodi liitettiin leikepöydältä automaattisesti + Syötä 6-numeroinen PIN-koodisi. + Haluatko varmasti perua rekisteröintiprosessin? + Kyllä + Ei + Varmistetaan... + Pyydetään tekstiviestiä + Syöttämäsi PIN-koodi on väärä. + Lähettämämme PIN-koodi on vanhentunut. + Tuntematon verkkovirhe. + Tuntematon vastaus palvelimelta. + Palvelimeen ei saatu yhteyttä. + Turvallinen yhteys epäonnistui. + Palvelinta ei löytynyt. + Pyyntösi käsittelyssä tapahtui jokin virhe. + Ei verkkoyhteyttä + Odota %s ja yritä uudelleen + Liian monta yritystä + Käytät vanhentunutta versiota tästä sovelluksesta. + Päivitä + Puhelinnumerolla on tällä hetkellä kirjauduttu sisään toiselle laitteelle. + Nimesi + Kirjoita nimesi + Napauta muokkaa-nappia valitaksesi nimesi. + Hylkää pyyntö + Asenna Orbot + Käynnistä Orbot + Sovelluskauppaa ei löydy. + Tämä kanava julkaisee XMPP-osoitteesi + e-kirja + Alkuperäinen (pakkaamaton) + Avaa sovelluksella... + Conversations-profiilikuva + Valitse tili + Palauta varmuuskopiosta + Palauta + Syötä salasanasi tilille %s palauttaaksesi varmuuskopion. + Älä käytä varmuuskopion palautusta asennuksen kloonaamiseksi (käyttääksesi yhtä aikaa toisella laitteella). Varmuuskopion palautus on tarkoitettu ainoiastaan laitteen tai sovelluksen vaihtamiseksi tai jos olet kadottanut alkuperäisen laitteesi. + Varmuuskopion palautus epäonnistui. + Varmuuskopion salauksen purku epäonnistui. Onko salasana oikein? + Varmuuskopiointi & palautus + Syötä XMPP-osoite + Luo ryhmäkeskustelu + Liity julkiselle kanavalle + Luo yksityinen ryhmäkeskustelu + Luo julkinen kanava + Kanavan nimi + XMPP-osoite + Anna kanavalle nimi + Anna XMPP-osoite + Tämä on XMPP-osoite. Anna nimi sen sijaan. + Luodaan julkista kanavaa... + Kanava on jo olemassa + Liityit olemassa olevalle kanavalle + Kanavan asetuksia ei saatu tallennettua + Salli kenen tahansa vaihtaa aihetta + Salli kenen tahansa kutsua ryhmään + Kuka tahansa voi muokata aihetta. + Omistajat voivat muokata aihetta. + Ylläpitäjät voivat muokata aihetta. + Omistajat voivat kutsua muita. + Kuka tahansa voi kutsua muita. + XMPP-osoitteet ovat näkyvillä ylläpitäjille. + XMPP-osoiteet ovat näkyllä kaikille. + Tällä julkisella kanavalla ei ole osallistujia. Kutsu yhteystietojasi tai käytä jakopainiketta kanavan XMPP-osoitteen levittämiseen. + Tässä yksityisessä ryhmässä ei ole ketään. + Hallitse oikeuksia + Tiedosto on liian iso + Liitä + Löydä kanavia + Hae kanavia + Mahdollinen yksityisyyden loukkaus! + search.jabber.network.

Sen käyttö lähettää IP-osoitteesi ja hakusanat palvelulle. Katso heidän yksityisyyskäytännöstään lisätietoa.]]>
+ Minulla on jo tili + Lisää olemassa oleva tili + Rekisteröi uusi tili + Tämä vaikuttaa verkkotunnukselta + Lisää silti + Tämä vaikuttaa kanavan osoitteelta + Jaa varmuuskopiotiedostot + Tapahtuma + Avaa varmuuskopio + Valitsemasi tiedosto ei ole Conversationsin varmuuskopio + Tämä tili on jo asennettu + Syötä tämän tilin salasana + Toiminnon suorittaminen epäonnistui + Liity julkiselle kanavalle... + Jakava sovellus ei antanut tarvittavaa lupaa lukea tiedostoa. + + jabber.network + Paikallinen palvelin + Suurimman osan käyttäjistä kannattaa valita \'jabber.network\' sillä se tarjoaa parempia ehdotuksia koko julkisesta XMPP-ekosysteemistä. + Kanavien löytötapa + Varmuuskopio + Tietoa + Ota jokin tili käyttöön + Soita puhelu + Saapuva puhelu + Saapuva videopuhelu + Yhdistetään + Yhdistetty + Hyväksytään puhelua + Päätetään puhelua + Vastaa + Hylkää + Etsitään laitteita + Soi + Kiireinen + Puhelun yhdistäminen epäonnistui + Yhteys katkesi + Puhelu peruttu + Sovelluksen virhe + Varmennusvirhe + Päätä puhelu + Puhelu kesken + Videopuhelu kesken + Poista Tor käytöstä soittaaksesi puhelun + Saapuva puhelu + Saapuva puhelu · %s + Vastaamaton puhelu · %s + Lähtevä puhelu + Lähtevä puhelu · %s + Vastaamaton puhelu + Äänipuhelu + Videopuhelu + Apua + Vaihda keskusteluun + Mikrofoni ei käytettävissä + Voit osallistua vain yhteen puheluun kerrallaan. + Takaisin keskeneräiseen puheluun + Kameran vaihtaminen epäonnistui + Kiinnitä + Irrota + GPX-reitti + Viestin korjaaminen epäonnistui + Kaikki keskustelut + Tämä keskustelu + Profiilikuvasi + %s:n profiilikuva + OMEMO-salattu + OpenPGP-salattu + Salaamaton + Poistu + Jätä viesti vastaajaan + Toista ääni + Keskeytä ääni + Lisää yhteystieto, luo tai liity ryhmäkeskusteluun, tai etsi kanava + + Näytä %1$d osallistuja + Näytä %1$d osallistujaa + + + Viestiä ei saatu toimitettua + Joitain viestejä ei saatu toimitettua + + Toimitusvirheet + Lisää vaihtoehtoja + Sovellusta ei löytynyt + Kutsu Conversationsiin + Kutsun jäsentäminen epäonnistui + Palvelin ei tue kutsujen luomista + Yksikään aktiivinen tili ei tue tätä toimintoa + Varmuuskopion teko aloitettu. Saat ilmoituksen kun se on valmis. + Videon käyttöönotto epäonnistui + Perustekstiasiakirja + +
diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index cb8669df9f08c08d228a8341f8aaad15ae01978b..256baacab56fdad1fd7e25b3acc26e201f447fbe 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -124,6 +124,7 @@ Sonnerie Son des notifications Son de notification pour les nouveaux messages + Sonnerie d\'appel entrant Période sans notification La durée pendant laquelle les notifications sont désactivées après la détection d\'une activité sur l\'un de vos autres appareils. Avancé @@ -149,6 +150,7 @@ Impossible de trouver le fichier Erreur générale d\'E/S. Avez-vous encore de l\'espace libre ? L\'application utilisée ne donne pas la permission de lire l\'image.\n\nUtilisez une autre application pour choisir une image. + L\'app avec laquelle vous avez partagé ce fichier n\'a pas fourni assez de permissions. Inconnu Désactivé temporairement En ligne @@ -163,6 +165,7 @@ Inscription non supportée par le serveur Jeton d’inscription invalide La négociation TLS a échoué + Domaine non vérifiable Violation de politique Serveur incompatible Erreur de flux @@ -215,6 +218,7 @@ ID de clé OpenPGP Empreinte OMEMO v\\Empreinte OMEMO + Empreinte OMEMO (origine du message) Autres appareils Faire confiance aux empreintes OMEMO Récupération des clés… @@ -462,6 +466,7 @@ Le serveur n\'est pas responsable pour ce domaine Détraqué Disponibilité + Absent quand l\'appareil est verrouillé Occupé en mode silence Occupé lorsque l\'appareil est en mode silencieux Indisponible en mode vibreur diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index f7c151661a7d5957e036f7493bad6db1e2be52ae..fb320cfaa6f7dff739cf419c553ff114cc982236 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -47,8 +47,8 @@ Participante Visitante Desexas eliminar a %s da túa lista de contactos? As conversas con este contacto non se eliminarán. - Desexa evitar que %s lle envíe mensaxes? - Desexa desbloquear %s e permitirlles que lle envíen mensaxes? + Queres evitar que %s che envíe mensaxes? + Queres desbloquear a %s e permitirlle que che envíe mensaxes? Bloquear todos os contactos desde 1%s? Desbloquear todos os contactos desde 1%s? Contacto bloqueado @@ -58,7 +58,7 @@ Cambiar o contrasinal no servidor Compartir con Comezar conversa - Invitar contacto + Convidar contacto Convidar Contactos Contacto @@ -85,12 +85,12 @@ Preparándose para enviar a imaxe Preparándose para enviar imaxes Compartindo ficheiros. Por favor agarde... - Limpar historial - Limpar historial de conversa + Baleirar historial + Eliminar historial da conversa ¿Queres eliminar as mensaxes desta conversa?\n\nAviso: Esto non lle afecta as mensaxes gardadas noutros dispositivos ou servidores. Eliminar ficheiro - Está segura de querer eliminar este ficheiro?\n\nAviso: Esto non eliminará as copias de este ficheiro que están gardadas en outros dispositivos ou servidores. - Pechar esta conversa a posteriori + Tes a certeza de querer eliminar este ficheiro?\n\nAviso: Esto non eliminará as copias de este ficheiro que están gardadas noutros dispositivos ou servidores. + Pechar a conversa tras baleirar Escoller dispositivo Enviar mensaxe non cifrada Enviar mensaxe @@ -131,7 +131,9 @@ Nunca enviar informe de erros Ao enviar trazas do sistema estás axudando ao desenvolvemento Confirmación de mensaxes - Permitir aos teus contactos saber se recibiches e liches as súas mensaxes + Permitir aos teus contactos saber se recibiches e leches as súas mensaxes + Evitar capturas de pantalla + Agochar os contidos da app no cambiador de apps e bloquear pantallazos Interface OpenKeychain producíu un erro. Chave incorrecta para cifrar. @@ -144,12 +146,12 @@ Solicitar actualizacións de presenza Seleccionar imaxe Facer foto - Por defecto otorgar peticiones de suscripción + Por defecto conceder solicitudes de subscrición O arquivo seleccionado non é unha imaxe Non se puido converter o ficheiro de imaxe Arquivo non atopado Erro xeral de I/O. ¿Quedaches sen espazo no disco? - A app utilizada para escoller esta imaxe non deu permisos suficientes para ler o ficheiro.\n\nUsa un xestor de ficheiros diferente para escoller a imaxe + A app utilizada para seleccionar esta imaxe non deu permisos suficientes para ler o ficheiro.\n\nUsa un xestor de ficheiros diferente para elexir a imaxe A app que usaches para compartir este ficheiro non concedeu os permisos suficientes. Descoñecido Desactivado temporalmente @@ -242,7 +244,7 @@ Eliminar marcador Destruír a conversa en grupo Eliminar canle - Está segura de querer destruír esta conversa en grupo?\n\nAviso: A conversa en grupo será totalmente eliminada do servidor. + Tes a certeza de querer destruír esta conversa en grupo?\n\nAviso: A conversa en grupo será totalmente eliminada do servidor. Tes a certeza de querer eliminar a canle?\n\nAviso: A canle será eliminada completamente do servidor. Non se desfixo a conversa en grupo Non se puido eliminar a canle @@ -252,7 +254,7 @@ Saír Contacto engadido a túa lista de contactos Volver a engadir - %s leeu ate este punto + %s leu ata este punto %s leu ate este punto %1$s + %2$d outras leron ata este punto Todas leron ate este punto @@ -349,8 +351,8 @@ Non se atopou unha app para abrir o ficheiro Non se atopou app para abrir a ligazón Non se atopou app para ver o contacto - Etiquetas dinámicas - Mostrar etiquetas de só lectura baixo os contactos + Información do estado + Mostra o estado debaixo do nome do contacto Habilitar notificacións Non se atopou ningún servidor de conversa en grupo Non se puido crear a conversa en grupo @@ -414,6 +416,7 @@ son video imaxe + gráfico de vector documento PDF App Android Contacto @@ -489,7 +492,7 @@ Anovar certificado Fallo obtendo a chave OMEMO! Comprobouse a chave OMEMO co certificado! - O seu dispositivo non admite a selección de certificados do cliente! + O teu dispositivo non admite a selección de certificados do cliente! Conexión Conectar vía Tor Facer pasar todas as conexións a través da rede Tor. Require Orbot @@ -538,7 +541,7 @@
Podes rexistrarte co teu número de teléfono e Quicksy suxerirache automáticamente —tomando os números da túa libreta de enderezos como referencia— posibles contactos para ti.

Ao rexistrarte aceptas a nosa política de privacidade.]]>
Aceptar e continuar Tes unha guía para crear unha conta en conversations.im¹\nAo escoller conversations.im como provedor poderás comunicarte con outras usuarias de outros provedores con só darlles o teu enderezo XMPP completo. - O seu enderezo XMPP completo será: %s + O teu enderezo XMPP completo será: %s Crear conta Utilizar o meu propio proveedor Elixe un identificador @@ -551,12 +554,12 @@ Non dipoñible Ocupada Xerouse un contrasinal seguro - O seu dispositivo non permite non optimizar a batería + O teu dispositivo non permite evitar a optimización a batería Fallo no rexistro: inténteo de novo Fallo no rexistro: contrasinal moi feble Escoller participantes Creando unha conversa en grupo... - Invitar de novo + Convidar de novo Desactivar Breve Medio @@ -591,7 +594,7 @@ Borrar identidades OMEMO Rexenerar chaves OMEMO. Todos os teus contactos terán que verificar a túa conta de novo. Utiliza esto só como último recurso. Eliminar as chaves seleccionadas. - Precisa estar conectada para publicar o seu avatar. + Debes ter conexión para publicar o teu avatar. Mostrar mensaxe do fallo Mensaxe de fallo Aforrador de datos habilitado @@ -614,14 +617,14 @@ Non confiables Código de barras 2D non válido Baleirar o cartafol da caché (utilizado pola cámara) - Limpar caché + Baleirar caché Baleirar almacenaxe privada Baleirar a almacenaxe privada onde se gardan os ficheiros (poderán volver a descargarse desde o servidor) Seguín esta ligazón desde unha fonte de confianza - Vai verificar as chaves OMEMO de %1$s despois de pulsar na ligazón. Esto só é seguro si sigueu esta ligazón desde unha fonte de confianza onde só %2$s a podería ter publicado. + Vas verificar as chaves OMEMO de %1$s despois de premer na ligazón. Esto só é seguro se seguiches esta ligazón desde unha fonte de confianza onde só %2$s a podería ter publicado. Validar chaves OMEMO Mostrar inactivos - Amagar inactivos + Agochar inactivos Retirar confianza a dispositivo Tes a certeza de que queres eliminar a verificación deste dispositivo?\nEste dispositivo e as súas mensaxes serán marcados como \"Non confiable\". @@ -685,7 +688,7 @@ ¿Aceptar certificado descoñecido? O certificado do servidor non está asinado por unha autoridade de certificación coñecida. ¿Aceptar un nome de servidor que non coincida? - O servidor non pode autenticarse como \"%s\". O certificado só é válido para: + O servidor non pode autenticarse como \"%s\". O certificado só é válido para: Queres conectarte de todos os xeitos? Detalles do certificado: Unha vez @@ -707,8 +710,8 @@ OMEMO utilizarase por defecto para as novas conversas. OMEMO terá que ser activado explícitamente para novas conversacións. Crear acceso directo - Tamaño da fonte - O tamaño de fonte relativo a esta app + Tamaño da letra + O tamaño relativo da letra que utiliza a app. Activado por defecto Desactivado por defecto Pequena @@ -780,15 +783,15 @@ Código de país non válido Indica un país número de teléfono - Valide o seu número de teléfono - Quicsy enviaralle unha mensaxe SMS (poderíanse aplicar cargos) para validar o seu número de teléfono. Introduza o código de país e número de teléfono. + Valida o teu número de teléfono + Quicksy vaiche enviar unha mensaxe SMS (podería ter custos) para validar o teu número de teléfono. Escribe o código de país e número de teléfono.
%s

É correcto, ou quere modificar o número?]]>
%s non é un número de teléfono válido. - Por favor introduza o seu número de teléfono. + Por favor escribe o teu número de teléfono. Buscar países Validar %s - %s.]]> - Enviamoslle outro SMS con un código de 6 díxitos. + %s.]]> + Enviamosche outro SMS cun código de 6 díxitos. Por favor, introduza o pin de 6 díxitos inferior. Reenviar SMS Reenviar SMS (%s) @@ -802,7 +805,7 @@ Validando... Solicitando SMS... O pin introducido non é correcto. - O pin que lle enviamos caducou. + O pin que che enviamos caducou. Fallo descoñecido na rede. Resposta descoñecida desde o servidor. Non se puido conectar co servidor. @@ -819,19 +822,19 @@ Actualizar Este número de teléfono está actualmente ligado a outro dispositivo. Por favor, escribe o teu nome para permitir que a xente que non te ten na axenda de enderezos sepa quen es. - O seu nome - Introduza o seu nome - Utilice o botón editar para escribir o seu nome. + O teu nome + Escribe o teu nome + Establece o teu nome usando o botón editar. Rexeitar solicitude Instalar Orbot Iniciar Orbot - Non ten loxa de aplicacións instalada. + Non hai tenda de apps instalada. Esta canle fará público o teu enderezo XMPP e-book Orixinal (non comprimido) Abrir con... Imaxe de perfil en Conversations - Escoller conta + Elexir conta Restablecer copia de apoio Restablecer Escribe o contrasinal da conta %s para restablecer a copia. @@ -912,6 +915,7 @@ Perdeuse a conexión Chamada cortada Fallo na aplicación + Problema na verificación Colgar Chamada en curso Videochamada en curso @@ -963,4 +967,6 @@ Ningunha conta activa soporta esta función Comezou a creación da copia de apoio. Recibirás unha notificación cando remate. Non se puido activar o vídeo. + Documento de texto plano + diff --git a/src/main/res/values-hu/strings.xml b/src/main/res/values-hu/strings.xml index 7ef8338a6ee9b97593e6fce27092dab1e806ede0..162212928314f90fae219975deb5a3112dd316a6 100644 --- a/src/main/res/values-hu/strings.xml +++ b/src/main/res/values-hu/strings.xml @@ -53,6 +53,7 @@ %s összes partnerének tiltását feloldja? Partner tiltva Tiltva + Szeretné eltávolítani ezt a könyvjelzőkből: %s? Ezzel a könyvjelzővel megjelölt beszélgetései nem lesznek eltávolítva. Új fiók regisztrálása a kiszolgálón Jelszó megváltoztatása a kiszolgálón Megosztás ezzel… @@ -70,6 +71,8 @@ Tiltás feloldása Mentés Rendben + %1$s összeomlott + Azzal, hogy az XMPP fiókja használatával beküldi a veremkiíratásokat, elősegítheti a %1$s alkalmazás folyamatos fejlesztését. Küldés most Sose kérdezzen újra Nem sikerült kapcsolódni a fiókhoz @@ -99,6 +102,7 @@ Küldés titkosítatlanul A visszafejtés sikertelen. Talán nem rendelkezik a megfelelő személyes kulccsal. OpenKeychain + OpenKeychain alkalmazást használja az üzenetek titkosításához és visszafejtéséhez, valamint a személyes kulcsai kezeléséhez.

Ez GPLv3+ szerint licencelt, és elérhető az F-Droid és a Google Play szoftveráruházakban.

(Ezután indítsa újra a %1$s alkalmazást.)]]>
Újraindítás Telepítés Telepítse az OpenKeychain alkalmazás @@ -120,6 +124,7 @@ Csengőhang Értesítési hang Értesítési hang új üzeneteknél + Csengőhang bejövő hívásnál Türelmi idő Az időtartam, amíg az értesítések némítva vannak az egyéb eszközei egyikén történt tevékenység észlelése után. Speciális @@ -127,7 +132,10 @@ A veremkiíratások elküldésével segíti a fejlesztést Üzenetek megerősítése Tudassa a partnereivel, hogy megkapta és elolvasta az üzeneteiket + Képernyőfotó készítésének megakadályozása + Az alkalmazás tartalmának elrejtése az alkalmazásváltóban és a képernyőfotók blokkolása Felhasználói felület + Az OpenKeychain hibát produkált. Rossz kulcs a titkosításhoz. Elfogadás Hiba történt @@ -143,6 +151,8 @@ Nem sikerült átalakítani a képet A fájl nem található Általános bemeneti/kimeneti hiba. Talán elfogyott a tárolóhely? + A kép kiválasztásához használt alkalmazás nem biztosított számunkra elegendő jogosultságot a fájl olvasásához.\n\nHasználjon másik fájlkezelőt a kép kiválasztásához + A fájl megosztásához használt alkalmazás nem biztosított számunkra elegendő jogosultságot. Ismeretlen Átmenetileg letiltva Elérhető @@ -157,6 +167,7 @@ A kiszolgáló nem támogatja a regisztrációt Érvénytelen regisztrációs token A TLS-egyeztetés sikertelen + Tartomány nem ellenőrizhető Irányelv megsértése Nem kompatibilis kiszolgáló Adatfolyamhiba @@ -174,6 +185,7 @@ Az OpenPGP nyilvános kulcs közzé lett téve. Fiók engedélyezése Biztos benne? + A fiók törlésével az összes beszélgetési előzményei is eltávolításra kerülnek Hang rögzítése XMPP-cím XMPP-cím tiltása @@ -204,6 +216,7 @@ egy napja volt aktív %d napja volt aktív Titkosított üzenet. Telepítse az OpenKeychain alkalmazást a visszafejtéshez. + Új OpenPGP titkosítású üzenetek találhatók OpenPGP kulcsazonosító OMEMO ujjlenyomat v\\OMEMO ujjlenyomat @@ -394,6 +407,7 @@ hang videó kép + vektorgrafika PDF-dokumentum Android alkalmazás Partner @@ -447,6 +461,8 @@ A kiszolgáló nem felelős a tartományért Törött Elérhetőség + Távol, ha az eszköz le van zárva + Mutasson „Távoli”-ként, ha az eszköz le van zárva Rezgés kezelése csendes módként Kiterjesztett kapcsolati beállítások Gépnév és port beállításainak megjelenítése egy fiók beállításakor diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 5fc619622fe577e6bafbfde01afb7eea1a703eb4..3a760a44e8466ba90518b92d7d10547f7ac23908 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -2,13 +2,13 @@ Impostazioni Nuova conversazione - Gestisci account - Gestisci account + Gestisci profili + Gestisci profilo Chiudi conversazione Dettagli del contatto Dettagli chat di gruppo Dettagli canale - Aggiungi account + Aggiungi profilo Modifica il nome Aggiungi alla rubrica Cancella dalla lista @@ -18,7 +18,7 @@ Sblocca dominio Blocca partecipante Sblocca partecipante - Gestisci account + Gestisci profili Impostazioni Condividi con Conversation Inizia una conversazione @@ -54,7 +54,7 @@ Contatto bloccato Bloccato Vuoi rimuovere %s dai segnalibri? Le conversazioni con questo segnalibro non verranno rimosse. - Registra un nuovo account sul server + Registra un nuovo profilo sul server Cambia la password sul server Condividi con Inizia conversazione @@ -72,12 +72,12 @@ Salva OK Errore di %1$s - Usare il tuo account XMPP per inviare segnalazioni di errore aiuta lo sviluppo in corso di %1$s. + Usare il tuo profilo XMPP per inviare segnalazioni di errore aiuta lo sviluppo in corso di %1$s. Invia adesso Non chiedere più - Impossibile connettersi all\'account - Impossibile connettersi a più account - Tocca per gestire i tuoi account + Impossibile connettersi al profilo + Impossibile connettersi a più profili + Tocca per gestire i tuoi profili Allega file Aggiungere questo contatto alla lista dei contatti? Aggiungi contatto @@ -130,15 +130,17 @@ Avanzate Non inviare mai segnalazioni di errore Se scegli di inviare una segnalazione dell’errore aiuterai lo sviluppo - Conferma messaggi + Conferma i messaggi Fai sapere ai tuoi contatti quando hai ricevuto e letto i loro messaggi + Impedisci la cattura dello schermo + Nascondi i contenuti dell\'app nell\'elenco recenti e blocca la cattura delle schermate Interfaccia utente OpenKeychain ha generato un errore. Chiave di cifratura non valida. Accetta Si è verificato un errore Errore - Il tuo account + Il tuo profilo Invia aggiornamenti della presenza Ricevi aggiornamenti della presenza Chiedi aggiornamenti della presenza @@ -150,9 +152,10 @@ File non trovato Errore di I/O generico. Forse hai esaurito lo spazio? L’app che hai usato per selezionare questa immagine non ha fornito autorizzazioni sufficienti per leggere il file.\n\nUsa un gestore di file differente per scegliere un’immagine + L\'app che hai usato per condividere questo file non ha fornito autorizzazioni sufficienti. Sconosciuto Disattivato temporaneamente - Online + In linea In connessione\u2026 Offline Non autorizzato @@ -164,6 +167,7 @@ Registrazione non supportata dal server Token di registrazione non valido Negoziazione TLS fallita + Dominio non verificabile Violazione della policy Server non compatibile Errore di stream @@ -172,16 +176,16 @@ OTR OpenPGP OMEMO - Elimina utente + Elimina profilo Disattiva temporaneamente Pubblica avatar Pubblica chiave pubblica OpenPGP Rimuovi chiave pubblica OpenPGP Sei sicuro di volere rimuovere la tua chiave pubblica OpenPGP dalla dichiarazione di presenza?\nI tuoi contatti non potranno più inviarti messaggi cifrati con OpenPGP. Chiave pubblica OpenPGP pubblicata. - Attiva utente + Attiva profilo Sei sicuro? - L\'eliminazione del tuo account cancellerà tutta la cronologia dielle conversazioni + L\'eliminazione del tuo profilo cancellerà tutta la cronologia dielle conversazioni Registra la voce Indirizzo XMPP Blocca indirizzo XMPP @@ -215,9 +219,9 @@ Nuovi messaggi cifrati con OpenPGP trovati ID chiave OpenPGP Impronta OMEMO - v\\OMEMO impronta - OMEMO fingerprint (messaggio originatore) - v\\OMEMO fingerprint (messaggio originatore) + v\\Impronta OMEMO + Impronta OMEMO (origine del messaggio) + v\\Impronta OMEMO (origine del messaggio) Altri dispositivi Fidati delle impronte OMEMO Ricezione chiavi... @@ -266,7 +270,7 @@ a %s Invia messaggio privato a %s Connetti - Questo utente esiste già + Questo profilo esiste già Successivo Sessione stabilita Salta @@ -281,7 +285,7 @@ Sicurezza Permetti correzione del messaggio Consenti ai tuoi contatti di modificare retroattivamente i loro messaggi - Impostazioni esperto + Impostazioni per esperti Fai attenzione con queste impostazioni Informazioni su %s Ore di quiete @@ -299,7 +303,7 @@ Sei stato buttato fuori da questa chat di gruppo La chat di gruppo è stata chiusa Non sei più in questa chat di gruppo - usando l’account %s + usando il profilo %s ospitato su %s Controllo %s su host HTTP Non sei connesso. Riprova più tardi @@ -318,19 +322,19 @@ Scansiona codice a barre 2D Mostra codice a barre 2D Mostra la lista nera - Dettagli account + Dettagli del profilo Conferma Prova di nuovo Servizio in primo piano Evita che il sistema operativo chiuda la connessione - Crea backup + Crea un backup I file di backup verranno salvati in %s - Creazione file di backup + Creazione dei file di backup Il tuo backup è stato creato I file di backup sono stati salvati in %s Ripristino backup Il tuo backup è stato ripristinato - Non dimenticare di attivare l\'account. + Non dimenticare di attivare il profilo. Scegli un file Ricezione di %1$s file (%2$d%% completato) Scarica %s @@ -352,7 +356,7 @@ Attiva le notifiche Nessun server per chat di gruppo trovato Impossibile creare la chat di gruppo - Avatar utente + Avatar del profilo Copia impronta OMEMO negli appunti Rigenera chiave OMEMO Pulisci dispositivi @@ -369,8 +373,8 @@ Password attuale Nuova password La password non può essere vuota - Attiva tutti gli account - Disattiva tutti gli account + Attiva tutti i profili + Disattiva tutti i profili Esegui azione con Nessuna affiliazione Offline @@ -405,13 +409,14 @@ Rispondi Segna come già letto Input - Invio spedisce + Il tasto Invio spedisce Usa il tasto Invio per spedire il messaggio. Puoi sempre usare Ctrl+Invio per spedire, anche se questa opzione è disattivata. Mostra il tasto invio Cambia il tasto delle faccine nel tasto di invio audio video immagine + grafica vettoriale Documento PDF Applicazione Android Contatto @@ -472,7 +477,7 @@ Tratta vibrazione come modalità silenziosa Imposta come occupato quando il dispositivo è in modalità vibrazione Impostazioni estese di connessione - Mostra nome host e impostazioni della porta quando configuri un account + Mostra nome host e impostazioni della porta quando configuri un profilo xmpp.esempio.it Accedi con certificato Impossibile analizzare il certificato @@ -496,7 +501,7 @@ Indirizzo server o .onion Questo non è un numero di porta valido Questo non è un nome host valido - %1$d su %2$d account connessi + %1$d su %2$d profili connessi %d messaggio %d messaggi @@ -515,7 +520,7 @@ Notifica solo quando menzionato Notifiche disattivate Notifiche in pausa - Compressione immagini + Compressione delle immagini Suggerimento: usa \"Scegli un file\" invece di \"Scegli un\'immagine\" per inviare singole immagini non compresse a prescindere da questa impostazione. Sempre Solo immagini grandi @@ -524,27 +529,27 @@ Il tuo dispositivo sta facendo delle ingenti ottimizzazioni della batteria per %1$s che potrebbero portare ritardi alle notifiche o anche perdita di messaggi.\n\nTi verrà ora chiesto di disattivarle. Disattiva L\'area selezionata è troppo grande - (Nessun account attivo) + (Nessun profilo attivo) Questo campo è obbligatorio Correggi messaggio Invia messaggio corretto Hai già validato l\'impronta di questa persona in modo sicuro per confermarne la fiducia. Selezionando “Fatto” stai solo confermando che %s fa parte di questa chat di gruppo. - Hai disattivato questo account + Hai disattivato questo profilo Errore di sicurezza: accesso file non valido! Nessuna app trovata per condividere l\'URI Condividi l\'URI con...
Ti registri con il tuo numero di telefono e Quicksy ti suggerirà—in base ai numeri di telefono nella tua rubrica—automaticamente i possibili contatti.

Registrandoti accetti la nostra politica sulla privacy.]]>
Accetta e continua - È disponibile una guida per la creazione di un account su conversations.im.¹\nQuando scegli conversations.im come fornitore potrai comunicare con utenti di altri fornitori dando il tuo indirizzo XMPP completo. + È disponibile una guida per la creazione di un profilo su conversations.im.¹\nQuando scegli conversations.im come fornitore potrai comunicare con utenti di altri fornitori dando il tuo indirizzo XMPP completo. Il tuo indirizzo XMPP completo sarà: %s - Crea account - Usa un altro provider + Crea profilo + Usa un altro fornitore Scegli il tuo nome utente Gestisci manualmente la disponibilità Imposta la tua disponibilità quando modifichi il messaggio di stato. Messaggio di stato Disponibile a chattare - Online + In linea Assente Non disponibile Occupato @@ -559,7 +564,7 @@ Breve Medio Lungo - Trasmissione + Trasmetti l\'utilizzo Fa sapere ai tuoi contatti quando usi Conversations Privacy Tema @@ -568,7 +573,7 @@ Chiaro Scuro Sfondo verde - Usa sfondo verde per messaggi ricevuti + Usa uno sfondo verde per i messaggi ricevuti Impossibile connettersi a OpenKeychain Questo dispositivo non è più in uso Computer @@ -584,9 +589,9 @@ Nessuna autorizzazione per accedere a %s Server remoto non trovato Scadenza server remoto - Impossibile aggiornare l\'account + Impossibile aggiornare il profilo Segnala questo indirizzo XMPP per spam. - Elimina identità OMEMO + Elimina le identità OMEMO Rigenera le tue chiavi OMEMO. I tuoi contatti dovranno verificare un\'altra volta la tua identità. Usalo solo come ultima spiaggia. Cancella le chiavi selezionate Devi essere connesso per pubblicare l\'avatar. @@ -646,7 +651,7 @@ %d mese %d mesi
- Eliminazione automatica messaggi + Eliminazione automatica dei messaggi Elimina automaticamente da questo dispositivo i messaggi più vecchi del lasso di tempo configurato. Cifratura del messaggio Nessun recupero di messaggi a causa del periodo di conservazione locale. @@ -701,11 +706,11 @@ Disattiva adesso Bozza: Cifratura OMEMO - OMEMO verrà sempre usato per chat singole e di gruppi privati. + OMEMO verrà sempre usato per chat singole e gruppi privati. OMEMO verrà usato in modo predefinito nelle nuove conversazioni. OMEMO dovrà essere attivato a mano nelle nuove conversazioni. Crea scorciatoia - Dimensione carattere + Dimensione dei caratteri La dimensione dei caratteri usata all\'interno dell\'app. On in modo predefinito Off in modo predefinito @@ -751,7 +756,7 @@ Questa categoria di notifiche è usata per mostrare una notifica permanente per indicare che %1$s è in esecuzione. Informazioni di stato Problemi di connettività - Questa categoria di notifiche è usata per mostrare un notifica in caso si verifichi un problema nella connessione ad un account. + Questa categoria di notifiche è usata per mostrare un notifica in caso si verifichi un problema nella connessione ad un profilo. Messaggi Chiamate Messaggi @@ -768,7 +773,7 @@ Partecipanti Browser multimediale File omesso per violazione di sicurezza. - Qualità video + Qualità dei video Una qualità inferiore comporta file più piccoli Media (360p) Alta (720p) @@ -829,10 +834,10 @@ Originale (non compresso) Apri con… Immagine profilo di Conversations - Scegli account + Scegli un profilo Ripristina backup Ripristina - Inserisci la tua password per l\'account %s per ripristinare il backup. + Inserisci la tua password per il profilo %s per ripristinare il backup. Non usare la funzione di ripristino del backup tentando di clonare (eseguire simultaneamente) un\'installazione. Il ripristino di un backup è inteso solo per migrazioni o in caso di smarrimento del dispositivo. Impossibile ripristinare il backup. Impossibile decifrare il backup. La password è giusta? @@ -869,10 +874,10 @@ Individua i canali Cerca i canali Possibile violazione della privacy! - search.jabber.network.

L\'uso di questa funzione trasmetterà il tuo indirizzo IP e i termini di ricerca a quel servizio. Vedi la loro Informativa sulla Privacy per saperne di più.]]>
- Ho già un account - Aggiungi un account pre-esistente - Registra un nuovo account + search.jabber.network.

L\'uso di questa funzione trasmetterà il tuo indirizzo IP e i termini di ricerca a quel servizio. Vedi la loro informativa sulla privacy per maggiori informazioni.]]>
+ Ho già un profilo + Aggiungi un profilo esistente + Registra un nuovo profilo Questo sembra un indirizzo di dominio Aggiungere comunque Questo sembra un indirizzo di canale @@ -881,8 +886,8 @@ Evento Apri backup Il file selezionato non è un file di backup di Conversations - Questo account è già stato configurato - Inserisci la password per questo account + Questo profilo è già stato configurato + Inserisci la password per questo profilo Impossibile eseguire questa azione Entra in un canale pubblico... L\'app di condivisione non ha concesso l\'autorizzazione per accedere a questo file. @@ -890,10 +895,10 @@ jabber.network Server locale La maggior parte degli utenti dovrebbe scegliere ‘jabber.network’ per migliori suggerimenti dall\'intero ecosistema XMPP pubblico. - Metodo di scoperta canali + Metodo di scoperta dei canali Backup Al riguardo - Devi attivare un account + Devi attivare un profilo Chiama Chiamata in arrivo Chiamata video in arrivo @@ -910,6 +915,7 @@ Connessione persa Chiamata ritirata Errore dell\'app + Problema di verifica Riaggancia Chiamata in corso Chiamata video in corso @@ -958,6 +964,9 @@ Invita su Conversations Impossibile analizzare l\'invito Il server non supporta la generazione di inviti - Nessun account attivo supporta questa funzione + Nessun profilo attivo supporta questa funzione Il backup è iniziato. Riceverai una notifica una volta completato. -
+ Impossibile attivare il video. + Documento di testo + + diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 83b981881341d77136cce1564915feb496c222fd..9963796ae7e1605246277c72e917b01459a17e0c 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -27,31 +27,31 @@ アカウントで共有 ブロック一覧 ちょうど今 - 1 分前 - %d 分前 + 1分前 + %d分前 - 未読%d件 + %d件の未読の会話 送信中… メッセージを復号しています。しばらくお待ちください… OpenPGP 暗号化メッセージ ニックネームは既に使用されています - 正しくないニックネームです + 不正なニックネーム 管理者 所有者 - モデレーター + 調停者 参加者 訪問者 連絡先名簿から %s を削除しますか? この連絡先との会話は削除されません。 %s からあなたに送信されるメッセージをブロックしますか? %s のブロックを解除し、あなたにメッセージを送信できるようにしますか? %s からの連絡をすべてブロックしますか? - %s からの連絡をすべてブロック解除しますか? + %s からすべての連絡先のブロックを解除しますか? 連絡先をブロックしました ブロックしました %s のブックマークを削除しますか? このブックマークとの会話は削除されません。 - サーバーに新しいアカウントを登録 + サーバーに新規アカウントを登録 サーバーのパスワードを変更 …で共有 会話を始める @@ -59,7 +59,7 @@ 招待 連絡先 連絡先 - キャンセル + 中止 設定 追加 編集 @@ -71,23 +71,23 @@ %1$s がクラッシュしました あなたの XMPP アカウントを使用してスタックトレースの送信をすることで、 %1$s の継続的な開発を支援します。 今すぐ送信 - 今後表示しない + 今後は表示しない アカウントに接続できません 複数のアカウントに接続できません タップしてアカウントを管理 - 添付ファイル - 連絡先が連絡先名簿にありません。追加しますか? + ファイルを添付 + 連絡先が連絡先名簿にありません。名簿に追加しますか? 連絡先を追加 配信に失敗しました - 転送用画像の準備中 - 転送用画像の準備中 + 送信用画像の準備中 + 送信用画像の準備中 ファイル共有中。しばらくお待ちください… - 履歴をクリア - 会話履歴をクリア + 履歴を消去 + 会話履歴を消去 この会話のすべてのメッセージを削除してもよろしいですか?\n\n警告: 他のデバイスやサーバーに保存されているメッセージのコピーには影響しません。 ファイルを削除 このファイルを削除してもよろしいですか?\n\n警告: これは、他のデバイスやサーバーに保存されているファイルのコピーは削除しません。 - その後、この会話を閉じる + この後、この会話を閉じる デバイスを選択 暗号化されていないメッセージを送信 メッセージを送信 @@ -96,8 +96,8 @@ v\\OMEMO 暗号化メッセージを送信 OpenPGP 暗号化メッセージを送信 ニックネームが変更されました - 暗号化されていない送信 - 復号に失敗しました。おそらく秘密鍵が正しくないようです。 + 暗号化せずに送信 + 復号に失敗しました。適切な秘密鍵を持っていないのかもしれません。 OpenKeychain OpenKeychain を利用して、メッセージの暗号化および復号、そしてあなたの公開鍵を管理します。

それは GPLv3+ ライセンスの下で、F-Droid および Google Play から利用可能です。

(後で %1$s を再起動してください。)]]>
再起動 @@ -105,9 +105,9 @@ OpenKeychain をインストールしてください 依頼中… 待機中… - OpenPGP の鍵はありません + OpenPGP 鍵が見つかりません 連絡先が公開鍵を通知しないため、あなたのメッセージを暗号化することができません。\n\n連絡先に OpenPGP をセットアップするように依頼してください。 - OpenPGP の鍵はありません + OpenPGP 鍵が見つかりません 連絡先が公開鍵を通知しないため、あなたのメッセージを暗号化することができません。\n\n連絡先に OpenPGP をセットアップするように依頼してください。 全般 ファイルを受取 @@ -115,9 +115,9 @@ 添付ファイル 通知 振動 - 新しいメッセージが届いたときに振動します + 新着メッセージが届いたときに振動します LED 通知 - 新しいメッセージが届いたときに通知ライトを点滅します + 新着メッセージが届いたときに通知ライトを点滅します 着信音 通知音 新着メッセージの通知音 @@ -126,27 +126,30 @@ 別のデバイスでの操作を検知した際に、通知を止める時間の長さ 詳細 クラッシュレポートを送信しない - スタックトレースを送信して、Conversations の継続的な開発を支援します - メッセージの確認 - あなたがメッセージを受信して読んだときに、連絡先に知らせます + スタックトレースを送信すると、 Conversations の開発を支援します + メッセージを確認 + あなたがメッセージを受信して読んだときに、連絡先に知らせる + スクリーンショットを防ぐ + アプリスイッチャー内でアプリの内容を隠し、スクリーンショットを防ぐ UI OpenKeychain でエラーが発生しました。 暗号化の鍵が不正です。 - 受付 + 受け入れる エラーが発生しました エラー あなたのアカウント - 参加アップデートを送信 - 参加アップデートを受信 - 参加アップデートを問合せ - 写真を選択 + 出席情報アップデートを送信 + 出席情報アップデートを受信 + 出席情報アップデートを求める + 画像を選択 写真を撮影 - 事前にサブスクリプション要求を許可する + サブスクリプション要求を事前に付与する 選択したファイルは画像ではありません 画像ファイルを変換できません ファイルが見つかりません - 一般的な I/O エラー。おそらく空き容量がなくなっていませんか? - あなたが画像の選択のために使用したアプリは、読み取りに必要なアクセス権がありません。\n\n別のファイルマネージャを使用して、画像を選択してください。 + 一般的な入出力エラー。空き容量がなくなっていませんか? + あなたが画像の選択のために使用したアプリは、読み取りに必要なアクセス権がありません。\n\n画像を選択するために、別のファイルマネージャーを使ってください + このファイルを共有するために使用したアプリは、十分な許可が与えられていませんでした。 不明 一時的に無効 オンライン @@ -154,13 +157,14 @@ オフライン 許可されていません サーバーが見つかりません - 接続エラー + 接続なし 登録に失敗しました ユーザー名は既に使用されています 登録が完了しました - サーバーが登録をサポートしていません - トークンが無効です + サーバーは登録をサポートしていません + 登録トークンが無効です TLS ネゴシエーションに失敗しました + 検証不可能なドメイン ポリシー違反 互換性のないサーバー ストリーム エラー @@ -170,13 +174,13 @@ OpenPGP OMEMO アカウントを削除 - 一時的に無効にする + 一時的に無効化 アバターを公開 OpenPGP 公開鍵を公開 OpenPGP 公開鍵を削除 - 在席告知から OpenPGP 公開鍵を削除してもよろしいですか?\n連絡先はあなたに OpenPGP 暗号化メッセージを送信できなくなります。 + 出席情報告知から OpenPGP 公開鍵を削除してもよろしいですか?\n連絡先はあなたに OpenPGP 暗号化メッセージを送信できなくなります。 OpenPGP 公開鍵を公開しました。 - アカウントを有効にする + アカウントを有効化 よろしいですか? アカウントを削除すると会話履歴がすべて消去されます 音声を録音 @@ -188,11 +192,11 @@ メモリ不足です。画像が大きすぎます %s をお使いのアドレス帳に追加しますか? サーバー情報 - XEP-0313: メッセージ アーカイブ管理 - XEP-0280: メッセージ カーボン + XEP-0313: メッセージ中央管理 + XEP-0280: メッセージ複写 XEP-0352: クライアント状態表示 XEP-0191: ブロッキング コマンド - XEP-0237: 名簿バージョニング + XEP-0237: 名簿バージョン管理 XEP-0198: ストリーム管理 XEP-0215: 外部サービスの発見 XEP-0163: PEP (アバター / OMEMO) @@ -202,14 +206,14 @@ 利用不可 公開鍵の告知がありません ちょうど今会いました - 1 分前に会いました - %d 分前に会いました - 1 時間前に会いました - %d 時間前に会いました - 1 日前に会いました - %d 日前に会いました + 1分前に会いました + %d分前に会いました + 1時間前に会いました + %d時間前に会いました + 1日前に会いました + %d日前に会いました 暗号化されたメッセージです。復号するには OpenKeychain をインストールしてください。 - 新しい OpenPGP 暗号化されたメッセージが見つかりました + 新規の OpenPGP で暗号化されたメッセージが見つかりました OpenPGP 鍵 ID OMEMO フィンガープリント v\\OMEMO フィンガープリント @@ -231,7 +235,7 @@ 参加 channel@conference.example.com/nick channel@conference.example.com - ブックマークとして保存 + ブックマークに保存 ブックマークを削除 グループチャットを破棄する 談話室を破棄する @@ -247,32 +251,32 @@ 戻りを追加 %s はここまで読みました %s はここまで読みました - %1$s +%2$d 全員ここまで読みました + %1$s +%2$d人がここまで読みました 全員がここまで読みました 公開 アバターをタップしてギャラリーから画像を選択します 公開中… - サーバーがあなたの公開を拒否しました + サーバーはあなたが公開するものを拒否しました 画像を変換できません - ディスクにアバターを保存できませんでした + ディスクにアバターを保存できません (または長押しするとデフォルトに戻します) ご利用のサーバーは、アバターの公開をサポートしていません ささやいた %s へ - 非公開メッセージを %s に送信 + 非公開メッセージを %s へ送信 接続 このアカウントは既に存在します 次へ セッションが確立 スキップ - 通知を無効にする + 通知を無効化 有効 グループチャットにはパスワードが必要 パスワードを入力してください - 最初に連絡先から参加アップデートを要求してください。\n\nこれは、連絡先が何のクライアントを使用しているかを決めるために使用されます。 + 最初に、連絡先から出席情報アップデートを要求してください。\n\nこれは、連絡先が何のクライアントを使用しているかを特定するために使用されます。 今すぐ要求 無視 - 警告: 相互の参加アップデートなしにこれを送信すると、予期しない問題が発生する可能性があります。\n\nあなたの参加サブスクリプションを検証するために、“連絡先”の詳細に移動します。 + 警告: 相互の出席情報アップデートなしにこれを送信すると、予期しない問題が発生する可能性があります。\n\nあなたの出席情報サブスクリプションを検証するために、“連絡先の詳細”に移動します。 セキュリティ メッセージの修正を許可 連絡先が、遡及的に自分のメッセージを編集することを許可します @@ -280,42 +284,42 @@ ご利用には注意してください %s について 消音時間 - 開始時間 - 終了時間 - 消音時間を有効にする + 開始時刻 + 終了時刻 + 消音時間を有効化 消音時間の間、通知は無音になります その他 ブックマークと同期 ブックマークに従って、グループチャットに自動で参加します。 OMEMO フィンガープリントをクリップボードにコピーしました - このグループチャットから追い出されています + このグループチャットから出禁にされています このグループチャットはメンバー制です - リソース制約 + リソース制限 このグループチャットから蹴り出されています このグループチャットは閉鎖されました - 既にこのグループチャットに参加していません + あなたはもうこのグループチャットに参加していません アカウント %s を使用 %s 上でホストされた - HTTP ホストの %s を確認中 + HTTP ホスト上の %s を確認中 接続されていません。後でもう一度お試しください - %s サイズを確認 - %2$s で %1$s のサイズを確認 + %s の大きさを確認 + %2$s で %1$s の大きさを確認 メッセージオプション 引用 引用として貼り付け 元の URL をコピー 再送 - ファイル URL + ファイルの URL URL をクリップボードにコピーしました XMPP アドレスをクリップボードにコピーしました エラーメッセージをクリップボードにコピーしました ウェブアドレス - 2D バーコードをスキャン - 2D バーコードを表示 + 二次元バーコードをスキャン + 二次元バーコードを表示 ブロック一覧を表示 アカウントの詳細 確認 - 再度実行してください + 再試行 フォアグラウンドサービス オペレーティングシステムが接続を切断するのを防止します バックアップを作成 @@ -325,7 +329,7 @@ バックアップファイルは %s に保存されました バックアップを復元 バックアップを復元しました - アカウントを有効にしてください。 + アカウントを有効化してください。 ファイルを選択 %1$s 受信中 (%2$d%% 完了) %s をダウンロード @@ -335,37 +339,37 @@ 送信中 (%1$d%% 完了) 転送用ファイルの準備中 %s ダウンロード依頼中 - 転送をキャンセル + 転送を中止 ファイル転送に失敗しました - 転送をキャンセルしました + ファイル転送を中止しました ファイルを削除しました ファイルを開くアプリケーションが見つかりません リンクを開くアプリケーションが見つかりません 連絡先を表示するアプリケーションが見つかりません ダイナミック タグ 連絡先の下に、読み取り専用タグを表示します - 通知を有効にする + 通知を有効化 グループチャットのサーバーが見つかりませんでした グループチャットを作成できません アカウントのアバター - OMEMO フィンガープリントをクリップボードにコピー + クリップボードに OMEMO フィンガープリントをコピー OMEMO 鍵を再生成 - デバイスをクリア - OMEMO の告知から他のすべてのデバイスをクリアしてもよろしいですか? お使いのデバイスが次回接続したとき、それらは自分自身を再告知しますが、その間に送信されたメッセージを受信できない場合があります。 + デバイスを消去 + OMEMO の告知から他のすべてのデバイスを消去してもよろしいですか? お使いのデバイスが次回接続したとき、それらのデバイスは自分自身を再告知しますが、その間に送信されたメッセージを受信できない場合があります。 この連絡先で使用可能な鍵がありません。\nサーバーから新しい鍵を取得できませんでした。連絡先のサーバーに問題がある可能性があります。 - この連絡先で利用可能な鍵はありません。\n双方に存在サブスクリプションあることを確認してください。 - 何か問題が発生しました。 + この連絡先で利用可能な鍵はありません。\n双方に出席情報サブスクリプションがあることを確認してください。 + 何か問題が発生しました サーバーから履歴を取得中 - サーバーにこれ以上履歴はありません - アップデート中… + サーバーにこれ以上履歴がありません + 更新中… パスワードを変更しました! パスワードを変更できません パスワードを変更 現在のパスワード 新しいパスワード パスワードは空にできません - すべてのアカウントを有効にする - すべてのアカウントを無効にする + すべてのアカウントを有効化 + すべてのアカウントを無効化 アクションを実行... 所属なし オフライン @@ -381,10 +385,10 @@ グループチャットから削除 談話室から削除 %s の所属を変更できません - グループチャットから追い出す - 談話室から追い出す - あなたは公開談話室から %s を削除しようとしています。その唯一の手段は、そのユーザーを永久に追い出すことです。 - 今すぐ追い出す + グループチャットから出禁にする + 談話室から出禁にする + あなたは公開談話室から %s を削除しようとしています。その唯一の手段は、そのユーザーを永久に出禁にすることです。 + 今すぐ出禁にする %s の役割を変更できません 非公開グループチャットの環境設定 公開談話室の環境設定 @@ -392,21 +396,22 @@ XMPPアドレスを誰でも見れるようにする 談話室の調停をする あなたは参加していません - グループチャットのオプションが変更されました! - グループチャットのオプションを変更できませんでした + グループチャットの設定が変更されました! + グループチャットの設定を変更できませんでした なし - 通知があるまで + 通知が来るまで スヌーズ - 返信する + 返信 既読にする 入力 - Enter は送信 - メッセージの送信に Enter キーを使用する。このオプションが無効でも、常に Ctrl+Enter でメッセージを送信できます。 + Enter で送信 + メッセージの送信に Enter キーを使用します。このオプションが無効でも、常に Ctrl+Enter でメッセージを送信できます。 Enter キーを表示 絵文字キーを Enter キーに変更 音声 ビデオ 画像 + ベクター画像 PDF 文書 Android アプリ 連絡先 @@ -419,7 +424,7 @@ %s さんが入力中… %s さんが入力を止めました 入力中通知 - あなたがメッセージを書いているときに、連絡先に知らせます + あなたがメッセージを書いているときに、連絡先に知らせる 位置を送信 位置を表示 位置を表示するアプリケーションが見つかりません @@ -430,15 +435,15 @@ システムの CA を信頼しない すべての証明書を手動で承認する必要があります 証明書を削除 - 手動で承認した証明書を削除します - 手動で承認した証明書はありません + 手動で承認した証明書を削除 + 手動で承認した証明書がありません 証明書を削除 - 選択を削除 - キャンセル + 選択したものを削除 + 中止 - %d 証明書を削除しました + %d個の証明書を削除しました - “送信”ボタンをクイックアクションで置き換えます + “送信”ボタンをクイックアクションで置き換える クイックアクション なし 最近使用した @@ -456,27 +461,27 @@ ダウンロードに失敗しました: ファイルに書き込みできません Tor ネットワークが利用できません バインド失敗 - サーバーがこのドメインに応答しません + そのサーバーはこのドメインに責任を持ちません 壊れています 在席状況 - デバイスがロックされたときは離席 - デバイスがロックされたときは離席と表示 - サイレントモード時は取込中 - デバイスがサイレントモードの時は取込中と表示 + デバイスがロックされているときは離席 + デバイスがロックされているときは離席と表示 + サイレントモードのときは取込中 + デバイスがサイレントモードのときは取込中と表示 バイブレートをサイレントモードとして扱う - デバイスがバイブレート時は取込中と表示 + デバイスがバイブレートのときは取込中と表示 拡張接続設定 - アカウントを設定するときにホスト名とポートの設定を表示します + アカウントを設定するときに、ホスト名とポートの設定を表示 xmpp.example.com 証明書でログイン 証明書を解析できません - アーカイブの設定 - サーバー側のアーカイブの設定 - アーカイブの設定を取得しています。しばらくお待ちください… - アーカイブの設定を取得できません + アーカイブ設定 + サーバー側のアーカイブ設定 + アーカイブ設定を取得しています。しばらくお待ちください… + アーカイブ設定を取得できません キャプチャが要求されました 上の画像からテキストを入力してください - 信頼されていない証明書チェーン + 信頼できない証明書チェーン XMPP アドレスが証明書と一致しません 証明書を更新 OMEMO 鍵の取得中にエラー! @@ -488,18 +493,18 @@ ホスト名 ポート サーバーまたは .onion のアドレス - これは有効なポート番号ではありません - これは有効なホスト名ではありません - %1$d / %2$d アカウントが接続しました + 有効なポート番号ではありません + 有効なホスト名ではありません + %2$d個中%1$d個のアカウントが接続しました - %d メッセージ + %d件のメッセージ - さらにメッセージをロード + さらにメッセージを読み込む %s でファイル共有 %s で画像共有 %s で画像共有 %s でテキスト共有 - %1$s に外部ストレージへのアクセス権を付与 + %1$s に外部ストレージへのアクセス権を付与してください %1$s にカメラへのアクセス権を付与 連絡先と同期 %1$s はあなたのアドレス帳にアクセスして、あなたのXMPP 連絡先名簿と照合する権限を求めています。\nこれにより、連絡先のフルネームとアバターが表示されます。\n\n%1$s は、あなたのサーバーに何かをアップロードすることなく、あなたのアドレス帳を読み込んでローカルに照合するだけです。 @@ -513,8 +518,8 @@ 常に 大きい画像のみ 電池最適化が有効 - お使いのデバイスは、%1$s で通知の遅延やメッセージの損失につながる可能性のある、重い電池の最適化を使用しています。\nそれらを無効にすることをお勧めします。 - お使いのデバイスは、%1$s で通知の遅延やメッセージの損失につながる可能性のある、重い電池の最適化を使用しています。\n\n今、それらを無効にするように求められます。 + お使いのデバイスは、%1$s で通知の遅延やメッセージの損失につながる可能性のある、重い電池の最適化を使用しています。\nそれを無効化することをお勧めします。 + お使いのデバイスは、%1$s で通知の遅延やメッセージの損失につながる可能性のある、重い電池の最適化を使用しています。\n\n今、それらを無効化するように求められます。 無効 選択した範囲が大きすぎます (アクティベートしたアカウントはありません) @@ -522,28 +527,28 @@ メッセージを修正 修正したメッセージを送信 あなたは信頼を確認するために、この人の指紋を安全に検証しました。“完了”を選択すると、 %s がこのグループチャットの一員であることを確認したことになります。 - このアカウントを無効にしました + このアカウントを無効化しました セキュリティエラー: 不正なファイルアクセス! URI を共有するアプリが見つかりません …で URI を共有
電話番号を入力して登録すると、アドレス帳に登録されている電話番号をもとに、Quicksyが自動的に連絡先を提案します。

登録すると、我々のプライバシーポリシーに同意することになります。]]>
同意して続行 - conversations.im のアカウント作成のための指南が設定されています。¹\nconversations.im をプロバイダーとして選択した場合、あなたの完全なXMPPアドレスを与えることで、他のプロバイダーのユーザーと連絡をとることができます。 + conversations.im 上にアカウントを作成する設定の指南です。¹\nconversations.im をプロバイダーとして選択した場合、あなたの完全な XMPP アドレスを他のプロバイダーのユーザーに示すことで、その人と連絡をとることができます。 あなたの完全なXMPPアドレスは: %s アカウントを作成 - 独自のプロバイダーを使用する + 自分のプロバイダーを使用 ユーザー名を選択 在席状況を手動で管理 ステータスメッセージの編集時に、在席状況を設定します。 ステータスメッセージ いつでもチャットできます オンライン - 離席中 - 利用不可 + 離席 + 不在 取込中 安全なパスワードが生成されました お使いのデバイスは電池最適化の停止をサポートしていません - 登録に失敗しました: 後でもう一度試してください + 登録に失敗しました: 後でもう一度お試しください 登録に失敗しました: パスワードが弱すぎます 参加者を選択 グループチャットを作成しています… @@ -553,13 +558,13 @@ ブロードキャストを使用 - Conversations を使用するとき、連絡先に知らせましょう + Conversations を使用するときに、連絡先に知らせましょう プライバシー テーマ カラーパレットの選択 自動 - ライト - ダーク + + 緑の背景 受信したメッセージに緑の背景を使用します OpenKeychain に接続できません @@ -567,16 +572,16 @@ コンピューター 携帯電話 タブレット - Web ブラウザー + ウェブブラウザ コンソール 支払が必要です インターネット使用権限の付与 自分 - 連絡先が、参加サブスクリプションを問い合わせしています + 連絡先が出席情報サブスクリプションを求めています 許可 %s にアクセスする権限がありません リモートサーバーが見つかりません - リモートサーバーのタイムアウト + リモートサーバーがタイムアウト アカウントを更新できません この XMPP アドレスをスパムとして報告する。 OMEMO ID を削除 @@ -585,29 +590,29 @@ アバターを公開するには接続する必要があります。 エラーメッセージを表示 エラーメッセージ - データセーバーを有効にしました - お使いのオペレーティングシステムは、%1$s がバックグラウンドのときにインターネットにアクセスすることを制限しています。新しいメッセージの通知を受信するには、“データセーバー”がオンになっているとき、%1$s に無制限のアクセスを許可する必要があります。\n%1$s は可能なときにデータを保存するための努力をします。 + データセーバーを有効化しました + お使いのオペレーティングシステムは、%1$s がバックグラウンドのときにインターネットにアクセスすることを制限しています。新着メッセージの通知を受信するには、“データセーバー”がオンならば、%1$s に無制限のアクセスを許可する必要があります。\n%1$s は可能なときにデータを保存するための努力をします。 お使いのデバイスは、%1$s のデータセーバーを無効にできません。 一時ファイルを作成できません このデバイスは検証済です フィンガープリントをコピー - 所有するすべての OMEMO 鍵を確認完了 + 所有するすべての OMEMO 鍵を検証完了 バーコードに、この会話のフィンガープリントが含まれていません。 フィンガープリントを検証しました カメラを使用して連絡先のバーコードをスキャンします 鍵が取得されるのをお待ちください - バーコードとして共有 - XMPP URI として共有 - HTTP リンクとして共有 - 検証前に白紙信託する + バーコードで共有 + XMPP URI で共有 + HTTP リンクで共有 + 検証前の盲目的な信頼 認証されていない連絡先からの新規デバイスを信頼するが、認証されている連絡先からの新規デバイスについては手動での確認を求める。 OMEMO 鍵を盲目的に信用していた。つまり、他の人かもしれないし、誰かが盗聴しているかもしれない。 - 信頼されていない - 不正な 2D バーコード - キャッシュフォルダーをクリアします (カメラアプリで使用) - キャッシュをクリア - プライベートストレージをクリア - ファイルが保存されているプライベートストレージをクリアします (サーバーから再ダウンロードできます) + 信頼できない + 不正な二次元バーコード + キャッシュフォルダを消去します (カメラアプリで使用) + キャッシュを消去 + プライベートストレージを消去 + ファイルが保存されているプライベートストレージを消去します (サーバーから再ダウンロードできます) 信頼できるソースからこのリンクをたどりました リンクをクリックした後、%1$s の OMEMO 鍵を検証しようとしています。 これは、%2$s がこのリンクを公開した、信頼できるソースからこのリンクをたどった場合にのみ安全です。 OMEMO 鍵を検証 @@ -642,21 +647,21 @@ 連絡先をブロックしました 見知らぬ人からの通知 見知らぬ人から受信したメッセージと通話を通知します。 - 見知らぬ人からメッセージを受け取りました + 見知らぬ人からメッセージを受信しました 見知らぬ人をブロック ドメイン全体をブロック 今すぐオンライン 復号を再試行 セッション失敗 ダウングレードされた SASL メカニズム - サーバーはWebサイトでの登録が必要です - Webサイトを開く - Webサイトを開くアプリが見つかりません + サーバーはウェブサイトでの登録が必要です + ウェブサイトを開く + ウェブサイトを開くアプリが見つかりません Heads-up 通知 Heads-up 通知を表示 今日 昨日 - DNSSEC でホスト名を検証 + DNSSEC でホスト名の妥当性を確認 検証されたホスト名を含むサーバー証明書は検証済みと見なされます 証明書は XMPP アドレスを含みません 一時的 @@ -664,12 +669,12 @@ クリップボードにコピー メッセージをクリップボードにコピーしました メッセージ - 非公開メッセージを無効にしました + 非公開メッセージを無効化しました 保護されたアプリ - 画面がオフになっている場合でも通知を受信し続けるには、保護されたアプリの一覧に Conversations を追加する必要があります。 + 画面がオフになっているときでも通知を受信し続けるには、保護されたアプリの一覧に Conversations を追加する必要があります。 未知の証明書を受け入れますか? サーバー証明書が既知の認証局によって署名されていません。 - 不一致なサーバー名を受け入れますか? + 不一致のサーバー名を受け入れますか? サーバーは\"%s\"として認証できませんでした。証明書は次の場合にのみ有効です: それでも接続を希望しますか? 証明書の詳細: @@ -679,12 +684,12 @@ メッセージ送信後に下へスクロール ステータスメッセージを編集 ステータスメッセージを編集 - 暗号化をしない + 暗号化が無効 %1$s は %2$s に暗号化メッセージを送れません。連絡先が利用しているサーバーが古すぎるか、クライアントが OMEMO を扱えません。 デバイスの一覧を取得できません 暗号化の鍵を取得できません ヒント: お互いが連絡先名簿に加えれば解決するでしょう。 - この会話でOMEMOの暗号化を無効にしてよろしいですか?\n\nこれにより、サーバー管理者がメッセージを読むことが可能になりますが、時代遅れのクライアントを使っている人と連絡をとるには、この方法しかないかもしれません。 + この会話で OMEMO の暗号化を無効化してもよろしいですか?\nこれにより、サーバー管理者がメッセージを読むことが可能になりますが、時代遅れのクライアントを使っている人と連絡をとるには、この方法しかないかもしれません。 今すぐ無効化 下書き: OMEMO 暗号化 @@ -693,7 +698,7 @@ 新しい会話をするためには、OMEMOを明示的にオンにする必要があります。 ショートカットを作成 フォントの大きさ - このアプリで使用される相対フォントサイズ + このアプリで使用される相対的なフォントの大きさ デフォルトでオン デフォルトでオフ @@ -702,10 +707,10 @@ このデバイス向けにメッセージは暗号化されませんでした。 OMEMO メッセージの復号に失敗しました。 元に戻す - 場所の共有が無効 + 位置の共有が無効 位置を固定 位置を固定しない - 場所をコピー + 位置をコピー 位置を共有 位置を共有 位置を表示 @@ -716,13 +721,13 @@ メッセージを検索 GIF 会話を表示 - 場所共有プラグイン - 場所共有プラグインの代わりに、組み込みの地図を使う + 位置共有プラグイン + 位置共有プラグインの代わりに、組み込みの地図を使う ウェブアドレスをコピー XMPP アドレスをコピー S3 の HTTP ファイル共有 直接検索 - ‘会話の開始’画面でキーボードを開き、検索フィールドにカーソルを置きます + ‘会話を開始’画面でキーボードを開き、検索フィールドにカーソルを置きます グループチャットのアバター ホストはグループチャットのアバターをサポートしていません 所有者だけが、グループチャットのアバターを変更可能です @@ -732,20 +737,22 @@ 名前の記入は任意です グループチャット名 このグループチャットは破棄されました + 録音を保存できません フォアグラウンドサービス - この通知カテゴリーは %1$s が実行されていることを表示する、永続的な通知を表示するために使用されます。 + この通知カテゴリーは %1$s が実行していることを表示する、永続的な通知を表示するために使用されます。 ステータス情報 接続の問題 この通知カテゴリーは、アカウントへの接続に問題があった場合に、通知を表示するために使用されます。 メッセージ 通話 メッセージ - 着信 - 発信 + 着信通話 + 継続中の通話 + サイレントメッセージ この通知グループは、音を鳴らしてはいけない通知を表示するために使用します。例えば、他のデバイスでアクティブになっているときなどです (猶予期間)。 配信に失敗 メッセージ通知設定 - 通話着信の通知設定 + 着信通話の通知設定 重要性、音、振動 ビデオの圧縮 メディアを表示 @@ -756,8 +763,9 @@ 質が低い程、ファイルは小さくなります 中 (360p) 高 (720p) + 中止しました あなたは既にメッセージを作成中です。 - 未実装の機能 + 実装されてない機能 不正な国コード 国を選択 電話番号 @@ -777,7 +785,7 @@ 戻る クリップボードから可能な pin を自動的に貼り付ける。 6桁の pin を入力してください。 - 本当に登録手続きを中止してもよろしいのですか? + 登録手続きを中止してもよろしいのですか? はい いいえ 検証しています… @@ -791,7 +799,7 @@ サーバーが見つかりません。 要求の処理中に、何か問題が発生しました。 無効なユーザーの入力 - 一時的に入手不可能です。後でもう一度試してください。 + 一時的に入手不可能です。後でもう一度お試しください。 ネットワーク接続なし。 %s でもう一度お試しください。 上限に到達しました @@ -807,11 +815,11 @@ Orbot をインストール Orbot を開始 マーケットアプリがインストールされていません。 - この談話室では、あなたのXMPPアドレスを公開します + この談話室では、あなたの XMPP アドレスを公開します 電子書籍 - 原物 (非圧縮) + そのまま (非圧縮) …で開く - Conversations プロフィール写真 + Conversations プロフィール画像 アカウントを選択 バックアップを復元 復元 @@ -843,19 +851,19 @@ 誰でも他の人を招待できます。 XMPP アドレスは管理者が見れます。 XMPP アドレスは誰でも見れます。 - この公開談話室には参加者がいません。連絡先を招待したり、共有ボタンを使って XMPP アドレスを配布できます。 + この公開談話室には参加者がいません。連絡先を招待したり、共有ボタンを使用して XMPP アドレスを配布できます。 この非公開グループチャットには参加者がいません。 権限を管理 参加者を検索 ファイルが大きすぎます 添付 - 談話室発見 + 談話室を発見 談話室を検索 プライバシー侵害の可能性あり! - search.jabber.networkを利用します。

この機能を使うと、あなたののIPアドレスや検索キーワードがそのサービスに送信されます。詳しくは、プライバシーポリシーをご覧ください。]]>
- 私は既にアカウントを持っています - 存在するアカウントを追加 - 新しいアカウントを登録 + search.jabber.networkを利用します。

この機能を使うと、あなたののIPアドレスや検索キーワードがそのサービスに送信されます。詳しくは、プライバシーポリシーをご覧ください。]]>
+ 既にアカウントを持っています + 既存アカウントを追加 + 新規アカウントを登録 これはドメインアドレスのようです とにかく追加 これは談話室アドレスのようです @@ -868,16 +876,17 @@ このアカウントのパスワートを入力してください この操作を実行できません 公開談話室に参加… - 共有アプリがこのファイルへのアクセスを許可していませんでした。 + 共有アプリがこのファイルへのアクセス権限を付与していませんでした。 jabber.network ローカルサーバー ほとんどのユーザーは、公開されている XMPP エコシステム全体からより良い提案を得るために、‘jabber.network’を選択するはずです。 - 談話室発見方法 - アカウントを有効にしてください + 談話室の発見方法 + バックアップ + アカウントを有効化してください 通話をする - 通話着信 - 映像通話着信 + 着信通話 + 着信映像通話 接続中 接続しました 通話受入 @@ -890,21 +899,23 @@ 通話に接続できません 接続切断 撤回された通話 - ハングアップ + アプリの失敗 + 検証に問題 + 電話を切る 継続中の通話 継続中の映像通話 - 通話するのに Tor を無効にする - 通話着信 - 通話着信・%s - 不在通話着信・%s - 通話発信 - 通話発信・%s - 不在通話着信 + 通話するのに Tor を無効化 + 着信通話 + 着信通話・%s + 不在着信通話・%s + 発信通話 + 発信通話・%s + 不在着信通話 音声通話 映像通話 会話に切り替え マイクが利用できません - 1回につき1回線のみ + 1度に1回線の通話のみ。 継続中の通話に戻る カメラを切り替えできません 最上に留める @@ -934,7 +945,10 @@ アプリケーションが見つかりません 会話に招待 招待を解析できません - サーバーは招待をサポートしていません + サーバーは招待の作成をサポートしていません この機能をサポートするアクティブなアカウントがありません バックアップを開始しました。 バックアップが完了すると通知が届きます。 - + 映像を有効化できません。 + プレーンテキスト文書 + + diff --git a/src/main/res/values-ml/strings.xml b/src/main/res/values-ml/strings.xml index e4e82c214a54824f7cc923351f83a5432a7098a6..7adca9d89d252a4945f75279628366118facb29a 100644 --- a/src/main/res/values-ml/strings.xml +++ b/src/main/res/values-ml/strings.xml @@ -86,6 +86,7 @@ ഫയലുകൾ സ്വീകരിക്കൂ അറ്റാച്ചുമെന്റുകൾ അറിയിപ്പ് + വൈബ്രേറ്റ് ചെയ്യൂ LED അറിയിപ്പ് റിംഗ്‌ടോൺ അറിയിപ്പ് ശബ്‌ദം @@ -106,8 +107,11 @@ ഓൺലൈൻ ഓഫ്‌ലൈൻ സെർവർ കണ്ടെത്തിയില്ല + കണക്റ്റിവിറ്റി ഇല്ല + രജിസ്ട്രേഷൻ പരാജയപ്പെട്ടു ഉപയോക്തൃനാമം ഇതിനകം നിലവിലുണ്ട് രജിസ്ട്രേഷൻ പൂർത്തിയായി + നയ ലംഘനം സുരക്ഷിതമല്ലാത്ത OTR OpenPGP @@ -152,6 +156,7 @@ തിരഞ്ഞെടുക്കൂ കോൺ‌ടാക്റ്റ് ഇതിനകം നിലവിലുണ്ട് ചേരുക + അടയാളക്കുറിപ്പായി സംരക്ഷിക്കൂ ഗ്രൂപ്പ് ചാറ്റ് നശിപ്പിക്കൂ ചാനൽ നശിപ്പിക്കൂ വിഷയം @@ -166,6 +171,7 @@ അടുത്തത് സെഷൻ സ്ഥാപിച്ചു ഒഴിവാക്കൂ + പ്രാപ്തമാക്കൂ ഗ്രൂപ്പ് ചാറ്റിന് രഹസ്യവാക്ക് ആവശ്യമാണ് രഹസ്യവാക്ക് നൽകുക ഇപ്പോൾ അഭ്യർത്ഥിക്കുക @@ -175,6 +181,7 @@ %s-നെ കുറിച്ച് ആരംഭ സമയം മറ്റുള്ളവ + ഈ ഗ്രൂപ്പ് ചാറ്റിൽ നിന്ന് നിങ്ങളെ നിരോധിച്ചിരിക്കുന്നു നിങ്ങളെ ഗ്രൂപ്പ് ചാറ്റിൽ നിന്ന് പുറത്താക്കി %s അക്കൗണ്ട് ഉപയോഗിക്കുന്നു %s-ന്റെ വലുപ്പം പരിശോധിക്കൂ @@ -214,6 +221,7 @@ ചാനലിൽ നിന്ന് നിരോധിക്കൂ ഇപ്പോൾ നിരോധിക്കൂ സ്വകാര്യ, അംഗങ്ങൾ മാത്രം + നിങ്ങൾ പങ്കെടുക്കുന്നില്ല മറുപടി വായിച്ചതായി കാണിക്കൂ എന്റെർ കീ അയയ്ക്കും @@ -222,6 +230,10 @@ ചിത്രം %s അയയ്ക്കുന്നു %s ടൈപ്പുചെയ്യുന്നു… + %s ടൈപ്പുചെയ്യുന്നു… + ലൊക്കേഷൻ അയയ്‌ക്കുക + ലൊക്കേഷൻ കാണിക്കൂ + ലൊക്കേഷൻ റദ്ദാക്കൂ സമീപകാലത്ത് ഉപയോഗിച്ചത് കോൺ‌ടാക്റ്റുകൾ തിരയുക @@ -250,25 +262,40 @@ ഓൺലൈൻ ലഭ്യമല്ല തിരക്കിലാണ് + പങ്കെടുക്കുന്നവരെ തിരഞ്ഞെടുക്കുക + ഗ്രൂപ്പ് ചാറ്റ് സൃഷ്ടിക്കുന്നു… വീണ്ടും ക്ഷണിക്കൂ + സ്വകാര്യത + രൂപഭംഗി + കമ്പ്യൂട്ടർ + മൊബൈൽ ഫോൺ ഞാൻ അനുവദിക്കൂ + + %d മാസം + %d മാസം + മുഴുവൻ മേഖലയും തടയുക ഇപ്പോൾ സജീവം വെബ്സൈറ്റ് തുറക്കൂ ഇന്ന് ഇന്നലെ + ക്ലിപ്പ്ബോർഡിലേയ്ക്ക് പകർത്തുക സന്ദേശം ഒരിക്കൽ പങ്കിടുക സന്ദേശങ്ങൾ തിരയുക GIF + വിളിപ്പേര് പേര് + ഗ്രൂപ്പ് ചാറ്റിന്റെ പേര് സന്ദേശങ്ങൾ കോളുകൾ സന്ദേശങ്ങൾ നിശബ്‌ദ സന്ദേശങ്ങൾ പങ്കെടുക്കുന്നവർ + റദ്ദാക്കി + രാജ്യ കോഡ് തെറ്റാണ് ഒരു രാജ്യം തിരഞ്ഞെടുക്കൂ ഫോൺ നമ്പർ നിങ്ങളുടെ ഫോൺ നമ്പർ ഉറപ്പാക്കൂ @@ -283,8 +310,25 @@ നിങ്ങളുടെ പേര് നൽകുക അക്കൗണ്ട് തിരഞ്ഞെടുക്കൂ XMPP വിലാസം നൽകുക + ഗ്രൂപ്പ് ചാറ്റ് സൃഷ്ടിക്കുക + പൊതു ചാനലിൽ ചേരുക XMPP വിലാസം + ഫയൽ വളരെ വലുതാണ് + ചാനലുകൾ തിരയുക നിലവിലുള്ള അക്കൗണ്ട് ചേർക്കുക + ഈ പ്രവർത്തനം നടത്താൻ കഴിഞ്ഞില്ല + പൊതു ചാനലിൽ ചേരുക… + jabber.network + കോൾ സ്വീകരിക്കുന്നു + കോൾ അവസാനിപ്പിക്കുന്നു + ഇൻകമിംഗ് കോൾ സഹായം + നിങ്ങളുടെ മൈക്രോഫോൺ ലഭ്യമല്ല GPX ട്രാക്ക് + എല്ലാ സംഭാഷണങ്ങളും + ഈ സംഭാഷണം + നിങ്ങളുടെ അവതാർ + കൂടുതൽ ഓപ്ഷനുകൾ + Conversations-ലേക്ക് ക്ഷണിക്കുക + ബാക്കപ്പ് ആരംഭിച്ചു. അത് പൂർത്തിയായിക്കഴിഞ്ഞാൽ നിങ്ങൾക്ക് ഒരു അറിയിപ്പ് ലഭിക്കും. diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 04bee88883934a176eea3f8ed9245b8e3d9f9a86..7325816ed51dee7ffe32697e5d403d566912a0e5 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -138,6 +138,8 @@ Wysyłając nam ślady stosu pomagasz w rozwoju Potwierdzenia wiadomości Zezwól na wysyłanie do osób z twojej listy kontaktów informacji o tym, kiedy otrzymałeś i przeczytałeś wiadomość od nich + Zapobiegaj zrzutom ekranu + Ukryj zawartość aplikacji w podglądzie aplikacji oraz zablokuj zrzuty ekranu UI OpenKeychain zgłosiło błąd. Zły klucz szyfrowania. @@ -156,6 +158,7 @@ Nie odnaleziono pliku Ogólny błąd wejścia/wyjścia Aplikacja użyta do wyboru obrazu nie zezwoliła na odczyt pliku.\n\nWybierz obraz przy użyciu innego menedżera plików + Aplikacja której użyłeś do udostępnienia pliku nie dostarczyła odpowiednich uprawnień. Nieznany Tymczasowo wyłączono Połączono @@ -170,6 +173,7 @@ Ten serwer nie wspiera rejestracji Nieprawidłowy żeton rejestracji Nie powiodła się negocjacja TLS + Nie można zweryfikować tej domeny Naruszenie zasad Serwer niekompatybilny Błąd strumienia @@ -418,6 +422,7 @@ plik audio plik wideo obraz + grafika wektorowa Dokument PDF Aplikacja Androida Kontakt @@ -924,7 +929,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Połączony Akceptowanie połączenia Kończenie połączenia - Odbierz + Połącz Odrzuć Wyszukiwanie urządzeń Dzwonienie @@ -933,6 +938,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Utracono połączenie Anulowane połączenie Błąd aplikacji + Problem z weryfikacją Rozłącz Połączenie wychodzące Wideorozmowa wychodząca @@ -987,4 +993,7 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Serwer nie wspiera tworzenia zaproszeń Nie ma aktywnych kont wspierających tę funkcję Tworzenie kopii zapasowej się rozpoczęło. Dostaniesz powiadomienie kiedy się zakończy. - + Nie można włączyć wideo. + Dokument zwykłego tekstu + + diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index f638735f875aba4e1edc2dd97db05eb163b689b8..5e15e47c15e6e9cc9e211bf74a0c9f492194392a 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -132,6 +132,8 @@ Ao enviar os stack traces você está colaborando com o desenvolvimento Confirmação de mensagens Permite que seus contatos saibam quando você recebeu e leu as mensagens deles. + Impedir capturas de tela + Esconde o conteúdo do app no alternador de apps e bloqueia capturas de tela IU O OpenKeychain produziu um erro. Chave ruim para a criptografia @@ -414,6 +416,7 @@ áudio vídeo imagem + gráfico vetorial Documento PDF Aplicativo Android Contato @@ -912,6 +915,7 @@ Conexão perdida Chamada rejeitada Falha no aplicativo + Problema de verificação Desligar Chamada em andamento Chamada de vídeo em andamento @@ -963,4 +967,6 @@ Nenhuma conta ativa suporta esse recurso O backup foi iniciado. Você receberá uma notificação assim que ele for concluído. Não foi possível habilitar o vídeo. + Documento em texto puro + diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index abc6a99afa154882d7fa4464a29412a790b3fbcb..d3ddbe6ca0c634ebba90654413c215f6cfe9b702 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -135,6 +135,8 @@ Trimițând date despre erori ajutați la continuarea dezvoltării aplicației Confirmă mesajele Contactele sunt notificate atunci când ați primit un mesaj și l-ați citit + Previne captura ecranului + Ascunde conținutul în managerul de aplicații și blochează captura de ecran Opțiuni interfață OpenKeychain a raportat o eroare. Cheie invalidă pentru criptare. @@ -153,6 +155,7 @@ Fișierul nu a fost găsit Eroare I/O generala. Poate ați rămas fără spațiu liber? Aplicația folosită pentru selecția acestei imagini nu a oferit destule permisiuni pentru a putea citii fișierul.\n\nFolosiți un alt manager de fișiere pentru a alege o imagine + Aplicația pe care ați folosit-o pentru a partaja acest fișier nu a furnizat suficiente permisiuni. Necunoscut Dezactivat temporar Conectat @@ -167,6 +170,7 @@ Serverul nu permite înregistrarea Simbol de înregistrare invalid Negociere TLS eşuată + Domeniul nu se poate verifica Încălcare condiții furnizare serviciu Server incompatibil Eroare de date @@ -415,6 +419,7 @@ audio video imagine + grafic vectorial document PDF Aplicație Android Contact @@ -921,6 +926,7 @@ Conexiune pierdută Apel anulat Eroare de aplicație + Problemă la verificare Închide Apel în curs Apel video în curs @@ -973,4 +979,7 @@ Serverul nu suportă generarea de invitații Nici un cont activ nu suporta această caracteristică Se creează copia de siguranță. Veți primi o notificare când acesta este completă. - + Nu s-a putut activa camera video. + Document text + + diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index d2b0c26e783de7619139635efaf155fea01e57ec..2c2f413d06a6ee71ab5680950d14c44835e4204a 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -138,6 +138,8 @@ Отправляя отчеты об ошибках, вы помогаете разработке этого приложения Отчёты о получении Позволяет вашим контактам видеть, когда вы получили и прочитали их сообщения + Запретить скриншоты + Прятать содержимое приложения при переключении приложений и запретить скриншоты Интерфейс OpenKeychain вызвал ошибку. Неподходящий ключ для шифрования. @@ -156,6 +158,7 @@ Файл не найден Общая ошибка ввода/вывода. Возможно, на устройстве недостаточно свободного места? У приложения, которым вы выбрали это изображение, недостаточно прав, чтобы прочитать этот файл.\n\nПожалуйста, используйте другой файловый менеджер, чтобы выбрать это изображение. + Приложение, которое вы использовали для публикации этого файла, не предоставило достаточно разрешений. Неизвестен Временно отключён В сети @@ -170,6 +173,7 @@ Сервер не поддерживает возможность регистрации Неправильный токен регистрации Не удалось согласовать TLS + Домен не поддается проверке Нарушение правил Несовместимый сервер Ошибка потока @@ -418,6 +422,7 @@ аудио звук изображение + векторная графика PDF-документ Приложение Android Контакт @@ -986,4 +991,7 @@ Сервер не поддерживает создание приглашений Ни один активный аккаунт не поддерживает эту функцию Резервное копирование было начато. Вы получите уведомление, как только оно будет завершено. - + Невозможно включить видео. + Текстовые данные + + diff --git a/src/main/res/values-sk/strings.xml b/src/main/res/values-sk/strings.xml index 6fb10af6b48efbc0748a20ff7eb83d163128a9cf..25e10cd611ca10f39f246df3bcfd7cd4224b7fd5 100644 --- a/src/main/res/values-sk/strings.xml +++ b/src/main/res/values-sk/strings.xml @@ -3,38 +3,69 @@ Nastavenia Nová konverzácia Nastavenie účtov + Nastaviť účet + Zavrieť rozhovor Detaily kontaktu + Detaily skupinového rozhovoru + Detaily kanála Pridať účet Upraviť meno + Pridať do kontaktov Vymazať zo zoznamu Zablokovať kontakt Odblokovať kontakt Zablokovať doménu Odblokovať doménu + Zablokovať účastníka + Odblokovať účastníka Nastavenie účtov Nastavenia Zdieľať s konverzáciou Začať konverzáciu + Vybrať Kontakt + Vyberte Kontakty + Zdieľať cez účet Zablokovať zoznam práve teraz pred 1 minútou pred %d minútami + + %dneprečítaný rozhovor + + + %dneprečítaných rozhovorov + + + %dneprečítaných rozhovorov + + + %dneprečítaných rozhovorov + + posielam... Dešifrujem správu. Čakajte, prosím… + OpenPGP šifrovaná správa Prezývka už existuje + Chybná prezývka Administrátor Vlastník Moderátor Účastník Návštevník + Chcete vymazať %sz vašich kontaktov? Rozhovory s týmto kontaktom nebudú zmazané. Chceli by ste zablokovať prijímanie správ od %s? Chceli by ste odblokovať %s a povoliť prijímanie správ? Zablokovať všetky kontakty od %s? Odblokovať všetky kontakty od %s? Kontakt zablokovaný + Zablokovaný + Chcete vymazať %sako záložku? Rozhovory s touto záložkou nebudú zmazané. Registrovať nový účet na serveri Zmeniť heslo na serveri Zdieľať s + Začať rozhovor + Pozvať kontakt + Pozvať Kontakty Kontakt Zrušiť @@ -46,33 +77,73 @@ Odblokovať Uložiť OK + %1$ssa zrútila + Pomocou vášho XMPP konta nám pošlite záznam o zlyhaní, ktorý nám pomôže vo vývoji %1$s. Poslať teraz Nepýtať sa znova + Nedá sa pripojiť k účtu + Nedá sa pripojiť k viacerým kontám + Ťapnite na správu vášho účtu Priložiť súbor + Pridať tento chýbajúci kontakt do vašich kontaktov? Pridať kontakt doručenie zlyhalo + Pripravujem odoslanie obrázka + Pripravujem odoslanie obrázkov + Zdieľam súbory. Prosím čakajte... Vymazať históriu Vymazať históriu konverzácií + Chcete vymazať všetky správy v tomto rozhovore?\n\nUpozornenie:Nebude to mať vplyv na správy uložené na ostatných zariadeniach alebo serveroch. + Zmazať súbor + Ste si istý, že chcete tento súbor zmazať?\n\nUpozornenie:Nevymažú sa kópie súborov, ktoré sú uložené na ostatných zariadeniach alebo serveroch. + Potom zavrieť tento rozhovor + Zvoliť zariadenie Poslať nezašifrovanú správu + Poslať správu + Poslať správu na %s Poslať OMEMO šifrovanú správu + Poslať v\\OMEMO šifrovanú správu Poslať OpenPGP šifrovanú správu + Používa sa nová prezývka Poslať nešifrované Zašifrovanie zlyhalo. Možno nemáte správny privátny kľúč. OpenKeychain Reštartovať Inštalovať + Prosím, nainštalujte OpenKeychain ponúka… čakám… Nenašiel sa žiadny OpenPGP kľúč + Nepodarilo sa zašifrovať vašu správu, pretože váš kontakt nezverejňuje jeho verejný kľúč.\n\nPožiadajte prosím váš kontakt, aby si nastavil OpenPGP. Nenašli sa žiadne OpenPGP kľúče + Nemôžem zašifrovať Vašu správu, pretože Vaše kontakty neoznamujú ich verejné kľúče.\n\nPoproste ich, aby si nastavili OpenPGP. Všeobecné Prijať súbory Automaticky prijať súbory menšie ako… + Prílohy + Oznámenie Vibrovať + Vibrovať, keď príde nová správa + LED notifikácia + Blikať notifikačným svetlom, keď príde nová správa + Zvonenie + Zvuk oznámenia + Zvuk oznámenia nových správ + Zvonenie pre prichádzajúce hovory + Ochranná doba + Doba, počas ktorej budú oznámenia stíšené po detekcii aktivity na jednom z vašich ostatných zariadení. + Pokročilé Neodosielať detaily o zlyhaní aplikácie + Keď pošlete detaily o dôvode zlyhania, pomáhate vývoju Potvrdzovať správy + Dajte vedieť svojim kontaktom, keď prijmete a prečítate si správy + Zakázať snímok obrazovky + Skryje obsah aplikácie v posledných aplikáciách a zablokuje snímky obrazovky. + Prostredie + Nesprávny kľúč na šifrovanie. Prijať Došlo k chybe + Chyba Váš účet Zasielať zmeny stavu Prijímať zmeny stavu @@ -81,8 +152,11 @@ Odfotiť Aktívne povoliť vyžiadanie zmeny stavu Vybraný súbor nie je obrázok + Nemohol som konvertovať obrázkový súbor Súbor sa nenašiel Všeobecná I/O chyba. Možno už nie je voľné miesto? + Aplikácia, ktorú ste použili pre výber obrázka neposkytla dostatočné oprávnenia na prečítanie súboru.\n\nSkúste použiť iného správcu súborov pre výber obrázka + Aplikácia, ktorú ste použili na zdieľanie tohto súboru neposkytla dostatočné povolenia. Neznámy Dočasne vypnutý Online @@ -94,6 +168,11 @@ Registrácia zlyhala Užívateľské meno už existuje Registrácia ukončená + Registrácia nie je podporovaná serverom. + Neplatný registračný token + Nadviazanie spojenia TLS zlyhalo + Doména sa nedá overiť + Porušenie pravidiel Nekompatibilný server Nezašifrovaný OTR @@ -103,11 +182,17 @@ Dočasne vypnúť Zverejniť avatar Zverejniť OpenPGP kľúč + Odstrániť OpenPGP  verejný kľúč Povoliť účet Ste si istý? Nahrať hlas + XMPP adresa + Zablokovať adresu XMPP meno@priklad.com Heslo + Toto nie je platná XMPP adresa + Nedostatok pamäte. Obrázok príliš veľký + Chcete pridať do vašich kontaktov %s? Informácie o serveri XEP-0313: MAM XEP-0280: Message Carbons @@ -115,82 +200,157 @@ XEP-0191: Blocking Command XEP-0237: Roster Versioning XEP-0198: Stream Management + XEP-0215: Zistenie externej služby XEP-0163: PEP (Avatars / OMEMO) XEP-0363: HTTP File Upload + XEP-0357: Oznámenia dostupný nedostupný Chýba oznámenie o verejnom kľúči práve prihlásený + naposledy videný pred minútou naposledy prihlásený pred %d minútami + naposledy videný pred hodinou naposledy prihlásený pred %d hodinami + naposledy videný včera naposledy prihlásený pred %d dňami OMEMO identifikátor + v\\OMEMO odtlačok + OMEMO odtlačok (pôvod správy) + v\\OMEMO odtlačok (pôvod správy) Ostatné zariadenia Dôverovať OMEMO identifikátoru + Načítavam kľúče... Dokončený Dešifrovať + Záložky Hľadať + Vložiť Kontakt + Zmazať kontakt Zobraziť detaily kontaktu Zablokovať kontakt Odblokovať kontakt Vytvoriť + Vybrať Kontakt už existuje Vstúpiť + channel@conference.example.com/nick + channel@conference.example.com Uložiť ako záložku Vymazať záložku + Vymazať skupinový rozhovor + Vymazať kanál + Ste si istý, že chcete zrušiť tento skupinový rozhovor?\n\nUpozornenie: Skupinový rozhovor bude kompletne vymazaný zo servera. + Nemohol som vymazať skupinový rozhovor + Nemohol som vymazať kanál + Upraviť predmet skupinového rozhovoru + Téma + Pripájam skupinový rozhovor... Odísť Kontakt pridaný do zoznamu Znova pridať %s dočítal až potiaľ + %sdočítal potiaľto + %1$s+%2$dostatní dočítali potiaľto + Každý dočítal potiaľto Zverejniť + Ťuknite na avatar pre vybranie obrázka z galérie Zverejňujem… Server odmietol toto zverejnenie + Nemôžem skonvertovať váš obrázok Nepodarilo sa uložiť avatar na disk (Dlho podržať pre obnovenie pôvodného stavu) + Váš server nepodporuje zverejnenie avatarov súkromná správa pre %s Odoslať súkromnú správu %s Pripojiť Tento účet už existuje Ďalší + Spojenie naviazané Preskočiť Vypnúť upozornenia Povoliť + Skupinový rozhovor požaduje heslo Vložiť heslo Ihneď vyžiadať Ignorovať + Bezpečnosť + Povoliť úpravu správy + Povoliť vašim kontaktom spätne upraviť ich správy + Nastavenia pre skúsených S týmto narábajte veľmi opatrne, prosím + O %s Tichý režim Čas začiatku Čas konca Povoliť tichý režim Upozornenia budú počas tichého režimu stlmené Ďalší + Synchronizovať so záložkami + Automaticky sa pripojiť k skupinovému rozhovoru, ak to hovorí záložka + OMEMO odtlačok skopírovaný do schránky + Ste zakázaný na tomto skupinovom rozhovore + Skupinový rozhovor len pre členov + Boli ste vyhodení z tohto skupinového rozhovoru + Skupinový rozhovor bol zastavený + Už viac nie ste v tomto skupinovom rozhovore Používa sa účet %s + Hostovaný na %s Overiť %s na HTTP host Nie ste pripojený. Skúste to neskôr Overiť %s veľkosť + Skontrolujte %1$sveľkosť na %2$s Možnosti správy + Citovať + Vložiť ako citát Skopírovať originálny URL Poslať znova URL súbor + URL skopírovaná do schránky + XMPP adresa skopírovaná do schránky + Správa o chybe skopírovaná do schránky + web adresa + Snímať 2D Bar kód + Ukázať 2D Bar kód Zobraziť zoznam blokovaných Detaily účtu Potvrdiť Skúste znova + Služba v popredí Zamedzí operačnému systému ukončiť pripojenie + Vytvoriť zálohu + Súbory zálohy budú uložené v %s + Vytváram súbor zálohy + Vaša záloha bola vytvorená + Súbor zálohy bol uložený v %s + Obnovujem zálohu + Vaša záloha bola obnovená + Nezabudnite si zapnúť konto. Vybrať súbor Prijímam %1$s (%2$d%% ukončený) Stiahnuť %s + Zmazať %s súbor Otvoriť %s posielam (%1$d%% ukončený) + Pripravuje sa zdieľanie súboru %s ponúknutý na stiahnutie Zrušiť prenos - Zobraziť etikety na čítanie pod kontakty + Nedá sa zdieľať súbor + prenos súboru zrušený + Súbor zmazaný + Nebola nájdená aplikácia na otvorenie súboru + Nebola nájdená aplikácia na otvorenie odkazu + Nebola nájdená aplikácia na prezretie kontaktu + Dynamické štítky + Zobraziť štítky pod kontaktmi Povoliť upozornenia Avatar účtu Skopírovať OMEMO identifikátor do schránky + Regenerovať OMEMO kľúč + Vymazať zariadenia + Ste si istý, že chcete odstrániť všetky ostatné zariadenia z OMEMO oznámenia? Keď sa nabudúce vaše zariadenia pripoja, znova sa samé ohlásia, ale nemusia prijať správy odoslané medzitým. Načítať históriu zo serveru Na serveri nie je žiadna ďalšia história Aktualizujem... @@ -228,8 +388,12 @@ Posielam %s Ponúkam %s Skryť neprihlásených + %s píše... %s prestal písať + %s píšu... + %s prestali písať Upozornenia pri písaní + Dajte svojim kontaktom vedieť že im práve píšete správu. Poslať polohu Zobraziť polohu Poloha @@ -256,8 +420,111 @@ Užívateľské meno Užívateľské meno Toto nie je platné užívateľské meno + Prihlásiť sa s certifikátom Obnoviť certifikát + Chyba pri načítaní OMEMO kľúča! + kľúč OMEMO overený certifikátom! + Pripojiť cez Tor + %1$dz%2$dúčtov pripojených + + %dspráva + %dsprávy + %d správ + %dspráv + + Načítať viac správ + Súbor zdieľaný s %s + Obrázok zdieľaný s %s + Obrázky zdieľané s %s + Text zdieľaný s %s + Oznamovať na všetkých správach + Vždy + Deaktivovať + (Žiadne aktivované účty) + Nebola nájdená žiadna aplikácia na zdieľanie URI + Zdieľať URI s... + Nastaviť vašu dostupnosť pri úprave vašej status správy. Online + Zaneprázdnený + Deaktivovať + Oznamovať používanie + Súkromie + Téma + Zvoľte si farebnú schému + Zelené pozadie + Použiť zelené pozadie pre prijaté správy + Vymazať OMEMO identifikátory + Re-generuje vaše kľúče OMEMO. Všetky vaše kontakty vás budú musieť znova overiť. Použite to ako poslednú možnosť. + Odstrániť označené kľúče + Overili ste všetky kľúče OMEMO vo vašom vlastníctve. + Zdieľať ako qr kód + Zdieľať ako XMPP URI + Zdieľať ako HTTP odkaz + Overiť kľúče OMEMO + Automaticky vymazávať správy z tohto zariadenia, ktoré sú staršie ako nastavené časové obdobie. + online práve teraz + Nahrať video + Kopírovať do schránky Správa skopírovaná do schránky + Upraviť status správu + Upraviť status správu + OMEMO šifrovanie + OMEMO bude vždy používané pre individuálne a súkromné skupinové rozhovory. + OMEMO bude predvolene zapnuté pre všetky rozhovory. + Veľkosť písma + Predvolene zapnuté + Predvolene vypnuté + Nepodarilo sa dešifrovať OMEMO správu. + Zdieľať Polohu + Zdieľať polohu Zobraziť polohu + Zdieľať + Prehľadávať správy + Plugin na Zdieľanie Polohy + Používať Plugin na Zdieľanie Polohy namiesto vstavanej mapy. + Služba v popredí + Správy + Hovory + Správy + Prichádzajúce hovory + Prebiehajúce hovory + Zlyhané doručenia + Nastavenia oznámení prichádzajúcich hovorov + Prosím vložte vaše meno, aby ľudia, ktorí vás nemajú v adresári, vedeli kto ste. + Vaše meno + Vložte vaše meno + Pre nastavenie vášho mena, použite tlačidlo Upraviť. + Obnoviť zálohu + XMPP adresa + Priložiť + Zdieľať súbory záloh + Zálohy + O aplikácii + Prichádzajúci hovor + Prichádzajúci video hovor + Prijímam hovor + Ukončujem hovor + Prijať + Odmietnuť + Vyhľadávanie zariadení + Zvoní + Zaneprázdnený + Nedá sa pripojiť hovor + Prebiehajúci hovor + Prebiehajúci video hovor + Prichádzajúci hovor + Prichádzajúci hovor - %s + Zmeškaný hovor - %s + Odchádzajúci hovor + Odchádzajúci hovor - %s + Zmeškaný hovor + Hlasový hovor + Video hovor + Naraz môžete mať iba jeden hovor. + Vrátiť sa do prebiehajúceho hovoru + Pripnúť na vrch + Odopnúť z vrchu + Zašifrované s OMEMO + Zlyhané doručenia + Viac možnosťí diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml index 623bb6a949059db3337a6e5a2f6040d5b31fdfd4..cd39b936972887c555397175ae352c4961472d9d 100644 --- a/src/main/res/values-sr/strings.xml +++ b/src/main/res/values-sr/strings.xml @@ -111,7 +111,9 @@ нудим… чекам… Нема ОпенПГП кључа + Није могуће шифровати вашу поруку јер контакт није објавио свој јавни кључ.\n\nЗамолите контакт да подеси ОпенПГП. Нема ОпенПГП кључева + Није могуће шифровати вашу поруку јер контакти нису објавили своје јавне кључеве.\n\nЗамолите контакте да подесе ОпенПГП. Опште Прихватај фајлове Аутоматски прихватај фајлове мање од… @@ -128,6 +130,7 @@ Период одгоде Напредно Никад не шаљи извештаје о паду + Слањем извештаја рада помажете развоју апликације. Потврди поруке Обзнаните контактима када примите и прочитате њихове поруке Сучеље @@ -147,6 +150,8 @@ Не могу преобратити датотеку фотографије Фајл није нађен Општа У/И грешка. Можда вам је нестало простора у складишту? + Апликација из које делите ову слику не даје дозволу довољну да се датотека учита.\n\nПоделите слику другим претраживачем датотека. + Апликација из које делите овај садржај не даје довољну дозволу. Непознато Привремено искључен На вези @@ -179,6 +184,7 @@ ОпенПГП кључ је објављен. Укључи налог Да ли сте сигурни? + Брисањем налога бришете и целу историју ваших разговора. Сними глас ИксМПП адреса Блокирај ИксМПП адресу @@ -237,6 +243,8 @@ Обриши обележивач Уклони групно ћаскање Уклони канал + Да ли сигурно жеите да уклоните ово групно ћаскање?\n\nУпозорење: Групно ћаскање ће бити потпуно обрисано са сервера. + Да ли сигурно жеите да уклоните овај јавни канал?\n\nУпозорење: Канал ће бити потпуно обрисан са сервера. Не могу уклонити групно ћаскање Не могу уклонити канал Уреди предмет групног ћаскања @@ -269,8 +277,10 @@ Укључи Групно ћаскање захтева лозинку Унесите лозинку + Најпре захтевајте ажурирање присутности од вашег контакта.\n\nОво ће омогућити да се одреди који клијент ваш контакт користи. Захтевај одмах Занемари + Упозорење: Слањем овога без обостраног ажурирања присутности може изазвати неочекиване проблеме.\n\nИдите у „Детаљи контакта” да потврдите вашу претплату за присутност. Безбедност Дозволи исправљање порука Дозвољава вашим контактима да ретроактивно уређују њихове поруке @@ -349,6 +359,7 @@ Копирај ОМЕМО отисак на клипборд Поново генериши ОМЕМО кључ Очисти уређаје + Нема употребљивих кључева за овај контакт.\nПроверите да ли сте одобрили узајамно ажурирање присутности. Нешто је пошло по злу Добављам историјат са сервера Нема више историјата на серверу @@ -515,6 +526,7 @@ Исправи поруку Пошаљи исправљену поруку Искључили сте овај налог + Безбедносна грешка: неисправан приступ датотеци! Нема апликације за дељење ресурса Подели везу помоћу… Сложи се и настави @@ -523,6 +535,7 @@ Користићу сопствени провајдер Одредите ваше корисничко име Ручно мењај доступност + Поставите присутност при измени ваше поруке стања. Порука стања Слободан за ћаскање На вези @@ -564,12 +577,17 @@ Дозволи Нема дозвола за приступ %s Удаљени сервер није нађен + Удаљени сервер се не одазива + Не могу да ажурирам налог + Пријавите ову ИксМПП адресу због нежељених порука. Обриши ОМЕМО идентитете Обриши изабране кључеве Морате бити повезани да бисте објавили ваш аватар. Прикажи поруку грешке Порука грешке Чувар протока укључен + Ваш уређај не подржава искључење „Уштеде података” за %1$s. + Не могу да направим привремену датотеку Овај уређај је оверен. Копирај отисак Оверени отисци diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index 595a6a3455e6bb7aa5a704e92e311f77ea2176e9..13858623763a3e5bc709dd09e14a09739f734752 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -4,6 +4,7 @@ Ny konversation Kontoinställningar Hantera konto + Stäng konversation Kontaktdetaljer Gruppchattdetaljer Kanaldetaljer @@ -28,6 +29,13 @@ just nu 1 min sedan %d min sedan + + %d oläst konversation + + + %d olästa konversationer + + skickar… Avkrypterar meddelande. Vänta… OpenPGP-krypterat meddelande @@ -63,11 +71,15 @@ Avblockera Spara Ok + %1$s har kraschat + Att använda ditt XMPP-konto för att skicka in \'stack traces\' hjälper den pågående utvecklingen av %1$s. Skicka nu Fråga aldrig igen Kunde inte ansluta till konto Kunde inte ansluta till flera konton + Tryck för att hantera dina konton Bifoga fil + Vill du lägga till den här saknade kontakten i din kontaktlista? Lägg till kontakt sändning misslyckades Förbereder att skicka bild @@ -75,7 +87,9 @@ Delar filer. Vänta... Rensa historik Rensa konversationshistorik + Vill du radera alla meddelanden i den här konversationen?\n\nVarning: Det här påverkar inte meddelanden som finns lagrade på andra enheter eller servrar. Ta bort fil + Är du säker på att du vill ta bort den här filen?\n\nVarning: Den här åtgärden kommer inte att ta bort kopior av den här filen som finns lagrad på andra enheter eller servrar. Stäng denna konversation efteråt Välj enhet Skicka okrypterat meddelande @@ -88,13 +102,16 @@ Skicka okrypterat Avkryptering misslyckades. Du har kanske kanske inte rätt privat nyckel. OpenKeychain + OpenKeychain för att kryptera och avkryptera dina publika nycklar.

Programmet är licensierat under GPLv3+ och finns tillgänglig via F-Droid and Google Play.

(Var god och starta om %1$s efter installationen.)]]>
Starta om Installera Installera OpenKeychain erbjuder… väntar… Ingen OpenPGP-nyckel funnen + Det gick inte att kryptera ditt meddelande eftersom att din kontakt inte har annonserat sin publika nyckel.\n\nVänligen be din kontakt att sätta upp OpenPGP. Inga OpenPGP-nycklar funna + Det gick inte att kryptera ditt meddelande eftersom att din kontakt inte har annonserat sina publika nycklar.\n\nVänligen be din kontakt att sätta upp OpenPGP. Generellt Acceptera filer Acceptera automatiskt filer som är mindre än… @@ -105,12 +122,20 @@ LED notifieringar Blinka med notifieringsljuset när ett meddelande tagits emot Meddelandesignal + Aviseringsljud + Aviseringsljud för nya meddelande + Ringsignal för inkommande samtal Notifieringsfrist + Tidsgräns för hur länge notiser ska tystas efter att aktivitet har upptäckts på en av dina andra enheter. Avancerat Skicka aldrig krasch-rapporter + Genom att skicka in stack traces hjälper du utvecklingen Bekräfta meddelanden Låt dina kontakter veta när du har mottagit och läst deras meddelanden + Förhindra skärmdumpar + Dölj innehållet från applikationen i applikationsväxlaren och blockera skärmdumpar Gränssnitt + OpenKeychain genererade ett fel. Dålig krypterings-nyckel. Acceptera Ett fel har inträffat @@ -123,8 +148,11 @@ Ta ny bild Tillåt abonnemangsbegäran i förväg Filen du valt är inte en bild + Det gick inte att konvertera bildfilen Filen hittas ej Generellt I/O-fel. Du kanske fick slut på plats? + Applikationen som du använde för att välja den här bilden tillhandahöll inte tillräckligt med rättigheter för att läsa filen.\n\nVar god och använd en annan filhanterare för att välja en bild. + Applikationen du använde för att dela den här filen tillhandahöll inte tillräckligt med behörigheter. Okänd Tillfälligt inaktiverad Online @@ -136,10 +164,14 @@ Registreringsfel Användarnamn används redan Registrering klar + Registrering stöds ej av server + Ogiltigt registreringstoken TLS-förhandling misslyckades + Domänen kan inte verifieras Kränkning av policy Inkompatibel server Strömningsfel + Fel vid öppning av ström Okrypterat OTR OpenPGP @@ -150,14 +182,17 @@ Publicera OpenPGP publik nyckel Ta bort OpenPGP publik nyckel Är du säker på att du vill ta bort din OpenPGP publik nyckel från din tillgänglighetsuppdatering?\nDina kontakter kommer inte längre att kunna skicka dig OpenPGP-krypterade meddelande. + OpenPGP-nyckel har publicerats. Aktivera konto Är du säker? + Om du tar bort ditt konto raderas hela din konversationshistorik Spela in röst XMPP-adress Blockera XMPP-adress användarnamn@exempel.se Lösenord Detta är inte en giltig XMPP-adress + Slut på minne. Bilden är för stor Vill du lägga till %s i din enhets kontakter? Server-info XEP-0313: Message Archive @@ -173,12 +208,19 @@ otillgänglig Annonsering om publik nyckel saknas senast sedd just nu + senast sedd för en minut sedan senast sedd %d minuter sedan + senast sedd för en timme sedan senast sedd %d timmar sedan + senast sedd för en dag sedan senast sedd %d dagar sedan + Krypterat meddelande. Installera OpenKeychain för att dekryptera meddelandet. + Nytt OpenPGP krypterat meddelande hittades OpenPGP-nyckel-ID OMEMO-fingeravtryck v\\OMEMO-fingeravtryck + OMEMO-fingeravtryck (meddelandets ursprung) + v\\OMEMO-fingeravtryck (meddelandets ursprung) Andra enheter Lita på OMEMO-fingeravtryck Hämtar nycklar... @@ -195,33 +237,50 @@ Välj Kontakten finns redan Gå med + rum@konferens.exempel.se/användarnamn + rum@konferens.exempel.se Spara som bokmärke Ta bort bokmärke + Förstör gruppchat + Förstör kanal + Är du säker på att du vill förstöra den här gruppchatten?\n\nVarning: Gruppchatten kommer att tas bort helt från servern. + Är du säker på att du vill förstöra den här publika chattgruppen?\n\nVarning: Den här gruppchatten kommer att tas bort helt från servern. + Det gick inte att ta bort gruppchatten + Det gick inte att ta bort kanalen + Redigera ämnet för gruppchatten Ämne Går med i gruppchatt... Lämna Kontakten lade till dig i sin kontaktlista Addera tillbaka %s har läst hit + %s har läst till den här punkten + %1$s +%2$d andra har läst till den här punkten Alla har läst fram till hit Publicera + Tryck på visningsbilden för att välja en bild från galleriet Publicerar… Servern kunde inte publicera + Det gick inte att konvertera din bild Kunde inte spara avatarbild till disk (Eller tryck länge för att få tillbaks förvald) + Din server stödjer inte publicering av visningsbilder privat meddelande till %s Skicka privat meddelande till %s Anslut Detta konto finns redan Nästa + Session etablerad Hoppa över Inaktivera notifieringar Aktivera Gruppchatten kräver lösenord Fyll i lösenord + Var god begär närvarouppdateringar från din kontakt först.\n\nDetta kommer att användas för att avgöra vilken chattapplikationen din kontakt använder. Begär nu Ignorera + Varning: Att skicka detta utan ömsesidiga närvarouppdateringar kan orsaka oväntade problem.\n\nGå till \"Kontaktuppgifter\" för att verifiera dina närvaroprenumerationer. Säkerhet Tillåt korrigeringar av meddelanden Tillåt att dina kontakter kan ändra sina meddelanden i efterhand @@ -235,9 +294,16 @@ Notifieringar kommer vara tysta under tysta timmar Annat Synkronisera med bokmärken + Gå med i gruppchattar automatiskt om bokmärket säger det + OMEMO-fingeravtryck kopierat till urklipp + Du är avstängd från denna gruppchatt + Denna gruppchatt är endast för medlemmar Resursbegränsning + Du har blivit sparkad från den här gruppchatten Gruppchatten stängdes ner + Du är inte längre med i denna gruppchatt använder konto %s + huseras hos %s Kontrollerar %s på webbserver Du är inte ansluten. Försök igen senare Kontrollera filstorleken på %s @@ -258,6 +324,7 @@ Kontodetaljer Bekräfta Försök igen + Förgrundstjänst Förehindrar operativsystemet att ta ner uppkopplingen Skapa säkerhetskopia Säkerhetskopians filer lagras i %s @@ -274,14 +341,27 @@ fil Öppna %s skickar (%1$d%% klart) + Förbereder för delning av fil %s erbjuden för nedladdning Avbryt överföring + det gick inte att dela fil + filöverföring avbruten + Fil borttagen + Ingen applikation som kunde öppna filen hittades + Ingen applikation som kunde öppna länken hittades + Ingen applikation som kunde visa kontakten hittades + Dynamiska etiketter Visa skrivskyddade taggar under kontakter Aktivera notifieringar + Ingen gruppchattserver hittades + Det gick inte att skapa gruppchatten Kontots avatarbild Kopiera OMEMO-fingeravtryck till urklipp Regenerera OMEMO-nyckel Rensa enheter + Är du säker på att du vill ta bort alla andra enheter från OMEMO-tillkännagivandet? Nästa gång dina enheter ansluter, kommer de att tillkännage sig själva igen, men de kanske inte får meddelanden som skickas under tiden. + Det finns inga användbara nycklar tillgängliga för den här kontakten.\nDet gick inte att hämta nya nycklar från servern. Kanske är det något fel på din kontakts server? + Det finns inga användbara nycklar tillgängliga för den här kontakten.\nSe till att ni båda har närvaroprenumeration. Något gick fel Hämtar historik från server Ingen mer historik på server @@ -291,6 +371,7 @@ Byt lösenord Nuvarande lösenord Nytt lösenord + Lösenord kan inte vara tomma Aktivera alla konton Deaktivera alla konton Utför åtgärden med @@ -299,26 +380,42 @@ Utstött Medlem Avancerat läge + Bevilja medlemsprivilegier + Återkalla medlemsprivilegier Bevilja administratörsbehörighet Återkalla administratörsbehörighet + Bevilja ägarprivilegier + Återkalla ägarprivilegier Ta bort från gruppchatt Ta bort från kanal Kunde inte ändra tillhörigheten för %s + Förbjud från gruppchatt + Förbjud från kanal + Du försöker ta bort %s från en offentlig kanal. Det enda sättet att göra det är att förbjuda den användaren för alltid. Bannlys nu Kunde inte ändra rollen för %s + Privat gruppchattskonfiguration + Publik kanalkonfiguration Privat, medlemsskap krävs Gör XMPP-adresser synliga för alla + Gör kanalen modererad Du deltar ej + Ändrade gruppchattalternativ! + Det gick inte att ändra alternativ för gruppchatt Aldrig Tills vidare + Snooza + Svara Läsmarkera Input Skicka med enter + Använd Enter-tangenten för att skicka meddelandet. Du kan alltid använda Ctrl+Enter för att skicka meddelandet, även om det här alternativet är inaktiverat. Visa enter-knappen Byt ut emoticons-tangenten mot en enter-tangent ljud video bild + vektorgrafik PDF-dokument Android-app Kontakt @@ -334,8 +431,11 @@ Låt dina kontakter veta när du skriver meddelande till dem Skicka position Visa position + Ingen applikation hittades för att visa platsdata Position Konversation stängd + Lämnade privat gruppchatt + Lämnade publik kanal Lita inte på systemets CAs Alla certifikat måste manuellt godkännas Ta bort certifikat @@ -348,11 +448,15 @@ %d certifikat borttaget %d certifikat borttagna
+ Ersätt \"Skicka\"-knappen med snabbåtgärd Snabbfunktion Ingen Senast använd Välj snabbfunktion + Sök kontakter + Sök bokmärken Skicka privat meddelande + %1$s har lämnat gruppchatten Användarnamn Användarnamn Inte ett giltigt användanamn @@ -362,16 +466,28 @@ Nerladdning gick fel: Kunde inte skriva fil Tor-nätverk ej tillgängligt Bind-fel + Den här servern ansvarar inte för den här domänen Sönder Tillgänglighet + Frånvarande när enheten är låst + Visa som frånvarande när enheten är låst + Upptagen i ljudlöst läge + Visa som Upptagen i ljudlöst läge Hantera vibrationsläge som tyst läge + Visa som Upptagen när enheten är satt på att endast vibrera Utökade anslutningsinställningar Visa val av servernamn och port vid inställning av konto xmpp.example.com + Logga in med certifikat + Det gick inte att analysera certifikatet Arkiveringsinställningar Arkiveringsinställningar på servern Hämtar arkiveringsinställningar, vänta... + Det gick inte att hämta arkiveringsinställningar + CAPTCHA behövs Skriv i texten från bilden ovan + Otillförlitlig certifikatkedja + XMPP-adressen matchar inte certifikatet Förnya certifikat Misslyckades med att hämta OMEMO-nyckel! Verifierade OMEMO-nyckel med certifikat! @@ -381,6 +497,7 @@ Tunnla alla anslutningar genom Tor-nätverket. Kräver Orbot Servernamn Port + Server- eller .onion-adress Inte ett giltigt portnummer Inte ett giltigt servernamn %1$d av %2$d konton anslutna @@ -389,20 +506,36 @@ %d meddelanden Ladda fler meddelanden + Fil delad med %s + Bild delad med %s + Bilder som delats med %s + Text som delats med %s + Ge %1$s åtkomst till extern lagring + Ge %1$s åtkomst till kameran Synkronisera med kontakter + %1$s vill ha behörighet att komma åt din adressbok för att matcha den med din XMPP-kontaktlista.\nDetta visar dina kontakters fullständiga namn och visningsbilder.\n\n%1$s kommer bara att läsa din adressbok och matcha den lokalt, utan att ladda upp något till din server. +
Vi kommer inte att lagra någon kopia av dessa telefonnummer.\n\nLäs vår integritetspolicy för mer information.

Du kommer nu bli ombedd att ge åtkomst till dina kontakter.]]>
Notifiera för alla meddelanden + Notis endast vid omnämnande Notifieringar deaktiverade Notifieringar pausade Bildkomprimering + Tips: Använd \"Välj fil\" istället för \"Välj bild\" för att skicka enskilda bilder okomprimerade, oavsett denna inställning. Alltid + Endast stora bilder Batterioptimeringar aktiverade + Din enhet använder kraftiga batterioptimeringar för %1$s, vilket kan leda till försenade aviseringar eller till och med förlust av meddelanden.\nVi rekommenderar att du inaktiverar dem. + Din enhet använder kraftiga batterioptimeringar för %1$s, vilket kan leda till försenade aviseringar eller till och med förlust av meddelanden.\nDu kommer nu att bli ombedd att inaktivera dem. Deaktivera The valda området är för stort (Inget konto aktiverat) Detta fält måste fyllas i Korrigera meddelanden Skicka korrigerat meddelande + Du har redan validerat den här personens fingeravtryck säkert för att bekräfta förtroendet. Genom att välja \"Klar\" bekräftar du bara att %s är en del av den här gruppchatten. Du har deaktiverat detta konto + Säkerhetsfel: Ogiltig filåtkomst! + Ingen applikation hittades för att dela URI Dela URI med... Din fullständiga XMPP-adress kommer att vara: %s Skapa konto @@ -429,8 +562,11 @@ Tema Välj färgschema Automatisk + Ljus + Mörk Grön bakgrund Använd grön bakgrund för mottagna meddelanden + Det gick inte att ansluta till OpenKeychain Denna enhet används inte längre Dator Mobiltelefon @@ -438,20 +574,26 @@ Webbläsare Konsoll Betalning krävs + Ge behörighet till att använda Internet Jag Kontakt ber om tillgänglighetsuppdateringar Tillåt Saknar rättigheter för access till %s Fjärrserver hittas inte + Timeout för fjärrserver Kunde inte uppdatera konto + Rapportera den här XMPP-adressen för spam. Ta bort OMEMO identiteter + Återskapa dina OMEMO-nycklar. Alla dina kontakter måste verifiera dig igen. Använd endast det här som en sista utväg. Ta bort valda nycklar Du måste vara ansluten för att publicera din avatarbild Visa felmeddelande Felmeddelande Databesparing + Det gick inte att skapa en tillfällig fil Denna enhet har verifierats Kopiera fingeravtryck + Du har verifierat alla OMEMO-nycklar i din ägo Streckkoden innehåller inte fingeravtryck för denna konversation. Verifierade fingeravtryck Använd kameran för att scanna en kontakts streckkod @@ -460,8 +602,11 @@ Dela som XMPP URI Dela som HTTP länk Blint förtroende före verifiering + Lita på nya enheter från icke-verifierade kontakter, men begär manuell bekräftelse av nya enheter för verifierade kontakter. + Att blint lita på OMEMO-nycklar, innebär att det skulle kunna vara någon annan eller att någon annan har fått åtkomst. Ej betrodd Ogiltig 2D-streckkod + Töm cache-mapp (används av kameraapplikationen) Rensa cache Rensa private lagring Rensa privat lagring där filer lagras (De kan om-laddas från servern) @@ -500,7 +645,7 @@ Krypterar meddelande Hämtar inte meddelanden på grund av inställningen för borttagning av gamla meddelanden. Komprimerar video - Motsvarande konversationer är stängda. + Korresponderande konversationer är stängda. Kontakt blockerad. Notifieringar från främlingar Mottagna meddelanden från främlingar @@ -509,6 +654,10 @@ online just nu Försök dekryptera igen Sessionsfel + Öppna webbsida + Ingen applikation hittades för att kunna öppna webbsidan + Se upp-notifikationer + Visa se upp-notifikationer Idag Igår Bekräfta värdnamn med DNSSEC @@ -531,6 +680,7 @@ Visa plats Dela Var god dröj... + Söka i meddelanden GIF Kopiera XMPP-adress Smeknamn @@ -557,7 +707,10 @@ e-bok Öppna med... Välj konto + Återställa säkerhetskopiering + Återställa Ange ditt lösenord till kontot %s för att återställa säkerhetskopian. + Det gick inte att återställa säkerhetskopian. Skapa gruppchatt Skapa sluten gruppchatt Kanalnamn @@ -584,4 +737,7 @@ Om Aktivera ett konto Upptagen + Fäst flik till toppen + Ta bort flik från toppen + Fler alternativ diff --git a/src/main/res/values-tr-rTR/strings.xml b/src/main/res/values-tr-rTR/strings.xml index 3460ee2934b2527d15d5ae2f04ecada1f723275f..72094ced9684ff3211ab65813a90dfe4ed445b3a 100644 --- a/src/main/res/values-tr-rTR/strings.xml +++ b/src/main/res/values-tr-rTR/strings.xml @@ -132,6 +132,8 @@ Yığın izi göndererek gelişime yardımcı oluyorsunuz. İletileri onayla Onların iletilerini aldığınızda ve okuduğunuzda, kişilerinizin bunu bilmesini sağlayın + Ekran görüntülerini engelle + Uygulama anahtarlayıcısında uygulama içeriklerini sakla ve ekran görüntülerini engelle Arabirim OpenKeychain bir hata verdi. Kötü anahar şifrelemesi. @@ -150,6 +152,7 @@ Dosya bulunamadı Genel G/Ç hatası. Depolama yeri kalmamış olabilir mi? Bu görüntüyü seçmekte kullandığınız uygulama, dosyanın okunması için yeterli izinleri sağlayamadı. Görüntüyü seçmek için farklı bir dosya yöneticisi kullan. + Bu dosyayı paylaşmakta kullandığınız uygulama yeterince yetki sağlamamaktadır. Bilinmeyen Geçici olarak devre dışı Çevrim içi @@ -164,6 +167,7 @@ Hesap, sunucu tarafından desteklenmiyor. Geçersiz hesak simgesi TLS uzlaşması başarısız + Alan adı doğrulanamıyor Politika ihlali Sunucu uyuşmazlığı Akış hatası @@ -412,6 +416,7 @@ ses video görüntü + Vektör grafik PDF belgesi Android uygulaması Kişi @@ -910,6 +915,7 @@ Bağlantı kesildi Geri çekilmiş arama Uygulama hatası + Doğrulama sorunu Çağrıyı sonlandır Devam eden arama Deaam eden görüntülü arama @@ -959,4 +965,8 @@ Davet iletilemedi Sunucu, davet oluşturulmasını desteklemiyor Bu özelliği destekleyen aktif bir hesap yok - + Yedekleme başlatıldı. Tamamlandığı zaman bir bildirim alacaksınız. + Video etkinleştirilemedi + Düz metin dosyası + + diff --git a/src/main/res/values-vi/strings.xml b/src/main/res/values-vi/strings.xml index deebac8f8a79ac086533ee8e1b510ec38bbebc71..9fef998b039213ac67c19d8fc01dcacc82a07aaf 100644 --- a/src/main/res/values-vi/strings.xml +++ b/src/main/res/values-vi/strings.xml @@ -129,6 +129,8 @@ Bằng việc gửi báo cáo hoạt động, bạn đang hỗ trợ sự phát triển Xác nhận tin nhắn Báo cho liên hệ của bạn biết khi bạn đã nhận và đọc tin nhắn + Ngăn chặn chụp màn hình + Ẩn nội dung ứng dụng trong màn hình chuyển ứng dụng và chặn chụp màn hình UI OpenKeychain đã có lỗi. Mã khoá mã hoá bị lỗi. @@ -411,6 +413,7 @@ âm thanh video hình ảnh + hình ảnh véc tơ tài liệu PDF Ứng dụng Android Liên hệ @@ -504,7 +507,7 @@ Đã chia sẻ các hình ảnh với %s Đã chia sẻ văn bản với %s Cấp quyền truy cập bộ nhớ cho %1$s - Cấp quyền truy cập camera cho %1$s + Cấp quyền truy cập máy ảnh cho %1$s Đồng bộ với danh bạ %1$s muốn quyền truy cập sổ địa chỉ của bạn để nối nó với danh sách liên hệ XMPP của bạn.\nViệc này sẽ hiển thị họ tên và ảnh đại diện của các liên hệ của bạn.\n\n%1$s sẽ chỉ đọc sổ địa chỉ của bạn và nối nó một cách cục bộ mà không tải gì cả lên máy chủ của bạn.
Chúng tôi sẽ không lưu trữ bản sao của các số điện thoại đó.\n\nĐể biết thêm thông tin hãy đọc chính sách riêng tư của chúng tôi.

Bây giờ bạn sẽ được hỏi cấp quyền truy cập danh bạ.]]>
@@ -530,11 +533,427 @@ Lỗi bảo mật: Truy cập tệp không hợp lệ! Không tìm thấy ứng dụng nào để chia sẻ URI Chia sẻ URI với... +
Bạn đăng ký bằng số điện thoại của bạn và Quicksy sẽ tự động—dựa trên những số điện thoại trong sổ địa chỉ của bạn—đề xuất các liên hệ có thể có cho bạn.

Bằng cách đăng ký, bạn đồng ý với chính sách riêng tư của chúng tôi.]]>
Đồng ý và tiếp tục + Một hướng dẫn đã được thiết lập cho việc tạo tài khoản trên conversations.im.¹\nKhi chọn conversations.im làm 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. Địa chỉ XMPP đầy đủ của bạn sẽ là: %s Tạo tài khoản + Dùng nhà cung cấp của tôi + Hãy chọn tên người dùng + Quản lý tính khả dụng thủ công + Đặt tính khả dụng của bạn khi chỉnh sửa thông báo trạng thái của bạn. + Thông báo trạng thái + Rảnh để trò chuyện Trực tuyến + Vắng mặt + Không khả dụng + Bận + Một mật khẩu bảo mật đã được tạo + Thiết bị của bạn không hỗ trợ tắt tối ưu hoá pin + Đăng ký thất bại: Hãy thử lại sau + Đăng ký thất bại: Mật khẩu quá yếu + Chọn các thành viên + Tạo nhóm chat... + Mời lại Tắt + Ngắn + Vừa + Dài + Sử dụng truyền phát + Cho các liên hệ của bạn biết bạn dùng Conversations + Riêng tư + Chủ đề + Chọn bộ màu sáng + Tự động + Sáng + Tối + Nền xanh lá cây + Dùng nền xanh lá cây cho tin nhắn nhận được + Không thể kết nối với OpenKeychain + Thiết bị này không còn được dùng nữa + Máy tính + Điện thoại di động + Máy tính bảng + Trình duyệt web + Bảng điều khiển + Yêu cầu thanh toán + Cho phép sử dụng Internet + Tôi + Liên hệ yêu cầu đăng ký sự có mặt + Cho phép + Không có quyền truy cập%s + Không tìm thấy máy chủ trên mạng + Hết thời gian chờ cho máy chủ trên mạng + Không thể cập nhật tài khoản + Báo cáo địa chỉ XMPP này vì spam. + Xoá các danh tính OMEMO + Tái tạo lại các mã khoá OMEMO của bạn. Tất cả các liên hệ của bạn sẽ phải xác minh lại bạn. Chỉ sử dụng việc này làm giải pháp cuối cùng. + Xoá các mã khoá đã chọn + Bạn cần phải có kết nối để xuất bản ảnh đại diện của bạn. + Hiện thông báo lỗi + Thông báo lỗi + Trình tiết kiệm dữ liệu đang bật + Hệ điều hành của bạn đang giới hạn %1$s truy cập Internet trong nền. Để nhận các thông báo tin nhắn mới, bạn nên cho phép %1$s truy cập không giới hạn khi \"Trình tiết kiệm dữ liệu\" đang bật.\n%1$s vẫn sẽ nỗ lực tiết kiệm dữ liệu khi có thể. + Thiết bị của bạn không hỗ trợ việc tắt Trình tiết kiệm dữ liệu cho %1$s. + Không thể tạo tệp tạm + Thiết bị này đã được xác thực + Sao chép mã vân tay + Bạn đã xác minh tất cả mã khoá OMEMO mà bạn đang sở hữu + Mã vạch không chứa mã vân tay cho cuộc trò chuyện này. + Mã vân tay đã xác minh + Sử dụng máy ảnh để quét mã vạch của liên hệ + Vui lòng đợi để lấy các mã khoá + Chia sẻ dưới dạng mã vạch + Chia sẻ dưới dạng URI XMPP + Chia sẻ dưới dạng liên kết HTTP + Tin tưởng mù quáng trước khi xác minh + Tin tưởng các thiết bị mới từ các liên hệ chưa xác minh, nhưng hỏi xác nhận thủ công các thiết bị mới từ các liên hệ đã xác minh. + Các mã khoá OMEMO đã tin tưởng mù quáng, có nghĩa là họ có thể là một ai đó khác hoặc ai đó có thể đã can thiệp. + Chưa tin tưởng + Mã vạch 2D không hợp lệ + Dọn dẹp thư mục bộ nhớ tạm (được ứng dụng máy ảnh sử dụng) + Dọn dẹp bộ nhớ tạm + Dọn dẹp bộ nhớ riêng + Dọn dẹp bộ nhớ riêng nơi các tệp được giữ (Chúng có thể được tải xuống lại từ máy chủ) + Tôi đã đi theo liên kết này từ một nguồn được tin tưởng + Bạn sắp xác minh các mã khoá OMEMO của %1$s sau khi nhấn vào một liên kết. Việc này chỉ là bảo mật nếu bạn đã đi theo liên kết này từ một nguồn được tin tưởng, nơi chỉ có %2$s có thể đã xuất bản liên kết này. + Xác minh các mã khoá OMEMO + Hiện không hoạt động + Ẩn không hoạt động + Huỷ tin tưởng thiết bị + Bạn có chắc bạn muốn bỏ xác minh thiết bị này không?\nThiết bị này và các tin nhắn từ nỏ sẽ được đánh dấu là \"Chưa tin tưởng\". + + %d giây + + + %d phút + + + %d giờ + + + %d ngày + + + %d tuần + + + %d tháng + + Tự động xoá tin nhắn + Tự động xoá các tin nhắn cũ hơn phạm vi thời gian được thiết lập khỏi thiết bị. + Đang mã hoá tin nhắn + Không lấy tin nhắn do khoảng thời gian giữ lại cục bộ. + Đang nén video + Đã đóng các cuộc hội thoại tương ứng. + Đã chặn liên hệ. + Thông báo từ người lạ + Thông báo về các tin nhắn và cuộc gọi được nhận từ những người lạ. + Đã nhận tin nhắn từ người lạ + Chặn người lạ + Chặn toàn bộ miền + trực tuyến ngay lúc này + Thử giải mã lại + Lỗi phiên làm việc + Cơ chế SASL đã bị hạ cấp + Máy chủ yêu cầu đăng ký trên trang web + Mở trang web + Không tìm thấy ứng dụng nào để mở trang web + Thông báo gây chú ý + Hiện thông báo gây chú ý + Hôm nay + Hôm qua + Xác thực tên máy chủ bằng DNSSEC + Các chứng chỉ máy chủ chứa tên miền được xác thực được coi là đã xác minh + Chứng chỉ không chứa địa chỉ XMPP hợp lệ + một phần + Ghi video + Sao chép vào bộ nhớ tạm Đã chép tin nhắn vào clipboard + Tin nhắn + Tin nhắn riêng tư bị tắt + Ứng dụng được bảo vệ + Để tiếp tục nhận các thông báo, kể cả khi màn hình đã tắt, bạn cần thêm Conversations vào danh sách các ứng dụng được bảo vệ. + Chấp nhận chứng chỉ không xác định? + Chứng chỉ máy chủ này không được một người có quyền chứng chỉ đã biết ký. + Chấp nhận tên máy chủ không khớp? + Máy chủ không thể xác thực với tư cách \"%s\". Chứng chỉ chỉ hợp lệ cho: + Bạn có muốn vẫn kết nối không? + Chi tiết chứng chỉ: + Một lần + Trình quét mã QR cần quyền truy cập máy ảnh + Cuộn xuống dưới cùng + Cuộn xuống sau khi gửi một tin nhắn + Chỉnh sửa thông báo trạng thái + Chỉnh sửa thông báo trạng thái + Tắt mã hoá + %1$s không thể gửi tin nhắn được mã hoá đến %2$s. Điều này có thể là do liên hệ của bạn sử dụng một máy chủ hoặc ứng dụng khách lỗi thời không thể xử lý OMEMO. + Không thể lấy danh sách thiết bị + Không thể lấy mã khoá mã hoá + Gợi ý: Trong một số trường hợp, điều này có thể được sửa bằng cách thêm lẫn nhau vào danh sách liên hệ của bạn. + Bạn có chắc bạn muốn tắt mã hoá OMEMO cho cuộc hội thoại này không?\nViệc này sẽ cho phép quản trị viên máy chủ đọc các tin nhắn của bạn, nhưng việc này có thể là cách duy nhất để giao tiếp với những người sử dụng các ứng dụng khách lỗi thời. + Tắt ngay + Bản nháp: + Mã hoá OMEMO + OMEMO sẽ luôn được sử dụng cho các cuộc trò chuyện nhóm một đối một và riêng tư. + OMEMO sẽ được sử dụng theo mặc định cho các cuộc hội thoại mới. + OMEMO sẽ phải được bật một cách rõ ràng cho các cuộc hội thoại mới. + Tạo lối tắt + Cỡ chữ + Cỡ chữ tương đối được sử dụng trong ứng dụng. + Bật theo mặc định + Tắt theo mặc định + Nhỏ + Trung bình + Lớn + Tin nhắn đã không được mã hoá cho thiết bị này. + Giải mã tin nhắn OMEMO thất bại. + hoàn tác + Chia sẻ vị trí bị tắt + Cố định vị trí + Bỏ cố định vị trí + Sao chép vị trí + Chia sẻ vị trí + Hướng + Chia sẻ vị trí Hiện vị trí - + Chia sẻ + Không thể bắt đầu ghi lại + Vui lòng đợi... + Cấp quyền truy cập micro cho %1$s + Tìm kiếm tin nhắn + GIF + Xem cuộc hội thoại + Chia sẻ plugin vị trí + Sử dụng plugin chia sẻ vị trí thay vì bản đồ được tích hợp + Sao chép địa chỉ web + Sao chép địa chỉ XMPP + Chia sẻ tệp HTTP cho S3 + Tìm kiếm trực tiếp + Tại màn hình \'Bắt đầu cuộc hội thoại\', mở bàn phím và đặt con trỏ trong trường tìm kiếm + Ảnh đại diện cuộc trò chuyện nhóm + Máy chủ không hỗ trợ ảnh đại diện cuộc trò chuyện nhóm + Chỉ có chủ sở hữu mới có thể thay đổi ảnh đại diện cuộc trò chuyện nhóm + Tên liên hệ + Biệt danh + Tên + Việc cung cấp tên là không bắt buộc + Tên cuộc trò chuyện nhóm + Cuộc trò chuyện nhóm này đã bị phá huỷ + Không thể lưu bản ghi + Dịch vụ ở trước + Hạng mục thông báo này được sử dụng để hiển thị một thông báo vĩnh viễn chỉ ra rằng %1$s đang chạy. + Thông tin trạng thái + Vấn đề kết nối + Hạng mục thông báo này được sử dụng để hiển thị một thông báo trong trường hợp có vấn đề khi kết nối đến một tài khoản. + Tin nhắn + Cuộc gọi + Tin nhắn + Cuộc gọi đến + Cuộc gọi đang diễn ra + Tin nhắn im lặng + Nhóm thông báo này được sử dụng để hiển thị các thông báo không nên phát ra tiếng động. Ví dụ là khi đang hoạt động trên một thiết bị khác (thời gian ân hạn). + Gửi đi thất bại + Cài đặt thông báo tin nhắn + Cài đặt thông báo cuộc gọi đến + Sự quan trọng, âm thanh, rung + Nén video + Xem phương tiện + Thành viên + Trình duyệt phương tiện + Tệp đã bị bỏ vì vi phạm bảo mật. + Chất lượng video + Chất lượng thấp hơn có nghĩa là tệp nhỏ hơn + Trung bình (360p) + Cao (720p) + đã huỷ + Bạn đã đang tạo bản nháp một tin nhắn rồi. + Tính năng chưa được thêm + Mã quốc gia không hợp lệ + Chọn quốc gia + số điện thoại + Xác minh số điện thoại của bạn + Quicksy sẽ gửi một tin nhắn SMS (có thể áp dụng phí nhà mạng) để xác minh số điện thoại của bạn. Hãy nhập mã quốc gia và số điện thoại của bạn: +
%s

Điều này có ổn không, hay bạn muốn chỉnh sửa số điện thoại?]]>
+ %s không phải là số điện thoại hợp lệ. + Vui lòng nhập số điện thoại của bạn. + Tìm kiếm quốc gia + Xác minh %s + %s.]]> + Chúng tôi đã gửi một SMS khác có mã 6 chữ số cho bạn. + Vui lòng nhập mã PIN 6 chữ số ở dưới. + Gửi lại SMS + Gửi lại SMS (%s) + Vui lòng đợi (%s) + quay lại + Đã tự động dán mã PIN có thể có từ bộ nhớ tạm. + Vui lòng nhập mã PIN 6 chữ số. + Bạn có chắc bạn muốn huỷ quá trình đăng ký không? + + Không + Đang xác minh... + Đang yêu cầu SMS... + Mã PIN bạn đã nhập không chính xác. + Mã PIN chúng tôi gửi cho bạn đã hết hạn. + Lỗi mạng không xác định. + Phản hồi không xác định từ máy chủ. + Không thể kết nối đến máy chủ. + Không thể lập kết nối bảo mật. + Không thể tìm máy chủ. + Có gì đó sai đã xảy ra khi xử lý yêu cầu của bạn. + Đầu vào người dùng không hợp lệ + Tạm thời không có sẵn. Hãy thử lại sau. + Không có kết nối mạng. + Vui lòng thử lại trong %s + Bạn bị giới hạn tốc độ + Quá nhiều lần thử + Bạn đang sử dụng một phiên bản lỗi thời của ứng dụng này. + Cập nhật + Số điện thoại này hiện đã được đăng nhập ở một thiết bị khác. + Vui lòng nhập tên của bạn để cho những người không có bạn trong sổ địa chỉ của họ biết bạn là ai. + Tên của bạn + Nhập tên của bạn + Sử dụng nút chỉnh sửa để đặt tên của bạn. + Từ chối yêu cầu + Cài đặt Orbot + Khởi động Orbot + Không có ứng dụng chợ nào được cài đặt. + Kênh này sẽ làm cho địa chỉ XMPP của bạn trở thành công khai + sách điện tử + Gốc (không nén) + Mở bằng... + Ảnh hồ sơ Conversations + Chọn tài khoản + Khôi phục bản sao lưu + Khôi phục + Nhập mật khẩu của bạn cho tài khoản %s để khôi phục bản sao lưu. + Đừng sử dụng tính năng khôi phục bản sao lưu để cố gắng nhân bản (chạy đồng thời) một lượt cài đặt. Việc khôi phục một bản sao lưu chỉ dành cho việc di cư hoặc trong trường hợp bạn đã mất thiết bị gốc. + Không thể khôi phục bản sao lưu. + Không thể giải mã bản sao lưu. Mật khẩu có đúng không? + Sao lưu & khôi phục + Nhập địa chỉ XMPP + Tạo cuộc trò chuyện nhóm + Tham gia kênh công khai + Tạo cuộc trò chuyện nhóm riêng tư + Tạo kênh công khai + Tên kênh + Địa chỉ XMPP + Vui lòng cung cấp tên cho kênh + Vui lòng cung cấp địa chỉ XMPP + Đây là một địa chỉ XMPP. Vui lòng cung cấp một cái tên. + Đang tạo kênh công khai... + Kênh này đã tồn tại + Bạn đã tham gia một kênh đang tồn tại + Không thể lưu thiết lập kênh + Cho phép bất kỳ ai chỉnh sửa chủ đề + Cho phép bất kỳ ai mời những người khác + Bất kỳ ai cũng có thể chỉnh sửa chủ đề. + Chủ sở hữu có thể chỉnh sửa chủ đề. + Quản trị viên có thể chỉnh sửa chủ đề. + Chủ sở hữu có thể mời những người khác. + Bất kỳ ai cũng có thể mời những người khác. + Các địa chỉ XMPP có thể được quản trị viên nhìn thấy. + Các địa chỉ XMPP có thể được bất kỳ ai nhìn thấy. + Kênh công khai này không có thành viên nào. Hãy mời các liên hệ của bạn hoặc sử dụng nút chia sẻ để phân phát địa chỉ XMPP của kênh. + Cuộc trò chuyện nhóm riêng tư này không có thành viên nào. + Quản lý đặc quyền + Tìm kiếm thành viên + Tệp quá lớn + Đính kèm + Khám phá các kênh + Tìm kiếm kênh + Sự vi phạm tính riêng tư có thể có! + search.jabber.network.

Việc sử dụng tính năng này sẽ truyền địa chỉ IP và câu từ tìm kiếm của bạn đến dịch vụ đó. Hãy xem Chính sách riêng tư của họ để biết thêm thông tin.]]>
+ Tôi đã có một tài khoản rồi + Thêm tài khoản đang tồn tại + Đăng ký tài khoản mới + Cái này trông giống một địa chỉ miền + Vẫn thêm + Cái này trông giống một địa chỉ kênh + Chia sẻ tệp sao lưu + Bản sao lưu Conversations + Sự kiện + Mở bản sao lưu + Tệp bạn đã chọn không phải là tệp sao lưu của Conversations + Tài khoản này đã được thiết lập rồi + Vui lòng nhập mật khẩu cho tài khoản này + Không thể thực hiện hành động này + Tham gia kênh công khai... + Ứng dụng chia sẻ đã không cấp quyền truy cập tệp này. + + jabber.network + Máy chủ cục bộ + Đa số người dùng nên chọn \'jabber.network\' để có những đề xuất tốt hơn từ toàn thể hệ sinh thái XMPP. + Phương pháp khám phá kênh + Sao lưu + Giới thiệu + Vui lòng bật một tài khoản + Tạo cuộc gọi + Cuộc gọi đến + Cuộc gọi video đến + Đang kết nối + Đã kết nối + Đang chấp nhận cuộc gọi + Đang kết thúc cuộc gọi + Trả lời + Từ chối + Đang khám phá các thiết bị + Đang đổ chuông + Bận + Không thể kết nối cuộc gọi + Đã mất kết nối + Cuộc gọi đã bị rút lại + Lỗi ứng dụng + Vấn đề xác minh + Cúp máy + Cuộc gọi đang diễn ra + Cuộc gọi video đang diễn ra + Tắt Tor để tạo cuộc gọi + Cuộc gọi đến + Cuộc gọi đến · %s + Cuộc gọi nhỡ · %s + Cuộc gọi đi + Cuộc gọi đi · %s + Cuộc gọi nhỡ + Cuộc gọi âm thanh + Cuộc gọi video + Trợ giúp + Chuyển sang cuộc hội thoại + Micro của bạn không có sẵn + Bạn chỉ có thể có một cuộc gọi trong một lúc. + Quay lại cuộc gọi đang diễn ra + Không thể chuyển máy ảnh + Ghim lên đầu + Bỏ ghim khỏi đầu + Tuyến đường GPS + Không thể sửa tin nhắn + Tất cả cuộc hội thoại + Cuộc hội thoại này + Ảnh đại diện của bạn + Ảnh đại diện cho %s + Được mã hoá bằng OMEMO + Được mã hoá bằng OpenPGP + Không được mã hoá + Thoát + Ghi lại tin nhắn thoại + Phát âm thanh + Tạm dừng âm thanh + Thêm liên hệ, tạo hoặc tham gia cuộc trò chuyện nhóm, hoặc khám phá các kênh + + Xem %1$d thành viên + + + Một số tin nhắn không thể được gửi + + Gửi đi thất bại + Thêm tuỳ chọn + Không tìm thấy ứng dụng nào + Mời vào Conversations + Không thể xử lý lời mời + Máy chủ không hỗ trợ tạo lời mời + Không có tài khoản đang hoạt động nào hỗ trợ tính năng này + Việc sao lưu đã được bắt đầu. Bạn sẽ nhận một thông báo khi việc đó đã hoàn tất. + Không thể bật video. + Tài liệu văn bản thuần + + diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 4ef9ecc32893138af264e1e8a4b4d56107a48a4c..b3dcf959d3bd3bdc997990ced5c15dcf919e3460 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -129,6 +129,8 @@ 通过发送堆栈跟踪,您可以帮助Conversations持续发展 确认消息 让对方知道你收到并阅读了他们的消息 + 防止截屏 + 在应用切换中隐藏应用程序内容并阻止截图 用户界面 OpenKeychain报告一个错误。 错误的密钥 @@ -411,6 +413,7 @@ 音频 视频 图片 + 矢量图 PDF文档 Android App 联系人 @@ -901,6 +904,7 @@ 连接丢失 通话已撤销 程序错误 + 验证问题 挂断 正在进行的通话 正在进行的视频通话 @@ -950,4 +954,6 @@ 没有活跃帐户支持此功能 已启动备份。一旦完成,你会收到通知。 无法启用视频 + 纯文本文档 + diff --git a/src/main/res/values/defaults.xml b/src/main/res/values/defaults.xml index a031a9149a4c2a2330cf5cc5698b7985a54ddd73..60085d0f9bb0c32636d8d429f15c429c1b43464b 100644 --- a/src/main/res/values/defaults.xml +++ b/src/main/res/values/defaults.xml @@ -44,4 +44,5 @@ false 360 JABBER_NETWORK + false diff --git a/src/main/res/values/dimens.xml b/src/main/res/values/dimens.xml index 4bcfddaa148ee55c5dacec0ae83103b447e10f64..16a8cef1ac844d79e10e7aab61292cc6f4f4a8d6 100644 --- a/src/main/res/values/dimens.xml +++ b/src/main/res/values/dimens.xml @@ -45,12 +45,10 @@ 96dp 24dp - 30sp 12sp 8dp 16dp 4dp 20sp - diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index d8dba55886ee9f22bc183a4a88d4cc5dbfd5d849..5ad945e6f25d8b903419df06af6b596c02256e5d 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -131,6 +131,8 @@ By sending in stack traces you are helping the development Confirm Messages Let your contacts know when you have received and read their messages + Prevent Screenshots + Hide app contents in the app switcher and block screenshots UI OpenKeychain produced an error. Bad key for encryption. @@ -621,6 +623,8 @@ Clean private storage where files are kept (They can be re-downloaded from the server) I followed this link from a trusted source You are about to verify the OMEMO keys of %1$s after clicking a link. This is only secure if you followed this link from a trusted source where only %2$s could have published this link. + You are about to verify the OMEMO keys of your own account. This is only secure if you followed this link from a trusted source where only you could have published this link. + Continue Verify OMEMO keys Show inactive Hide inactive @@ -903,6 +907,7 @@ Incoming video call Connecting Connected + Reconnecting Accepting call Ending call Answer @@ -918,6 +923,8 @@ Hang up Ongoing call Ongoing video call + Reconnecting call + Reconnecting video call Disable Tor to make calls Incoming call Incoming call · %s @@ -967,4 +974,7 @@ The backup has been started. You’ll get a notification once it has been completed. Unable to enable video. Plain text document + Account registrations are not supported + No XMPP address found + diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml index 7f70736025917022f7bc80e4daf330b82ca5b75e..05b2841147610db013835c9a7e1b46a89f11e4bb 100644 --- a/src/main/res/values/styles.xml +++ b/src/main/res/values/styles.xml @@ -160,8 +160,6 @@ @color/white70 - - - - \ No newline at end of file + diff --git a/src/main/res/xml/backup_content.xml b/src/main/res/xml/backup_content.xml new file mode 100644 index 0000000000000000000000000000000000000000..863e280e4ecf68802ad7e1ede1e93008bbc8797c --- /dev/null +++ b/src/main/res/xml/backup_content.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml index 99f9e83abb09cc3839dc0ffafbdcd39a985819c9..91b07210c55fd00bce03a02562b94dc77714ec76 100644 --- a/src/main/res/xml/preferences.xml +++ b/src/main/res/xml/preferences.xml @@ -32,6 +32,13 @@ android:key="last_activity" android:summary="@string/pref_broadcast_last_activity_summary" android:title="@string/pref_broadcast_last_activity" /> + + + contacts = new HashMap<>(); - while (cursor != null && cursor.moveToNext()) { - try { - final PhoneNumberContact contact = new PhoneNumberContact(context, cursor); - final PhoneNumberContact preexisting = contacts.get(contact.getPhoneNumber()); - if (preexisting == null || preexisting.rating() < contact.rating()) { - contacts.put(contact.getPhoneNumber(), contact); + try (final Cursor cursor = context.getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, PROJECTION, null, null, null)){ + while (cursor != null && cursor.moveToNext()) { + try { + final PhoneNumberContact contact = new PhoneNumberContact(context, cursor); + final PhoneNumberContact preexisting = contacts.get(contact.getPhoneNumber()); + if (preexisting == null || preexisting.rating() < contact.rating()) { + contacts.put(contact.getPhoneNumber(), contact); + } + } catch (final IllegalArgumentException ignored) { + } - } catch (final IllegalArgumentException e) { - Log.d(Config.LOGTAG, e.getMessage()); } - } - if (cursor != null) { - cursor.close(); + } catch (final Exception e) { + return ImmutableMap.of(); } return ImmutableMap.copyOf(contacts); } + public static PhoneNumberContact findByUriOrNumber(Collection haystack, Uri uri, String number) { + final PhoneNumberContact byUri = findByUri(haystack, uri); + return byUri != null || number == null ? byUri : findByNumber(haystack, number); + } + public static PhoneNumberContact findByUri(Collection haystack, Uri needle) { for (PhoneNumberContact contact : haystack) { if (needle.equals(contact.getLookupUri())) { @@ -78,4 +78,13 @@ public class PhoneNumberContact extends AbstractPhoneContact { } return null; } + + private static PhoneNumberContact findByNumber(Collection haystack, String needle) { + for (PhoneNumberContact contact : haystack) { + if (needle.equals(contact.getPhoneNumber())) { + return contact; + } + } + return null; + } } diff --git a/src/quicksy/java/eu/siacs/conversations/entities/Entry.java b/src/quicksy/java/eu/siacs/conversations/entities/Entry.java index 6ff19f09d9303b240410c1e9e891baf2fa6e990a..c202be4707a0344143a3e6be3553e062aaa7d33d 100644 --- a/src/quicksy/java/eu/siacs/conversations/entities/Entry.java +++ b/src/quicksy/java/eu/siacs/conversations/entities/Entry.java @@ -2,6 +2,9 @@ package eu.siacs.conversations.entities; import android.util.Base64; +import com.google.common.base.Charsets; +import com.google.common.hash.Hashing; + import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; @@ -63,20 +66,15 @@ public class Entry implements Comparable { builder.append(jid.asBareJid().toEscapedString()); } } - MessageDigest md; - try { - md = MessageDigest.getInstance("SHA-1"); - } catch (NoSuchAlgorithmException e) { - return ""; - } - byte[] sha1 = md.digest(builder.toString().getBytes()); + @SuppressWarnings("deprecation") + final byte[] sha1 = Hashing.sha1().hashString(builder.toString(), Charsets.UTF_8).asBytes(); return new String(Base64.encode(sha1, Base64.DEFAULT)).trim(); } private static List ofPhoneNumberContactsAndContacts(final Collection phoneNumberContacts, Collection systemContacts) { - ArrayList entries = new ArrayList<>(); + final ArrayList entries = new ArrayList<>(); for(Contact contact : systemContacts) { - PhoneNumberContact phoneNumberContact = PhoneNumberContact.findByUri(phoneNumberContacts, contact.getSystemAccount()); + final PhoneNumberContact phoneNumberContact = PhoneNumberContact.findByUri(phoneNumberContacts, contact.getSystemAccount()); if (phoneNumberContact != null && phoneNumberContact.getPhoneNumber() != null) { Entry entry = findOrCreateByPhoneNumber(entries, phoneNumberContact.getPhoneNumber()); entry.jids.add(contact.getJid().asBareJid()); diff --git a/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java b/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java index f977758f2ae547e0a9a9953d16370378379c6544..14a2c1734cfb8fcd013165f57de222958e3bb49b 100644 --- a/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java +++ b/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java @@ -382,9 +382,13 @@ public class QuickConversationsService extends AbstractQuickConversationsService if (uri == null) { continue; } - PhoneNumberContact phoneNumberContact = PhoneNumberContact.findByUri(contacts, uri); + final String number = getNumber(contact); + final PhoneNumberContact phoneNumberContact = PhoneNumberContact.findByUriOrNumber(contacts, uri, number); final boolean needsCacheClean; if (phoneNumberContact != null) { + if (!uri.equals(phoneNumberContact.getLookupUri())) { + Log.d(Config.LOGTAG, "lookupUri has changed from " + uri + " to " + phoneNumberContact.getLookupUri()); + } needsCacheClean = contact.setPhoneContact(phoneNumberContact); } else { needsCacheClean = contact.unsetPhoneContact(PhoneNumberContact.class); @@ -396,6 +400,14 @@ public class QuickConversationsService extends AbstractQuickConversationsService } } + private static String getNumber(final Contact contact) { + final Jid jid = contact.getJid(); + if (jid.getLocal() != null && Config.QUICKSY_DOMAIN.equals(jid.getDomain())) { + return jid.getLocal(); + } + return null; + } + private boolean considerSync(final Account account, final Map contacts, final boolean forced) { final int hash = contacts.keySet().hashCode(); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": consider sync of " + hash); diff --git a/src/quicksy/java/eu/siacs/conversations/ui/ChooseCountryActivity.java b/src/quicksy/java/eu/siacs/conversations/ui/ChooseCountryActivity.java index 3e1d39db02b0199e1b88eca0d944d808bd79dcb8..77c29e60347625f3f8362ce3eb2a82a93324ffc5 100644 --- a/src/quicksy/java/eu/siacs/conversations/ui/ChooseCountryActivity.java +++ b/src/quicksy/java/eu/siacs/conversations/ui/ChooseCountryActivity.java @@ -2,10 +2,7 @@ package eu.siacs.conversations.ui; import android.content.Context; import android.content.Intent; -import androidx.databinding.DataBindingUtil; import android.os.Bundle; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.appcompat.widget.Toolbar; import android.text.Editable; import android.text.TextWatcher; import android.view.Menu; @@ -15,6 +12,10 @@ import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.TextView; +import androidx.appcompat.widget.Toolbar; +import androidx.databinding.DataBindingUtil; +import androidx.recyclerview.widget.LinearLayoutManager; + import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -25,6 +26,7 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityChooseCountryBinding; import eu.siacs.conversations.ui.adapter.CountryAdapter; import eu.siacs.conversations.utils.PhoneNumberUtilWrapper; +import eu.siacs.conversations.utils.ThemeHelper; public class ChooseCountryActivity extends ActionBarActivity implements CountryAdapter.OnCountryClicked { @@ -70,16 +72,17 @@ public class ChooseCountryActivity extends ActionBarActivity implements CountryA return true; } }; - private TextView.OnEditorActionListener mSearchDone = (v, actionId, event) -> { - if (countries.size() == 1) { - onCountryClicked(countries.get(0)); - } - return true; - }; + private TextView.OnEditorActionListener mSearchDone = (v, actionId, event) -> { + if (countries.size() == 1) { + onCountryClicked(countries.get(0)); + } + return true; + }; @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setTheme(ThemeHelper.find(this)); this.binding = DataBindingUtil.setContentView(this, R.layout.activity_choose_country); setSupportActionBar((Toolbar) this.binding.toolbar); configureActionBar(getSupportActionBar()); @@ -115,9 +118,9 @@ public class ChooseCountryActivity extends ActionBarActivity implements CountryA private void filterCountries(String needle) { List countries = PhoneNumberUtilWrapper.getCountries(this); Iterator iterator = countries.iterator(); - while(iterator.hasNext()) { + while (iterator.hasNext()) { final PhoneNumberUtilWrapper.Country country = iterator.next(); - if(needle != null && !country.getName().toLowerCase(Locale.getDefault()).contains(needle.toLowerCase(Locale.getDefault()))) { + if (needle != null && !country.getName().toLowerCase(Locale.getDefault()).contains(needle.toLowerCase(Locale.getDefault()))) { iterator.remove(); } } diff --git a/src/quicksy/java/eu/siacs/conversations/ui/EnterNameActivity.java b/src/quicksy/java/eu/siacs/conversations/ui/EnterNameActivity.java index a15419eba5e054c29c83624ce7f91bd874f63d45..30acdd2ed7c5c72f21da4e7c5d8ea8da847cceb3 100644 --- a/src/quicksy/java/eu/siacs/conversations/ui/EnterNameActivity.java +++ b/src/quicksy/java/eu/siacs/conversations/ui/EnterNameActivity.java @@ -20,7 +20,7 @@ public class EnterNameActivity extends XmppActivity implements XmppConnectionSer private Account account; - private AtomicBoolean setNick = new AtomicBoolean(false); + private final AtomicBoolean setNick = new AtomicBoolean(false); @Override protected void onCreate(final Bundle savedInstanceState) { diff --git a/src/quicksy/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java b/src/quicksy/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java index e146ab3be249d290d87a34d33890c3a1efb4aa30..52f61a96b4980a56711aa745e828d738061bdce7 100644 --- a/src/quicksy/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java +++ b/src/quicksy/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java @@ -14,6 +14,8 @@ import android.view.KeyEvent; import android.view.View; import android.widget.EditText; +import org.jetbrains.annotations.NotNull; + import java.util.concurrent.atomic.AtomicBoolean; import eu.siacs.conversations.Config; @@ -126,7 +128,7 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve } @Override - public void onSaveInstanceState(Bundle savedInstanceState) { + public void onSaveInstanceState(@NotNull Bundle savedInstanceState) { if (this.region != null) { savedInstanceState.putString("region", this.region); } diff --git a/src/quicksy/java/eu/siacs/conversations/ui/drawable/TextDrawable.java b/src/quicksy/java/eu/siacs/conversations/ui/drawable/TextDrawable.java index 3b49962a601b2ef2c62976fbfbc019ebbe2a842e..39ec59235de20a6d4945e94289740e9ae4da1810 100644 --- a/src/quicksy/java/eu/siacs/conversations/ui/drawable/TextDrawable.java +++ b/src/quicksy/java/eu/siacs/conversations/ui/drawable/TextDrawable.java @@ -26,6 +26,8 @@ import android.widget.TextView; import java.lang.ref.WeakReference; +import eu.siacs.conversations.ui.util.StyledAttributes; + public class TextDrawable extends Drawable implements TextWatcher { private WeakReference ref; private String mText; @@ -62,6 +64,7 @@ public class TextDrawable extends Drawable implements TextWatcher { */ public TextDrawable(TextView tv, String initialText, boolean bindToViewsText, boolean bindToViewsPaint) { this(tv.getPaint(), initialText); + mPaint.setColor(StyledAttributes.getColor(tv.getContext(), android.R.attr.textColorPrimary)); ref = new WeakReference<>(tv); if (bindToViewsText || bindToViewsPaint) { if (bindToViewsText) { @@ -157,6 +160,10 @@ public class TextDrawable extends Drawable implements TextWatcher { setBounds(bounds); } + public Paint getPaint() { + return mPaint; + } + public void setPaint(Paint paint) { mPaint = new Paint(paint); //Since this can change the font used, we need to recalculate bounds. @@ -168,8 +175,8 @@ public class TextDrawable extends Drawable implements TextWatcher { invalidateSelf(); } - public Paint getPaint() { - return mPaint; + public String getText() { + return mText; } public void setText(String text) { @@ -183,10 +190,6 @@ public class TextDrawable extends Drawable implements TextWatcher { invalidateSelf(); } - public String getText() { - return mText; - } - @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { diff --git a/src/quicksy/res/layout/activity_enter_number.xml b/src/quicksy/res/layout/activity_enter_number.xml index d54ead3bfbf1074b27506dfa6f7b67dc0292cdec..b6898d8751980a4ed0d651879a2d71909c0c5a44 100644 --- a/src/quicksy/res/layout/activity_enter_number.xml +++ b/src/quicksy/res/layout/activity_enter_number.xml @@ -1,5 +1,6 @@ - + + طول الوقت الذي يبقى فيه كويكسي صامتا بعد رؤية النشاط في جهاز آخر + عبر إرسال أثار الأخطاء تقوم بالمساعدة في تطوير برمجة كويكسي إجعل كلّ جهات إتصالك تعلم أنك تستعمل كويكسي + للمواصلة في إستقبال التنبيهات، حتى والشاشة مغلقة، يجب عليك أن تضيف كويكسي إلى قائمة التطبيقات المحميّة. صورة حساب كويكسي إن كويكسي Quicksy غير متوفر في بلدكم. - + لا يمكن التأكد من خادم الهويّة. + خطأ أمني مجهول. + تجاوز الوقت أثناء الإتصال بالخادم. + diff --git a/src/quicksy/res/values-bg/strings.xml b/src/quicksy/res/values-bg/strings.xml index 1854c3e3c8f3365b331254a354772db63dbeca62..c41bf67c992b3938f3550656f89c68aa501f2ac7 100644 --- a/src/quicksy/res/values-bg/strings.xml +++ b/src/quicksy/res/values-bg/strings.xml @@ -3,7 +3,7 @@ Времето, през което Quicksy няма да прави нищо, след като забележи дейност на друго устройство Изпращайки проследявания на стека, Вие помагате за непрекъснатото развитие на Quicksy Така всичките Ви контакти ще знаят кога използвате Quicksy - Ако искате да продължите да получавате известия дори когато екранът е заключен, трябва да добавите „Quicksy“ към списъка от защитени приложения. + Ако искате да продължите да получавате известия дори когато екранът е заключен, трябва да добавите Quicksy към списъка със защитени приложения. Профилна снимка за Quicksy Quicksy не може да се използва във Вашата страна. Идентичността на сървъра не може да бъде потвърдена. diff --git a/src/quicksy/res/values-fi/strings.xml b/src/quicksy/res/values-fi/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..9a988a15db678f4e9df6f9968f143c15690945b1 --- /dev/null +++ b/src/quicksy/res/values-fi/strings.xml @@ -0,0 +1,12 @@ + + + Kuinka kauan Quicksy pysyy hiljaa nähtyään toisella laitteellasi toimintaa + Lähettämällä virheenkorjaustietoja autat Quicksyn kehittäjiä + Kerro kaikille yhteystiedoillesi kun käytät Quicksya + Saadaksesi ilmoituksia silloinkin kun näyttö on sammutettu, Quicksy pitää lisätä suojattujen sovellusten luetteloon. + Quicksy-profiilikuva + Quicksy ei ole saatavilla maassasi. + Palvelimen identiteetin varmennus epäonnistui. + Tuntematon turvallisuusvirhe. + Palvelimeen yhdistäminen aikakatkaistiin. + diff --git a/src/quicksy/res/values-sk/strings.xml b/src/quicksy/res/values-sk/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..df1b5a1e059e7bd7b309f933b7b22f523753a321 --- /dev/null +++ b/src/quicksy/res/values-sk/strings.xml @@ -0,0 +1,12 @@ + + + Doba, počas ktorej bude Quicksy stíšený po detekcii aktivity na inom zariadení. + Zaslaním detailov o dôvode zlyhania pomáhate ďalšiemu vývoju aplikácie Quicksy + Dajte svojim kontaktom vedieť, keď používate Quicksy + Aby ste dostávali oznámenia aj pri vypnutej obrazovke, pridajte Quicksy medzi chránené aplikácie. + Quicksy profilový obrázok + Quicksy nie je dostupné vo vašej krajine. + Nemôžem overiť identitu servera. + Neznáma bezpečnostná chyba. + Vypršal časový limit pri pripájaní k serveru. +