diff --git a/annotation-processor/build.gradle b/annotation-processor/build.gradle new file mode 100644 index 000000000..4518d0e5b --- /dev/null +++ b/annotation-processor/build.gradle @@ -0,0 +1,14 @@ +apply plugin: "java-library" + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} +dependencies { + + implementation project(':annotation') + + annotationProcessor 'com.google.auto.service:auto-service:1.0.1' + compileOnly 'com.google.auto.service:auto-service:1.0.1' + +} \ No newline at end of file diff --git a/libs/annotation-processor/src/main/java/im/conversations/android/annotation/processor/XmlElementProcessor.java b/annotation-processor/src/main/java/im/conversations/android/annotation/processor/XmlElementProcessor.java similarity index 100% rename from libs/annotation-processor/src/main/java/im/conversations/android/annotation/processor/XmlElementProcessor.java rename to annotation-processor/src/main/java/im/conversations/android/annotation/processor/XmlElementProcessor.java diff --git a/annotation/build.gradle b/annotation/build.gradle new file mode 100644 index 000000000..d5917e460 --- /dev/null +++ b/annotation/build.gradle @@ -0,0 +1,6 @@ +apply plugin: "java-library" + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} \ No newline at end of file diff --git a/libs/annotation/src/main/java/im/conversations/android/annotation/XmlElement.java b/annotation/src/main/java/im/conversations/android/annotation/XmlElement.java similarity index 100% rename from libs/annotation/src/main/java/im/conversations/android/annotation/XmlElement.java rename to annotation/src/main/java/im/conversations/android/annotation/XmlElement.java diff --git a/libs/annotation/src/main/java/im/conversations/android/annotation/XmlPackage.java b/annotation/src/main/java/im/conversations/android/annotation/XmlPackage.java similarity index 100% rename from libs/annotation/src/main/java/im/conversations/android/annotation/XmlPackage.java rename to annotation/src/main/java/im/conversations/android/annotation/XmlPackage.java diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 000000000..0981a4a1c --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,127 @@ +apply plugin: "com.android.application" +apply plugin: "androidx.navigation.safeargs" +apply plugin: "com.diffplug.spotless" + + +android { + namespace 'im.conversations.android' + compileSdk 33 + + defaultConfig { + minSdk 23 + targetSdk 33 + versionCode 1 + versionName "2.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + javaCompileOptions { + annotationProcessorOptions { + arguments += ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + coreLibraryDesugaringEnabled true + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + buildFeatures { + dataBinding true + } + flavorDimensions "product" + productFlavors { + quicksy { + dimension "product" + applicationId = "im.quicksy.client" + + def appName = "Quicksy" + + resValue "string", "applicationId", applicationId + resValue "string", "app_name", appName + buildConfigField "String", "APP_NAME", "\"$appName\"" + } + monocleschat { + dimension "product" + applicationId "eu.monocles.chat" + + def appName = "monocles chat" + + resValue "string", "applicationId", applicationId + resValue "string", "app_name", appName + buildConfigField "String", "APP_NAME", "\"$appName\"" + } + } + +} + +spotless { + java { + target '**/*.java' + googleJavaFormat('1.8').aosp().reflowLongStrings() + } +} + +dependencies { + implementation project(':annotation') + annotationProcessor project(':annotation-processor') + + // make Java 8 API available + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2' + + + // Jetpack / AndroidX libraries + implementation "androidx.appcompat:appcompat:$rootProject.ext.appcompatVersion" + + implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.ext.lifecycleVersion" + implementation "androidx.navigation:navigation-fragment:$rootProject.ext.navVersion" + implementation "androidx.navigation:navigation-ui:$rootProject.ext.navVersion" + + implementation "androidx.room:room-runtime:$rootProject.ext.roomVersion" + implementation "androidx.room:room-guava:$rootProject.ext.roomVersion" + annotationProcessor "androidx.room:room-compiler:$rootProject.ext.roomVersion" + + implementation "androidx.security:security-crypto:1.0.0" + + + // Google material design libraries + implementation "com.google.android.material:material:$rootProject.ext.material" + + + // crypto libraries + implementation 'org.whispersystems:signal-protocol-java:2.6.2' + implementation 'org.conscrypt:conscrypt-android:2.5.2' + implementation 'org.bouncycastle:bcmail-jdk15on:1.64' + + + // XMPP Address library + implementation 'org.jxmpp:jxmpp-jid:1.0.3' + + // DNS library (XMPP needs to resolve SRV records) + implementation 'de.measite.minidns:minidns-hla:0.2.4' + + // Guava + implementation 'com.google.guava:guava:31.1-android' + + // HTTP library + implementation "com.squareup.okhttp3:okhttp:4.10.0" + + // JSON parser + implementation 'com.google.code.gson:gson:2.10.1' + + // logging framework + logging api + implementation 'org.slf4j:slf4j-api:1.7.36' + implementation 'com.github.tony19:logback-android:2.0.1' + + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.robolectric:robolectric:4.9.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation "androidx.test.espresso:espresso-core:$rootProject.ext.espressoVersion" +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/schemas/im.conversations.android.database.ConversationsDatabase/1.json b/app/schemas/im.conversations.android.database.ConversationsDatabase/1.json similarity index 100% rename from schemas/im.conversations.android.database.ConversationsDatabase/1.json rename to app/schemas/im.conversations.android.database.ConversationsDatabase/1.json diff --git a/src/androidTest/java/im/conversations/android/xmpp/TransformationTest.java b/app/src/androidTest/java/im/conversations/android/xmpp/TransformationTest.java similarity index 100% rename from src/androidTest/java/im/conversations/android/xmpp/TransformationTest.java rename to app/src/androidTest/java/im/conversations/android/xmpp/TransformationTest.java diff --git a/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/conversations/res/drawable/ic_launcher_foreground.xml similarity index 100% rename from src/main/res/drawable/ic_launcher_foreground.xml rename to app/src/conversations/res/drawable/ic_launcher_foreground.xml diff --git a/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/conversations/res/mipmap-anydpi-v26/new_launcher.xml similarity index 67% rename from src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to app/src/conversations/res/mipmap-anydpi-v26/new_launcher.xml index 7353dbd1f..82814604f 100644 --- a/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/conversations/res/mipmap-anydpi-v26/new_launcher.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/conversations/res/mipmap-anydpi-v26/new_launcher_round.xml similarity index 67% rename from src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to app/src/conversations/res/mipmap-anydpi-v26/new_launcher_round.xml index 7353dbd1f..82814604f 100644 --- a/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/conversations/res/mipmap-anydpi-v26/new_launcher_round.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/conversations/res/mipmap-hdpi/new_launcher.png similarity index 100% rename from src/main/res/mipmap-hdpi/ic_launcher.png rename to app/src/conversations/res/mipmap-hdpi/new_launcher.png diff --git a/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/conversations/res/mipmap-hdpi/new_launcher_foreground.png similarity index 100% rename from src/main/res/mipmap-hdpi/ic_launcher_foreground.png rename to app/src/conversations/res/mipmap-hdpi/new_launcher_foreground.png diff --git a/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/conversations/res/mipmap-hdpi/new_launcher_round.png similarity index 100% rename from src/main/res/mipmap-hdpi/ic_launcher_round.png rename to app/src/conversations/res/mipmap-hdpi/new_launcher_round.png diff --git a/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/conversations/res/mipmap-mdpi/new_launcher.png similarity index 100% rename from src/main/res/mipmap-mdpi/ic_launcher.png rename to app/src/conversations/res/mipmap-mdpi/new_launcher.png diff --git a/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/conversations/res/mipmap-mdpi/new_launcher_foreground.png similarity index 100% rename from src/main/res/mipmap-mdpi/ic_launcher_foreground.png rename to app/src/conversations/res/mipmap-mdpi/new_launcher_foreground.png diff --git a/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/conversations/res/mipmap-mdpi/new_launcher_round.png similarity index 100% rename from src/main/res/mipmap-mdpi/ic_launcher_round.png rename to app/src/conversations/res/mipmap-mdpi/new_launcher_round.png diff --git a/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/conversations/res/mipmap-xhdpi/new_launcher.png similarity index 100% rename from src/main/res/mipmap-xhdpi/ic_launcher.png rename to app/src/conversations/res/mipmap-xhdpi/new_launcher.png diff --git a/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/conversations/res/mipmap-xhdpi/new_launcher_foreground.png similarity index 100% rename from src/main/res/mipmap-xhdpi/ic_launcher_foreground.png rename to app/src/conversations/res/mipmap-xhdpi/new_launcher_foreground.png diff --git a/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/conversations/res/mipmap-xhdpi/new_launcher_round.png similarity index 100% rename from src/main/res/mipmap-xhdpi/ic_launcher_round.png rename to app/src/conversations/res/mipmap-xhdpi/new_launcher_round.png diff --git a/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/conversations/res/mipmap-xxhdpi/new_launcher.png similarity index 100% rename from src/main/res/mipmap-xxhdpi/ic_launcher.png rename to app/src/conversations/res/mipmap-xxhdpi/new_launcher.png diff --git a/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/conversations/res/mipmap-xxhdpi/new_launcher_foreground.png similarity index 100% rename from src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png rename to app/src/conversations/res/mipmap-xxhdpi/new_launcher_foreground.png diff --git a/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/conversations/res/mipmap-xxhdpi/new_launcher_round.png similarity index 100% rename from src/main/res/mipmap-xxhdpi/ic_launcher_round.png rename to app/src/conversations/res/mipmap-xxhdpi/new_launcher_round.png diff --git a/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/conversations/res/mipmap-xxxhdpi/new_launcher.png similarity index 100% rename from src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to app/src/conversations/res/mipmap-xxxhdpi/new_launcher.png diff --git a/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/conversations/res/mipmap-xxxhdpi/new_launcher_foreground.png similarity index 100% rename from src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png rename to app/src/conversations/res/mipmap-xxxhdpi/new_launcher_foreground.png diff --git a/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/conversations/res/mipmap-xxxhdpi/new_launcher_round.png similarity index 100% rename from src/main/res/mipmap-xxxhdpi/ic_launcher_round.png rename to app/src/conversations/res/mipmap-xxxhdpi/new_launcher_round.png diff --git a/app/src/conversations/res/values-ar/strings.xml b/app/src/conversations/res/values-ar/strings.xml new file mode 100644 index 000000000..6483bc9df --- /dev/null +++ b/app/src/conversations/res/values-ar/strings.xml @@ -0,0 +1,20 @@ + + + اختر مزود خدمة XMPP الخاص بك + استخدِم conversations.im + أنشئ حسابًا جديدًا + هل تملك حساب XMPP؟؟ قد يكون ذلك ممكنا لو كنت تستعمل خدمة XMPP أخرى أو إستعملت تطبيق Conversations سابقا. أو يمكنك صنع حساب XMPP جديد الآن. +\nملاحظة: بعض خدمات البريد الإلكتروني تقدم حسابات XMPP. + XMPP هو مزود مستقل لشبكة المراسلة الفورية. يمكنك استخدام هذا العميل مع أي خادم XMPP تختاره. +\nولكن من أجل راحتك ، فقد جعلنا من السهل إنشاء حساب على موقع chat. مزود مناسب بشكل خاص للاستخدام مع المحادثات. + لقد تمت دعوتك إلى%1$s. سنوجهك خلال عملية إنشاء حساب. +\nعند اختيار%1$s كموفر ، ستتمكن من التواصل مع مستخدمي مقدمي الخدمات الآخرين من خلال منحهم عنوان XMPP الكامل الخاص بك. + لقد تمت دعوتك إلى%1$s. تم بالفعل اختيار اسم مستخدم لك. سنوجهك خلال عملية إنشاء حساب. +\nستتمكن من التواصل مع مستخدمي مقدمي الخدمات الآخرين من خلال منحهم عنوان XMPP الكامل الخاص بك. + سيرفر دعوتك + لم يتم التقاط الكود بطريقة جيّدة + إضغط على زر مشاركة لترسل إلى المتصل بك دعوة إلى %1$s. + إذا كان المتصل بك قريبا منك، يمكنه فحص الكود بالأسفل ليقبل دعوتك. + إنظم %1$s وتحدّث معي: %2$s + شارك الدعوة مع… + \ No newline at end of file diff --git a/app/src/conversations/res/values-bg/strings.xml b/app/src/conversations/res/values-bg/strings.xml new file mode 100644 index 000000000..92667523d --- /dev/null +++ b/app/src/conversations/res/values-bg/strings.xml @@ -0,0 +1,17 @@ + + + Изберете своя XMPP доставчик + Използвайте conversations.im + Създаване не нов профил + Имате ли вече XMPP профил? Може да имате, ако вече използвате друг клиент на XMPP или сте използвали Conversations и преди. Ако не, можете да създадете нов XMPP профил сега.\nСъвет: някои доставчици на е-поща също предоставят XMPP профили. +  + XMPP е мрежа за общуване чрез мигновени съобщения, която не е обвързана с конкретен доставчик. Можете да използвате клиента с всеки сървър, който работи с XMPP.\nЗа Ваше удобство, обаче, ние предоставяме лесен начин да си създадете профил в conversations.im — сървър, пригоден да работи най-добре с Conversations. + Получихте покана за %1$s. Ще Ви преведем през процеса на създаване на профил.\nИзбирайки %1$s за доставчик, Вие ще можете да общувате и с потребители на други доставчици, като им предоставите своя пълен XMPP адрес. + Получихте покана за %1$s. Вече Ви избрахме потребителско име. Ще Ви преведем през процеса на създаване на профил.\nЩе можете да общувате и с потребители на други доставчици, като им предоставите своя пълен XMPP адрес. + Вашата покана за сървъра + Неправилно форматиран код за достъп + Докоснете бутона за споделяне, за да изпратите на контакта си покана за %1$s. + Ако контактът Ви е наблизо, може да сканира кода по-долу, за да приеме поканата Ви. + Присъедини се в %1$s и си пиши с мен: %2$s + Споделяне на поканата чрез… + \ No newline at end of file diff --git a/app/src/conversations/res/values-bn-rIN/strings.xml b/app/src/conversations/res/values-bn-rIN/strings.xml new file mode 100644 index 000000000..382343a37 --- /dev/null +++ b/app/src/conversations/res/values-bn-rIN/strings.xml @@ -0,0 +1,16 @@ + + + XMPP সার্ভার নির্বাচন করুন + conversations.im-ই ব্যবহার করা যাক + নতুন অ্যকাউন্ট তৈরী করা যাক + আপনার কি একটা XMPP অ্যকাউন্ট ইতিমধ্যে করা আছে? সেরকমটা হতেই পারে যদি এর আগে আপনি কোনো অন্য XMPP প্রোগ্রাম বা অ্যাপ ব্যবহার করে থাকেন। এই মুহুর্তে আরেকটা অ্যকাউন্ট তৈরী করা সম্ভব না।‌\nHint: মাঝে মাঝে ইমেল অ্যকাউন্ট খুললেও এরকম অ্যকাউন্ট নিজে থেকেই তৈরী হয়ে যায়। + XMPP কোনো একটি নির্দিষ্ট সংস্থার উপরে নির্ভরশীল নয়। এই অ্যপটি আপনি যেকোনো সংস্থার XMPP সার্ভারের সাথে ব্যবহার করতে পারেন।\nমনে রাখবেন, সুধুমাত্র আপনার সুবিধার্থেই conversations.im -এ আপনার জন্যে একটি অ্যকাউন্ট তৈরী করে দেওয়া হয়েছে। Conversations অ্যপটি এই সার্ভারের সাথে সবথেকে বেশী কার্যকারী। + আপনাকে %1$s-এ আমন্ত্রিত করা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\n%1$s ব্যবহার করলেও, অন্য সেবা-প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনি কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে। + আপনাকে %1$s-এ নিমন্ত্রণ করা হয়েছে। একটি username-ও আপনার জন্যে নির্দিষ্ট করে রাখা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\nঅন্য XMPP সেবা প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনিও কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে। + আপনার নিমন্ত্রণপত্র, সার্ভার থেকে + Provisioning code-এ গরমিল আছে + Share বোতামটা টিপে %1$s-কে একটি আমন্ত্রপত্র পাঠান + পরিচিত ব্যক্তি যদি নিকটেই থাকেন, তাহলে তারা এই কোডটাও স্ক্যান করে নিতে পারেন + %1$sতে এসো, আর আমার সাথে কথা বলো: %2$s + একটি আমন্ত্রণপত্র দেওয়া যাক... + \ No newline at end of file diff --git a/app/src/conversations/res/values-ca/strings.xml b/app/src/conversations/res/values-ca/strings.xml new file mode 100644 index 000000000..7606e4708 --- /dev/null +++ b/app/src/conversations/res/values-ca/strings.xml @@ -0,0 +1,17 @@ + + + Triï el seu proveïdor de XMPP + + Fer servir conversations.im + Crear un compte nou + Ja tens un compte XMPP? Aquest podria ser el cas si ja estàs usant un client XMPP diferent o has usat Converses abans. Si no, pots crear un nou compte XMPP ara mateix.\nPista: Alguns proveïdors de correu electrònic també proporcionen comptes XMPP. + XMPP és una xarxa de missatgeria instantània independent del proveïdor. Pots usar aquest client amb qualsevol servidor XMPP que triïs. No obstant això, per a la teva conveniència, hem fet fàcil la creació d\'un compte en Conversaciones.im; un proveïdor especialment adequat per a l\'ús amb Conversations. + Has estat convidat a %1$s. Et guiarem a través del procés de creació d\'un compte.\nEn triar%1$s com a proveïdor podràs comunicar-se amb els usuaris d\'altres proveïdors donant-los la seva adreça XMPP completa. + Has estat convidat a %1$s . Ja s\'ha triat un nom d\'usuari per a tu. Et guiarem en el procés de creació d\'un compte. Podràs comunicar-te amb usuaris d\'altres proveïdors donant-los la teva adreça XMPP completa. + La teva invitació al servidor + Codi d\'aprovisionament mal formatat + Toca el botó de compartir per a enviar al teu contacte una invitació a %1$s . + Si el teu contacte està a prop, també pot escanejar el codi de baix per a acceptar la teva invitació. + Uneix-te %1$s i xerra amb mi: %2$s + Comparteix la invitació amb... + \ No newline at end of file diff --git a/app/src/conversations/res/values-da-rDK/strings.xml b/app/src/conversations/res/values-da-rDK/strings.xml new file mode 100644 index 000000000..fb5992a1b --- /dev/null +++ b/app/src/conversations/res/values-da-rDK/strings.xml @@ -0,0 +1,16 @@ + + + Vælg din XMPP-udbyder + Brug conversations.im + Opret ny konto + Har du allerede en XMPP-konto? Dette kan være tilfældet, hvis du allerede bruger en anden XMPP-klient eller har brugt Conversations før. Hvis ikke, kan du lige nu oprette en ny XMPP-konto.\nTip: Nogle e-mail-udbydere leverer også XMPP-konti. + XMPP er et udbyderuafhængigt onlinemeddelelsesnetværk. Du kan bruge denne klient med hvilken XMPP-server du end vælger.\nMen for din nemhedsskyld har vi gjort vi det let at oprette en konto på conversations.im; en udbyder, der er specielt velegnet til brug med Conversations. + Du er blevet inviteret til %1$s. Vi guider dig gennem processen med at oprette en konto.\nNår du vælger %1$s som udbyder, kan du kommunikere med brugere fra andre udbydere ved at give dem din fulde XMPP-adresse. + Du er blevet inviteret til %1$s. Der er allerede valgt et brugernavn til dig. Vi guider dig gennem processen med at oprette en konto.\nDu vil være i stand til at kommunikere med brugere fra andre udbydere ved at give dem din fulde XMPP-adresse. + Din server invitation + Forkert formateret klargøringskode + Tryk på deleknappen for at sende din kontakt en invitation til %1$s. + Hvis din kontakt er i nærheden, kan de også skanne koden nedenfor for at acceptere din invitation. + Deltag med %1$s og chat med mig: %2$s + Del invitation med... + \ No newline at end of file diff --git a/app/src/conversations/res/values-de/strings.xml b/app/src/conversations/res/values-de/strings.xml new file mode 100644 index 000000000..2fd0319a9 --- /dev/null +++ b/app/src/conversations/res/values-de/strings.xml @@ -0,0 +1,16 @@ + + + Wähle deinen XMPP-Provider + Benutze conversations.im + Neues Konto erstellen + Hast du bereits ein XMPP-Konto? Dies kann der Fall sein, wenn du bereits einen anderen XMPP-Client verwendest oder bereits Conversations verwendet hast. Wenn nicht, kannst du jetzt ein neues XMPP-Konto erstellen.\nTipp: Einige E-Mail-Anbieter bieten auch XMPP-Konten an. + XMPP ist ein anbieterunabhängiges Instant Messaging Netzwerk. Du kannst diesen Client mit jedem beliebigen XMPP-Server nutzen.\nUm es dir leicht zu machen, haben wir die Möglichkeit geschaffen, ein Konto auf conversations.im anzulegen; ein Anbieter, der speziell für die Verwendung mit Conversations geeignet ist. + Du wurdest zu %1$s eingeladen. Wir führen dich durch den Prozess der Kontoerstellung.\nWenn du %1$s als Provider wählst, kannst du mit Nutzern anderer Anbieter kommunizieren, indem du ihnen deine vollständige XMPP-Adresse gibst. + Du wurdest zu %1$seingeladen. Ein Benutzername ist bereits für dich ausgewählt worden. Wir führen dich durch den Prozess der Kontoerstellung.\nDu kannst mit Nutzern anderer Anbieter kommunizieren, indem du ihnen deine vollständige XMPP-Adresse gibst. + Deine Einladung für den Server + Falsch formatierter Provisionierungscode + Tippe auf die \"Teilen\"-Schaltfläche, um deinem Kontakt eine Einladung an %1$s zu senden. + Wenn dein Kontakt in der Nähe ist, kann er auch den untenstehenden Code einscannen, um deine Einladung anzunehmen. + Komme zu %1$s und chatte mit mir: %2$s + Einladung teilen mit… + \ No newline at end of file diff --git a/app/src/conversations/res/values-el/strings.xml b/app/src/conversations/res/values-el/strings.xml new file mode 100644 index 000000000..bb7bcadf0 --- /dev/null +++ b/app/src/conversations/res/values-el/strings.xml @@ -0,0 +1,16 @@ + + + Επιλέξτε τον πάροχο XMPP σας + Χρήση του conversations.im + Δημιουργία νέου λογαριασμού + Έχετε ήδη λογαριασμό XMPP; Αυτό μπορεί να συμβαίνει αν ήδη χρησιμοποιείτε ένα άλλο πρόγραμμα XMPP ή έχετε χρησιμοποιήσει το Conversations παλιότερα. Αν όχι, μπορείτε να δημιουργήσετε ένα νέο λογαριασμό XMPP τώρα.\nΧρήσιμη πληροφορία: Κάποιοι πάροχοι e-mail παρέχουν επίσης και λογαριασμούς XMPP. + Το XMPP είναι ένα δίκτυο άμεσης ανταλλαγής μηνυμάτων ανεξάρτητο παρόχου. Μπορείτε να χρησιμοποιήσετε αυτό το πρόγραμμα με όποιον διακομιστή XMPP επιθυμείτε.\nΓια διευκόλυνση πάντως μπορείτε να δημιουργήσετε έναν λογαριασμό στο conversations.im, έναν πάροχο ειδικά σχεδιασμένο για χρήση με το Conversations. + Έχετε προσκληθεί στο %1$s. Θα σας καθοδηγήσουμε στη διαδικασία δημιουργίας λογαριασμού.\nΕπιλέγοντας τον %1$s ως πάροχο θα μπορείτε να επικοινωνείτε με χρήστες άλλων παρόχων δίνοντάς τους την πλήρη διεύθυνση XMPP σας. + Έχετε προσκληθεί στο %1$s. Ένα όνομα χρήστη έχει ήδη επιλεγεί για εσάς. Θα σας καθοδηγήσουμε στη διαδικασία δημιουργίας λογαριασμού.\nΘα μπορείτε να επικοινωνείτε με χρήστες άλλων παρόχων δίνοντάς τους την πλήρη διεύθυνση XMPP σας. + Η πρόσκλησή σας στον διακομιστή + Λάθος μορφοποίηση κώδικα παροχής + Πατήστε το πλήκτρο διαμοιρασμού για να στείλετε στην επαφή σας μια πρόσκληση στο %1$s. + Αν η επαφή σας βρίσκεται κοντά σας, μπορεί επίσης να σαρώσει τον κωδικό παρακάτω για να αποδεχτεί την πρόσκλησή σας. + Μπείτε στο %1$s και συνομιλήστε μαζί μου: %2$s + Διαμοιρασμός πρόσκλησης με... + \ No newline at end of file diff --git a/app/src/conversations/res/values-es/strings.xml b/app/src/conversations/res/values-es/strings.xml new file mode 100644 index 000000000..80958fadc --- /dev/null +++ b/app/src/conversations/res/values-es/strings.xml @@ -0,0 +1,17 @@ + + + Elige tu proveedor XMPP + Usa conversations.im + Crear nueva cuenta + ¿Ya tienes una cuenta XMPP? Este puede ser el caso si ya estás usando un cliente XMPP diferente o has usado Conversations anteriormente. Si no es así, puedes crear una nueva cuenta XMPP ahora mismo.\nConsejo: Algunos proveedores de email también ofrecen una cuenta XMPP. + XMPP es una red de mensajería instantánea independiente del proveedor. Puedes usar este cliente con cualquier servidor XMPP que elijas. +\nSin embargo, para tu conveniencia, hacemos de forma sencilla la creación de una cuenta en conversations.im; un proveedor especializado para el uso con Conversations. + Has sido invitado a %1$s. Te guiaremos durante el proceso de creación de la cuenta.\nCuando selecciones %1$s como proveedor podrás comunicarte con usuarios de otros servidores proporcionándoles tu dirección XMPP completa. + Has sido invitado a %1$s. Un nombre de usuario ya ha sido escogido para ti. Te guiaremos durante el proceso de creación de la cuenta.\nPodrás comunicarte con otros usuarios de otros servidores proporcionándoles tu dirección XMPP completa. + Tu invitación al servidor + Código de abastecimiento formateado incorrectamente + Pulsa el botón de compartir para enviar a tu contacto una invitación a %1$s. + Si tu contacto está cerca, también puede escanear el código mostrado debajo para aceptar tu invitación. + Únete a %1$s y chatea conmigo: %2$s + Comparte la invitación con… + \ No newline at end of file diff --git a/app/src/conversations/res/values-eu/strings.xml b/app/src/conversations/res/values-eu/strings.xml new file mode 100644 index 000000000..bf9555311 --- /dev/null +++ b/app/src/conversations/res/values-eu/strings.xml @@ -0,0 +1,8 @@ + + + Hautatu zure XMPP hornitzailea + Erabili conversations.im + Kontu berria sortu + XMPP kontu bat badaukazu dagoeneko? Horrela izan daiteke beste XMPP aplikazio bat erabiltzen baduzu edo Conversations lehenago erabili baduzu. Bestela XMPP kontu berri bat sortu dezakezu oraintxe bertan.\nIradokizuna: email hornitzaile batzuek XMPP kontuak hornitzen dituzte ere. + XMPP hornitzailez independientea den bat-bateko mezularitza sare bat da. Aplikazio hau nahi duzun XMPP zerbitzariarekin erabili dezakezu.\nHala ere zure erosotasunerako conversations.im-en, Conversationsekin bereziki erabiltzeko egokia den hornitzaile batean, kontu bat sortzea erraz egin dugu. + \ No newline at end of file diff --git a/app/src/conversations/res/values-fa-rIR/strings.xml b/app/src/conversations/res/values-fa-rIR/strings.xml new file mode 100644 index 000000000..0f3362506 --- /dev/null +++ b/app/src/conversations/res/values-fa-rIR/strings.xml @@ -0,0 +1,6 @@ + + + لطفا سرویس دهنده پیام خود را انتخاب نمائید. برای مثال artalk.im + از Conversations.im استفاده کنید + حساب کاربری جدیدی بسازید + \ No newline at end of file diff --git a/app/src/conversations/res/values-fi/strings.xml b/app/src/conversations/res/values-fi/strings.xml new file mode 100644 index 000000000..17c75a297 --- /dev/null +++ b/app/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/app/src/conversations/res/values-fr/strings.xml b/app/src/conversations/res/values-fr/strings.xml new file mode 100644 index 000000000..47badf219 --- /dev/null +++ b/app/src/conversations/res/values-fr/strings.xml @@ -0,0 +1,16 @@ + + + Choisissez votre fournisseur XMPP + Utiliser conversations.im + Créer un nouveau compte + Avez-vous déjà un compte XMPP ? Cela peut être le cas si vous utilisez déjà un autre client XMPP ou si vous avez déjà utilisé Conversations auparavant. Sinon, vous pouvez créer un nouveau compte XMPP dès maintenant.\nRemarque : Certains fournisseurs de messagerie proposent également des comptes XMPP. + XMPP est un réseau de messagerie instantanée indépendant du fournisseur. Vous pouvez utiliser ce client avec n’importe quel serveur XMPP de votre choix.\nToutefois, pour votre commodité, nous avons facilité la création d’un compte sur conversations.im ; un fournisseur spécialement conçu pour Conversations. + Vous avez été invité à %1$s. Nous allons vous guider à travers le processus de création d’un compte.\nEn choisissant %1$s comme fournisseur, vous pourrez communiquer avec les utilisateurs des autres fournisseurs en leur donnant votre adresse XMPP complète. + Vous avez été invité à %1$s. Un nom d’utilisateur a déjà été choisi pour vous. Nous allons vous guider à travers le processus de création d’un compte.\nVous pourrez communiquer avec les utilisateurs des autres fournisseurs en leur donnant votre adresse XMPP complète. + Votre invitation au serveur + Code de provisionnement mal formaté + Appuyez sur le bouton partager pour envoyer à votre contact une invitation pour %1$s + Si vos contacts sont à votre côté, ils peuvent aussi scanner le code ci dessous pour accepter votre invitation + Rejoignez %1$set discutez avec moi : %2$s + Partager une invitation avec ... + \ No newline at end of file diff --git a/app/src/conversations/res/values-gl/strings.xml b/app/src/conversations/res/values-gl/strings.xml new file mode 100644 index 000000000..2becd8bea --- /dev/null +++ b/app/src/conversations/res/values-gl/strings.xml @@ -0,0 +1,16 @@ + + + Elixe o teu provedor XMPP + Utilizar conversations.im + Crear nova conta + Xa posúes unha conta XMPP? Este pode ser o caso se xa estás a utilizar outro cliente XMPP ou utilizaches Conversations previamente. Se non é así podes crear unha nova conta agora mesmo.\nTruco: Algúns provedores de correo tamén proporcionan contas XMPP. + XMPP é unha rede de mensaxería independente do provedor. Podes utilizar este cliente con calquera provedor XMPP da túa elección.\nMais para a tua conveniencia fixemos que fose doado crear unha conta en conversations.im; un provedor especialmente axeitado para utilizar con Conversations. + Convidáronte a %1$s. Guiarémoste no proceso para crear unha conta.\nAo elexir %1$s como provedor poderás comunicarte con usuarias doutros provedores cando lles deas o teu enderezo XMPP completo. + Convidáronte a %1$s. Xa eleximos un nome de usuaria para ti. Guiarémoste no proceso de crear unha conta.\nPoderás comunicarte con usuarias doutros provedores cando lles digas o teu enderezo XMPP completo. + O convite do teu servidor + Código de aprovisionamento con formato non válido + Toca no botón compartir para convidar ao teu contacto a %1$s. + Se o contacto está preto de ti, pode escanear o código inferior para aceptar o teu convite. + Únete a %1$s e conversa conmigo: %2$s + Enviar convite a… + \ No newline at end of file diff --git a/app/src/conversations/res/values-hr/strings.xml b/app/src/conversations/res/values-hr/strings.xml new file mode 100644 index 000000000..093c6d0b9 --- /dev/null +++ b/app/src/conversations/res/values-hr/strings.xml @@ -0,0 +1,16 @@ + + + Odaberite svog XMPP davatelja usluga. + Koristite conversations.im + Napravi novi račun + Već imate XMPP račun? To može biti slučaj ako već koristite drugi XMPP klijent ili ste prije koristili Razgovore. Ako niste, možete odmah stvoriti novi XMPP račun.\nSavjet: Neki pružatelji usluga e-pošte također nude XMPP račune. + XMPP je mreža za razmjenu izravnih poruka neovisna o pružatelju usluga. Možete koristiti ovaj klijent s bilo kojim XMPP poslužiteljem koji odaberete.\nMeđutim, radi vaše udobnosti olakšali smo kreiranje računa na conversations.im; pružatelj usluga posebno prilagođen za korištenje s Conversations. + Pozvani ste na %1$s. Vodit ćemo vas kroz postupak kreiranja računa.\nPrilikom odabira %1$s pružatelja moći ćete komunicirati s korisnicima drugih pružatelja dajući im svoju punu XMPP adresu. + Pozvani ste na %1$s. Korisničko ime je već odabrano za vas. Vodit ćemo vas kroz postupak kreiranja računa.\nMoći ćete komunicirati s korisnicima drugih pružatelja tako da im date svoju punu XMPP adresu. + Vaša pozivnica za poslužitelj + Neispravno formatiran kod za dodjelu + Dodirnite gumb za dijeljenje kako biste svom kontaktu poslali pozivnicu na %1$s. + Ako je vaš kontakt u blizini, također može skenirati kod u nastavku kako bi prihvatio vašu pozivnicu. + Pridružite se %1$s i razgovarajte sa mnom: %2$s + Podijelite pozivnicu s... + \ No newline at end of file diff --git a/app/src/conversations/res/values-hu/strings.xml b/app/src/conversations/res/values-hu/strings.xml new file mode 100644 index 000000000..f4c180889 --- /dev/null +++ b/app/src/conversations/res/values-hu/strings.xml @@ -0,0 +1,16 @@ + + + Válassza ki az XMPP szolgáltatóját + A conversations.im használata + Új fiók létrehozása + Már rendelkezik XMPP-fiókkal? Ez az eset állhat fenn, ha már egy másik XMPP-klienst használ, vagy ha már korábban használta a Conversations alkalmazást. Ha nem, akkor most létrehozhat egy új XMPP-fiókot.\nTipp: egyes e-mail szolgáltatók is biztosítanak XMPP-fiókokat. + Az XMPP egy szolgáltatófüggetlen, azonnali üzenetküldő hálózat. Ezt a kliensprogramot bármely XMPP-kiszolgálóhoz használhatja.\nAzonban a kényelem érdekében megkönnyítettük a conversations.im szolgáltatón való fióklétrehozást, ami kifejezetten a Conversations alkalmazással történő használatra lett tervezve. + Meghívást kapott a(z) %1$s kiszolgálóra. Végig fogjuk vezetni egy fiók létrehozásának folyamatán.\nHa a(z) %1$s kiszolgálót választja szolgáltatóként, akkor képes lesz más szolgáltatók felhasználóival is kommunikálni, ha megadja nekik a teljes XMPP-címét. + Meghívást kapott a(z) %1$s kiszolgálóra. Már kiválasztottak Önnek egy felhasználónevet. Végig fogjuk vezetni egy fiók létrehozásának folyamatán.\nKépes lesz más szolgáltatók felhasználóival is kommunikálni, ha megadja nekik a teljes XMPP-címét. + Az Ön kiszolgálómeghívása + Helytelenül formázott kiépítési kód + Koppintson a megosztás gombra, hogy meghívót küldjön a partnerének erre: %1$s. + Ha a partnere a közelben van, akkor a meghívás elfogadásához leolvashatja a lenti kódot. + Csatlakozzon ehhez: %1$s, és csevegjen velem: %2$s + Meghívás megosztása… + \ No newline at end of file diff --git a/app/src/conversations/res/values-id/strings.xml b/app/src/conversations/res/values-id/strings.xml new file mode 100644 index 000000000..a316ee848 --- /dev/null +++ b/app/src/conversations/res/values-id/strings.xml @@ -0,0 +1,16 @@ + + + Pilih XMPP server anda + Gunakan conversations.im + Buat akun baru + Anda sudah memiliki akun XMPP\? Ini mungkin terjadi jika Anda sudah menggunakan aplikasi XMPP yang berbeda atau pernah menggunakan Conversations sebelumnya. Jika tidak, Anda dapat membuat akun XMPP baru. NPetunjuk: Beberapa penyedia layanan email juga menyediakan akun XMPP. + XMPP adalah jaringan penyedia pesan instan independen. Anda dapat menggunakan aplikasi ini dengan server XMPP pilihan Anda. NNamun demi kenyamanan Anda, kami permudah untuk membuat akun di Conversations.im; provider yang sangat cocok digunakan dengan Conversations. + Anda telah diundang ke %1$s. Kami akan memandu Anda melalui proses pembuatan akun. \nSaat memilih %1$s sebagai penyedia, Anda akan dapat berkomunikasi dengan pengguna provider lain dengan memberikan alamat XMPP lengkap Anda kepada mereka. + Anda telah diundang ke%1$s. Username telah dipilihkan untuk Anda. Kami akan memandu Anda melalui proses pembuatan akun. \nAnda dapat berkomunikasi dengan pengguna provider lain dengan memberi mereka alamat XMPP lengkap Anda. + Undangan server Anda + Kode provisioning tidak diformat dengan benar + Klik tombol bagikan untuk mengirim undangan ke kontak Anda %1$s. + Jika kontak Anda di dekat Anda, mereka juga dapat memindai kode di bawah ini untuk menerima undangan Anda + Bergabung %1$s dan mengobrol dengan saya: %2$s + Bagikan undangan dengan... + \ No newline at end of file diff --git a/app/src/conversations/res/values-it/strings.xml b/app/src/conversations/res/values-it/strings.xml new file mode 100644 index 000000000..428a2b032 --- /dev/null +++ b/app/src/conversations/res/values-it/strings.xml @@ -0,0 +1,18 @@ + + + Scegli il tuo fornitore XMPP + Usa conversations.im + Crea un nuovo profilo + Hai già un profilo XMPP\? Può accadere se stai già usando un client XMPP diverso o hai già usato prima Conversations. In caso negativo, puoi creare un profilo XMPP adesso. +\nNota: alcuni fornitori di email offrono anche account XMPP. + XMPP è una rete di messaggistica istantanea indipendente dal fornitore. Puoi usare questo client con qualsiasi server XMPP. +\nTuttavia, per comodità, 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. + Se il contatto è vicino, può anche scansionare il codice sottostante per accettare il tuo invito. + Unisciti a %1$s e chatta con me: %2$s + Condividi invito con… + \ No newline at end of file diff --git a/app/src/conversations/res/values-ja/strings.xml b/app/src/conversations/res/values-ja/strings.xml new file mode 100644 index 000000000..2d240bedc --- /dev/null +++ b/app/src/conversations/res/values-ja/strings.xml @@ -0,0 +1,16 @@ + + + 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 に招待を送信する。 + あなたの連絡先が近くにいる場合は、下のコードをスキャンして、あなたの招待を受け取ることもできます。 + %1$s に参加して私とお話しましょう: %2$s + …で招待を共有 + \ No newline at end of file diff --git a/app/src/conversations/res/values-nl/strings.xml b/app/src/conversations/res/values-nl/strings.xml new file mode 100644 index 000000000..f04a6b2de --- /dev/null +++ b/app/src/conversations/res/values-nl/strings.xml @@ -0,0 +1,14 @@ + + + Kies je XMPP-dienst + Conversations.im gebruiken + Nieuwe account registreren + Heb je al een XMPP-account? Als je al een andere XMPP-cliënt gebruikt, of Conversations vroeger al eens hebt gebruikt, is dit waarschijnlijk het geval. Zo niet, kan je nu een nieuwe XMPP-account aanmaken.\nTip: sommige e-mailproviders bieden ook XMPP-accounts aan. + XMPP is een provider-onafhankelijk berichtennetwerk. Je kan deze cliënt gebruiken met eender welke XMPP-server.\nOm het je gemakkelijker te maken kun je simpelweg een account aanmaken op conversations.im; een provider speciaal geschikt voor Conversations. + Je ontving een uitnodiging voor %1$s. We zullen je helpen een account aan te maken.\nWanneer je %1$s als je provider kiest kan je met gebruikers van andere providers communiceren door hen je volledige XMPP-adres te geven. + Je ontving een uitnodiging voor %1$s. Er werd reeds een gebruikersnaam voor jou gekozen. We zullen je helpen een account aan te maken.\nJe zal met gebruikers van andere providers communiceren door hen je volledige XMPP-adres te geven. + Je server uitnodiging + Tik op de delen knop om een uitnodiging te versturen naar %1$s + Als je contactpersoon in de buurt is, kan deze ook onderstaande code scannen om de uitnodiging te aanvaarden. + Deel de uitnodiging met ... + \ No newline at end of file diff --git a/app/src/conversations/res/values-pl/strings.xml b/app/src/conversations/res/values-pl/strings.xml new file mode 100644 index 000000000..f3771aed2 --- /dev/null +++ b/app/src/conversations/res/values-pl/strings.xml @@ -0,0 +1,16 @@ + + + Wybierz dostawcę XMPP + Użyj conversations.im + Utwórz nowe konto + Czy masz już konto XMPP? Tak może być jeśli używasz już innego klienta XMPP lub używałeś już Conversations. Jeśli nie możesz stworzyć nowe konto XMPP teraz.\nPodpowiedź: Niektórzy dostawcy poczty oferują również konta XMPP. + XMPP to niezależna od dostawcy sieć komunikacji błyskawicznej. Możesz użyć tego klienta z dowolnym serwerem XMPP.\nDla twojej wygody jednak ułatwiliśmy stworzenie konta na conversations.im; dostawcy specjalnie dostosowanego do pracy z Conversations. + Zostałeś zaproszony do %1$s. Poprowadzimy ciebie przez proces tworzenia konta.\nWybierając %1$s jako dostawcę będziesz mógł komunikować się z innymi użytkownikami podając swój pełny adres XMPP. + Zostałeś zaproszony do %1$s. Nazwa użytkownika została już dla ciebie wybrana. Poprowadzimy ciebie przez proces tworzenia konta.\nBęziesz mógł komunikować się z innymi użytkownikami podając swój adres XMPP. + Zaproszenie twojego serwera + Niepoprawnie sformatowany kod zaopatrywania + Użyj przycisku udostępniania aby wysłać swojemu kontaktowi zaproszenie do %1$s. + Jeśli twój kontakt jest blisko może przeskanować kod poniżej aby zaakceptować twoje zaproszenie. + Dołącz do %1$s aby porozmawiać ze mną: %2$s + Udostępnij zaproszenie… + \ No newline at end of file diff --git a/app/src/conversations/res/values-pt-rBR/strings.xml b/app/src/conversations/res/values-pt-rBR/strings.xml new file mode 100644 index 000000000..0a4b54191 --- /dev/null +++ b/app/src/conversations/res/values-pt-rBR/strings.xml @@ -0,0 +1,16 @@ + + + Selecione o seu provedor XMPP + Usar o conversations.im + Criar uma nova conta + Você já possui uma conta XMPP? Esse pode ser o seu caso caso já esteja usando um outro cliente XMPP ou tenha usado o Conversations antes. Caso contrário, você pode criar uma nova conta XMPP agora.\nDica: alguns provedores de e-mail também fornecem contas XMPP. + O XMPP é uma rede de mensageria instantânea independente de provedor. Você pode usar esse cliente com qualquer servidor XMPP que você escolher.\nEntretanto, para sua conveniência, nós simplificamos o processo de criação de uma conta em conversations.im, um provedor especialmente configurado para se usar com o Conversations. + Você foi convidado para %1$s. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nAo escolher %1$s como um provedor você conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo. + Você foi convidado para %1$s. Um nome de usuário já foi escolhido para você. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nVocê conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo. + Seu convite do servidor + Código de provisionamento formatado de maneira imprópria + Toque no botão compartilhar para enviar, para seu contato, um convite para %1$s. + Se seu contato estiver por perto, ele também pode escanear o código abaixo para aceitar seu convite. + Junte-se a %1$s e converse comigo: %2$s + Compartilhe o convite com... + \ No newline at end of file diff --git a/app/src/conversations/res/values-pt/strings.xml b/app/src/conversations/res/values-pt/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/app/src/conversations/res/values-pt/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/conversations/res/values-ro-rRO/strings.xml b/app/src/conversations/res/values-ro-rRO/strings.xml new file mode 100644 index 000000000..baefb00c6 --- /dev/null +++ b/app/src/conversations/res/values-ro-rRO/strings.xml @@ -0,0 +1,16 @@ + + + Alegeți-vă furnizorul XMPP + Folosește conversations.im + Creează un cont nou + Aveți deja un cont XMPP? S-ar putea să fie așa dacă deja utilizați un alt client XMPP sau dacă ați folosit Conversations în trecut. Dacă nu, puteți crea un cont nou XMPP chiar acum.\nIdee: Unii furnizori de e-mail oferă de asemenea și conturi XMPP. + XMPP este o rețea de mesagerie instant ce nu depinde de un anumit furnizor. Aveți posibilitatea să utilizați acest client cu orice server XMPP doriți.\nTotuși, pentru confortul dumneavoastră, am facilitat crearea unui cont pe conversations.im; un furnizor potrivit pentru utilizarea cu aplicația Conversations. + Ați fost invitați la %1$s. Vă vom ghida prin procesul de creare al unui cont.\nCând alegeți %1$s ca furnizor veți putea comunica cu utilizatorii altor furnizori oferindu-le adresa dumneavoastră completă XMPP. + Ați fost invitați la %1$s. Un nume de utilizator a fost deja ales pentru dumneavoastră. Vă vom ghida prin procesul de creare al unui cont.\nVeți putea comunica cu utilizatorii altor furnizori oferindu-le adresa dumneavoastră completă XMPP. + Invitația serverului dumneavoastră + Cod de acces formatat necorespunzător + Atingeți butonul de partajare pentru a trimite contactului o invitație la %1$s. + Dacă e în apropiere, contactul poate scana codul de mai jos pentru a vă accepta invitația. + Alătură-te %1$s și discută cu mine: %2$s + Partajează invitația cu… + \ No newline at end of file diff --git a/app/src/conversations/res/values-ru/strings.xml b/app/src/conversations/res/values-ru/strings.xml new file mode 100644 index 000000000..20b99a7b3 --- /dev/null +++ b/app/src/conversations/res/values-ru/strings.xml @@ -0,0 +1,19 @@ + + + Выберите своего XMPP-провайдера + Использовать conversations.im + Создать новый аккаунт + У вас есть аккаунт XMPP\? Если вы использовали Conversations или другой XMPP-клиент в прошлом, то скорее всего, он у вас есть. Если у вас нет аккаунта, вы можете создать его прямо сейчас. +\nПодсказка: Некоторые провайдеры электронной почты также регистрируют аккаунты XMPP. + XMPP - это независимая сеть обмена сообщениями. Conversations позволяет вам подключиться к любому XMPP-серверу на ваш выбор.\nЕсли у вас нет сервера, предлагаем вам зарегистрировать аккаунт на conversations.im, сервере, специально предназначенном для работы с Conversations. + Вас пригласили на %1$s. Мы проведём вас через процесс создания аккаунта. +\nАккаунт на %1$s позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес. + Вас пригласили на %1$s. Вам уже назначили имя пользователя. Мы проведём вас через процесс создания аккаунта. +\nЭтот аккаунт позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес. + Ваше приглашение + Неправильный формат кода + Нажмите кнопку «Поделиться», чтобы отправить вашему контакту приглашение в %1$s. + Если ваш контакт находится поблизости, он также может отсканировать приведенный ниже код, чтобы принять ваше приглашение. + Присоединяйтесь к %1$s и пообщайтесь со мной: %2$s + Поделиться приглашением с… + \ No newline at end of file diff --git a/app/src/conversations/res/values-sk/strings.xml b/app/src/conversations/res/values-sk/strings.xml new file mode 100644 index 000000000..ed58bbefb --- /dev/null +++ b/app/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/app/src/conversations/res/values-sq/strings.xml b/app/src/conversations/res/values-sq/strings.xml new file mode 100644 index 000000000..1e3f34b5b --- /dev/null +++ b/app/src/conversations/res/values-sq/strings.xml @@ -0,0 +1,20 @@ + + + XMPP është një rrjet shkëmbimi mesazhesh të atypëratyshëm i pavarur nga shërbimet. Këtë klient mund ta përdorni me cilindo shërbyes XMPP që zgjidhni. +\nMegjithatë, për lehtësi, e kemi bërë të kollajshme të krijohet një llogari te conversations.im, një shërbim posaçërisht i përshtatshëm për përdorim me Conversations. + Jeni ftuar te %1$s. Do t’ju udhëheqim përmes procesit të krijimit të një llogarie. +\nKur zgjidhet %1$s si shërbim, do të jeni në gjendje të komunikoni me përdorues nga shërbime të tjera duke u dhënë adresën tuaj të plotë XMPP. + Jeni ftuar te %1$s. Për ju është zgjedhur tashmë një emër përdoruesi. Do t’ju udhëheqim përmes procesit të krijimit të një llogarie. +\nDo të jeni në gjendje të komunikoni me përdorues nga shërbime të tjera duke u dhënë adresën tuaj të plotë XMPP. + Prekni butonin e ndarjes me të tjerë që t’i dërgoni kontaktit tuaj një ftesë për te %1$s. + Nëse kontakti juaj është atypari, mund të skanojë gjithashtu kodin më poshtë, që të pranojë ftesën tuaj. + Bëhuni pjesë e %1$s dhe bisedoni me: %2$s + Ndajeni ftesën me… + Krijoni llogari të re + Zgjidhni shërbimin tuaj XMPP + Përdor conversations.im + Keni tashmë një llogari XMPP\? Mund të jetë kështu nëse përdorni tashmë një klient tjetër XMPP, ose e keni përdorur Conversations më parë. Nëse jo, mund të krijoni një llogari të re XMPP që tani. +\nNdihmëz: Disa shërbime email-i ofrojnë gjithashtu llogari XMPP. + Ftesë nga shërbyesi juaj + Kod i formatuar jo saktësisht + \ No newline at end of file diff --git a/app/src/conversations/res/values-sr/strings.xml b/app/src/conversations/res/values-sr/strings.xml new file mode 100644 index 000000000..e668ed7e6 --- /dev/null +++ b/app/src/conversations/res/values-sr/strings.xml @@ -0,0 +1,9 @@ + + + Одаберите вашег ИксМПП провајдера + Користи conversations.im + Направи нови налог + Да ли већ имате ИксМПП налог? Извесно је да га имате ако користите неки ИксМПП клијент или сте раније користили Конверзацију. Ако немате, сада можете направити нови ИксМПП налог.\nСавет: неки поштански провајдери такође омогућавају и ИксМПП налоге. + ИксМПП је мрежа брзих порука, независна од провајдера. Овај клијент можете користити уз било који сервер по вашем избору.\nДа бисмо вам олакшали, омогућили смо креирање налога на conversations.im; провајдеру специјално прилаг.ођеном за коришћење уз Конверзацију + Ваша серверска позивница + \ No newline at end of file diff --git a/app/src/conversations/res/values-sv/strings.xml b/app/src/conversations/res/values-sv/strings.xml new file mode 100644 index 000000000..062a0c26f --- /dev/null +++ b/app/src/conversations/res/values-sv/strings.xml @@ -0,0 +1,19 @@ + + + Välj din XMPP-leverantör + Använd conversations.im + 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… + Du har blivit inbjuden till %1$s. Ett användarnamn har redan valts åt dig. Vi guidar dig genom processen för att skapa ett konto. +\nDu kommer att kunna kommunicera med användare av andra leverantörer genom att ge dem din fullständiga XMPP-adress. + XMPP är ett leverantörsoberoende snabbmeddelandenätverk. Du kan använda den här klienten med vilken XMPP-server du än väljer. +\nMen för din bekvämlighet har vi gjort det enkelt att skapa ett konto på conversations.im; en leverantör som är speciellt lämpad för användning med Conversations. + Du har blivit inbjuden till %1$s. Vi guidar dig genom processen för att skapa ett konto. +\nNär du väljer %1$s som leverantör kommer du att kunna kommunicera med användare av andra leverantörer genom att ge dem din fullständiga XMPP-adress. + \ No newline at end of file diff --git a/app/src/conversations/res/values-szl/strings.xml b/app/src/conversations/res/values-szl/strings.xml new file mode 100644 index 000000000..6e0134d06 --- /dev/null +++ b/app/src/conversations/res/values-szl/strings.xml @@ -0,0 +1,16 @@ + + + Wybier liferanta XMPP + Użyj conversations.im + Stwōrz nowe kōnto + Mosz już kōnto XMPP? Tak może być, jeźli już używosz inkszego klijynta XMPP aboś używoł abo używała wcześnij Conversations. Jak niy, to możesz stworzić teroz nowe kōnto XMPP.\nDorada: Niykerzi liferańcio emaili dowajōm tyż kōnta XMPP. + XMPP to je nec wartkich wiadōmości niyzależny ôd liferanta. Możesz używać tego klijynta ze serwerym XMPP, jaki sie wybieresz.\nAle dlo twojij wygody ułacniyli my tworzynie kōnt na conversations.im; liferańcie ekstra dopasowanym do używanio ze Conversations. + Mosz zaproszynie na %1$s. Pokludzymy cie bez proces tworzynio kōnta.\nPo wybraniu %1$s za liferanta, poradzisz kōmunikować sie ze używoczami ôd inkszych liferantōw bez danie im swojij połnyj adresy XMPP. + Mosz zaproszynie na %1$s. Miano ôd używocza już je do ciebie wybrane. Pokludzymy cie bez proces tworzynio kōnta.\nBydzie szło kōmunikować sie ze używoczami ôd inkszych liferantōw bez danie im swojij połnyj adresy XMPP. + Twoje zaproszynie na serwer + Niynoleżnie sformatowany kod lifrowanio + Tyknij knefla dzielynio sie, żeby posłać kōntaktowi zaproszynie na %1$s. + Jeźli kōntakt je blisko, to może tyż zeskanować kod niżyj, żeby zaakceptować twoje zaproszynie. + Pōdź na %1$s i pogodej zy mnōm: %2$s + Poślij zaproszynie do… + \ No newline at end of file diff --git a/app/src/conversations/res/values-tr-rTR/strings.xml b/app/src/conversations/res/values-tr-rTR/strings.xml new file mode 100644 index 000000000..6fb383cf7 --- /dev/null +++ b/app/src/conversations/res/values-tr-rTR/strings.xml @@ -0,0 +1,16 @@ + + + XMPP sağlayıcınızı seçin + conversations.im kullan + Yeni hesap oluştur + Zaten bir XMPP hesabınız var mı? Bunun sebebi, zaten başka bir XMPP istemcisi kullanıyor oluşunuz veya Conversations\'ı önceden kullanmış olmanız olabilir. Eğer durum bu değilse şimdi yeni bir XMPP hesabı oluşturabilirsiniz.\nİpucu: Bağzı e-posta sağlayıcıları da XMPP hesapları kullanabilir. + XMPP; anlık yazışmalar için bağımsız bir sağlayıcıdır. Bu istemciyi istediğiniz herhangi bir XMPP sunucusu ile birlikte kullanabilirsiniz.\nAncak kullanım rahatlığı adına sizin için conversations.im; Conversations için özellikle tasarlanmış bir sağlayıcıda hesap açmanızı kolaylaştırdık. + %1$s sağlayıcısına davet edildiniz. Sizi hesap oluşturulması konusunda yönlendireceğiz.\n%1$s bir sağlayıcı olark seçildiğinde, başka sağlayıcılar kullanan kullanıcılarla, onlara tam XMPP adresinizi vererek iletişim kurabileceksiniz. + %1$s sağlayıcısına davet edildiniz. Sizin için zaten bir kullanıcı adı seçildi. Sizi hesap oluşturulması konusunda yönlendireceğiz.\nBaşka sağlayıcılar kullanan kullanıcılarla, onlara tam XMPP adresinizi vererek iletişim kurabileceksiniz. + Sunucu davetiyeniz + Yanlış ayarlanmış düzenleme kodu + Kişinize, %1$s grubuna davet etmek için Paylaş düğmesine basın. + Kişiniz yakınınızda ise, aşağıdaki kodu tarayak daveti kabul edebilirler. + %1$s grubuna katıl ve benimle sohpet et: %2$s + Daveti şununla paylaş... + \ No newline at end of file diff --git a/app/src/conversations/res/values-uk/strings.xml b/app/src/conversations/res/values-uk/strings.xml new file mode 100644 index 000000000..3b855ab5c --- /dev/null +++ b/app/src/conversations/res/values-uk/strings.xml @@ -0,0 +1,12 @@ + + + Виберіть постачальника послуг обміну повідомленнями XMPP + Скористатися conversations.im + Створити новий обліковий запис + Вже маєте обліковий запис XMPP? Можливо, користуєтеся іншою програмою XMPP або користувалися цією програмою раніше. Якщо ні, можете створити новий обліковий запис XMPP просто зараз.\nЗверніть увагу, що деякі постачальники електронної пошти у той же час надають облікові записи XMPP. + XMPP — це мережа обміну повідомленнями, незалежна від постачальників. Можете використовувати цю програму з будь-яким XMPP сервером, який оберете.\nПроте, для зручності, ми спростили створення облікового запису на conversations.im — у постачальника, який спеціально налаштований на роботу з цією програмою. + Вас запросили до %1$s. Ми проведемо вас крок за кроком, щоб створити обліковий запис.\nОбираючи %1$s в якості свого постачальника, ви зможете спілкуватися з користувачами інших постачальників, для цього повідомте їм свою повну адресу XMPP. + Вас запросили до %1$s. Для вас створено ім\'я користувача. Ми проведемо вас крок за кроком, щоб створити обліковий запис.\nВи зможете спілкуватися з користувачами інших постачальників, для цього повідомите їм свою повну адресу XMPP. + Ваше запрошення до сервера + Неправильно відформатований код забезпечення + \ No newline at end of file diff --git a/app/src/conversations/res/values-vi/strings.xml b/app/src/conversations/res/values-vi/strings.xml new file mode 100644 index 000000000..f80ceacf8 --- /dev/null +++ b/app/src/conversations/res/values-vi/strings.xml @@ -0,0 +1,16 @@ + + + Chọn nhà cung cấp XMPP của bạn + Sử dụng conversations.im + Tạo tài khoản mới + Bạn đã có tài khoản XMPP chưa? Điều này có thể đúng nếu bạn đang dùng một ứng dụng khách cho XMPP khác hoặc đã sử dụng Conversations trước đó. Nếu không, bạn có thể tạo tài khoản XMPP mới ngay bây giờ.\nGợi ý: Một số nhà cung cấp email cũng cung cấp tài khoản XMPP. + XMPP là một mạng nhắn tin ngay lập tức không phụ thuộc vào nhà cung cấp. Bạn có thể sử dụng ứng dụng khách này với bất kỳ máy chủ XMPP nào mà bạn chọn.\nTuy nhiên, vì sự thuận tiện của bạn, chúng tôi đã làm cho việc tạo tài khoản trên conversations.im được dễ dàng; một nhà cung cấp đặc biệt phù hợp với việc sử dụng Conversations. + Bạn đã được mời vào %1$s. Chúng tôi sẽ hướng dẫn bạn trong quá trình tạo tài khoản.\nKhi chọn %1$s là nhà cung cấp, bạn sẽ có thể giao tiếp với những người dùng của các nhà cung cấp khác bằng cách đưa cho họ địa chỉ XMPP đầy đủ của bạn. + Bạn đã được mời vào %1$s. Một tên người dùng đã được chọn sẵn cho bạn. Chúng tôi sẽ hướng dẫn bạn trong quá trình tạo tài khoản.\nBạn sẽ có thể giao tiếp với những người dùng của các nhà cung cấp khác bằng cách đưa cho họ địa chỉ XMPP đầy đủ của bạn. + Lời mời vào máy chủ của bạn + Mã cung cấp không được định dạng đúng + Nhấn nút chia sẻ để gửi lời mời vào %1$s đến liên hệ của bạn. + Nếu liên hệ của bạn ở gần đây, họ cũng có thể quét mã ở dưới để chấp nhận lời mời của bạn. + Hãy tham gia vào %1$s và trò chuyện với tôi: %2$s + Chia sẻ lời mời với... + \ No newline at end of file diff --git a/app/src/conversations/res/values-zh-rCN/strings.xml b/app/src/conversations/res/values-zh-rCN/strings.xml new file mode 100644 index 000000000..961973b8c --- /dev/null +++ b/app/src/conversations/res/values-zh-rCN/strings.xml @@ -0,0 +1,16 @@ + + + 选择您的 XMPP 提供者 + 使用 conversations.im + 创建新账户 + 您有 XMPP 账户吗?如果您之前使用过其他的 XMPP 客户端,那么您已经拥有这种账户了。如果没有的话,您现在可以创建一个。\n提示:有些电子邮件服务也提供XMPP账户。 + XMPP 是独立于提供者的即时消息网络。您可以将此客户端与任意 XMPP 服务器一同使用。\n不过,您可以很容易地在 conversations.im 上创建账户;它是特别适合与“Conversations”一起使用的提供者。 + 您已受邀加入 %1$s。我们将指导您完成创建帐户的过程。\n使用 %1$s 作为提供者时,您可以通过您的完整 XMPP 地址与其他提供者的用户进行交流。 + 您已受邀加入 %1$s。已为您选择了一个用户名。我们将指导您完成创建帐户的过程。\n您可以使用完整的 XMPP 地址来与其他提供者的用户进行交流。 + 你的服务器邀请 + 格式不正确的配置代码 + 点击分享按钮向您的联系人发送加入 %1$s 的邀请。 + 如果你的联系人在附近,他们也可以扫描下面的代码来接受你的邀请。 + 加入 %1$s 和我聊天:%2$s + 分享邀请… + \ No newline at end of file diff --git a/app/src/conversations/res/values-zh-rTW/strings.xml b/app/src/conversations/res/values-zh-rTW/strings.xml new file mode 100644 index 000000000..8f1828bf6 --- /dev/null +++ b/app/src/conversations/res/values-zh-rTW/strings.xml @@ -0,0 +1,16 @@ + + + 挑選您的 XMPP 提供者 + 使用 conversations.im + 建立新帳戶 + 您已經擁有一個 XMPP 賬戶嗎?如果您之前使用過其他 XMPP 客戶端或 Conversations 的話,那麼您已經擁有 XMPP 賬戶了。如果沒有賬戶的話,您可以現在建立一個。\n提示:有些電子郵件服務供應商也會提供 XMPP 賬戶。 + XMPP 是提供者無關的即時訊息網絡。 任何你選擇的 XMPP 伺服器都可在此客戶端上使用。\n不過,我們令它在 Coversations.im 中建立帳戶變得更方便; Conversations.im 是特別適合 Conversations 的提供者 + 你已受邀參加 %1$s 。 我們將指導你完成建立帳戶的過程。選擇 %1$s 作爲提供者後,你可以將你完整的 XMPP 地址交給使用其他提供者的用戶,你便能與他們交流。 + 您已被邀請參加 %1$s 。 我們已經爲你選擇了一個用戶名。 我們將指導你完成建立帳戶的過程。\n將你完整的 XMPP 地址交給使用其他提供者的用戶後,你便能與他們交流。 + 您的伺服器邀請 + 配置代碼格式不正確 + 輕觸分享按鍵向您的聯絡人發送加入 %1$s 的邀請。 + 如果你的聯絡人就在附近,他們也可以掃描下面的代碼來接受你的邀請。 + 加入 %1$s 和我聊天:%2$s + 分享邀請到... + \ No newline at end of file diff --git a/src/main/res/values/ic_launcher_background.xml b/app/src/conversations/res/values/ic_launcher_background.xml similarity index 100% rename from src/main/res/values/ic_launcher_background.xml rename to app/src/conversations/res/values/ic_launcher_background.xml diff --git a/app/src/conversations/res/values/strings.xml b/app/src/conversations/res/values/strings.xml new file mode 100644 index 000000000..fffee31d6 --- /dev/null +++ b/app/src/conversations/res/values/strings.xml @@ -0,0 +1,16 @@ + + + Pick your XMPP provider + Use conversations.im + Create new account + Do you already have an XMPP account? This might be the case if you are already using a different XMPP client or have used Conversations before. If not you can create a new XMPP account right now.\nHint: Some email providers also provide XMPP accounts. + XMPP is a provider independent instant messaging network. You can use this client with what ever XMPP server you choose.\nHowever for your convenience we made it easy to create an account on conversations.im; a provider specially suited for the use with Conversations. + You have been invited to %1$s. We will guide you through the process of creating an account.\nWhen picking %1$s as a provider you will be able to communicate with users of other providers by giving them your full XMPP address. + You have been invited to %1$s. A username has already been picked for you. We will guide you through the process of creating an account.\nYou will be able to communicate with users of other providers by giving them your full XMPP address. + Your server invitation + Improperly formatted provisioning code + Tap the share button to send your contact an invitation to %1$s. + If your contact is nearby, they can also scan the code below to accept your invitation. + Join %1$s and chat with me: %2$s + Share invite with… + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..808baf278 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/assets/logback.xml b/app/src/main/assets/logback.xml similarity index 100% rename from src/main/assets/logback.xml rename to app/src/main/assets/logback.xml diff --git a/src/main/java/im/conversations/android/Conversations.java b/app/src/main/java/im/conversations/android/Conversations.java similarity index 79% rename from src/main/java/im/conversations/android/Conversations.java rename to app/src/main/java/im/conversations/android/Conversations.java index f34ffed9c..16005816d 100644 --- a/src/main/java/im/conversations/android/Conversations.java +++ b/app/src/main/java/im/conversations/android/Conversations.java @@ -1,7 +1,11 @@ package im.conversations.android; import android.app.Application; + +import androidx.appcompat.app.AppCompatDelegate; + import com.google.android.material.color.DynamicColors; +import im.conversations.android.dns.Resolver; import im.conversations.android.xmpp.ConnectionPool; import java.security.SecureRandom; import java.security.Security; @@ -23,7 +27,9 @@ public class Conversations extends Application { } catch (final Throwable throwable) { LOGGER.warn("Could not initialize security provider", throwable); } + Resolver.init(this); ConnectionPool.getInstance(this).reconfigure(); + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); //For night mode theme DynamicColors.applyToActivitiesIfAvailable(this); } } diff --git a/src/main/java/im/conversations/android/IDs.java b/app/src/main/java/im/conversations/android/IDs.java similarity index 100% rename from src/main/java/im/conversations/android/IDs.java rename to app/src/main/java/im/conversations/android/IDs.java diff --git a/src/main/java/im/conversations/android/database/AxolotlDatabaseStore.java b/app/src/main/java/im/conversations/android/database/AxolotlDatabaseStore.java similarity index 100% rename from src/main/java/im/conversations/android/database/AxolotlDatabaseStore.java rename to app/src/main/java/im/conversations/android/database/AxolotlDatabaseStore.java diff --git a/src/main/java/im/conversations/android/database/ConversationsDatabase.java b/app/src/main/java/im/conversations/android/database/ConversationsDatabase.java similarity index 100% rename from src/main/java/im/conversations/android/database/ConversationsDatabase.java rename to app/src/main/java/im/conversations/android/database/ConversationsDatabase.java diff --git a/src/main/java/im/conversations/android/database/Converters.java b/app/src/main/java/im/conversations/android/database/Converters.java similarity index 100% rename from src/main/java/im/conversations/android/database/Converters.java rename to app/src/main/java/im/conversations/android/database/Converters.java diff --git a/src/main/java/im/conversations/android/database/CredentialStore.java b/app/src/main/java/im/conversations/android/database/CredentialStore.java similarity index 100% rename from src/main/java/im/conversations/android/database/CredentialStore.java rename to app/src/main/java/im/conversations/android/database/CredentialStore.java diff --git a/src/main/java/im/conversations/android/database/dao/AccountDao.java b/app/src/main/java/im/conversations/android/database/dao/AccountDao.java similarity index 100% rename from src/main/java/im/conversations/android/database/dao/AccountDao.java rename to app/src/main/java/im/conversations/android/database/dao/AccountDao.java diff --git a/src/main/java/im/conversations/android/database/dao/AvatarDao.java b/app/src/main/java/im/conversations/android/database/dao/AvatarDao.java similarity index 100% rename from src/main/java/im/conversations/android/database/dao/AvatarDao.java rename to app/src/main/java/im/conversations/android/database/dao/AvatarDao.java diff --git a/src/main/java/im/conversations/android/database/dao/AxolotlDao.java b/app/src/main/java/im/conversations/android/database/dao/AxolotlDao.java similarity index 100% rename from src/main/java/im/conversations/android/database/dao/AxolotlDao.java rename to app/src/main/java/im/conversations/android/database/dao/AxolotlDao.java diff --git a/src/main/java/im/conversations/android/database/dao/BlockingDao.java b/app/src/main/java/im/conversations/android/database/dao/BlockingDao.java similarity index 100% rename from src/main/java/im/conversations/android/database/dao/BlockingDao.java rename to app/src/main/java/im/conversations/android/database/dao/BlockingDao.java diff --git a/src/main/java/im/conversations/android/database/dao/BookmarkDao.java b/app/src/main/java/im/conversations/android/database/dao/BookmarkDao.java similarity index 100% rename from src/main/java/im/conversations/android/database/dao/BookmarkDao.java rename to app/src/main/java/im/conversations/android/database/dao/BookmarkDao.java diff --git a/src/main/java/im/conversations/android/database/dao/ChatDao.java b/app/src/main/java/im/conversations/android/database/dao/ChatDao.java similarity index 100% rename from src/main/java/im/conversations/android/database/dao/ChatDao.java rename to app/src/main/java/im/conversations/android/database/dao/ChatDao.java diff --git a/src/main/java/im/conversations/android/database/dao/DiscoDao.java b/app/src/main/java/im/conversations/android/database/dao/DiscoDao.java similarity index 100% rename from src/main/java/im/conversations/android/database/dao/DiscoDao.java rename to app/src/main/java/im/conversations/android/database/dao/DiscoDao.java diff --git a/src/main/java/im/conversations/android/database/dao/MessageDao.java b/app/src/main/java/im/conversations/android/database/dao/MessageDao.java similarity index 100% rename from src/main/java/im/conversations/android/database/dao/MessageDao.java rename to app/src/main/java/im/conversations/android/database/dao/MessageDao.java diff --git a/src/main/java/im/conversations/android/database/dao/NickDao.java b/app/src/main/java/im/conversations/android/database/dao/NickDao.java similarity index 100% rename from src/main/java/im/conversations/android/database/dao/NickDao.java rename to app/src/main/java/im/conversations/android/database/dao/NickDao.java diff --git a/src/main/java/im/conversations/android/database/dao/PresenceDao.java b/app/src/main/java/im/conversations/android/database/dao/PresenceDao.java similarity index 100% rename from src/main/java/im/conversations/android/database/dao/PresenceDao.java rename to app/src/main/java/im/conversations/android/database/dao/PresenceDao.java diff --git a/src/main/java/im/conversations/android/database/dao/RosterDao.java b/app/src/main/java/im/conversations/android/database/dao/RosterDao.java similarity index 100% rename from src/main/java/im/conversations/android/database/dao/RosterDao.java rename to app/src/main/java/im/conversations/android/database/dao/RosterDao.java diff --git a/src/main/java/im/conversations/android/database/entity/AccountEntity.java b/app/src/main/java/im/conversations/android/database/entity/AccountEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/AccountEntity.java rename to app/src/main/java/im/conversations/android/database/entity/AccountEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/AvatarAdditionalEntity.java b/app/src/main/java/im/conversations/android/database/entity/AvatarAdditionalEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/AvatarAdditionalEntity.java rename to app/src/main/java/im/conversations/android/database/entity/AvatarAdditionalEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/AvatarEntity.java b/app/src/main/java/im/conversations/android/database/entity/AvatarEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/AvatarEntity.java rename to app/src/main/java/im/conversations/android/database/entity/AvatarEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/AxolotlDeviceListEntity.java b/app/src/main/java/im/conversations/android/database/entity/AxolotlDeviceListEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/AxolotlDeviceListEntity.java rename to app/src/main/java/im/conversations/android/database/entity/AxolotlDeviceListEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/AxolotlDeviceListItemEntity.java b/app/src/main/java/im/conversations/android/database/entity/AxolotlDeviceListItemEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/AxolotlDeviceListItemEntity.java rename to app/src/main/java/im/conversations/android/database/entity/AxolotlDeviceListItemEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/AxolotlIdentityEntity.java b/app/src/main/java/im/conversations/android/database/entity/AxolotlIdentityEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/AxolotlIdentityEntity.java rename to app/src/main/java/im/conversations/android/database/entity/AxolotlIdentityEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/AxolotlIdentityKeyPairEntity.java b/app/src/main/java/im/conversations/android/database/entity/AxolotlIdentityKeyPairEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/AxolotlIdentityKeyPairEntity.java rename to app/src/main/java/im/conversations/android/database/entity/AxolotlIdentityKeyPairEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/AxolotlPreKeyEntity.java b/app/src/main/java/im/conversations/android/database/entity/AxolotlPreKeyEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/AxolotlPreKeyEntity.java rename to app/src/main/java/im/conversations/android/database/entity/AxolotlPreKeyEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/AxolotlSessionEntity.java b/app/src/main/java/im/conversations/android/database/entity/AxolotlSessionEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/AxolotlSessionEntity.java rename to app/src/main/java/im/conversations/android/database/entity/AxolotlSessionEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/AxolotlSignedPreKeyEntity.java b/app/src/main/java/im/conversations/android/database/entity/AxolotlSignedPreKeyEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/AxolotlSignedPreKeyEntity.java rename to app/src/main/java/im/conversations/android/database/entity/AxolotlSignedPreKeyEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/BlockedItemEntity.java b/app/src/main/java/im/conversations/android/database/entity/BlockedItemEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/BlockedItemEntity.java rename to app/src/main/java/im/conversations/android/database/entity/BlockedItemEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/BookmarkEntity.java b/app/src/main/java/im/conversations/android/database/entity/BookmarkEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/BookmarkEntity.java rename to app/src/main/java/im/conversations/android/database/entity/BookmarkEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/ChatEntity.java b/app/src/main/java/im/conversations/android/database/entity/ChatEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/ChatEntity.java rename to app/src/main/java/im/conversations/android/database/entity/ChatEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/DiscoEntity.java b/app/src/main/java/im/conversations/android/database/entity/DiscoEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/DiscoEntity.java rename to app/src/main/java/im/conversations/android/database/entity/DiscoEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/DiscoExtensionEntity.java b/app/src/main/java/im/conversations/android/database/entity/DiscoExtensionEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/DiscoExtensionEntity.java rename to app/src/main/java/im/conversations/android/database/entity/DiscoExtensionEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/DiscoExtensionFieldEntity.java b/app/src/main/java/im/conversations/android/database/entity/DiscoExtensionFieldEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/DiscoExtensionFieldEntity.java rename to app/src/main/java/im/conversations/android/database/entity/DiscoExtensionFieldEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/DiscoExtensionFieldValueEntity.java b/app/src/main/java/im/conversations/android/database/entity/DiscoExtensionFieldValueEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/DiscoExtensionFieldValueEntity.java rename to app/src/main/java/im/conversations/android/database/entity/DiscoExtensionFieldValueEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/DiscoFeatureEntity.java b/app/src/main/java/im/conversations/android/database/entity/DiscoFeatureEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/DiscoFeatureEntity.java rename to app/src/main/java/im/conversations/android/database/entity/DiscoFeatureEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/DiscoIdentityEntity.java b/app/src/main/java/im/conversations/android/database/entity/DiscoIdentityEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/DiscoIdentityEntity.java rename to app/src/main/java/im/conversations/android/database/entity/DiscoIdentityEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/DiscoItemEntity.java b/app/src/main/java/im/conversations/android/database/entity/DiscoItemEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/DiscoItemEntity.java rename to app/src/main/java/im/conversations/android/database/entity/DiscoItemEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/MessageContentEntity.java b/app/src/main/java/im/conversations/android/database/entity/MessageContentEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/MessageContentEntity.java rename to app/src/main/java/im/conversations/android/database/entity/MessageContentEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/MessageEntity.java b/app/src/main/java/im/conversations/android/database/entity/MessageEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/MessageEntity.java rename to app/src/main/java/im/conversations/android/database/entity/MessageEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/MessageReactionEntity.java b/app/src/main/java/im/conversations/android/database/entity/MessageReactionEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/MessageReactionEntity.java rename to app/src/main/java/im/conversations/android/database/entity/MessageReactionEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/MessageStateEntity.java b/app/src/main/java/im/conversations/android/database/entity/MessageStateEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/MessageStateEntity.java rename to app/src/main/java/im/conversations/android/database/entity/MessageStateEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/MessageVersionEntity.java b/app/src/main/java/im/conversations/android/database/entity/MessageVersionEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/MessageVersionEntity.java rename to app/src/main/java/im/conversations/android/database/entity/MessageVersionEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/NickEntity.java b/app/src/main/java/im/conversations/android/database/entity/NickEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/NickEntity.java rename to app/src/main/java/im/conversations/android/database/entity/NickEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/PresenceEntity.java b/app/src/main/java/im/conversations/android/database/entity/PresenceEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/PresenceEntity.java rename to app/src/main/java/im/conversations/android/database/entity/PresenceEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/RosterItemEntity.java b/app/src/main/java/im/conversations/android/database/entity/RosterItemEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/RosterItemEntity.java rename to app/src/main/java/im/conversations/android/database/entity/RosterItemEntity.java diff --git a/src/main/java/im/conversations/android/database/entity/RosterItemGroupEntity.java b/app/src/main/java/im/conversations/android/database/entity/RosterItemGroupEntity.java similarity index 100% rename from src/main/java/im/conversations/android/database/entity/RosterItemGroupEntity.java rename to app/src/main/java/im/conversations/android/database/entity/RosterItemGroupEntity.java diff --git a/src/main/java/im/conversations/android/database/model/Account.java b/app/src/main/java/im/conversations/android/database/model/Account.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/Account.java rename to app/src/main/java/im/conversations/android/database/model/Account.java diff --git a/src/main/java/im/conversations/android/database/model/AvatarBase.java b/app/src/main/java/im/conversations/android/database/model/AvatarBase.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/AvatarBase.java rename to app/src/main/java/im/conversations/android/database/model/AvatarBase.java diff --git a/src/main/java/im/conversations/android/database/model/AvatarExternal.java b/app/src/main/java/im/conversations/android/database/model/AvatarExternal.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/AvatarExternal.java rename to app/src/main/java/im/conversations/android/database/model/AvatarExternal.java diff --git a/src/main/java/im/conversations/android/database/model/AvatarThumbnail.java b/app/src/main/java/im/conversations/android/database/model/AvatarThumbnail.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/AvatarThumbnail.java rename to app/src/main/java/im/conversations/android/database/model/AvatarThumbnail.java diff --git a/src/main/java/im/conversations/android/database/model/ChatIdentifier.java b/app/src/main/java/im/conversations/android/database/model/ChatIdentifier.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/ChatIdentifier.java rename to app/src/main/java/im/conversations/android/database/model/ChatIdentifier.java diff --git a/src/main/java/im/conversations/android/database/model/ChatType.java b/app/src/main/java/im/conversations/android/database/model/ChatType.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/ChatType.java rename to app/src/main/java/im/conversations/android/database/model/ChatType.java diff --git a/src/main/java/im/conversations/android/database/model/Connection.java b/app/src/main/java/im/conversations/android/database/model/Connection.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/Connection.java rename to app/src/main/java/im/conversations/android/database/model/Connection.java diff --git a/src/main/java/im/conversations/android/database/model/Credential.java b/app/src/main/java/im/conversations/android/database/model/Credential.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/Credential.java rename to app/src/main/java/im/conversations/android/database/model/Credential.java diff --git a/src/main/java/im/conversations/android/database/model/MessageContent.java b/app/src/main/java/im/conversations/android/database/model/MessageContent.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/MessageContent.java rename to app/src/main/java/im/conversations/android/database/model/MessageContent.java diff --git a/src/main/java/im/conversations/android/database/model/MessageEmbedded.java b/app/src/main/java/im/conversations/android/database/model/MessageEmbedded.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/MessageEmbedded.java rename to app/src/main/java/im/conversations/android/database/model/MessageEmbedded.java diff --git a/src/main/java/im/conversations/android/database/model/MessageIdentifier.java b/app/src/main/java/im/conversations/android/database/model/MessageIdentifier.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/MessageIdentifier.java rename to app/src/main/java/im/conversations/android/database/model/MessageIdentifier.java diff --git a/src/main/java/im/conversations/android/database/model/MessageReaction.java b/app/src/main/java/im/conversations/android/database/model/MessageReaction.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/MessageReaction.java rename to app/src/main/java/im/conversations/android/database/model/MessageReaction.java diff --git a/src/main/java/im/conversations/android/database/model/MessageState.java b/app/src/main/java/im/conversations/android/database/model/MessageState.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/MessageState.java rename to app/src/main/java/im/conversations/android/database/model/MessageState.java diff --git a/src/main/java/im/conversations/android/database/model/MessageWithContentReactions.java b/app/src/main/java/im/conversations/android/database/model/MessageWithContentReactions.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/MessageWithContentReactions.java rename to app/src/main/java/im/conversations/android/database/model/MessageWithContentReactions.java diff --git a/src/main/java/im/conversations/android/database/model/Modification.java b/app/src/main/java/im/conversations/android/database/model/Modification.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/Modification.java rename to app/src/main/java/im/conversations/android/database/model/Modification.java diff --git a/src/main/java/im/conversations/android/database/model/PartType.java b/app/src/main/java/im/conversations/android/database/model/PartType.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/PartType.java rename to app/src/main/java/im/conversations/android/database/model/PartType.java diff --git a/src/main/java/im/conversations/android/database/model/PresenceShow.java b/app/src/main/java/im/conversations/android/database/model/PresenceShow.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/PresenceShow.java rename to app/src/main/java/im/conversations/android/database/model/PresenceShow.java diff --git a/src/main/java/im/conversations/android/database/model/PresenceType.java b/app/src/main/java/im/conversations/android/database/model/PresenceType.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/PresenceType.java rename to app/src/main/java/im/conversations/android/database/model/PresenceType.java diff --git a/src/main/java/im/conversations/android/database/model/Proxy.java b/app/src/main/java/im/conversations/android/database/model/Proxy.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/Proxy.java rename to app/src/main/java/im/conversations/android/database/model/Proxy.java diff --git a/src/main/java/im/conversations/android/database/model/StateType.java b/app/src/main/java/im/conversations/android/database/model/StateType.java similarity index 100% rename from src/main/java/im/conversations/android/database/model/StateType.java rename to app/src/main/java/im/conversations/android/database/model/StateType.java diff --git a/src/main/java/eu/siacs/conversations/utils/AndroidUsingExecLowPriority.java b/app/src/main/java/im/conversations/android/dns/AndroidUsingExecLowPriority.java similarity index 85% rename from src/main/java/eu/siacs/conversations/utils/AndroidUsingExecLowPriority.java rename to app/src/main/java/im/conversations/android/dns/AndroidUsingExecLowPriority.java index d6c421049..09d13813d 100644 --- a/src/main/java/eu/siacs/conversations/utils/AndroidUsingExecLowPriority.java +++ b/app/src/main/java/im/conversations/android/dns/AndroidUsingExecLowPriority.java @@ -8,9 +8,12 @@ * upon the condition that you accept all of the terms of either * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. */ +package im.conversations.android.dns; -package eu.siacs.conversations.utils; - +import de.measite.minidns.dnsserverlookup.AbstractDNSServerLookupMechanism; +import de.measite.minidns.dnsserverlookup.AndroidUsingReflection; +import de.measite.minidns.dnsserverlookup.DNSServerLookupMechanism; +import de.measite.minidns.util.PlatformDetection; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -19,14 +22,7 @@ import java.net.InetAddress; import java.util.HashSet; import java.util.logging.Level; -import de.measite.minidns.dnsserverlookup.AbstractDNSServerLookupMechanism; -import de.measite.minidns.dnsserverlookup.AndroidUsingReflection; -import de.measite.minidns.dnsserverlookup.DNSServerLookupMechanism; -import de.measite.minidns.util.PlatformDetection; - -/** - * Try to retrieve the list of DNS server by executing getprop. - */ +/** Try to retrieve the list of DNS server by executing getprop. */ public class AndroidUsingExecLowPriority extends AbstractDNSServerLookupMechanism { public static final DNSServerLookupMechanism INSTANCE = new AndroidUsingExecLowPriority(); @@ -41,8 +37,7 @@ public class AndroidUsingExecLowPriority extends AbstractDNSServerLookupMechanis try { Process process = Runtime.getRuntime().exec("getprop"); InputStream inputStream = process.getInputStream(); - LineNumberReader lnr = new LineNumberReader( - new InputStreamReader(inputStream)); + LineNumberReader lnr = new LineNumberReader(new InputStreamReader(inputStream)); String line; HashSet server = new HashSet<>(6); while ((line = lnr.readLine()) != null) { @@ -57,9 +52,11 @@ public class AndroidUsingExecLowPriority extends AbstractDNSServerLookupMechanis continue; } - if (property.endsWith(".dns") || property.endsWith(".dns1") || - property.endsWith(".dns2") || property.endsWith(".dns3") || - property.endsWith(".dns4")) { + if (property.endsWith(".dns") + || property.endsWith(".dns1") + || property.endsWith(".dns2") + || property.endsWith(".dns3") + || property.endsWith(".dns4")) { // normalize the address @@ -88,5 +85,4 @@ public class AndroidUsingExecLowPriority extends AbstractDNSServerLookupMechanis public boolean isAvailable() { return PlatformDetection.isAndroid(); } - -} \ No newline at end of file +} diff --git a/src/main/java/eu/siacs/conversations/utils/AndroidUsingLinkProperties.java b/app/src/main/java/im/conversations/android/dns/AndroidUsingLinkProperties.java similarity index 67% rename from src/main/java/eu/siacs/conversations/utils/AndroidUsingLinkProperties.java rename to app/src/main/java/im/conversations/android/dns/AndroidUsingLinkProperties.java index 2c846ff08..4a1a60ebe 100644 --- a/src/main/java/eu/siacs/conversations/utils/AndroidUsingLinkProperties.java +++ b/app/src/main/java/im/conversations/android/dns/AndroidUsingLinkProperties.java @@ -1,22 +1,18 @@ -package eu.siacs.conversations.utils; +package im.conversations.android.dns; -import android.annotation.TargetApi; import android.content.Context; import android.net.ConnectivityManager; import android.net.LinkProperties; import android.net.Network; import android.net.NetworkInfo; import android.net.RouteInfo; -import android.os.Build; - +import de.measite.minidns.dnsserverlookup.AbstractDNSServerLookupMechanism; +import de.measite.minidns.dnsserverlookup.AndroidUsingExec; import java.net.Inet4Address; import java.net.InetAddress; import java.util.ArrayList; import java.util.List; -import de.measite.minidns.dnsserverlookup.AbstractDNSServerLookupMechanism; -import de.measite.minidns.dnsserverlookup.AndroidUsingExec; - public class AndroidUsingLinkProperties extends AbstractDNSServerLookupMechanism { private final Context context; @@ -28,47 +24,47 @@ public class AndroidUsingLinkProperties extends AbstractDNSServerLookupMechanism @Override public boolean isAvailable() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; + return true; } @Override - @TargetApi(21) public String[] getDnsServerAddresses() { - final ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - final Network[] networks = connectivityManager == null ? null : connectivityManager.getAllNetworks(); + final ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + final Network[] networks = + connectivityManager == null ? null : connectivityManager.getAllNetworks(); if (networks == null) { return new String[0]; } - final Network activeNetwork = getActiveNetwork(connectivityManager); + final Network activeNetwork = connectivityManager.getActiveNetwork(); final List servers = new ArrayList<>(); int vpnOffset = 0; - for(Network network : networks) { + for (Network network : networks) { LinkProperties linkProperties = connectivityManager.getLinkProperties(network); if (linkProperties == null) { continue; } final NetworkInfo networkInfo = connectivityManager.getNetworkInfo(network); final boolean isActiveNetwork = network.equals(activeNetwork); - final boolean isVpn = networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_VPN; + final boolean isVpn = + networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_VPN; if (isActiveNetwork && isVpn) { final List tmp = getIPv4First(linkProperties.getDnsServers()); servers.addAll(0, tmp); vpnOffset += tmp.size(); - } else if (hasDefaultRoute(linkProperties) || isActiveNetwork || activeNetwork == null || isVpn) { + } else if (hasDefaultRoute(linkProperties) + || isActiveNetwork + || activeNetwork == null + || isVpn) { servers.addAll(vpnOffset, getIPv4First(linkProperties.getDnsServers())); } } return servers.toArray(new String[0]); } - @TargetApi(23) - private static Network getActiveNetwork(ConnectivityManager cm) { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? cm.getActiveNetwork() : null; - } - private static List getIPv4First(List in) { List out = new ArrayList<>(); - for(InetAddress address : in) { + for (InetAddress address : in) { if (address instanceof Inet4Address) { out.add(0, address.getHostAddress()); } else { @@ -78,9 +74,8 @@ public class AndroidUsingLinkProperties extends AbstractDNSServerLookupMechanism return out; } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) private static boolean hasDefaultRoute(LinkProperties linkProperties) { - for(RouteInfo route: linkProperties.getRoutes()) { + for (RouteInfo route : linkProperties.getRoutes()) { if (route.isDefaultRoute()) { return true; } diff --git a/app/src/main/java/im/conversations/android/dns/IP.java b/app/src/main/java/im/conversations/android/dns/IP.java new file mode 100644 index 000000000..b87e83561 --- /dev/null +++ b/app/src/main/java/im/conversations/android/dns/IP.java @@ -0,0 +1,39 @@ +package im.conversations.android.dns; + +import java.util.regex.Pattern; + +public class IP { + + private static final Pattern PATTERN_IPV4 = + Pattern.compile( + "\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = + Pattern.compile( + "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)" + + " ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_6HEX4DEC = + Pattern.compile( + "\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = + Pattern.compile( + "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z"); + private static final Pattern PATTERN_IPV6 = + Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z"); + + public static boolean matches(String server) { + return server != null + && (PATTERN_IPV4.matcher(server).matches() + || PATTERN_IPV6.matcher(server).matches() + || PATTERN_IPV6_6HEX4DEC.matcher(server).matches() + || PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches() + || PATTERN_IPV6_HEXCOMPRESSED.matcher(server).matches()); + } + + public static String wrapIPv6(final String host) { + if (matches(host)) { + return String.format("[%s]", host); + } else { + return host; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/utils/Resolver.java b/app/src/main/java/im/conversations/android/dns/Resolver.java similarity index 67% rename from src/main/java/eu/siacs/conversations/utils/Resolver.java rename to app/src/main/java/im/conversations/android/dns/Resolver.java index 0507e81b7..9a085d93a 100644 --- a/src/main/java/eu/siacs/conversations/utils/Resolver.java +++ b/app/src/main/java/im/conversations/android/dns/Resolver.java @@ -1,21 +1,10 @@ -package eu.siacs.conversations.utils; +package im.conversations.android.dns; +import android.app.Application; import android.content.ContentValues; +import android.content.Context; import android.database.Cursor; -import android.util.Log; - import androidx.annotation.NonNull; - -import java.io.IOException; -import java.lang.reflect.Field; -import java.net.Inet4Address; -import java.net.InetAddress; -import java.net.Socket; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - import de.measite.minidns.AbstractDNSClient; import de.measite.minidns.DNSCache; import de.measite.minidns.DNSClient; @@ -35,26 +24,34 @@ import de.measite.minidns.record.CNAME; import de.measite.minidns.record.Data; import de.measite.minidns.record.InternetAddressRR; import de.measite.minidns.record.SRV; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xmpp.Jid; +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.jxmpp.jid.DomainJid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class Resolver { + private static final Logger LOGGER = LoggerFactory.getLogger(Resolver.class); + public static final int DEFAULT_PORT_XMPP = 5222; private static final String DIRECT_TLS_SERVICE = "_xmpps-client"; private static final String STARTTLS_SERVICE = "_xmpp-client"; - private static XmppConnectionService SERVICE = null; + private static Context SERVICE; - - public static void init(XmppConnectionService service) { - Resolver.SERVICE = service; + public static void init(final Application application) { + SERVICE = application.getApplicationContext(); DNSClient.removeDNSServerLookupMechanism(AndroidUsingExec.INSTANCE); DNSClient.addDnsServerLookupMechanism(AndroidUsingExecLowPriority.INSTANCE); - DNSClient.addDnsServerLookupMechanism(new AndroidUsingLinkProperties(service)); + DNSClient.addDnsServerLookupMechanism(new AndroidUsingLinkProperties(application)); final AbstractDNSClient client = ResolverApi.INSTANCE.getClient(); if (client instanceof ReliableDNSClient) { disableHardcodedDnsServers((ReliableDNSClient) client); @@ -69,11 +66,12 @@ public class Resolver { if (dnsClient != null) { dnsClient.getDataSource().setTimeout(3000); } - final Field useHardcodedDnsServers = DNSClient.class.getDeclaredField("useHardcodedDnsServers"); + final Field useHardcodedDnsServers = + DNSClient.class.getDeclaredField("useHardcodedDnsServers"); useHardcodedDnsServers.setAccessible(true); useHardcodedDnsServers.setBoolean(dnsClient, false); - } catch (NoSuchFieldException | IllegalAccessException e) { - Log.e(Config.LOGTAG, "Unable to disable hardcoded DNS servers", e); + } catch (final NoSuchFieldException | IllegalAccessException e) { + LOGGER.error("Unable to disable hardcoded DNS servers", e); } } @@ -86,7 +84,7 @@ public class Resolver { return Collections.singletonList(result); } - public static void checkDomain(final Jid jid) { + public static void checkDomain(final DomainJid jid) { DNSName.from(jid.getDomain()); } @@ -103,50 +101,57 @@ public class Resolver { final AbstractDNSClient client = ResolverApi.INSTANCE.getClient(); final DNSCache dnsCache = client.getCache(); if (dnsCache instanceof LRUCache) { - Log.d(Config.LOGTAG,"clearing DNS cache"); + LOGGER.debug("clearing DNS cache"); ((LRUCache) dnsCache).clear(); } } - public static boolean useDirectTls(final int port) { return port == 443 || port == 5223; } public static List resolve(String domain) { - final List ipResults = fromIpAddress(domain); + final List ipResults = fromIpAddress(domain); if (ipResults.size() > 0) { return ipResults; } final List results = new ArrayList<>(); final List fallbackResults = new ArrayList<>(); final Thread[] threads = new Thread[3]; - threads[0] = new Thread(() -> { - try { - final List list = resolveSrv(domain, true); - synchronized (results) { - results.addAll(list); - } - } catch (Throwable throwable) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (direct TLS)", throwable); - } - }); - threads[1] = new Thread(() -> { - try { - final List list = resolveSrv(domain, false); - synchronized (results) { - results.addAll(list); - } - } catch (Throwable throwable) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (STARTTLS)", throwable); - } - }); - threads[2] = new Thread(() -> { - List list = resolveNoSrvRecords(DNSName.from(domain), true); - synchronized (fallbackResults) { - fallbackResults.addAll(list); - } - }); + threads[0] = + new Thread( + () -> { + try { + final List list = resolveSrv(domain, true); + synchronized (results) { + results.addAll(list); + } + } catch (final Throwable throwable) { + LOGGER.debug("error resolving SRV record (direct TLS)", throwable); + } + }); + threads[1] = + new Thread( + () -> { + try { + final List list = resolveSrv(domain, false); + synchronized (results) { + results.addAll(list); + } + + } catch (Throwable throwable) { + LOGGER.debug( + "error resolving SRV record (direct STARTTLS)", throwable); + } + }); + threads[2] = + new Thread( + () -> { + List list = resolveNoSrvRecords(DNSName.from(domain), true); + synchronized (fallbackResults) { + fallbackResults.addAll(list); + } + }); for (final Thread thread : threads) { thread.start(); } @@ -157,14 +162,14 @@ public class Resolver { threads[2].interrupt(); synchronized (results) { Collections.sort(results); - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results.toString()); + LOGGER.info("{}", results); return new ArrayList<>(results); } } else { threads[2].join(); synchronized (fallbackResults) { Collections.sort(fallbackResults); - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + fallbackResults.toString()); + LOGGER.info("fallback {}", fallbackResults); return new ArrayList<>(fallbackResults); } } @@ -190,8 +195,11 @@ public class Resolver { } } - private static List resolveSrv(String domain, final boolean directTls) throws IOException { - DNSName dnsName = DNSName.from((directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain); + private static List resolveSrv(String domain, final boolean directTls) + throws IOException { + DNSName dnsName = + DNSName.from( + (directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain); ResolverResult result = resolveWithFallback(dnsName, SRV.class); final List results = new ArrayList<>(); final List threads = new ArrayList<>(); @@ -199,24 +207,37 @@ public class Resolver { if (record.name.length() == 0 && record.priority == 0) { continue; } - threads.add(new Thread(() -> { - final List ipv4s = resolveIp(record, A.class, result.isAuthenticData(), directTls); - if (ipv4s.size() == 0) { - Result resolverResult = Result.fromRecord(record, directTls); - resolverResult.authenticated = result.isAuthenticData(); - ipv4s.add(resolverResult); - } - synchronized (results) { - results.addAll(ipv4s); - } - - })); - threads.add(new Thread(() -> { - final List ipv6s = resolveIp(record, AAAA.class, result.isAuthenticData(), directTls); - synchronized (results) { - results.addAll(ipv6s); - } - })); + threads.add( + new Thread( + () -> { + final List ipv4s = + resolveIp( + record, + A.class, + result.isAuthenticData(), + directTls); + if (ipv4s.size() == 0) { + Result resolverResult = Result.fromRecord(record, directTls); + resolverResult.authenticated = result.isAuthenticData(); + ipv4s.add(resolverResult); + } + synchronized (results) { + results.addAll(ipv4s); + } + })); + threads.add( + new Thread( + () -> { + final List ipv6s = + resolveIp( + record, + AAAA.class, + result.isAuthenticData(), + directTls); + synchronized (results) { + results.addAll(ipv6s); + } + })); } for (Thread thread : threads) { thread.start(); @@ -231,18 +252,22 @@ public class Resolver { return results; } - private static List resolveIp(SRV srv, Class type, boolean authenticated, boolean directTls) { + private static List resolveIp( + SRV srv, Class type, boolean authenticated, boolean directTls) { List list = new ArrayList<>(); try { ResolverResult results = resolveWithFallback(srv.name, type, authenticated); for (D record : results.getAnswersOrEmptySet()) { Result resolverResult = Result.fromRecord(srv, directTls); - resolverResult.authenticated = results.isAuthenticData() && authenticated; //TODO technically it doesn’t matter if the IP was authenticated + resolverResult.authenticated = + results.isAuthenticData() + && authenticated; // TODO technically it doesn’t matter if the IP + // was authenticated resolverResult.ip = record.getInetAddress(); list.add(resolverResult); } - } catch (Throwable t) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " " + t.getMessage()); + } catch (final Throwable t) { + LOGGER.info("error resolving {}", type.getSimpleName(), t); } return list; } @@ -253,26 +278,30 @@ public class Resolver { for (A a : resolveWithFallback(dnsName, A.class, false).getAnswersOrEmptySet()) { results.add(Result.createDefault(dnsName, a.getInetAddress())); } - for (AAAA aaaa : resolveWithFallback(dnsName, AAAA.class, false).getAnswersOrEmptySet()) { + for (AAAA aaaa : + resolveWithFallback(dnsName, AAAA.class, false).getAnswersOrEmptySet()) { results.add(Result.createDefault(dnsName, aaaa.getInetAddress())); } if (results.size() == 0 && withCnames) { - for (CNAME cname : resolveWithFallback(dnsName, CNAME.class, false).getAnswersOrEmptySet()) { + for (CNAME cname : + resolveWithFallback(dnsName, CNAME.class, false).getAnswersOrEmptySet()) { results.addAll(resolveNoSrvRecords(cname.name, false)); } } } catch (Throwable throwable) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable); + LOGGER.info("Error resolving fallback records", throwable); } results.add(Result.createDefault(dnsName)); return results; } - private static ResolverResult resolveWithFallback(DNSName dnsName, Class type) throws IOException { + private static ResolverResult resolveWithFallback( + DNSName dnsName, Class type) throws IOException { return resolveWithFallback(dnsName, type, validateHostname()); } - private static ResolverResult resolveWithFallback(DNSName dnsName, Class type, boolean validateHostname) throws IOException { + private static ResolverResult resolveWithFallback( + DNSName dnsName, Class type, boolean validateHostname) throws IOException { final Question question = new Question(dnsName, Record.TYPE.getType(type)); if (!validateHostname) { return ResolverApi.INSTANCE.resolve(question); @@ -280,17 +309,22 @@ public class Resolver { try { return DnssecResolverApi.INSTANCE.resolveDnssecReliable(question); } catch (DNSSECResultNotAuthenticException e) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e); + LOGGER.info( + "Error resolving {} with DNSSEC. Trying DNS instead", type.getSimpleName(), e); } catch (IOException e) { throw e; } catch (Throwable throwable) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable); + LOGGER.info( + "Error resolving {} with DNSSEC. Trying DNS instead", + type.getSimpleName(), + throwable); } return ResolverApi.INSTANCE.resolve(question); } private static boolean validateHostname() { - return SERVICE != null && SERVICE.getBooleanPreference("validate_hostname", R.bool.validate_hostname); + // TODO bring back in one form or another + return false; } public static class Result implements Comparable { @@ -301,20 +335,15 @@ public class Resolver { public static final String PRIORITY = "priority"; public static final String DIRECT_TLS = "directTls"; public static final String AUTHENTICATED = "authenticated"; - - public static final String TIME_REQUESTED = "time_requested"; private InetAddress ip; private DNSName hostname; private int port = DEFAULT_PORT_XMPP; private boolean directTls = false; private boolean authenticated = false; private int priority; - private long timeRequested; - private Socket socket; static Result fromRecord(SRV srv, boolean directTls) { Result result = new Result(); - result.timeRequested = System.currentTimeMillis(); result.port = srv.port; result.hostname = srv.name; result.directTls = directTls; @@ -324,7 +353,6 @@ public class Resolver { static Result createDefault(DNSName hostname, InetAddress ip) { Result result = new Result(); - result.timeRequested = System.currentTimeMillis(); result.port = DEFAULT_PORT_XMPP; result.hostname = hostname; result.ip = ip; @@ -348,7 +376,6 @@ public class Resolver { result.priority = cursor.getInt(cursor.getColumnIndex(PRIORITY)); result.authenticated = cursor.getInt(cursor.getColumnIndex(AUTHENTICATED)) > 0; result.directTls = cursor.getInt(cursor.getColumnIndex(DIRECT_TLS)) > 0; - result.timeRequested = cursor.getLong(cursor.getColumnIndex(TIME_REQUESTED)); return result; } @@ -400,14 +427,22 @@ public class Resolver { @Override public String toString() { - return "Result{" + - "ip='" + (ip == null ? null : ip.getHostAddress()) + '\'' + - ", hostame='" + (hostname == null ? null : hostname.toString()) + '\'' + - ", port=" + port + - ", directTls=" + directTls + - ", authenticated=" + authenticated + - ", priority=" + priority + - '}'; + return "Result{" + + "ip='" + + (ip == null ? null : ip.getHostAddress()) + + '\'' + + ", hostame='" + + (hostname == null ? null : hostname.toString()) + + '\'' + + ", port=" + + port + + ", directTls=" + + directTls + + ", authenticated=" + + authenticated + + ", priority=" + + priority + + '}'; } @Override @@ -441,18 +476,7 @@ public class Resolver { contentValues.put(PRIORITY, priority); contentValues.put(DIRECT_TLS, directTls ? 1 : 0); contentValues.put(AUTHENTICATED, authenticated ? 1 : 0); - contentValues.put(TIME_REQUESTED, timeRequested); return contentValues; } - - - public Socket getSocket() { - return socket; - } - public boolean isOutdated() { - return (System.currentTimeMillis() - timeRequested) > 300_000; - } - } - } diff --git a/src/main/java/im/conversations/android/repository/AbstractRepository.java b/app/src/main/java/im/conversations/android/repository/AbstractRepository.java similarity index 100% rename from src/main/java/im/conversations/android/repository/AbstractRepository.java rename to app/src/main/java/im/conversations/android/repository/AbstractRepository.java diff --git a/src/main/java/im/conversations/android/repository/AccountRepository.java b/app/src/main/java/im/conversations/android/repository/AccountRepository.java similarity index 100% rename from src/main/java/im/conversations/android/repository/AccountRepository.java rename to app/src/main/java/im/conversations/android/repository/AccountRepository.java diff --git a/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java b/app/src/main/java/im/conversations/android/socks/SocksSocketFactory.java similarity index 77% rename from src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java rename to app/src/main/java/im/conversations/android/socks/SocksSocketFactory.java index df68548d0..aa01901ad 100644 --- a/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java +++ b/app/src/main/java/im/conversations/android/socks/SocksSocketFactory.java @@ -1,7 +1,7 @@ -package eu.siacs.conversations.utils; +package im.conversations.android.socks; import com.google.common.io.ByteStreams; - +import im.conversations.android.xmpp.ConnectionPool; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -10,15 +10,16 @@ import java.net.InetSocketAddress; import java.net.Socket; import java.nio.ByteBuffer; -import eu.siacs.conversations.Config; - public class SocksSocketFactory { - 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 + 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.write(new byte[] {0x05, 0x01, 0x00}); proxyOs.flush(); final byte[] handshake = new byte[2]; ByteStreams.readFully(proxyIs, handshake); @@ -27,7 +28,7 @@ public class SocksSocketFactory { } final byte[] dest = destination.getBytes(); final ByteBuffer request = ByteBuffer.allocate(7 + dest.length); - request.put(new byte[]{0x05, 0x01, 0x00, 0x03}); + request.put(new byte[] {0x05, 0x01, 0x00, 0x03}); request.put((byte) dest.length); request.put(dest); request.putShort((short) port); @@ -46,7 +47,10 @@ public class SocksSocketFactory { 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)); + throw new IOException( + String.format( + "Destination mismatch. Received %s Expected %s", + receivedDestination, destination)); } } ByteStreams.readFully(proxyIs, bndPort); @@ -61,7 +65,8 @@ public class SocksSocketFactory { } } - private static byte[] readDestination(final byte type, final InputStream inputStream) throws IOException { + private static byte[] readDestination(final byte type, final InputStream inputStream) + throws IOException { final byte[] bndDestination; if (type == 0x01) { bndDestination = new byte[4]; @@ -86,10 +91,11 @@ public class SocksSocketFactory { return false; } - private static Socket createSocket(InetSocketAddress address, String destination, int port) throws IOException { + private static Socket createSocket(InetSocketAddress address, String destination, int port) + throws IOException { Socket socket = new Socket(); try { - socket.connect(address, Config.CONNECT_TIMEOUT * 1000); + socket.connect(address, ConnectionPool.CONNECT_TIMEOUT * 1000); } catch (IOException e) { throw new SocksProxyNotFoundException(); } @@ -98,11 +104,10 @@ public class SocksSocketFactory { } public static Socket createSocketOverTor(String destination, int port) throws IOException { - return createSocket(new InetSocketAddress(InetAddress.getLocalHost(), 9050), destination, port); - } - - public static Socket createSocketOverI2P(String destination, int port) throws IOException { - return createSocket(new InetSocketAddress(InetAddress.getLocalHost(), 4447), destination, port); + return createSocket( + new InetSocketAddress(InetAddress.getByAddress(LOCALHOST), 9050), + destination, + port); } private static class SocksConnectionException extends IOException { @@ -111,9 +116,7 @@ public class SocksSocketFactory { } } - public static class SocksProxyNotFoundException extends IOException { - - } + public static class SocksProxyNotFoundException extends IOException {} public static class HostNotFoundException extends SocksConnectionException { HostNotFoundException(String message) { diff --git a/src/main/java/im/conversations/android/tls/SSLSockets.java b/app/src/main/java/im/conversations/android/tls/SSLSockets.java similarity index 100% rename from src/main/java/im/conversations/android/tls/SSLSockets.java rename to app/src/main/java/im/conversations/android/tls/SSLSockets.java diff --git a/app/src/main/java/im/conversations/android/tls/TrustManagers.java b/app/src/main/java/im/conversations/android/tls/TrustManagers.java new file mode 100644 index 000000000..a7a7aa8f2 --- /dev/null +++ b/app/src/main/java/im/conversations/android/tls/TrustManagers.java @@ -0,0 +1,29 @@ +package im.conversations.android.tls; + +import java.security.KeyStore; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TrustManagers { + + private static final Logger LOGGER = LoggerFactory.getLogger(TrustManagers.class); + + public static X509TrustManager getTrustManager() { + try { + final TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); + tmf.init((KeyStore) null); + for (final TrustManager t : tmf.getTrustManagers()) { + if (t instanceof X509TrustManager) { + return (X509TrustManager) t; + } + } + return null; + } catch (final Exception e) { + LOGGER.info("Could not get default Trust Manager"); + return null; + } + } +} diff --git a/src/main/java/im/conversations/android/tls/XmppDomainVerifier.java b/app/src/main/java/im/conversations/android/tls/XmppDomainVerifier.java similarity index 100% rename from src/main/java/im/conversations/android/tls/XmppDomainVerifier.java rename to app/src/main/java/im/conversations/android/tls/XmppDomainVerifier.java diff --git a/src/main/java/im/conversations/android/transformer/Transformation.java b/app/src/main/java/im/conversations/android/transformer/Transformation.java similarity index 100% rename from src/main/java/im/conversations/android/transformer/Transformation.java rename to app/src/main/java/im/conversations/android/transformer/Transformation.java diff --git a/src/main/java/im/conversations/android/transformer/TransformationFactory.java b/app/src/main/java/im/conversations/android/transformer/TransformationFactory.java similarity index 100% rename from src/main/java/im/conversations/android/transformer/TransformationFactory.java rename to app/src/main/java/im/conversations/android/transformer/TransformationFactory.java diff --git a/src/main/java/im/conversations/android/transformer/Transformer.java b/app/src/main/java/im/conversations/android/transformer/Transformer.java similarity index 100% rename from src/main/java/im/conversations/android/transformer/Transformer.java rename to app/src/main/java/im/conversations/android/transformer/Transformer.java diff --git a/src/main/java/im/conversations/android/ui/Activities.java b/app/src/main/java/im/conversations/android/ui/Activities.java similarity index 100% rename from src/main/java/im/conversations/android/ui/Activities.java rename to app/src/main/java/im/conversations/android/ui/Activities.java diff --git a/src/main/java/im/conversations/android/ui/BindingAdapters.java b/app/src/main/java/im/conversations/android/ui/BindingAdapters.java similarity index 100% rename from src/main/java/im/conversations/android/ui/BindingAdapters.java rename to app/src/main/java/im/conversations/android/ui/BindingAdapters.java diff --git a/src/main/java/im/conversations/android/ui/Event.java b/app/src/main/java/im/conversations/android/ui/Event.java similarity index 100% rename from src/main/java/im/conversations/android/ui/Event.java rename to app/src/main/java/im/conversations/android/ui/Event.java diff --git a/src/main/java/im/conversations/android/ui/NavControllers.java b/app/src/main/java/im/conversations/android/ui/NavControllers.java similarity index 100% rename from src/main/java/im/conversations/android/ui/NavControllers.java rename to app/src/main/java/im/conversations/android/ui/NavControllers.java diff --git a/src/main/java/im/conversations/android/ui/activity/SetupActivity.java b/app/src/main/java/im/conversations/android/ui/activity/SetupActivity.java similarity index 90% rename from src/main/java/im/conversations/android/ui/activity/SetupActivity.java rename to app/src/main/java/im/conversations/android/ui/activity/SetupActivity.java index ca814d840..6ecc943bc 100644 --- a/src/main/java/im/conversations/android/ui/activity/SetupActivity.java +++ b/app/src/main/java/im/conversations/android/ui/activity/SetupActivity.java @@ -2,12 +2,13 @@ package im.conversations.android.ui.activity; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; import androidx.databinding.DataBindingUtil; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.NavController; -import eu.siacs.conversations.R; -import eu.siacs.conversations.SetupNavigationDirections; -import eu.siacs.conversations.databinding.ActivitySetupBinding; +import im.conversations.android.R; +import im.conversations.android.SetupNavigationDirections; +import im.conversations.android.databinding.ActivitySetupBinding; import im.conversations.android.ui.Activities; import im.conversations.android.ui.Event; import im.conversations.android.ui.NavControllers; diff --git a/src/main/java/im/conversations/android/ui/fragment/setup/AbstractSetupFragment.java b/app/src/main/java/im/conversations/android/ui/fragment/setup/AbstractSetupFragment.java similarity index 100% rename from src/main/java/im/conversations/android/ui/fragment/setup/AbstractSetupFragment.java rename to app/src/main/java/im/conversations/android/ui/fragment/setup/AbstractSetupFragment.java diff --git a/src/main/java/im/conversations/android/ui/fragment/setup/HostnameFragment.java b/app/src/main/java/im/conversations/android/ui/fragment/setup/HostnameFragment.java similarity index 100% rename from src/main/java/im/conversations/android/ui/fragment/setup/HostnameFragment.java rename to app/src/main/java/im/conversations/android/ui/fragment/setup/HostnameFragment.java diff --git a/src/main/java/im/conversations/android/ui/fragment/setup/PasswordFragment.java b/app/src/main/java/im/conversations/android/ui/fragment/setup/PasswordFragment.java similarity index 88% rename from src/main/java/im/conversations/android/ui/fragment/setup/PasswordFragment.java rename to app/src/main/java/im/conversations/android/ui/fragment/setup/PasswordFragment.java index 389aaca59..dda4f20a5 100644 --- a/src/main/java/im/conversations/android/ui/fragment/setup/PasswordFragment.java +++ b/app/src/main/java/im/conversations/android/ui/fragment/setup/PasswordFragment.java @@ -6,8 +6,8 @@ import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.databinding.DataBindingUtil; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.FragmentPasswordBinding; +import im.conversations.android.R; +import im.conversations.android.databinding.FragmentPasswordBinding; public class PasswordFragment extends AbstractSetupFragment { diff --git a/src/main/java/im/conversations/android/ui/fragment/setup/SignInFragment.java b/app/src/main/java/im/conversations/android/ui/fragment/setup/SignInFragment.java similarity index 88% rename from src/main/java/im/conversations/android/ui/fragment/setup/SignInFragment.java rename to app/src/main/java/im/conversations/android/ui/fragment/setup/SignInFragment.java index dac120a11..3bb96f74c 100644 --- a/src/main/java/im/conversations/android/ui/fragment/setup/SignInFragment.java +++ b/app/src/main/java/im/conversations/android/ui/fragment/setup/SignInFragment.java @@ -6,8 +6,8 @@ import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.databinding.DataBindingUtil; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.FragmentSignInBinding; +import im.conversations.android.R; +import im.conversations.android.databinding.FragmentSignInBinding; public class SignInFragment extends AbstractSetupFragment { diff --git a/src/main/java/im/conversations/android/ui/model/SetupViewModel.java b/app/src/main/java/im/conversations/android/ui/model/SetupViewModel.java similarity index 100% rename from src/main/java/im/conversations/android/ui/model/SetupViewModel.java rename to app/src/main/java/im/conversations/android/ui/model/SetupViewModel.java diff --git a/src/main/java/eu/siacs/conversations/ui/util/PendingItem.java b/app/src/main/java/im/conversations/android/util/PendingItem.java similarity index 98% rename from src/main/java/eu/siacs/conversations/ui/util/PendingItem.java rename to app/src/main/java/im/conversations/android/util/PendingItem.java index a02281f77..0a5d94f13 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/PendingItem.java +++ b/app/src/main/java/im/conversations/android/util/PendingItem.java @@ -27,7 +27,7 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package eu.siacs.conversations.ui.util; +package im.conversations.android.util; import java.util.function.Supplier; diff --git a/src/main/java/im/conversations/android/xml/Element.java b/app/src/main/java/im/conversations/android/xml/Element.java similarity index 100% rename from src/main/java/im/conversations/android/xml/Element.java rename to app/src/main/java/im/conversations/android/xml/Element.java diff --git a/src/main/java/im/conversations/android/xml/Entities.java b/app/src/main/java/im/conversations/android/xml/Entities.java similarity index 100% rename from src/main/java/im/conversations/android/xml/Entities.java rename to app/src/main/java/im/conversations/android/xml/Entities.java diff --git a/src/main/java/im/conversations/android/xml/Namespace.java b/app/src/main/java/im/conversations/android/xml/Namespace.java similarity index 100% rename from src/main/java/im/conversations/android/xml/Namespace.java rename to app/src/main/java/im/conversations/android/xml/Namespace.java diff --git a/src/main/java/im/conversations/android/xml/Tag.java b/app/src/main/java/im/conversations/android/xml/Tag.java similarity index 100% rename from src/main/java/im/conversations/android/xml/Tag.java rename to app/src/main/java/im/conversations/android/xml/Tag.java diff --git a/src/main/java/im/conversations/android/xml/TagWriter.java b/app/src/main/java/im/conversations/android/xml/TagWriter.java similarity index 100% rename from src/main/java/im/conversations/android/xml/TagWriter.java rename to app/src/main/java/im/conversations/android/xml/TagWriter.java diff --git a/src/main/java/im/conversations/android/xml/XmlElementReader.java b/app/src/main/java/im/conversations/android/xml/XmlElementReader.java similarity index 100% rename from src/main/java/im/conversations/android/xml/XmlElementReader.java rename to app/src/main/java/im/conversations/android/xml/XmlElementReader.java diff --git a/src/main/java/im/conversations/android/xml/XmlReader.java b/app/src/main/java/im/conversations/android/xml/XmlReader.java similarity index 100% rename from src/main/java/im/conversations/android/xml/XmlReader.java rename to app/src/main/java/im/conversations/android/xml/XmlReader.java diff --git a/src/main/java/im/conversations/android/xmpp/Closables.java b/app/src/main/java/im/conversations/android/xmpp/Closables.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/Closables.java rename to app/src/main/java/im/conversations/android/xmpp/Closables.java diff --git a/src/main/java/im/conversations/android/xmpp/ConnectionException.java b/app/src/main/java/im/conversations/android/xmpp/ConnectionException.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/ConnectionException.java rename to app/src/main/java/im/conversations/android/xmpp/ConnectionException.java diff --git a/src/main/java/im/conversations/android/xmpp/ConnectionPool.java b/app/src/main/java/im/conversations/android/xmpp/ConnectionPool.java similarity index 99% rename from src/main/java/im/conversations/android/xmpp/ConnectionPool.java rename to app/src/main/java/im/conversations/android/xmpp/ConnectionPool.java index a57b5e6ab..41b1bbd32 100644 --- a/src/main/java/im/conversations/android/xmpp/ConnectionPool.java +++ b/app/src/main/java/im/conversations/android/xmpp/ConnectionPool.java @@ -31,6 +31,7 @@ public class ConnectionPool { private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionPool.class); + public static final int SOCKET_TIMEOUT = 15; public static final int CONNECT_TIMEOUT = 90; public static final int PING_MAX_INTERVAL = 300; public static final int PING_MIN_INTERVAL = 30; diff --git a/src/main/java/im/conversations/android/xmpp/ConnectionState.java b/app/src/main/java/im/conversations/android/xmpp/ConnectionState.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/ConnectionState.java rename to app/src/main/java/im/conversations/android/xmpp/ConnectionState.java diff --git a/src/main/java/im/conversations/android/xmpp/Entity.java b/app/src/main/java/im/conversations/android/xmpp/Entity.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/Entity.java rename to app/src/main/java/im/conversations/android/xmpp/Entity.java diff --git a/src/main/java/im/conversations/android/xmpp/EntityCapabilities.java b/app/src/main/java/im/conversations/android/xmpp/EntityCapabilities.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/EntityCapabilities.java rename to app/src/main/java/im/conversations/android/xmpp/EntityCapabilities.java diff --git a/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java b/app/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java rename to app/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java diff --git a/src/main/java/im/conversations/android/xmpp/ExtensionFactory.java b/app/src/main/java/im/conversations/android/xmpp/ExtensionFactory.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/ExtensionFactory.java rename to app/src/main/java/im/conversations/android/xmpp/ExtensionFactory.java diff --git a/src/main/java/im/conversations/android/xmpp/IqErrorException.java b/app/src/main/java/im/conversations/android/xmpp/IqErrorException.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/IqErrorException.java rename to app/src/main/java/im/conversations/android/xmpp/IqErrorException.java diff --git a/src/main/java/im/conversations/android/xmpp/Managers.java b/app/src/main/java/im/conversations/android/xmpp/Managers.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/Managers.java rename to app/src/main/java/im/conversations/android/xmpp/Managers.java diff --git a/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java b/app/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/NodeConfiguration.java rename to app/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java diff --git a/src/main/java/im/conversations/android/xmpp/PreconditionNotMetException.java b/app/src/main/java/im/conversations/android/xmpp/PreconditionNotMetException.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/PreconditionNotMetException.java rename to app/src/main/java/im/conversations/android/xmpp/PreconditionNotMetException.java diff --git a/src/main/java/im/conversations/android/xmpp/PubSubErrorException.java b/app/src/main/java/im/conversations/android/xmpp/PubSubErrorException.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/PubSubErrorException.java rename to app/src/main/java/im/conversations/android/xmpp/PubSubErrorException.java diff --git a/src/main/java/im/conversations/android/xmpp/ServiceDescription.java b/app/src/main/java/im/conversations/android/xmpp/ServiceDescription.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/ServiceDescription.java rename to app/src/main/java/im/conversations/android/xmpp/ServiceDescription.java diff --git a/src/main/java/im/conversations/android/xmpp/Timestamps.java b/app/src/main/java/im/conversations/android/xmpp/Timestamps.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/Timestamps.java rename to app/src/main/java/im/conversations/android/xmpp/Timestamps.java diff --git a/src/main/java/im/conversations/android/xmpp/XmppConnection.java b/app/src/main/java/im/conversations/android/xmpp/XmppConnection.java similarity index 89% rename from src/main/java/im/conversations/android/xmpp/XmppConnection.java rename to app/src/main/java/im/conversations/android/xmpp/XmppConnection.java index 47301806e..f7d015c28 100644 --- a/src/main/java/im/conversations/android/xmpp/XmppConnection.java +++ b/app/src/main/java/im/conversations/android/xmpp/XmppConnection.java @@ -5,7 +5,6 @@ import android.os.Build; import android.os.SystemClock; import android.security.KeyChain; import android.util.Base64; -import android.util.Log; import android.util.Pair; import android.util.SparseArray; import androidx.annotation.NonNull; @@ -18,15 +17,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.services.MemorizingTrustManager; -import eu.siacs.conversations.services.NotificationService; -import eu.siacs.conversations.ui.util.PendingItem; -import eu.siacs.conversations.utils.PhoneHelper; -import eu.siacs.conversations.utils.Resolver; -import eu.siacs.conversations.utils.SocksSocketFactory; -import eu.siacs.conversations.xmpp.bind.Bind2; +import im.conversations.android.BuildConfig; import im.conversations.android.Conversations; import im.conversations.android.IDs; import im.conversations.android.database.ConversationsDatabase; @@ -34,8 +25,12 @@ import im.conversations.android.database.CredentialStore; import im.conversations.android.database.model.Account; import im.conversations.android.database.model.Connection; import im.conversations.android.database.model.Credential; +import im.conversations.android.dns.Resolver; +import im.conversations.android.socks.SocksSocketFactory; import im.conversations.android.tls.SSLSockets; +import im.conversations.android.tls.TrustManagers; import im.conversations.android.tls.XmppDomainVerifier; +import im.conversations.android.util.PendingItem; import im.conversations.android.xml.Element; import im.conversations.android.xml.Namespace; import im.conversations.android.xml.Tag; @@ -46,11 +41,13 @@ import im.conversations.android.xmpp.manager.CarbonsManager; import im.conversations.android.xmpp.manager.DiscoManager; import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.StreamElement; +import im.conversations.android.xmpp.model.bind2.BindInlineFeatures; import im.conversations.android.xmpp.model.csi.Active; import im.conversations.android.xmpp.model.csi.Inactive; import im.conversations.android.xmpp.model.error.Condition; import im.conversations.android.xmpp.model.error.Error; import im.conversations.android.xmpp.model.ping.Ping; +import im.conversations.android.xmpp.model.sasl2.Inline; import im.conversations.android.xmpp.model.sm.Ack; import im.conversations.android.xmpp.model.sm.Enable; import im.conversations.android.xmpp.model.sm.Request; @@ -82,7 +79,6 @@ import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Hashtable; import java.util.Iterator; import java.util.List; @@ -113,6 +109,9 @@ public class XmppConnection implements Runnable { private static final Logger LOGGER = LoggerFactory.getLogger(XmppConnection.class); + private static final boolean EXTENDED_SM_LOGGING = false; + private static final int CONNECT_DISCO_TIMEOUT = 20; + protected final Account account; private final SparseArray mStanzaQueue = new SparseArray<>(); private final Hashtable>> packetCallbacks = new Hashtable<>(); @@ -185,8 +184,7 @@ public class XmppConnection implements Runnable { if (Strings.isNullOrEmpty(resource)) { return null; } - int fixedPartLength = - context.getString(R.string.app_name).length() + 1; // include the trailing dot + int fixedPartLength = BuildConfig.APP_NAME.length() + 1; // include the trailing dot int randomPartLength = 4; // 3 bytes if (resource.length() > fixedPartLength + randomPartLength) { if (validBase64( @@ -208,8 +206,7 @@ public class XmppConnection implements Runnable { private void changeStatus(final ConnectionState nextStatus) { synchronized (this) { if (Thread.currentThread().isInterrupted()) { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": not changing status to " + nextStatus @@ -268,7 +265,7 @@ public class XmppConnection implements Runnable { ConversationsDatabase.getInstance(context) .accountDao() .getConnectionSettings(account.id); - Log.d(Config.LOGTAG, account.address + ": connecting"); + LOGGER.debug(account.address + ": connecting"); this.encryptionEnabled = false; this.inSmacksSession = false; this.quickStartInProgress = false; @@ -301,8 +298,7 @@ public class XmppConnection implements Runnable { directTls = connection.directTls; } - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": connect to " + destination @@ -318,8 +314,7 @@ public class XmppConnection implements Runnable { try { startXmpp(localSocket); } catch (final InterruptedException e) { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": thread was interrupted before beginning stream"); return; } catch (final Exception e) { @@ -334,11 +329,11 @@ public class XmppConnection implements Runnable { results = Resolver.resolve(domain); } if (Thread.currentThread().isInterrupted()) { - Log.d(Config.LOGTAG, account.address + ": Thread was interrupted"); + LOGGER.debug(account.address + ": Thread was interrupted"); return; } if (results.size() == 0) { - Log.e(Config.LOGTAG, account.address + ": Resolver results were empty"); + LOGGER.warn("Resolver results were empty"); return; } final Resolver.Result storedBackupResult; @@ -350,8 +345,7 @@ public class XmppConnection implements Runnable { null; // context.databaseBackend.findResolverResult(domain); if (storedBackupResult != null && !results.contains(storedBackupResult)) { results.add(storedBackupResult); - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": loaded backup resolver result from db: " + storedBackupResult); @@ -361,7 +355,7 @@ public class XmppConnection implements Runnable { iterator.hasNext(); ) { final Resolver.Result result = iterator.next(); if (Thread.currentThread().isInterrupted()) { - Log.d(Config.LOGTAG, account.address + ": Thread was interrupted"); + LOGGER.debug(account.address + ": Thread was interrupted"); return; } try { @@ -369,12 +363,11 @@ public class XmppConnection implements Runnable { this.encryptionEnabled = result.isDirectTls(); verifiedHostname = result.isAuthenticated() ? result.getHostname().toString() : null; - Log.d(Config.LOGTAG, "verified hostname " + verifiedHostname); + LOGGER.debug("verified hostname " + verifiedHostname); final InetSocketAddress addr; if (result.getIp() != null) { addr = new InetSocketAddress(result.getIp(), result.getPort()); - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": using values from resolver " + (result.getHostname() == null @@ -390,8 +383,7 @@ public class XmppConnection implements Runnable { new InetSocketAddress( IDN.toASCII(result.getHostname().toString()), result.getPort()); - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": using values from resolver " + result.getHostname().toString() @@ -402,13 +394,13 @@ public class XmppConnection implements Runnable { } localSocket = new Socket(); - localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000); + localSocket.connect(addr, ConnectionPool.SOCKET_TIMEOUT * 1000); if (this.encryptionEnabled) { localSocket = upgradeSocketToTls(localSocket); } - localSocket.setSoTimeout(Config.SOCKET_TIMEOUT * 1000); + localSocket.setSoTimeout(ConnectionPool.SOCKET_TIMEOUT * 1000); if (startXmpp(localSocket)) { localSocket.setSoTimeout( 0); // reset to 0; once the connection is established we don’t @@ -427,14 +419,12 @@ public class XmppConnection implements Runnable { throw e; } } catch (InterruptedException e) { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": thread was interrupted before beginning stream"); return; } catch (final Throwable e) { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": " + e.getMessage() @@ -459,15 +449,14 @@ public class XmppConnection implements Runnable { } catch (final SocksSocketFactory.SocksProxyNotFoundException e) { this.changeStatus(ConnectionState.TOR_NOT_AVAILABLE); } catch (final IOException | XmlPullParserException e) { - Log.d(Config.LOGTAG, account.address + ": " + e.getMessage()); + LOGGER.debug(account.address + ": " + e.getMessage()); this.changeStatus(ConnectionState.OFFLINE); this.attempt = Math.max(0, this.attempt - 1); } finally { if (!Thread.currentThread().isInterrupted()) { forceCloseSocket(); } else { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": not force closing socket because thread was interrupted"); } @@ -514,7 +503,6 @@ public class XmppConnection implements Runnable { private SSLSocketFactory getSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException { final SSLContext sc = SSLSockets.getSSLContext(); - final MemorizingTrustManager trustManager = MemorizingTrustManager.create(context); final KeyManager[] keyManager; final Credential credential = CredentialStore.getInstance(context).get(account); if (Strings.isNullOrEmpty(credential.privateKeyAlias)) { @@ -528,7 +516,7 @@ public class XmppConnection implements Runnable { // we need a better solution for this using live data or similar sc.init( keyManager, - new X509TrustManager[] {trustManager.getInteractive(domain)}, + new X509TrustManager[] {TrustManagers.getTrustManager()}, Conversations.SECURE_RANDOM); return sc.getSocketFactory(); } @@ -538,9 +526,7 @@ public class XmppConnection implements Runnable { synchronized (this) { this.mThread = Thread.currentThread(); if (this.mThread.isInterrupted()) { - Log.d( - Config.LOGTAG, - account.address + ": aborting connect because thread was interrupted"); + LOGGER.debug(account.address + ": aborting connect because thread was interrupted"); return; } forceCloseSocket(); @@ -578,8 +564,7 @@ public class XmppConnection implements Runnable { final Element challenge = tagReader.readElement(nextTag); processChallenge(challenge); } else { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": received 'challenge on an unsecure connection"); throw new StateChangingException(ConnectionState.INCOMPATIBLE_CLIENT); } @@ -591,30 +576,26 @@ public class XmppConnection implements Runnable { processResumed(resumed); } else if (nextTag.isStart("r")) { tagReader.readElement(nextTag); - if (Config.EXTENDED_SM_LOGGING) { - Log.d( - Config.LOGTAG, + if (EXTENDED_SM_LOGGING) { + LOGGER.debug( account.address + ": acknowledging stanza #" + this.stanzasReceived); } final Ack ack = new Ack(this.stanzasReceived); tagWriter.writeStanzaAsync(ack); } else if (nextTag.isStart("a")) { - synchronized (NotificationService.CATCHUP_LOCK) { - if (mWaitingForSmCatchup.compareAndSet(true, false)) { - final int messageCount = mSmCatchupMessageCounter.get(); - final int pendingIQs = packetCallbacks.size(); - Log.d( - Config.LOGTAG, - account.address - + ": SM catchup complete (messages=" - + messageCount - + ", pending IQs=" - + pendingIQs - + ")"); - if (messageCount > 0) { - // TODO finish notification backlog (ok to pling now) - // context.getNotificationService().finishBacklog(true, account); - } + if (mWaitingForSmCatchup.compareAndSet(true, false)) { + final int messageCount = mSmCatchupMessageCounter.get(); + final int pendingIQs = packetCallbacks.size(); + LOGGER.debug( + account.address + + ": SM catchup complete (messages=" + + messageCount + + ", pending IQs=" + + pendingIQs + + ")"); + if (messageCount > 0) { + // TODO finish notification backlog (ok to pling now) + // context.getNotificationService().finishBacklog(true, account); } } final Element ack = tagReader.readElement(nextTag); @@ -626,9 +607,7 @@ public class XmppConnection implements Runnable { acknowledgedMessages = acknowledgeStanzaUpTo(serverSequence.get()); } else { acknowledgedMessages = false; - Log.d( - Config.LOGTAG, - account.address + ": server send ack without sequence number"); + LOGGER.debug(account.address + ": server send ack without sequence number"); } } } else if (nextTag.isStart("failed")) { @@ -668,7 +647,7 @@ public class XmppConnection implements Runnable { saslMechanism.getResponse(challenge.getContent(), sslSocketOrNull(socket))); } catch (final SaslMechanism.AuthenticationException e) { // TODO: Send auth abort tag. - Log.e(Config.LOGTAG, e.toString()); + LOGGER.error("Authentication failed", e); throw new StateChangingException(ConnectionState.UNAUTHORIZED); } tagWriter.writeElement(response); @@ -697,16 +676,16 @@ public class XmppConnection implements Runnable { try { currentSaslMechanism.getResponse(challenge, sslSocketOrNull(socket)); } catch (final SaslMechanism.AuthenticationException e) { - Log.e(Config.LOGTAG, String.valueOf(e)); + LOGGER.error("Authentication failed", e); throw new StateChangingException(ConnectionState.UNAUTHORIZED); } - Log.d(Config.LOGTAG, account.address + ": logged in (using " + version + ")"); + LOGGER.debug(account.address + ": logged in (using " + version + ")"); if (SaslMechanism.pin(currentSaslMechanism)) { try { CredentialStore.getInstance(context) .setPinnedMechanism(account, currentSaslMechanism); } catch (final Exception e) { - Log.d(Config.LOGTAG, "unable to pin mechanism in credential store", e); + LOGGER.debug("unable to pin mechanism in credential store", e); } } if (version == SaslMechanism.Version.SASL_2) { @@ -719,8 +698,7 @@ public class XmppConnection implements Runnable { ? null : JidCreate.from(authorizationIdentifier); } catch (final XmppStringprepException e) { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": SASL 2.0 authorization identifier was not a valid jid"); throw new StateChangingException(ConnectionState.BIND_FAILURE); @@ -728,14 +706,12 @@ public class XmppConnection implements Runnable { if (authorizationJid == null) { throw new StateChangingException(ConnectionState.BIND_FAILURE); } - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": SASL 2.0 authorization identifier was " + authorizationJid); if (!account.address.getDomain().equals(authorizationJid.getDomain())) { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": server tried to re-assign domain to " + authorizationJid.getDomain()); @@ -748,9 +724,7 @@ public class XmppConnection implements Runnable { final Element tokenWrapper = success.findChild("token", Namespace.FAST); final String token = tokenWrapper == null ? null : tokenWrapper.getAttribute("token"); if (bound != null && resumed != null) { - Log.d( - Config.LOGTAG, - account.address + ": server sent bound and resumed in SASL2 success"); + LOGGER.debug(account.address + ": server sent bound and resumed in SASL2 success"); throw new StateChangingException(ConnectionState.INCOMPATIBLE_SERVER); } final boolean processNopStreamFeatures; @@ -773,7 +747,7 @@ public class XmppConnection implements Runnable { enableStreamManagement(); } if (carbonsEnabled != null) { - Log.d(Config.LOGTAG, account.address + ": successfully enabled carbons"); + LOGGER.debug(account.address + ": successfully enabled carbons"); } sendPostBindInitialization(carbonsEnabled != null); processNopStreamFeatures = true; @@ -792,15 +766,12 @@ public class XmppConnection implements Runnable { try { CredentialStore.getInstance(context) .setFastToken(account, tokenMechanism, token); - Log.d( - Config.LOGTAG, - account.address + ": storing hashed token " + tokenMechanism); + LOGGER.debug(account.address + ": storing hashed token " + tokenMechanism); } catch (final Exception e) { - Log.d(Config.LOGTAG, "could not store fast token", e); + LOGGER.debug("could not store fast token", e); } } else if (this.hashTokenRequest != null) { - Log.w( - Config.LOGTAG, + LOGGER.warn( account.address + ": no response to our hashed token request " + this.hashTokenRequest); @@ -829,9 +800,8 @@ public class XmppConnection implements Runnable { private void resetOutboundStanzaQueue() { synchronized (this.mStanzaQueue) { final List intermediateStanzas = new ArrayList<>(); - if (Config.EXTENDED_SM_LOGGING) { - Log.d( - Config.LOGTAG, + if (EXTENDED_SM_LOGGING) { + LOGGER.debug( account.address + ": stanzas sent before auth: " + this.stanzasSentBeforeAuthentication); @@ -847,9 +817,8 @@ public class XmppConnection implements Runnable { this.mStanzaQueue.put(i, intermediateStanzas.get(i)); } this.stanzasSent = intermediateStanzas.size(); - if (Config.EXTENDED_SM_LOGGING) { - Log.d( - Config.LOGTAG, + if (EXTENDED_SM_LOGGING) { + LOGGER.debug( account.address + ": resetting outbound stanza queue to " + this.stanzasSent); @@ -865,9 +834,8 @@ public class XmppConnection implements Runnable { "Processed NOP stream features after success {}", this.streamFeatures.getExtensionIds()); } else { - Log.d(Config.LOGTAG, account.address + ": received " + tag); - Log.d( - Config.LOGTAG, + LOGGER.debug(account.address + ": received " + tag); + LOGGER.debug( account.address + ": server did not send stream features after SASL2 success"); throw new StateChangingException(ConnectionState.INCOMPATIBLE_SERVER); } @@ -880,22 +848,21 @@ public class XmppConnection implements Runnable { } catch (final IllegalArgumentException e) { throw new StateChangingException(ConnectionState.INCOMPATIBLE_SERVER); } - Log.d(Config.LOGTAG, failure.toString()); - Log.d(Config.LOGTAG, account.address + ": login failure " + version); + LOGGER.debug(failure.toString()); + LOGGER.debug(account.address + ": login failure " + version); if (SaslMechanism.hashedToken(this.saslMechanism)) { - Log.d(Config.LOGTAG, account.address + ": resetting token"); + LOGGER.debug(account.address + ": resetting token"); try { CredentialStore.getInstance(context).resetFastToken(account); } catch (final Exception e) { - Log.d(Config.LOGTAG, "could not reset fast token in credential store", e); + LOGGER.debug("could not reset fast token in credential store", e); } } if (failure.hasChild("temporary-auth-failure")) { throw new StateChangingException(ConnectionState.TEMPORARY_AUTH_FAILURE); } if (SaslMechanism.hashedToken(this.saslMechanism)) { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": fast authentication failed. falling back to regular" + " authentication"); @@ -917,9 +884,9 @@ public class XmppConnection implements Runnable { final String streamId; if (enabled.getAttributeAsBoolean("resume")) { streamId = enabled.getAttribute("id"); - Log.d(Config.LOGTAG, account.address + ": stream management enabled (resumable)"); + LOGGER.debug(account.address + ": stream management enabled (resumable)"); } else { - Log.d(Config.LOGTAG, account.address + ": stream management enabled"); + LOGGER.debug(account.address + ": stream management enabled"); streamId = null; } this.streamId = streamId; @@ -946,10 +913,10 @@ public class XmppConnection implements Runnable { final boolean acknowledgedMessages; synchronized (this.mStanzaQueue) { if (serverCount < stanzasSent) { - Log.d(Config.LOGTAG, account.address + ": session resumed with lost packages"); + LOGGER.debug(account.address + ": session resumed with lost packages"); stanzasSent = serverCount; } else { - Log.d(Config.LOGTAG, account.address + ": session resumed"); + LOGGER.debug(account.address + ": session resumed"); } acknowledgedMessages = acknowledgeStanzaUpTo(serverCount); for (int i = 0; i < this.mStanzaQueue.size(); ++i) { @@ -957,7 +924,7 @@ public class XmppConnection implements Runnable { } mStanzaQueue.clear(); } - Log.d(Config.LOGTAG, account.address + ": resending " + failedStanzas.size() + " stanzas"); + LOGGER.debug(account.address + ": resending " + failedStanzas.size() + " stanzas"); for (final Stanza packet : failedStanzas) { if (packet instanceof Message) { Message message = (Message) packet; @@ -971,8 +938,7 @@ public class XmppConnection implements Runnable { } private void changeStatusToOnline() { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": online with resource " + connectionAddress.getResourceOrNull()); @@ -982,8 +948,7 @@ public class XmppConnection implements Runnable { private void processFailed(final Element failed, final boolean sendBindRequest) { final Optional serverCount = failed.getOptionalIntAttribute("h"); if (serverCount.isPresent()) { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": resumption failed but server acknowledged stanza #" + serverCount.get()); @@ -992,7 +957,7 @@ public class XmppConnection implements Runnable { acknowledgedMessages = acknowledgeStanzaUpTo(serverCount.get()); } } else { - Log.d(Config.LOGTAG, account.address + ": resumption failed"); + LOGGER.debug(account.address + ": resumption failed"); } resetStreamId(); if (sendBindRequest) { @@ -1002,8 +967,7 @@ public class XmppConnection implements Runnable { private boolean acknowledgeStanzaUpTo(final int serverCount) { if (serverCount > stanzasSent) { - Log.e( - Config.LOGTAG, + LOGGER.error( "server acknowledged more stanzas than we sent. serverCount=" + serverCount + ", ourCount=" @@ -1012,9 +976,8 @@ public class XmppConnection implements Runnable { boolean acknowledgedMessages = false; for (int i = 0; i < mStanzaQueue.size(); ++i) { if (serverCount >= mStanzaQueue.keyAt(i)) { - if (Config.EXTENDED_SM_LOGGING) { - Log.d( - Config.LOGTAG, + if (EXTENDED_SM_LOGGING) { + LOGGER.debug( account.address + ": server acknowledged stanza #" + mStanzaQueue.keyAt(i)); @@ -1045,8 +1008,7 @@ public class XmppConnection implements Runnable { if (inSmacksSession) { ++stanzasReceived; } else if (this.streamFeatures.streamManagement()) { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": not counting stanza(" + stanza.getClass().getSimpleName() @@ -1070,7 +1032,7 @@ public class XmppConnection implements Runnable { packetCallbacks.remove(packet.getId()); } else { callback = null; - Log.e(Config.LOGTAG, account.address + ": ignoring spoofed iq packet"); + LOGGER.warn("Ignoring spoofed iq stanza"); } } else { if (packet.getFrom() != null @@ -1079,7 +1041,7 @@ public class XmppConnection implements Runnable { packetCallbacks.remove(packet.getId()); } else { callback = null; - Log.e(Config.LOGTAG, account.address + ": ignoring spoofed iq packet"); + LOGGER.error(account.address + ": ignoring spoofed iq packet"); } } } else if (packet.getType() == Iq.Type.GET || packet.getType() == Iq.Type.SET) { @@ -1169,9 +1131,7 @@ public class XmppConnection implements Runnable { account.address.getDomain().toString(), this.verifiedHostname, sslSocket.getSession())) { - Log.d( - Config.LOGTAG, - account.address + ": TLS certificate domain verification failed"); + LOGGER.debug(account.address + ": TLS certificate domain verification failed"); Closables.close(sslSocket); throw new StateChangingException(ConnectionState.TLS_ERROR_DOMAIN); } @@ -1197,17 +1157,14 @@ public class XmppConnection implements Runnable { } if (isFastTokenAvailable( this.streamFeatures.findChild("authentication", Namespace.SASL_2))) { - Log.d( - Config.LOGTAG, - account.address + ": fast token available; resetting quick start"); + LOGGER.debug(account.address + ": fast token available; resetting quick start"); ConversationsDatabase.getInstance(context) .accountDao() .setQuickStartAvailable(account.id, false); } return; } - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": server lost support for SASL 2. quick start not possible"); ConversationsDatabase.getInstance(context) .accountDao() @@ -1234,10 +1191,8 @@ public class XmppConnection implements Runnable { && shouldAuthenticate) { authenticate(SaslMechanism.Version.SASL); } else if (this.streamFeatures.streamManagement() && streamId != null && !inSmacksSession) { - if (Config.EXTENDED_SM_LOGGING) { - Log.d( - Config.LOGTAG, - account.address + ": resuming after stanza #" + stanzasReceived); + if (EXTENDED_SM_LOGGING) { + LOGGER.debug(account.address + ": resuming after stanza #" + stanzasReceived); } final Resume resume = new Resume(this.streamId, stanzasReceived); this.mSmCatchupMessageCounter.set(0); @@ -1269,7 +1224,7 @@ public class XmppConnection implements Runnable { } private boolean isSecure() { - return this.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); + return this.encryptionEnabled || account.isOnion(); } private void authenticate(final SaslMechanism.Version version) throws IOException { @@ -1302,7 +1257,7 @@ public class XmppConnection implements Runnable { } quickStartAvailable = false; } else if (version == SaslMechanism.Version.SASL_2) { - final Element inline = authElement.findChild("inline", Namespace.SASL_2); + final Inline inline = authElement.getExtension(Inline.class); final boolean sm = inline != null && inline.hasChild("sm", Namespace.STREAM_MANAGEMENT); final HashedToken.Mechanism hashTokenRequest; if (usingFast) { @@ -1314,13 +1269,11 @@ public class XmppConnection implements Runnable { hashTokenRequest = HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket)); } - // TODO fix me. properly parse bind2 features - final Collection bindFeatures = - Collections.emptyList(); // Bind2.features(inline); + final Collection bindFeatures = BindInlineFeatures.get(inline); quickStartAvailable = sm && bindFeatures != null - && bindFeatures.containsAll(Bind2.QUICKSTART_FEATURES); + && bindFeatures.containsAll(BindInlineFeatures.QUICKSTART_FEATURES); this.hashTokenRequest = hashTokenRequest; authenticate = generateAuthenticationRequest( @@ -1333,8 +1286,7 @@ public class XmppConnection implements Runnable { .accountDao() .setQuickStartAvailable(account.id, quickStartAvailable); - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": Authenticating with " + version @@ -1357,8 +1309,7 @@ public class XmppConnection implements Runnable { final @Nullable SaslMechanism saslMechanism, Collection mechanisms) throws StateChangingException { if (saslMechanism == null) { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": unable to find supported SASL mechanism in " + mechanisms); throw new StateChangingException(ConnectionState.INCOMPATIBLE_SERVER); } @@ -1370,8 +1321,7 @@ public class XmppConnection implements Runnable { account, CredentialStore.getInstance(context).get(account)); final int pinnedMechanism = saslFactory.getPinnedMechanismPriority(); if (pinnedMechanism > saslMechanism.getPriority()) { - Log.e( - Config.LOGTAG, + LOGGER.error( "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() + " has lower priority (" @@ -1387,7 +1337,7 @@ public class XmppConnection implements Runnable { private Element generateAuthenticationRequest( final String firstMessage, final boolean usingFast) { return generateAuthenticationRequest( - firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true); + firstMessage, usingFast, null, BindInlineFeatures.QUICKSTART_FEATURES, true); } private Element generateAuthenticationRequest( @@ -1402,12 +1352,10 @@ public class XmppConnection implements Runnable { } final Element userAgent = authenticate.addChild("user-agent"); userAgent.setAttribute("id", account.getPublicDeviceId().toString()); - userAgent.addChild("software").setContent(context.getString(R.string.app_name)); - if (!PhoneHelper.isEmulator()) { - userAgent - .addChild("device") - .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL)); - } + userAgent.addChild("software").setContent(BuildConfig.APP_NAME); + userAgent + .addChild("device") + .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL)); if (bind != null) { authenticate.addChild(generateBindRequest(bind)); } @@ -1428,9 +1376,9 @@ public class XmppConnection implements Runnable { } private Element generateBindRequest(final Collection bindFeatures) { - Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures); + LOGGER.debug("inline bind features: " + bindFeatures); final Element bind = new Element("bind", Namespace.BIND2); - bind.addChild("tag").setContent(context.getString(R.string.app_name)); + bind.addChild("tag").setContent(BuildConfig.APP_NAME); if (bindFeatures.contains(Namespace.CARBONS)) { bind.addChild("enable", Namespace.CARBONS); } @@ -1496,8 +1444,7 @@ public class XmppConnection implements Runnable { try { assignedJid = JidCreate.from(jid); } catch (final XmppStringprepException e) { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": server reported invalid jid (" + jid @@ -1506,8 +1453,7 @@ public class XmppConnection implements Runnable { } if (!account.address.getDomain().equals(assignedJid.getDomain())) { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": server tried to re-assign domain to " + assignedJid.getDomain()); @@ -1523,8 +1469,7 @@ public class XmppConnection implements Runnable { } return; } else { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": disconnecting because of bind failure (" + packet); @@ -1537,8 +1482,7 @@ public class XmppConnection implements Runnable { ConversationsDatabase.getInstance(context) .accountDao() .setResource(account.id, alternativeResource); - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": switching resource due to conflict (" + alternativeResource @@ -1559,8 +1503,7 @@ public class XmppConnection implements Runnable { if (this.packetCallbacks.size() == 0) { return; } - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": clearing " + this.packetCallbacks.size() @@ -1577,8 +1520,7 @@ public class XmppConnection implements Runnable { try { callback.accept(failurePacket); } catch (StateChangingError error) { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": caught StateChangingError(" + error.state.toString() @@ -1586,8 +1528,7 @@ public class XmppConnection implements Runnable { // ignore } } - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": done clearing iq callbacks. " + this.packetCallbacks.size() @@ -1595,7 +1536,7 @@ public class XmppConnection implements Runnable { } private void sendStartSession() { - Log.d(Config.LOGTAG, account.address + ": sending legacy session to outdated server"); + LOGGER.debug(account.address + ": sending legacy session to outdated server"); final Iq startSession = new Iq(Iq.Type.SET); startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session"); this.sendIqPacketUnbound( @@ -1628,7 +1569,7 @@ public class XmppConnection implements Runnable { private void sendPostBindInitialization(final boolean carbonsEnabled) { getManager(CarbonsManager.class).setEnabled(carbonsEnabled); - Log.d(Config.LOGTAG, account.address + ": starting service discovery"); + LOGGER.debug(account.address + ": starting service discovery"); final ArrayList> discoFutures = new ArrayList<>(); final var discoManager = getManager(DiscoManager.class); @@ -1646,7 +1587,7 @@ public class XmppConnection implements Runnable { final var discoFuture = Futures.withTimeout( Futures.allAsList(discoFutures), - Config.CONNECT_DISCO_TIMEOUT, + CONNECT_DISCO_TIMEOUT, TimeUnit.SECONDS, ConnectionPool.CONNECTION_SCHEDULER); @@ -1661,7 +1602,7 @@ public class XmppConnection implements Runnable { @Override public void onFailure(@NonNull Throwable t) { - Log.d(Config.LOGTAG, "unable to fetch disco", t); + LOGGER.debug("unable to fetch disco", t); // TODO reset stream ID so we get a proper connect next time finalizeBind(); } @@ -1700,8 +1641,7 @@ public class XmppConnection implements Runnable { ConversationsDatabase.getInstance(context) .accountDao() .setResource(account.id, alternativeResource); - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": switching resource due to conflict (" + alternativeResource @@ -1712,11 +1652,11 @@ public class XmppConnection implements Runnable { } else if (streamError.hasChild("policy-violation")) { this.lastConnect = SystemClock.elapsedRealtime(); final String text = streamError.findChildContent("text"); - Log.d(Config.LOGTAG, account.address + ": policy violation. " + text); + LOGGER.debug(account.address + ": policy violation. " + text); failPendingMessages(text); throw new StateChangingException(ConnectionState.POLICY_VIOLATION); } else { - Log.d(Config.LOGTAG, account.address + ": stream error " + streamError); + LOGGER.debug(account.address + ": stream error " + streamError); throw new StateChangingException(ConnectionState.STREAM_ERROR); } } @@ -1747,7 +1687,6 @@ public class XmppConnection implements Runnable { SaslMechanism.ensureAvailable(saslFactory.getQuickStartMechanism(), sslVersion); final boolean secureConnection = sslVersion != SSLSockets.Version.NONE; if (secureConnection - && Config.QUICKSTART_ENABLED && quickStartMechanism != null && ConversationsDatabase.getInstance(context) .accountDao() @@ -1765,8 +1704,7 @@ public class XmppConnection implements Runnable { this.stanzasSentBeforeAuthentication = this.stanzasSent; tagWriter.writeElement(authenticate); } - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": quick start with " + quickStartMechanism.getMechanism()); return true; } else { @@ -1790,7 +1728,7 @@ public class XmppConnection implements Runnable { } private String createNewResource(final String postfixId) { - return String.format("%s.%s", context.getString(R.string.app_name), postfixId); + return String.format("%s.%s", BuildConfig.APP_NAME, postfixId); } public ListenableFuture sendIqPacket(final Iq packet) { @@ -1886,8 +1824,7 @@ public class XmppConnection implements Runnable { if (sendToUnboundStream || isBound) { tagWriter.writeStanzaAsync(packet); } else { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + " do not write stanza to unbound stream " + packet.toString()); @@ -1903,9 +1840,8 @@ public class XmppConnection implements Runnable { } ++stanzasSent; - if (Config.EXTENDED_SM_LOGGING) { - Log.d( - Config.LOGTAG, + if (EXTENDED_SM_LOGGING) { + LOGGER.debug( account.address + ": counting outbound " + packet.getName() @@ -1914,9 +1850,8 @@ public class XmppConnection implements Runnable { } this.mStanzaQueue.append(stanzasSent, stanza); if (stanza instanceof Message && stanza.getId() != null && inSmacksSession) { - if (Config.EXTENDED_SM_LOGGING) { - Log.d( - Config.LOGTAG, + if (EXTENDED_SM_LOGGING) { + LOGGER.debug( account.address + ": requesting ack for message stanza #" + stanzasSent); @@ -1969,7 +1904,7 @@ public class XmppConnection implements Runnable { public void disconnect(final boolean force) { interrupt(); - Log.d(Config.LOGTAG, account.address + ": disconnecting force=" + force); + LOGGER.debug(account.address + ": disconnecting force=" + force); if (force) { forceCloseSocket(); } else { @@ -1980,25 +1915,21 @@ public class XmppConnection implements Runnable { final CountDownLatch streamCountDownLatch = this.mStreamCountDownLatch; try { currentTagWriter.await(1, TimeUnit.SECONDS); - Log.d(Config.LOGTAG, account.address + ": closing stream"); + LOGGER.debug(account.address + ": closing stream"); currentTagWriter.writeTag(Tag.end("stream:stream")); if (streamCountDownLatch != null) { if (streamCountDownLatch.await(1, TimeUnit.SECONDS)) { - Log.d(Config.LOGTAG, account.address + ": remote ended stream"); + LOGGER.debug(account.address + ": remote ended stream"); } else { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": remote has not closed socket. force closing"); } } } catch (InterruptedException e) { - Log.d( - Config.LOGTAG, - account.address + ": interrupted while gracefully closing stream"); + LOGGER.debug(account.address + ": interrupted while gracefully closing stream"); } catch (final IOException e) { - Log.d( - Config.LOGTAG, + LOGGER.debug( account.address + ": io exception during disconnect (" + e.getMessage() @@ -2118,11 +2049,11 @@ public class XmppConnection implements Runnable { @Override public X509Certificate[] getCertificateChain(String alias) { - Log.d(Config.LOGTAG, "getting certificate chain"); + LOGGER.debug("getting certificate chain"); try { return KeyChain.getCertificateChain(context, alias); } catch (final Exception e) { - Log.d(Config.LOGTAG, "could not get certificate chain", e); + LOGGER.debug("could not get certificate chain", e); return new X509Certificate[0]; } } diff --git a/src/main/java/im/conversations/android/xmpp/axolotl/AxolotlAddress.java b/app/src/main/java/im/conversations/android/xmpp/axolotl/AxolotlAddress.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/axolotl/AxolotlAddress.java rename to app/src/main/java/im/conversations/android/xmpp/axolotl/AxolotlAddress.java diff --git a/src/main/java/im/conversations/android/xmpp/manager/AbstractManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/AbstractManager.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/manager/AbstractManager.java rename to app/src/main/java/im/conversations/android/xmpp/manager/AbstractManager.java diff --git a/src/main/java/im/conversations/android/xmpp/manager/ArchiveManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/ArchiveManager.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/manager/ArchiveManager.java rename to app/src/main/java/im/conversations/android/xmpp/manager/ArchiveManager.java diff --git a/src/main/java/im/conversations/android/xmpp/manager/AvatarManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/AvatarManager.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/manager/AvatarManager.java rename to app/src/main/java/im/conversations/android/xmpp/manager/AvatarManager.java diff --git a/src/main/java/im/conversations/android/xmpp/manager/AxolotlManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/AxolotlManager.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/manager/AxolotlManager.java rename to app/src/main/java/im/conversations/android/xmpp/manager/AxolotlManager.java diff --git a/src/main/java/im/conversations/android/xmpp/manager/BlockingManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/BlockingManager.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/manager/BlockingManager.java rename to app/src/main/java/im/conversations/android/xmpp/manager/BlockingManager.java diff --git a/src/main/java/im/conversations/android/xmpp/manager/BookmarkManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/BookmarkManager.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/manager/BookmarkManager.java rename to app/src/main/java/im/conversations/android/xmpp/manager/BookmarkManager.java diff --git a/src/main/java/im/conversations/android/xmpp/manager/CarbonsManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/CarbonsManager.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/manager/CarbonsManager.java rename to app/src/main/java/im/conversations/android/xmpp/manager/CarbonsManager.java diff --git a/src/main/java/im/conversations/android/xmpp/manager/ChatStateManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/ChatStateManager.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/manager/ChatStateManager.java rename to app/src/main/java/im/conversations/android/xmpp/manager/ChatStateManager.java diff --git a/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java similarity index 99% rename from src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java rename to app/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java index 50a910e9d..7e1756004 100644 --- a/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java +++ b/app/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java @@ -11,8 +11,8 @@ import com.google.common.io.BaseEncoding; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import eu.siacs.conversations.BuildConfig; -import eu.siacs.conversations.R; +import im.conversations.android.BuildConfig; +import im.conversations.android.R; import im.conversations.android.xml.Namespace; import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.EntityCapabilities; diff --git a/src/main/java/im/conversations/android/xmpp/manager/NickManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/NickManager.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/manager/NickManager.java rename to app/src/main/java/im/conversations/android/xmpp/manager/NickManager.java diff --git a/src/main/java/im/conversations/android/xmpp/manager/PepManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/PepManager.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/manager/PepManager.java rename to app/src/main/java/im/conversations/android/xmpp/manager/PepManager.java diff --git a/src/main/java/im/conversations/android/xmpp/manager/PresenceManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/PresenceManager.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/manager/PresenceManager.java rename to app/src/main/java/im/conversations/android/xmpp/manager/PresenceManager.java diff --git a/src/main/java/im/conversations/android/xmpp/manager/PubSubManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/PubSubManager.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/manager/PubSubManager.java rename to app/src/main/java/im/conversations/android/xmpp/manager/PubSubManager.java diff --git a/src/main/java/im/conversations/android/xmpp/manager/ReceiptManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/ReceiptManager.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/manager/ReceiptManager.java rename to app/src/main/java/im/conversations/android/xmpp/manager/ReceiptManager.java diff --git a/src/main/java/im/conversations/android/xmpp/manager/RegistrationManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/RegistrationManager.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/manager/RegistrationManager.java rename to app/src/main/java/im/conversations/android/xmpp/manager/RegistrationManager.java diff --git a/src/main/java/im/conversations/android/xmpp/manager/RosterManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/RosterManager.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/manager/RosterManager.java rename to app/src/main/java/im/conversations/android/xmpp/manager/RosterManager.java diff --git a/src/main/java/im/conversations/android/xmpp/manager/StanzaIdManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/StanzaIdManager.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/manager/StanzaIdManager.java rename to app/src/main/java/im/conversations/android/xmpp/manager/StanzaIdManager.java diff --git a/src/main/java/im/conversations/android/xmpp/model/ByteContent.java b/app/src/main/java/im/conversations/android/xmpp/model/ByteContent.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/ByteContent.java rename to app/src/main/java/im/conversations/android/xmpp/model/ByteContent.java diff --git a/src/main/java/im/conversations/android/xmpp/model/DeliveryReceipt.java b/app/src/main/java/im/conversations/android/xmpp/model/DeliveryReceipt.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/DeliveryReceipt.java rename to app/src/main/java/im/conversations/android/xmpp/model/DeliveryReceipt.java diff --git a/src/main/java/im/conversations/android/xmpp/model/DeliveryReceiptRequest.java b/app/src/main/java/im/conversations/android/xmpp/model/DeliveryReceiptRequest.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/DeliveryReceiptRequest.java rename to app/src/main/java/im/conversations/android/xmpp/model/DeliveryReceiptRequest.java diff --git a/src/main/java/im/conversations/android/xmpp/model/Extension.java b/app/src/main/java/im/conversations/android/xmpp/model/Extension.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/Extension.java rename to app/src/main/java/im/conversations/android/xmpp/model/Extension.java diff --git a/src/main/java/im/conversations/android/xmpp/model/Hash.java b/app/src/main/java/im/conversations/android/xmpp/model/Hash.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/Hash.java rename to app/src/main/java/im/conversations/android/xmpp/model/Hash.java diff --git a/src/main/java/im/conversations/android/xmpp/model/StreamElement.java b/app/src/main/java/im/conversations/android/xmpp/model/StreamElement.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/StreamElement.java rename to app/src/main/java/im/conversations/android/xmpp/model/StreamElement.java diff --git a/src/main/java/im/conversations/android/xmpp/model/avatar/Data.java b/app/src/main/java/im/conversations/android/xmpp/model/avatar/Data.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/avatar/Data.java rename to app/src/main/java/im/conversations/android/xmpp/model/avatar/Data.java diff --git a/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java b/app/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/avatar/Info.java rename to app/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/avatar/Metadata.java b/app/src/main/java/im/conversations/android/xmpp/model/avatar/Metadata.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/avatar/Metadata.java rename to app/src/main/java/im/conversations/android/xmpp/model/avatar/Metadata.java diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/Bundle.java b/app/src/main/java/im/conversations/android/xmpp/model/axolotl/Bundle.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/axolotl/Bundle.java rename to app/src/main/java/im/conversations/android/xmpp/model/axolotl/Bundle.java diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/Device.java b/app/src/main/java/im/conversations/android/xmpp/model/axolotl/Device.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/axolotl/Device.java rename to app/src/main/java/im/conversations/android/xmpp/model/axolotl/Device.java diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/DeviceList.java b/app/src/main/java/im/conversations/android/xmpp/model/axolotl/DeviceList.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/axolotl/DeviceList.java rename to app/src/main/java/im/conversations/android/xmpp/model/axolotl/DeviceList.java diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/ECPublicKeyContent.java b/app/src/main/java/im/conversations/android/xmpp/model/axolotl/ECPublicKeyContent.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/axolotl/ECPublicKeyContent.java rename to app/src/main/java/im/conversations/android/xmpp/model/axolotl/ECPublicKeyContent.java diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/Encrypted.java b/app/src/main/java/im/conversations/android/xmpp/model/axolotl/Encrypted.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/axolotl/Encrypted.java rename to app/src/main/java/im/conversations/android/xmpp/model/axolotl/Encrypted.java diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/Header.java b/app/src/main/java/im/conversations/android/xmpp/model/axolotl/Header.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/axolotl/Header.java rename to app/src/main/java/im/conversations/android/xmpp/model/axolotl/Header.java diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/IdentityKey.java b/app/src/main/java/im/conversations/android/xmpp/model/axolotl/IdentityKey.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/axolotl/IdentityKey.java rename to app/src/main/java/im/conversations/android/xmpp/model/axolotl/IdentityKey.java diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/Payload.java b/app/src/main/java/im/conversations/android/xmpp/model/axolotl/Payload.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/axolotl/Payload.java rename to app/src/main/java/im/conversations/android/xmpp/model/axolotl/Payload.java diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/PreKey.java b/app/src/main/java/im/conversations/android/xmpp/model/axolotl/PreKey.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/axolotl/PreKey.java rename to app/src/main/java/im/conversations/android/xmpp/model/axolotl/PreKey.java diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/PreKeys.java b/app/src/main/java/im/conversations/android/xmpp/model/axolotl/PreKeys.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/axolotl/PreKeys.java rename to app/src/main/java/im/conversations/android/xmpp/model/axolotl/PreKeys.java diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKey.java b/app/src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKey.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKey.java rename to app/src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKey.java diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKeySignature.java b/app/src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKeySignature.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKeySignature.java rename to app/src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKeySignature.java diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/axolotl/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/axolotl/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/axolotl/package-info.java diff --git a/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java b/app/src/main/java/im/conversations/android/xmpp/model/bind2/BindInlineFeatures.java similarity index 67% rename from src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java rename to app/src/main/java/im/conversations/android/xmpp/model/bind2/BindInlineFeatures.java index 21c957a0f..b8e825b03 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/bind/Bind2.java +++ b/app/src/main/java/im/conversations/android/xmpp/model/bind2/BindInlineFeatures.java @@ -1,22 +1,19 @@ -package eu.siacs.conversations.xmpp.bind; +package im.conversations.android.xmpp.model.bind2; import com.google.common.collect.Collections2; - +import im.conversations.android.xml.Element; +import im.conversations.android.xml.Namespace; +import im.conversations.android.xmpp.model.sasl2.Inline; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xml.Namespace; +public class BindInlineFeatures { -public class Bind2 { + public static final Collection QUICKSTART_FEATURES = + Arrays.asList(Namespace.CARBONS, Namespace.STREAM_MANAGEMENT); - public static final Collection QUICKSTART_FEATURES = Arrays.asList( - Namespace.CARBONS, - Namespace.STREAM_MANAGEMENT - ); - - public static Collection features(final Element inline) { + public static Collection get(final Inline inline) { final Element inlineBind2 = inline != null ? inline.findChild("bind", Namespace.BIND2) : null; final Element inlineBind2Inline = diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java b/app/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/blocking/Block.java rename to app/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java b/app/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java rename to app/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Item.java b/app/src/main/java/im/conversations/android/xmpp/model/blocking/Item.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/blocking/Item.java rename to app/src/main/java/im/conversations/android/xmpp/model/blocking/Item.java diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java b/app/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java rename to app/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/blocking/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/blocking/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/blocking/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java b/app/src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java rename to app/src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/capabilties/Capabilities.java b/app/src/main/java/im/conversations/android/xmpp/model/capabilties/Capabilities.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/capabilties/Capabilities.java rename to app/src/main/java/im/conversations/android/xmpp/model/capabilties/Capabilities.java diff --git a/src/main/java/im/conversations/android/xmpp/model/capabilties/EntityCapabilities.java b/app/src/main/java/im/conversations/android/xmpp/model/capabilties/EntityCapabilities.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/capabilties/EntityCapabilities.java rename to app/src/main/java/im/conversations/android/xmpp/model/capabilties/EntityCapabilities.java diff --git a/src/main/java/im/conversations/android/xmpp/model/capabilties/LegacyCapabilities.java b/app/src/main/java/im/conversations/android/xmpp/model/capabilties/LegacyCapabilities.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/capabilties/LegacyCapabilities.java rename to app/src/main/java/im/conversations/android/xmpp/model/capabilties/LegacyCapabilities.java diff --git a/src/main/java/im/conversations/android/xmpp/model/carbons/Enable.java b/app/src/main/java/im/conversations/android/xmpp/model/carbons/Enable.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/carbons/Enable.java rename to app/src/main/java/im/conversations/android/xmpp/model/carbons/Enable.java diff --git a/src/main/java/im/conversations/android/xmpp/model/carbons/Received.java b/app/src/main/java/im/conversations/android/xmpp/model/carbons/Received.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/carbons/Received.java rename to app/src/main/java/im/conversations/android/xmpp/model/carbons/Received.java diff --git a/src/main/java/im/conversations/android/xmpp/model/carbons/Sent.java b/app/src/main/java/im/conversations/android/xmpp/model/carbons/Sent.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/carbons/Sent.java rename to app/src/main/java/im/conversations/android/xmpp/model/carbons/Sent.java diff --git a/src/main/java/im/conversations/android/xmpp/model/carbons/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/carbons/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/carbons/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/carbons/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/correction/Replace.java b/app/src/main/java/im/conversations/android/xmpp/model/correction/Replace.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/correction/Replace.java rename to app/src/main/java/im/conversations/android/xmpp/model/correction/Replace.java diff --git a/src/main/java/im/conversations/android/xmpp/model/csi/Active.java b/app/src/main/java/im/conversations/android/xmpp/model/csi/Active.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/csi/Active.java rename to app/src/main/java/im/conversations/android/xmpp/model/csi/Active.java diff --git a/src/main/java/im/conversations/android/xmpp/model/csi/Inactive.java b/app/src/main/java/im/conversations/android/xmpp/model/csi/Inactive.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/csi/Inactive.java rename to app/src/main/java/im/conversations/android/xmpp/model/csi/Inactive.java diff --git a/src/main/java/im/conversations/android/xmpp/model/csi/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/csi/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/csi/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/csi/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Data.java b/app/src/main/java/im/conversations/android/xmpp/model/data/Data.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/data/Data.java rename to app/src/main/java/im/conversations/android/xmpp/model/data/Data.java diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Field.java b/app/src/main/java/im/conversations/android/xmpp/model/data/Field.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/data/Field.java rename to app/src/main/java/im/conversations/android/xmpp/model/data/Field.java diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Value.java b/app/src/main/java/im/conversations/android/xmpp/model/data/Value.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/data/Value.java rename to app/src/main/java/im/conversations/android/xmpp/model/data/Value.java diff --git a/src/main/java/im/conversations/android/xmpp/model/data/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/data/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/data/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/data/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/delay/Delay.java b/app/src/main/java/im/conversations/android/xmpp/model/delay/Delay.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/delay/Delay.java rename to app/src/main/java/im/conversations/android/xmpp/model/delay/Delay.java diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/info/Feature.java b/app/src/main/java/im/conversations/android/xmpp/model/disco/info/Feature.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/disco/info/Feature.java rename to app/src/main/java/im/conversations/android/xmpp/model/disco/info/Feature.java diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/info/Identity.java b/app/src/main/java/im/conversations/android/xmpp/model/disco/info/Identity.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/disco/info/Identity.java rename to app/src/main/java/im/conversations/android/xmpp/model/disco/info/Identity.java diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java b/app/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java rename to app/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/info/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/disco/info/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/disco/info/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/disco/info/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/items/Item.java b/app/src/main/java/im/conversations/android/xmpp/model/disco/items/Item.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/disco/items/Item.java rename to app/src/main/java/im/conversations/android/xmpp/model/disco/items/Item.java diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/items/ItemsQuery.java b/app/src/main/java/im/conversations/android/xmpp/model/disco/items/ItemsQuery.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/disco/items/ItemsQuery.java rename to app/src/main/java/im/conversations/android/xmpp/model/disco/items/ItemsQuery.java diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/items/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/disco/items/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/disco/items/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/disco/items/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/error/Condition.java b/app/src/main/java/im/conversations/android/xmpp/model/error/Condition.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/error/Condition.java rename to app/src/main/java/im/conversations/android/xmpp/model/error/Condition.java diff --git a/src/main/java/im/conversations/android/xmpp/model/error/Error.java b/app/src/main/java/im/conversations/android/xmpp/model/error/Error.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/error/Error.java rename to app/src/main/java/im/conversations/android/xmpp/model/error/Error.java diff --git a/src/main/java/im/conversations/android/xmpp/model/error/Text.java b/app/src/main/java/im/conversations/android/xmpp/model/error/Text.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/error/Text.java rename to app/src/main/java/im/conversations/android/xmpp/model/error/Text.java diff --git a/src/main/java/im/conversations/android/xmpp/model/forward/Forwarded.java b/app/src/main/java/im/conversations/android/xmpp/model/forward/Forwarded.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/forward/Forwarded.java rename to app/src/main/java/im/conversations/android/xmpp/model/forward/Forwarded.java diff --git a/src/main/java/im/conversations/android/xmpp/model/jabber/Body.java b/app/src/main/java/im/conversations/android/xmpp/model/jabber/Body.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/jabber/Body.java rename to app/src/main/java/im/conversations/android/xmpp/model/jabber/Body.java diff --git a/src/main/java/im/conversations/android/xmpp/model/jabber/Thread.java b/app/src/main/java/im/conversations/android/xmpp/model/jabber/Thread.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/jabber/Thread.java rename to app/src/main/java/im/conversations/android/xmpp/model/jabber/Thread.java diff --git a/src/main/java/im/conversations/android/xmpp/model/jabber/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/jabber/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/jabber/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/jabber/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/mam/Result.java b/app/src/main/java/im/conversations/android/xmpp/model/mam/Result.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/mam/Result.java rename to app/src/main/java/im/conversations/android/xmpp/model/mam/Result.java diff --git a/src/main/java/im/conversations/android/xmpp/model/mam/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/mam/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/mam/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/mam/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/markers/Displayed.java b/app/src/main/java/im/conversations/android/xmpp/model/markers/Displayed.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/markers/Displayed.java rename to app/src/main/java/im/conversations/android/xmpp/model/markers/Displayed.java diff --git a/src/main/java/im/conversations/android/xmpp/model/markers/Markable.java b/app/src/main/java/im/conversations/android/xmpp/model/markers/Markable.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/markers/Markable.java rename to app/src/main/java/im/conversations/android/xmpp/model/markers/Markable.java diff --git a/src/main/java/im/conversations/android/xmpp/model/markers/Received.java b/app/src/main/java/im/conversations/android/xmpp/model/markers/Received.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/markers/Received.java rename to app/src/main/java/im/conversations/android/xmpp/model/markers/Received.java diff --git a/src/main/java/im/conversations/android/xmpp/model/markers/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/markers/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/markers/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/markers/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/Affiliation.java b/app/src/main/java/im/conversations/android/xmpp/model/muc/Affiliation.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/muc/Affiliation.java rename to app/src/main/java/im/conversations/android/xmpp/model/muc/Affiliation.java diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/Role.java b/app/src/main/java/im/conversations/android/xmpp/model/muc/Role.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/muc/Role.java rename to app/src/main/java/im/conversations/android/xmpp/model/muc/Role.java diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/user/MultiUserChat.java b/app/src/main/java/im/conversations/android/xmpp/model/muc/user/MultiUserChat.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/muc/user/MultiUserChat.java rename to app/src/main/java/im/conversations/android/xmpp/model/muc/user/MultiUserChat.java diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/user/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/muc/user/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/muc/user/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/muc/user/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/nick/Nick.java b/app/src/main/java/im/conversations/android/xmpp/model/nick/Nick.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/nick/Nick.java rename to app/src/main/java/im/conversations/android/xmpp/model/nick/Nick.java diff --git a/src/main/java/im/conversations/android/xmpp/model/occupant/OccupantId.java b/app/src/main/java/im/conversations/android/xmpp/model/occupant/OccupantId.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/occupant/OccupantId.java rename to app/src/main/java/im/conversations/android/xmpp/model/occupant/OccupantId.java diff --git a/src/main/java/im/conversations/android/xmpp/model/oob/OutOfBandData.java b/app/src/main/java/im/conversations/android/xmpp/model/oob/OutOfBandData.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/oob/OutOfBandData.java rename to app/src/main/java/im/conversations/android/xmpp/model/oob/OutOfBandData.java diff --git a/src/main/java/im/conversations/android/xmpp/model/oob/URL.java b/app/src/main/java/im/conversations/android/xmpp/model/oob/URL.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/oob/URL.java rename to app/src/main/java/im/conversations/android/xmpp/model/oob/URL.java diff --git a/src/main/java/im/conversations/android/xmpp/model/oob/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/oob/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/oob/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/oob/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/pars/PreAuth.java b/app/src/main/java/im/conversations/android/xmpp/model/pars/PreAuth.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/pars/PreAuth.java rename to app/src/main/java/im/conversations/android/xmpp/model/pars/PreAuth.java diff --git a/src/main/java/im/conversations/android/xmpp/model/ping/Ping.java b/app/src/main/java/im/conversations/android/xmpp/model/ping/Ping.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/ping/Ping.java rename to app/src/main/java/im/conversations/android/xmpp/model/ping/Ping.java diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/Item.java b/app/src/main/java/im/conversations/android/xmpp/model/pubsub/Item.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/pubsub/Item.java rename to app/src/main/java/im/conversations/android/xmpp/model/pubsub/Item.java diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java b/app/src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java rename to app/src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/PubSub.java b/app/src/main/java/im/conversations/android/xmpp/model/pubsub/PubSub.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/pubsub/PubSub.java rename to app/src/main/java/im/conversations/android/xmpp/model/pubsub/PubSub.java diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/Publish.java b/app/src/main/java/im/conversations/android/xmpp/model/pubsub/Publish.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/pubsub/Publish.java rename to app/src/main/java/im/conversations/android/xmpp/model/pubsub/Publish.java diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/PublishOptions.java b/app/src/main/java/im/conversations/android/xmpp/model/pubsub/PublishOptions.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/pubsub/PublishOptions.java rename to app/src/main/java/im/conversations/android/xmpp/model/pubsub/PublishOptions.java diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/Retract.java b/app/src/main/java/im/conversations/android/xmpp/model/pubsub/Retract.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/pubsub/Retract.java rename to app/src/main/java/im/conversations/android/xmpp/model/pubsub/Retract.java diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/error/PubSubError.java b/app/src/main/java/im/conversations/android/xmpp/model/pubsub/error/PubSubError.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/pubsub/error/PubSubError.java rename to app/src/main/java/im/conversations/android/xmpp/model/pubsub/error/PubSubError.java diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/error/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/pubsub/error/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/pubsub/error/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/pubsub/error/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java b/app/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java rename to app/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java b/app/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java rename to app/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java b/app/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java rename to app/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/pubsub/event/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/pubsub/event/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/pubsub/event/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/Configure.java b/app/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/Configure.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/pubsub/owner/Configure.java rename to app/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/Configure.java diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/PubSubOwner.java b/app/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/PubSubOwner.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/pubsub/owner/PubSubOwner.java rename to app/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/PubSubOwner.java diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/pubsub/owner/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/pubsub/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/pubsub/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/pubsub/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/reactions/Reaction.java b/app/src/main/java/im/conversations/android/xmpp/model/reactions/Reaction.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/reactions/Reaction.java rename to app/src/main/java/im/conversations/android/xmpp/model/reactions/Reaction.java diff --git a/src/main/java/im/conversations/android/xmpp/model/reactions/Reactions.java b/app/src/main/java/im/conversations/android/xmpp/model/reactions/Reactions.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/reactions/Reactions.java rename to app/src/main/java/im/conversations/android/xmpp/model/reactions/Reactions.java diff --git a/src/main/java/im/conversations/android/xmpp/model/reactions/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/reactions/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/reactions/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/reactions/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/receipts/Received.java b/app/src/main/java/im/conversations/android/xmpp/model/receipts/Received.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/receipts/Received.java rename to app/src/main/java/im/conversations/android/xmpp/model/receipts/Received.java diff --git a/src/main/java/im/conversations/android/xmpp/model/receipts/Request.java b/app/src/main/java/im/conversations/android/xmpp/model/receipts/Request.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/receipts/Request.java rename to app/src/main/java/im/conversations/android/xmpp/model/receipts/Request.java diff --git a/src/main/java/im/conversations/android/xmpp/model/receipts/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/receipts/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/receipts/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/receipts/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/register/Instructions.java b/app/src/main/java/im/conversations/android/xmpp/model/register/Instructions.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/register/Instructions.java rename to app/src/main/java/im/conversations/android/xmpp/model/register/Instructions.java diff --git a/src/main/java/im/conversations/android/xmpp/model/register/Password.java b/app/src/main/java/im/conversations/android/xmpp/model/register/Password.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/register/Password.java rename to app/src/main/java/im/conversations/android/xmpp/model/register/Password.java diff --git a/src/main/java/im/conversations/android/xmpp/model/register/Register.java b/app/src/main/java/im/conversations/android/xmpp/model/register/Register.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/register/Register.java rename to app/src/main/java/im/conversations/android/xmpp/model/register/Register.java diff --git a/src/main/java/im/conversations/android/xmpp/model/register/Remove.java b/app/src/main/java/im/conversations/android/xmpp/model/register/Remove.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/register/Remove.java rename to app/src/main/java/im/conversations/android/xmpp/model/register/Remove.java diff --git a/src/main/java/im/conversations/android/xmpp/model/register/Username.java b/app/src/main/java/im/conversations/android/xmpp/model/register/Username.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/register/Username.java rename to app/src/main/java/im/conversations/android/xmpp/model/register/Username.java diff --git a/src/main/java/im/conversations/android/xmpp/model/register/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/register/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/register/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/register/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/reply/Reply.java b/app/src/main/java/im/conversations/android/xmpp/model/reply/Reply.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/reply/Reply.java rename to app/src/main/java/im/conversations/android/xmpp/model/reply/Reply.java diff --git a/src/main/java/im/conversations/android/xmpp/model/retract/Retract.java b/app/src/main/java/im/conversations/android/xmpp/model/retract/Retract.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/retract/Retract.java rename to app/src/main/java/im/conversations/android/xmpp/model/retract/Retract.java diff --git a/src/main/java/im/conversations/android/xmpp/model/retract/Retracted.java b/app/src/main/java/im/conversations/android/xmpp/model/retract/Retracted.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/retract/Retracted.java rename to app/src/main/java/im/conversations/android/xmpp/model/retract/Retracted.java diff --git a/src/main/java/im/conversations/android/xmpp/model/retract/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/retract/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/retract/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/retract/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/roster/Group.java b/app/src/main/java/im/conversations/android/xmpp/model/roster/Group.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/roster/Group.java rename to app/src/main/java/im/conversations/android/xmpp/model/roster/Group.java diff --git a/src/main/java/im/conversations/android/xmpp/model/roster/Item.java b/app/src/main/java/im/conversations/android/xmpp/model/roster/Item.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/roster/Item.java rename to app/src/main/java/im/conversations/android/xmpp/model/roster/Item.java diff --git a/src/main/java/im/conversations/android/xmpp/model/roster/Query.java b/app/src/main/java/im/conversations/android/xmpp/model/roster/Query.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/roster/Query.java rename to app/src/main/java/im/conversations/android/xmpp/model/roster/Query.java diff --git a/src/main/java/im/conversations/android/xmpp/model/roster/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/roster/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/roster/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/roster/package-info.java diff --git a/app/src/main/java/im/conversations/android/xmpp/model/sasl2/Inline.java b/app/src/main/java/im/conversations/android/xmpp/model/sasl2/Inline.java new file mode 100644 index 000000000..e424cb0dd --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/sasl2/Inline.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.sasl2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Inline extends Extension { + + public Inline() { + super(Inline.class); + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/sasl2/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/sasl2/package-info.java new file mode 100644 index 000000000..36f8927ff --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/sasl2/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.SASL_2) +package im.conversations.android.xmpp.model.sasl2; + +import im.conversations.android.annotation.XmlPackage; +import im.conversations.android.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/Ack.java b/app/src/main/java/im/conversations/android/xmpp/model/sm/Ack.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/sm/Ack.java rename to app/src/main/java/im/conversations/android/xmpp/model/sm/Ack.java diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/Enable.java b/app/src/main/java/im/conversations/android/xmpp/model/sm/Enable.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/sm/Enable.java rename to app/src/main/java/im/conversations/android/xmpp/model/sm/Enable.java diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/Request.java b/app/src/main/java/im/conversations/android/xmpp/model/sm/Request.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/sm/Request.java rename to app/src/main/java/im/conversations/android/xmpp/model/sm/Request.java diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/Resume.java b/app/src/main/java/im/conversations/android/xmpp/model/sm/Resume.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/sm/Resume.java rename to app/src/main/java/im/conversations/android/xmpp/model/sm/Resume.java diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/sm/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/sm/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/sm/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java b/app/src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java rename to app/src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java b/app/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/stanza/Message.java rename to app/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java b/app/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java rename to app/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Stanza.java b/app/src/main/java/im/conversations/android/xmpp/model/stanza/Stanza.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/stanza/Stanza.java rename to app/src/main/java/im/conversations/android/xmpp/model/stanza/Stanza.java diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/stanza/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/stanza/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/stanza/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/state/Active.java b/app/src/main/java/im/conversations/android/xmpp/model/state/Active.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/state/Active.java rename to app/src/main/java/im/conversations/android/xmpp/model/state/Active.java diff --git a/src/main/java/im/conversations/android/xmpp/model/state/ChatStateNotification.java b/app/src/main/java/im/conversations/android/xmpp/model/state/ChatStateNotification.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/state/ChatStateNotification.java rename to app/src/main/java/im/conversations/android/xmpp/model/state/ChatStateNotification.java diff --git a/src/main/java/im/conversations/android/xmpp/model/state/Composing.java b/app/src/main/java/im/conversations/android/xmpp/model/state/Composing.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/state/Composing.java rename to app/src/main/java/im/conversations/android/xmpp/model/state/Composing.java diff --git a/src/main/java/im/conversations/android/xmpp/model/state/Gone.java b/app/src/main/java/im/conversations/android/xmpp/model/state/Gone.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/state/Gone.java rename to app/src/main/java/im/conversations/android/xmpp/model/state/Gone.java diff --git a/src/main/java/im/conversations/android/xmpp/model/state/Inactive.java b/app/src/main/java/im/conversations/android/xmpp/model/state/Inactive.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/state/Inactive.java rename to app/src/main/java/im/conversations/android/xmpp/model/state/Inactive.java diff --git a/src/main/java/im/conversations/android/xmpp/model/state/Paused.java b/app/src/main/java/im/conversations/android/xmpp/model/state/Paused.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/state/Paused.java rename to app/src/main/java/im/conversations/android/xmpp/model/state/Paused.java diff --git a/src/main/java/im/conversations/android/xmpp/model/state/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/state/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/state/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/state/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/streams/Features.java b/app/src/main/java/im/conversations/android/xmpp/model/streams/Features.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/streams/Features.java rename to app/src/main/java/im/conversations/android/xmpp/model/streams/Features.java diff --git a/src/main/java/im/conversations/android/xmpp/model/streams/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/streams/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/streams/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/streams/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/unique/OriginId.java b/app/src/main/java/im/conversations/android/xmpp/model/unique/OriginId.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/unique/OriginId.java rename to app/src/main/java/im/conversations/android/xmpp/model/unique/OriginId.java diff --git a/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java b/app/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java rename to app/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java diff --git a/src/main/java/im/conversations/android/xmpp/model/unique/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/unique/package-info.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/unique/package-info.java rename to app/src/main/java/im/conversations/android/xmpp/model/unique/package-info.java diff --git a/src/main/java/im/conversations/android/xmpp/model/version/Version.java b/app/src/main/java/im/conversations/android/xmpp/model/version/Version.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/model/version/Version.java rename to app/src/main/java/im/conversations/android/xmpp/model/version/Version.java diff --git a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java b/app/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java rename to app/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java diff --git a/src/main/java/im/conversations/android/xmpp/processor/IqProcessor.java b/app/src/main/java/im/conversations/android/xmpp/processor/IqProcessor.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/processor/IqProcessor.java rename to app/src/main/java/im/conversations/android/xmpp/processor/IqProcessor.java diff --git a/src/main/java/im/conversations/android/xmpp/processor/MessageAcknowledgeProcessor.java b/app/src/main/java/im/conversations/android/xmpp/processor/MessageAcknowledgeProcessor.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/processor/MessageAcknowledgeProcessor.java rename to app/src/main/java/im/conversations/android/xmpp/processor/MessageAcknowledgeProcessor.java diff --git a/src/main/java/im/conversations/android/xmpp/processor/MessageProcessor.java b/app/src/main/java/im/conversations/android/xmpp/processor/MessageProcessor.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/processor/MessageProcessor.java rename to app/src/main/java/im/conversations/android/xmpp/processor/MessageProcessor.java diff --git a/src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java b/app/src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java rename to app/src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java diff --git a/src/main/java/im/conversations/android/xmpp/sasl/Anonymous.java b/app/src/main/java/im/conversations/android/xmpp/sasl/Anonymous.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/sasl/Anonymous.java rename to app/src/main/java/im/conversations/android/xmpp/sasl/Anonymous.java diff --git a/src/main/java/im/conversations/android/xmpp/sasl/ChannelBinding.java b/app/src/main/java/im/conversations/android/xmpp/sasl/ChannelBinding.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/sasl/ChannelBinding.java rename to app/src/main/java/im/conversations/android/xmpp/sasl/ChannelBinding.java diff --git a/src/main/java/im/conversations/android/xmpp/sasl/ChannelBindingMechanism.java b/app/src/main/java/im/conversations/android/xmpp/sasl/ChannelBindingMechanism.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/sasl/ChannelBindingMechanism.java rename to app/src/main/java/im/conversations/android/xmpp/sasl/ChannelBindingMechanism.java diff --git a/src/main/java/im/conversations/android/xmpp/sasl/DigestMd5.java b/app/src/main/java/im/conversations/android/xmpp/sasl/DigestMd5.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/sasl/DigestMd5.java rename to app/src/main/java/im/conversations/android/xmpp/sasl/DigestMd5.java diff --git a/src/main/java/im/conversations/android/xmpp/sasl/External.java b/app/src/main/java/im/conversations/android/xmpp/sasl/External.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/sasl/External.java rename to app/src/main/java/im/conversations/android/xmpp/sasl/External.java diff --git a/src/main/java/im/conversations/android/xmpp/sasl/HashedToken.java b/app/src/main/java/im/conversations/android/xmpp/sasl/HashedToken.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/sasl/HashedToken.java rename to app/src/main/java/im/conversations/android/xmpp/sasl/HashedToken.java diff --git a/src/main/java/im/conversations/android/xmpp/sasl/HashedTokenSha256.java b/app/src/main/java/im/conversations/android/xmpp/sasl/HashedTokenSha256.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/sasl/HashedTokenSha256.java rename to app/src/main/java/im/conversations/android/xmpp/sasl/HashedTokenSha256.java diff --git a/src/main/java/im/conversations/android/xmpp/sasl/HashedTokenSha512.java b/app/src/main/java/im/conversations/android/xmpp/sasl/HashedTokenSha512.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/sasl/HashedTokenSha512.java rename to app/src/main/java/im/conversations/android/xmpp/sasl/HashedTokenSha512.java diff --git a/src/main/java/im/conversations/android/xmpp/sasl/Plain.java b/app/src/main/java/im/conversations/android/xmpp/sasl/Plain.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/sasl/Plain.java rename to app/src/main/java/im/conversations/android/xmpp/sasl/Plain.java diff --git a/src/main/java/im/conversations/android/xmpp/sasl/SaslMechanism.java b/app/src/main/java/im/conversations/android/xmpp/sasl/SaslMechanism.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/sasl/SaslMechanism.java rename to app/src/main/java/im/conversations/android/xmpp/sasl/SaslMechanism.java diff --git a/src/main/java/im/conversations/android/xmpp/sasl/ScramMechanism.java b/app/src/main/java/im/conversations/android/xmpp/sasl/ScramMechanism.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/sasl/ScramMechanism.java rename to app/src/main/java/im/conversations/android/xmpp/sasl/ScramMechanism.java diff --git a/src/main/java/im/conversations/android/xmpp/sasl/ScramPlusMechanism.java b/app/src/main/java/im/conversations/android/xmpp/sasl/ScramPlusMechanism.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/sasl/ScramPlusMechanism.java rename to app/src/main/java/im/conversations/android/xmpp/sasl/ScramPlusMechanism.java diff --git a/src/main/java/im/conversations/android/xmpp/sasl/ScramSha1.java b/app/src/main/java/im/conversations/android/xmpp/sasl/ScramSha1.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/sasl/ScramSha1.java rename to app/src/main/java/im/conversations/android/xmpp/sasl/ScramSha1.java diff --git a/src/main/java/im/conversations/android/xmpp/sasl/ScramSha1Plus.java b/app/src/main/java/im/conversations/android/xmpp/sasl/ScramSha1Plus.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/sasl/ScramSha1Plus.java rename to app/src/main/java/im/conversations/android/xmpp/sasl/ScramSha1Plus.java diff --git a/src/main/java/im/conversations/android/xmpp/sasl/ScramSha256.java b/app/src/main/java/im/conversations/android/xmpp/sasl/ScramSha256.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/sasl/ScramSha256.java rename to app/src/main/java/im/conversations/android/xmpp/sasl/ScramSha256.java diff --git a/src/main/java/im/conversations/android/xmpp/sasl/ScramSha256Plus.java b/app/src/main/java/im/conversations/android/xmpp/sasl/ScramSha256Plus.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/sasl/ScramSha256Plus.java rename to app/src/main/java/im/conversations/android/xmpp/sasl/ScramSha256Plus.java diff --git a/src/main/java/im/conversations/android/xmpp/sasl/ScramSha512.java b/app/src/main/java/im/conversations/android/xmpp/sasl/ScramSha512.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/sasl/ScramSha512.java rename to app/src/main/java/im/conversations/android/xmpp/sasl/ScramSha512.java diff --git a/src/main/java/im/conversations/android/xmpp/sasl/ScramSha512Plus.java b/app/src/main/java/im/conversations/android/xmpp/sasl/ScramSha512Plus.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/sasl/ScramSha512Plus.java rename to app/src/main/java/im/conversations/android/xmpp/sasl/ScramSha512Plus.java diff --git a/src/main/java/im/conversations/android/xmpp/sasl/Tokenizer.java b/app/src/main/java/im/conversations/android/xmpp/sasl/Tokenizer.java similarity index 100% rename from src/main/java/im/conversations/android/xmpp/sasl/Tokenizer.java rename to app/src/main/java/im/conversations/android/xmpp/sasl/Tokenizer.java diff --git a/app/src/main/new_launcher-playstore.png b/app/src/main/new_launcher-playstore.png new file mode 100644 index 000000000..5c7115e26 Binary files /dev/null and b/app/src/main/new_launcher-playstore.png differ diff --git a/src/main/res/anim/slide_from_left.xml b/app/src/main/res/anim/slide_from_left.xml similarity index 100% rename from src/main/res/anim/slide_from_left.xml rename to app/src/main/res/anim/slide_from_left.xml diff --git a/src/main/res/anim/slide_from_right.xml b/app/src/main/res/anim/slide_from_right.xml similarity index 100% rename from src/main/res/anim/slide_from_right.xml rename to app/src/main/res/anim/slide_from_right.xml diff --git a/src/main/res/anim/slide_to_left.xml b/app/src/main/res/anim/slide_to_left.xml similarity index 100% rename from src/main/res/anim/slide_to_left.xml rename to app/src/main/res/anim/slide_to_left.xml diff --git a/src/main/res/anim/slide_to_right.xml b/app/src/main/res/anim/slide_to_right.xml similarity index 100% rename from src/main/res/anim/slide_to_right.xml rename to app/src/main/res/anim/slide_to_right.xml diff --git a/src/main/res/drawable/greybackground.xml b/app/src/main/res/color/ic_launcher_background.xml similarity index 55% rename from src/main/res/drawable/greybackground.xml rename to app/src/main/res/color/ic_launcher_background.xml index f820cf318..a8b409b1d 100644 --- a/src/main/res/drawable/greybackground.xml +++ b/app/src/main/res/color/ic_launcher_background.xml @@ -1,6 +1,4 @@ - - \ No newline at end of file diff --git a/src/main/res/drawable/ic_account_circle_24dp.xml b/app/src/main/res/drawable/ic_account_circle_24dp.xml similarity index 100% rename from src/main/res/drawable/ic_account_circle_24dp.xml rename to app/src/main/res/drawable/ic_account_circle_24dp.xml diff --git a/src/main/res/drawable/ic_settings_24dp.xml b/app/src/main/res/drawable/ic_settings_24dp.xml similarity index 100% rename from src/main/res/drawable/ic_settings_24dp.xml rename to app/src/main/res/drawable/ic_settings_24dp.xml diff --git a/app/src/main/res/drawable/new_launcher_foreground.xml b/app/src/main/res/drawable/new_launcher_foreground.xml new file mode 100644 index 000000000..81360971b --- /dev/null +++ b/app/src/main/res/drawable/new_launcher_foreground.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/src/main/res/layout/activity_setup.xml b/app/src/main/res/layout/activity_setup.xml similarity index 100% rename from src/main/res/layout/activity_setup.xml rename to app/src/main/res/layout/activity_setup.xml diff --git a/src/main/res/layout/fragment_password.xml b/app/src/main/res/layout/fragment_password.xml similarity index 99% rename from src/main/res/layout/fragment_password.xml rename to app/src/main/res/layout/fragment_password.xml index fa9d2b8ca..46deb9163 100644 --- a/src/main/res/layout/fragment_password.xml +++ b/app/src/main/res/layout/fragment_password.xml @@ -33,7 +33,7 @@ android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:title="@string/app_name" - app:menu="@menu/setup" + app:menu="@menu/activity_setup" app:titleTextColor="?colorPrimary" app:titleCentered="true" /> diff --git a/src/main/res/layout/fragment_sign_in.xml b/app/src/main/res/layout/fragment_sign_in.xml similarity index 98% rename from src/main/res/layout/fragment_sign_in.xml rename to app/src/main/res/layout/fragment_sign_in.xml index 018557a7d..0339896e7 100644 --- a/src/main/res/layout/fragment_sign_in.xml +++ b/app/src/main/res/layout/fragment_sign_in.xml @@ -32,7 +32,7 @@ android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:title="@string/app_name" - app:menu="@menu/setup" + app:menu="@menu/activity_setup" app:titleTextColor="?colorPrimary" app:titleCentered="true" /> @@ -111,7 +111,6 @@ android:layout_height="wrap_content" android:text="@string/no_account_register" android:layout_centerHorizontal="true" - android:textColor="?colorSecondary" android:enabled="@{!setupViewModel.isLoading()}" android:layout_below="@+id/xmpp_address_input_layout" /> diff --git a/src/main/res/menu/setup.xml b/app/src/main/res/menu/activity_setup.xml similarity index 100% rename from src/main/res/menu/setup.xml rename to app/src/main/res/menu/activity_setup.xml diff --git a/app/src/main/res/mipmap-anydpi-v26/new_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/new_launcher.xml new file mode 100644 index 000000000..d8028846d --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/new_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/new_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/new_launcher_round.xml new file mode 100644 index 000000000..d8028846d --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/new_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/new_launcher.png b/app/src/main/res/mipmap-hdpi/new_launcher.png new file mode 100644 index 000000000..d6d9d200d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/new_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/new_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/new_launcher_foreground.png new file mode 100644 index 000000000..7b3e3701c Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/new_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-hdpi/new_launcher_round.png b/app/src/main/res/mipmap-hdpi/new_launcher_round.png new file mode 100644 index 000000000..942cd47c1 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/new_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/new_launcher.png b/app/src/main/res/mipmap-mdpi/new_launcher.png new file mode 100644 index 000000000..0aac67b6f Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/new_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/new_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/new_launcher_foreground.png new file mode 100644 index 000000000..67cc8e081 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/new_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-mdpi/new_launcher_round.png b/app/src/main/res/mipmap-mdpi/new_launcher_round.png new file mode 100644 index 000000000..9aa3b186e Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/new_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/new_launcher.png b/app/src/main/res/mipmap-xhdpi/new_launcher.png new file mode 100644 index 000000000..2e93afe5a Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/new_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/new_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/new_launcher_foreground.png new file mode 100644 index 000000000..f429a1e1f Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/new_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xhdpi/new_launcher_round.png b/app/src/main/res/mipmap-xhdpi/new_launcher_round.png new file mode 100644 index 000000000..0c2095a83 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/new_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/new_launcher.png b/app/src/main/res/mipmap-xxhdpi/new_launcher.png new file mode 100644 index 000000000..a156e6826 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/new_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/new_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/new_launcher_foreground.png new file mode 100644 index 000000000..c3bf21d93 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/new_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/new_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/new_launcher_round.png new file mode 100644 index 000000000..68fc69b35 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/new_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/new_launcher.png b/app/src/main/res/mipmap-xxxhdpi/new_launcher.png new file mode 100644 index 000000000..028e997b2 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/new_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/new_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/new_launcher_foreground.png new file mode 100644 index 000000000..18edcfba7 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/new_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/new_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/new_launcher_round.png new file mode 100644 index 000000000..08ea1e0ae Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/new_launcher_round.png differ diff --git a/src/main/res/navigation/setup_navigation.xml b/app/src/main/res/navigation/setup_navigation.xml similarity index 100% rename from src/main/res/navigation/setup_navigation.xml rename to app/src/main/res/navigation/setup_navigation.xml diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml new file mode 100644 index 000000000..6ce130008 --- /dev/null +++ b/app/src/main/res/values-ar/strings.xml @@ -0,0 +1,658 @@ + + + الإعدادات + محادثة جديدة + إدارة الحسابات + إدارة الحساب + أغلق المحادثة + بيانات جهة الإتصال + تفاصيل مجموعة المحادثة + تفاصيل القناة + إضافة حساب + تعديل الإسم + أضف إلى دفتر العناوين + حذف من الإضافات + حجب جهة إتصال + إنهاء حجب جهة اتصال + حجب دومين + إنهاء حجب دومين + احجب المشارِك + إلغاء حجب المشارِك + إدارة الحسابات + إعدادات + مشاركة مع محادثة + ابدأ محادثة + إختيار جهة إتصال + إختار جهات الإتصال + شارك عبر حساب + قائمة المحجوبين + الآن + منذ 1 دقيقة + منذ %d دقائق + ارسال + حل شيفرة الرسالة. الرجاء الإنتظار ... + رسالة مشفرة عبر OpenPGP + اللقب مستخدم من قبل + إسم المستخدم غير صالح + مدير + مالك + مشرف + مشترك + زائر + هل تريد حذف %sمن قائمة إتصالك؟ المحادثات مع هذا الشخص لن تحذف. + هل ترغب في حجب %s من ارسال الرسائل لك? + هل ترغب في انهاء حجب %s والسماح له بمراسلتك? + هل تريد حجب جميع جهات الإتصال من %s? + الغاء حجب جميع جهات الإتصال من %s? + جهة الاتصال محجوبه + محجوب + هل تريد حذف %sمن قائمة المفضلة؟ المحادثات مع هذا المفضل لن تحذف. + تسجيل حساب جديد في سيرفر + تغيير كلمة المرور في سيرفر + مشاركة مع + إبداء المحادثة + دعوة جهة إتصال + دعوة + جهات الإتصال + جهة إتصال + الغاء + تعيين + اضافة + تعديل + إحذف + حجب + الغاء حجب + حفظ + موافق + %1$sتعطّل + ارسال الآن + لا تسألني ثانية + لا يمكن الإتصال بالحساب + لايمكن الإتصال بحسابات متعددة + ارفاق ملف + اضافة جهة اتصال + فشل التسليم + الإستعداد لإرسال الصورة + الإستعداد لإرسال الصور + جاري إرسال الملفات. الرجاء الإنتظار ... + حذف سجل المحفوظات + حذف سجل المحفوظات للمحادثة + حذف الملفّ + إختر جهازاً + إرسال رسالة غير مشفرة + إبعث رسالة + إبعث رسالة إلى %s + إرسال رسالة مشفرة عبر OMEMO + إبعث رسالة مشفَّرة بـ أومي مو OMEMO + إرسال رسالة مشفرة عبر OpenPGP + إسم مستخدم جديد تحت الإستعمال + إرسال بدون تشفير + فشل فك التشفير. ربما ليس لديك المفتاح الخاص الصحيح. + OpenKeychain + إعادة التشغيل + تثبيت + قم بتثبيت OpenKeychain من فضلك + عرض .. + انتظار .. + لم يتم العثور على أي مفتاح OpenPGP + لم يتم العثور على أية مفاتيح OpenPGP + عام + ضبط استقبال الملفات + اقبل تلقائيا الملفات أقل من + الملفات المرفقة + إشعار + إعداد الإهتزاز + إهتز عند وصول رسالة جديدة + إشعار ضوئي + التنبيه الصوتي + تنبيه صوتي + فترة السماح + متقدم + لا ترسل تقارير أخطاء + تأكيد الرسالة + دع مراسيلك يعرفون متى قد تلقيت و هل قرئت رسائلهم + واجهة المستخدم + قبول + حدث خطأ ما + خطأ + حسابك + ارسال تحديثات الظهور + تحصل على تحديثات حالة الحضور + أطلب تحديثات حالة الحضور + اختيار صورة + التقاط صورة + الملف الذي حددته ليس صورة + لا يمكن تحويل ملف الصورة + الملف غير موجود + غير معروف + معطلٌ موقتاً + متصل + جاري الإتصال\u2026 + غير متصل + غير مصرح له + لا يمكن الاتصال بالسرفر + تحقق من اتصالك بالانترنت + فشل تسجيل الحساب + اسم المستخدم مستخدم من قبل + تم تسجيل حسابك بنجاح + فشلت عملية التفاوض عبر TLS + خرق للقواعد + لا يتوافق مع السيرفر + خطأ في التدفق + خطأ عند فتح التدفق + غير مشفر + رسالة مشفرة عبر OTR + رسالة مشفرة عبر OpenPGP + أوميمو OMEMO + إحذف الحساب + عطل موقتاً + نشر الصورة الرمزية + أنشر المفتاح العمومي OpenPGP + حذف مفتاح OpenPGP العمومي + تفعيل الحساب + هل أنت متأكد ؟ + تسجيل صوت + عنوان XMPP + احجب عنوان XMPP + username@example.com + كلمة السر + الذاكرة مليئة، صورة كبيرة جدا + هل تود إضافة %s إلى سجل عناوينك ؟ + معلومات عن المضيف + XEP-0313: إدارة أرشيف الرسائل + XEP-0280: نُسَخ كربونية + XEP-0352: مؤشر حالة العميل + XEP-0191: بروتوكول حظر الاتصالات + XEP-0237: قائمة ترقيم الإصدارات + XEP-0198: إدارة التدفق + XEP-0163: بروتوكول الأحداث الشخصية (الصور الرمزية / OMEMO) + XEP-0363: رفع الملفات عبر أيش تي تي بي + XEP-0357: الدفع + متاح + غير متاح + آخر ظهور الآن + آخر مشاهدة منذ دقيقة + آخر ظهور منذ %d دقيقة + آخر ظهور منذ %d ساعة + آخر ظهور منذ %d يوم + معرف ID مفتاح ال OpenPGP + بصمة OMEMO + بصمة v\\OMEMO + أجهزة أخرى + الثقة في بصمات أوميمو OMEMO + جارإحضار المفاتيح ... + تم + فك الشيفرة + الفواصل المرجعية + بحث + قم بإدخال جهة إتصال + حذف جهة الإتصال + إعرض بيانات جهة الاتصال + حجب جهة اتصال + الغاء حجب جهة اتصال + أضف + إختر + جهة الاتصال موجودة لديك مسبقا + التحق + channel@conference.example.com/nick + channel@conference.example.com + حفظ بالمفضلة + إحذف من المفضلة + دمر فريق المحادثة + دمر القناة + لم نتمكن مِن تدمير فريق المحادثة + لم نتمكن مِن تدمير القناة + تعديل موضوع مجموعة المحادثة + الموضوع + في صدد الإنظمام إلى مجموعة المحادثة ... + غادر + جهة اتصال أضافتك + أضف من جهتك أيضا + لقد قرأ %s الرسائل إلى غاية هذا السطر + لقد قرأ %s الرسائل إلى غاية هذا السطر + أنشر + نشر ... + لقد رفض السيرفر منشورك + تعذر الإحتفاظ بالصورة الرمزية على الذاكرة + همس + الى %s + ارسال رسالة خاصة الى %s + اتصال + الحساب موجود من قبل + التالي + تجاهل + ايقاف التنبيهات + تفعيل + تتطلب مجموعة المحادثة إدخال كلمة مرور + أدخل كلمة المرور + اطلب الآن + تجاهل + الأمان + السماح بمراجعة الرسالة + السماح لمراسليك بتعديل رسائلهم + إعدادات متقدمة + كن حذراً مع هذه من فضلك + عن %s + ساعات السكون + وقت البداية + وقت النهاية + تفعيل ساعات السكون + سوف تكتم التنبيهات إبان ساعات السكون + أخرى + حسابك محظور للإلتحاق بمجموعة المحادثة هذه + هذه المجموعة متاحة للأعضاء المنتمين إليها فقط + تم طردك من مجموعة الدردشة هذه + تم إغلاق مجموعة المحادثة + لم تعد متواجدا داخل مجموعة المحادثة هذه + أنت تستعمل حساب %s + انقطع الإتصال .. حاول مرة أخرى + تحقق من حجم %s + تحقق مِن حجم %1$s على %2$s + خيارات الرسالة + إقتبس + ألصقه كاقتباس + أنسخ الرابط الأصلي + أعد الإرسال + رابط الملف + تم نسخ الرابط إلى الحافظة + تم نسخ عنوان الـ XMPP إلى الحافظة + تم نسخ رسالة الخطأ إلى الحافظة + عنوان الويب + إمسح شفرة التّعرّف 2D + أظهر شفرة التّعرّف 2D + إعرض قائمة المحبوسين + تفاصيل الحساب + تأكيد + حاول مرة أخرى + الخدمة الأمامية + منع نظام التشغيل من انهاء اتصالك + أنشئ نسخة احتياطية + تم إنشاء نسختك الاحتياطية + تم استرجاع نسختك الاحتياطية + لا تنسى تنشيط الحساب. + اختيار ملف + اكتمل الإستلام %1$s (%2$d%% بنسبة) + تنزيل %s + إحذف %s + ملف + فتح %s + إكتمل الإرسال (%1$d%% بنسبة) + %s عرض وتنزيل + الغاء الارسال + وسوم ديناميكية + عرض علامات للقراءة فقط أسفل بيانات جهات الإتصال + تفعيل الإشعارات + لم يُعثر على أي خادم للمحادثات الجماعية + الصورة الرمزية للحساب + انسخ بصمة OMEMO إلى الحافظة + إعادة إنشاء مفتاح أوميمو OMEMO + حذف أجهزة + لقد طرأ هناك خطأ + جلب المحفوظات من السرفر + لا مزيد من المحفوظات بالسرفر + جاري التحديث.. + تم تغيير كلمة السر! + لايمكن تغيير كلمة السر + تغيير كلمة المرور + كلمة المرور الحالية + كلمة المرور الجديدة + تفعيل كل الحسابات + تعطيل كل الحسابات + تنفيذ الإجراء مع + زائر + غير متصل + مفصول + عضو + الوضع المتقدم + منح امتيازات الإداره + إلغاء امتيازات الإدارة + التنحية من مجموعة المحادثة + أزله مِن القناة + لا يمكن تغيير انتساب %s + الحظر من دخول مجموعة المحادثة + اطرده مِن القناة + حظر الآن + لا يمكن تغيير دول %s + إعدادات القناة العمومية + سرِّي ، للأعضاء فقط + اجعل القناة تحت الإشراف + لست مشتركا في المجموعة + تم تعديل خيارات فريق المحادثة ! + تعذر تغيير خيارات فريق المحادثة + أبداً + حتى إشعار آخر + إغفاء + الرد + إعتباره كمقروء + أدخل للإرسال + عرض مفتاح الادخال + تغيير مفتاح الرموز إلى مفتاح الدخول + صوت + فيديو + صورة + مستند PDF + تطبيق اندرويد + تواصل + تم نشر الصورة الرمزية ! + ارسال %s + عرض %s + اخفاء غير المتصلين + %s يكتب … + توقف %s عن الكتابة + %s يكتبون … + %s توقفو عن الكتابة + إشعارات الكتابة + إعلم جهات الاتصال الخاصة بك عندما تكتب رسائل لهم. + إرسل موقعك + أظهر الموقع + الموقع + تم إغلاق المحادثة + لا تثق في سلطات شهادات النظام + تقبل أية شهادة يدويا + حذف شهادات + إحذف الشهادات التي تقبلتها يدويا + إحذف الشهادات + قم بحذف المختارة + الغاء + حركة سريعة + لا شيء + التي تم استعمالها كثيرا مؤخرا + إختر حركة سريعة + البحث في جهات الإتصال + البحث في الفواصل المرجعية + إبعث برسالة على الخاص + إسم المستخدم + إسم المستخدم + إسم المستخدم هذا ليس صالحاً + فشل التنزيل : لم يتم العثور على السيرفر + فشل التنزيل : لم يتم العثور على الملف + فشل التنزيل : لا يمكن الاتصال بالسيرفر + فشل التنزيل : لا يمكن كتابة الملف + شبكة طور غير متاحة + مكسر + حالة الحضور + إعدادات الربط الموسعة + عرض اسم المضيف وإعدادات المنفذ عند تنصيب حساب + xmpp.example.com + إعدادت الأرشيف + قم بإدخال الرموز المتواجدة في الصورة + تجديد الشهادة + خطأ أثناء جلب مفتاح أوميمو OMEMO ! + إنّ جهازك لا يدعم اختيار شهادات العملاء ! + الإتصال + الإتصال عبر شبكة طور Tor + تمرير كافة الاتصالات عبر شبكة تور. يتطلب أوربوت Orbot + إسم المضيف + المنفذ + هذا ليس برقم منفذٍ صالح + إسم السيرفر هذا ليس صالحاً + %1$d مِن أصل %2$d حسابات متصلة + + %d رسائل + %d رسالة + %d رسالتين + %d رسائل + %d رسائل + %d رسائل + + أظهر أكثر + المزامنة مع جهات الإتصال + اخبرني عند وصول أية رسالة + قم بإخطاري عندما يقوم أحدهم بذكري + تعطيل الإخطارات + الإشعارات موقفة + ضغط الصورة + دائماً + وضع تحسين أداء البطارية مفعّل + تعطيل + المساحة المحددة كبيرة جدا + )ليس هناك أي حساب مفعل( + هذه الخانة مطلوبة + صَحِّح الرسالة + إبعث الرسالةَ مُصححة + لقد قمت بتعطيل هذا الحساب + شارك الرابط مع ... + عنوان XMPP الخاص بك سيكون: %s + إنشاء حساب + إستخدم مزودي الخاص + إختر إسم المستخدم + ضبط حالة الحضور يدويًا + نص حالة الحضور + جاهز للدردشة + متصل + غائِب + مشغول + مشغول + تم توليد كلمة سرية آمنة جديدة + فشل تسجيل الحساب : حاول مرةً أخرى لاحقاً + فشل تسجيل الحساب : كلمة السر ضعيفة جداً + إختر المشاركين + جارٍ إنشاء مجموعة المحادثة ... + أعد إرسال الدعوة + تعطيل + قصير + متوسط + طويل + الخصوصية + السمة + إختر اللون المناسب + خلفية خضراء + إستخدم خلفية خضراء للرسائل المُستَلَمة + لم يعُد هذا الجهاز مُستعمَلًا + كمبيوتر + هاتف نقال + لوحة مفاتيحية + متصفح الواب + الواجهة الطرفية + مطلوب منك الدفع + أنا + لقد طلب منك مراسِك الإشتراك في حالات حضورك + إسمح + لم يتم العثور على السيرفر البعدي + إحذف تعريفات أو مي مو OMEMO + إحذف المفاتيح المختارة + يجب أن تكون متصلا ليسمح لك بنشر الصورة الرمزية الخاصة بك. + أظهر رسالة الخطأ + رسالة خطأ + خدمة توفير البيانات مُنشّطة + تم التحقق من هذا الجهاز + انسخ البصمة + البصمات المصادق عليها + إستخدم آلة التصوير لمسح شفرة التعرف على مراسلك + الرجاء الإنتظار ريثما يتم جلب المفاتيح + شاركه كشَفْرة خَيْطيّة + شاركه كعنوان XMPP + شاركه كرابط HTTP + الثقة العمياء قبل التحقق + غير موثوق بها + الشفْرة الخيْطيّة خاطئة + تفريغ الذاكرة الوسيطة + تفريغ مساحة التخرين الخاصة + لقد إتبعتُ هذا الرابط عبر مصدر موثوق + قم بفحص مفاتيح أو مي مو OMEMO + إظهار غير الناشطين + إخفاء المستخدمين غير الناشطين + إبطال الثقة مع هذا الجهاز + + %d ثانية + %d ثانية + %d ثانيتين + %d ثواني + %d ثواني + %d ثواني + + + %d دقيقة + %d دقيقة + %d دقيقتين + %d دقائق + %d دقائق + %d دقائق + + + %d ساعة + %d ساعة + %d ساعتين + %d ساعات + %d ساعات + %d ساعات + + + %d يوم + %d يوم + %d يومين + %d أيام + %d أيام + %d أيام + + + %d أسبوع + %d أسبوع + %d أسبوعين + %d أسابيع + %d أسابيع + %d أسابيع + + + %d شهر + %d شهر + %d شهرين + %d أشهر + %dأشهر + %d أشهر + + الحذف الآلي للرسائل + تشفير الرسالة + ضغط الفيديو + جهة الاتصال محجوبة. + الإشعارات من طرف غرباء + لقد تلقيت رسالة من شخص غريب + حظر الغريب + حظر إسم النطاق كاملا + على الهواء الآن + إعادة محاولة فك التشفير + فشلت الجلسة + لا يمكن تسجيل حسابات على هذا الخادوم إلا عبر موقع الويب + فتح موقع الإنترنت + اليوم + البارحة + التحقق من صحة إسم المضيف بواسطة DNSSEC + الشهادة لا تحتوي على عنوان XMPP + جُزْئِيًّا + تسجيل فيديو + النسخ إلى الحافظة + تم نسخ الرسالة إلى الحافظة + رسالة + الرسائل الخاصة معطلة + التطبيقات المُؤمَّنَة + تقبُّل الشهادات المجهولة ؟ + إنّ شهادة الخادوم غير مُوقَّعَة مِن طرف هيئة شهادات معروفة. + بالرغم مِن ذلك هل تريد مواصلة الإتصال ؟ + تفاصيل الشهادة : + مرة واحدة + السحب إلى أسفل + التمرير إلى أسفل بعد إرسال رسالة + تعديل رسالة حالة الحضور + تعديل رسالة حالة الحضور + تعطيل التعمية + تعطيله حالًا + المسودة: + التعمية بـ OMEMO + سوف يُستخدَم OMEMO افتراضيا في المحادثات الجديدة. + حجم الخط + نشِط مبدئيًا + معطل مبدئيًا + صغير + متوسط + كبير + تراجع + مشاركة الموقع معطّلة + نسخ الموقع + مشاركة الموقع + توجيهات + مشاركة الموقع + إظهار الموقع + مشاركة + يرجى الانتظار… + البحث عن رسائل + مشاهدة المحادثة + نسخ العنوان الإلكتروني + انسخ عنوان الـ XMPP + بحث مباشر + إسم جهة الإتصال + إسم مستعار + إسم + إدخال الاسم اختياري + اسم فريق المحادثة + الخدمة الأمامية + معلومات عن الحالة + مشاكل إتّصال + رسائل + رسائل + الأهمية ، الصوت ، الإهتزاز + ضغط الفيديو + اعرض الوسائط + المشارِكون + جودة الفيديو + متوسط (360ب) + عالي (720ب) + رقم دولة غير صحيح + إختار الدولة + رقم هاتف + تحقق من رقم هاتفك + ليس %s برقم هاتف صالح. + يرجى إدخال رقم هاتفك. + البحث عن الدول + تحقق مِن %s + إعادة إرسال الإرسالية القصيرة + يرجى الانتظار (%s) + رجوع + نعم + لا + جارٍ التحقق… + جارٍ طلب الرسالة النصية القصيرة… + خطأ شبكي مجهول. + ليس هناك اتصال بالشبكة. + يرجى إعادة المحاولة في غضون %s + تحديث + إسمك + أدخل إسمك + إضغط على زرّ التعديل لضبط إسمك + أرفض الطلب + نصّب أوربات + شغّل أوربات + كتاب إلكتروني + أصلي (غير مضغوط) + إفتح بـ... + صورة حساب كونفرسايشنز + إختيار الحساب + استرجِع نسخة احتياطية + استرجِع + ادخِل عنوان XMPP + أنشئ فريق محادثة + إلتحِق بقناة عمومية + أنشئ فريق محادثة خاص + أنشئ قناة عمومية + اسم القناة + عنوان XMPP + يرجى إدخال اسمٍ للقناة + جارٍ إنشاء القناة العمومية… + لقد التحقت بقناة موجودة سابقا + اسمح لأي كان دعوة الآخرين + يمكن للمالِكين تعديل الموضوع. + إدارة الصلاحيات + البحث عن مشارِكين + حجم الملف كبير جدًا + أرفِق + استكشاف القنوات + البحث عن قنوات + لدي حساب + إضافة حساب موجود + تسجيل حساب جديد + أضفه على أي حال + حَدَث + افتح النسخة الاحتياطية + يرجى إدخال الكلمة السرية للحساب + مشغول + خيارات أخرى + diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml new file mode 100644 index 000000000..a5018d6ec --- /dev/null +++ b/app/src/main/res/values-bg/strings.xml @@ -0,0 +1,966 @@ + + + Настройки + Нов разговор + Управление на профилите + Управление на профила + Затваряне на разговора + Подробности за контакта + Подробности за груповия разговор + Подробности за канала + Добавяне на профил + Редактиране на името + Добавяне към адресния указател + Изтриване от списъка + Блокиране на контакта + Деблокиране на контакта + Блокиране на домейна + Деблокиране на домейна + Блокиране на участника + Деблокиране на участника + Управление на профилите + Настройки + Споделяне в разговора + Започване на разговор + Изберете контакт + Изберете контакти + Споделяне чрез профил + Списък с блокирани + току-що + преди 1 минута + преди %d минути + + %d непрочетен разговор + + + %d непрочетени разговора + + + изпращане… + Дешифроване на съобщението. Моля, изчакайте… + Съобщение, шифр. чрез OpenPGP + Псевдонимът е зает + Неправилен псевдоним + Администратор + Собственик + Модератор + Участник + Посетител + Искате ли да премахнете %s от списъка си с контакти? Разговорите с него няма да бъдат премахнати. + Искате ли да блокирате %s, така че да не може да Ви праща съобщения? + Искате ли да деблокирате %s, така че отново да може да Ви праща съобщения? + Блокиране на всички контакти от %s? + Деблокиране на всички контакти от %s? + Контактът е блокиран + Блокиран + Искате ли да премахнете отметката за %s? Разговорите, свързани с тази отметка, няма да бъдат премахнати. + Регистриране на нов профил на сървъра + Промяна на паролата в сървъра + Споделяне с… + Започване на разговор + Поканете контакт + Поканете + Контакти + Контакт + Отказ + Задаване + Добавяне + Редактиране + Изтриване + Блокиране + Деблокиране + Запазване + Добре + %1$s се срина + Използването на Вашия профил в XMPP за изпращане на проследявания на стека помага на разработката на %1$s. + Изпращане сега + Не ме питайте повече + Свързването с профила е невъзможно + Свързването с няколко профила е невъзможно + Докоснете за управление на профилите си + Прикачане на файл + Добавяне на този липсващ контакт към списъка с контакти? + Добавяне на контакт + доставянето се провали + Изображението се подготвя за изпращане + Изображенията се подготвят за изпращане + Споделяне на файлове. Моля, изчакайте… + Изчистване на историята + Изчистване на историята на разговорите + Наистина ли искате да изтриете всички съобщения в този разговор?\n\nВнимание: Това няма да изтрие съобщенията, които се съхраняват на други устройства или сървъри. + Изтриване на файла + Наистина ли искате да изтриете този файл?\n\nВнимание: Това няма да изтрие копията на файла, които се съхраняват на други устройства или сървъри. + Затваряне на този разговор след това + Изберете устройство + Изпр. на нешифр. съобщение + Изпращане на съобщение + Изпращане на съобщение до %s + Изпр. на съобщение, шифр. чрез OMEMO + Изпр. на съобщение, шифр. чрез v\\OMEMO + Изпр. на съобщение, шифр. чрез OpenPGP + Новият псевдоним е зает + Изпращане нешифровано + Неуспешно дешифроване. Възможно е да нямате правилния частен ключ. + OpenKeychain + OpenKeychain, за да шифрова и дешифрова съобщенията и да управлява публичните Ви ключове.

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

(Моля, рестартирайте %1$s след това.)]]>
+ Рестартиране + Инсталиране + Моля, инсталирайте OpenKeychain + предлагане… + изчакване… + Не е открит OpenPGP ключ + Съобщението Ви не може да се шифрова, тъй като контактът Ви не е обявил публичния си ключ.\n\nМоля, помолете го да инсталира и настрои OpenPGP. + Не са открити OpenPGP ключове + Съобщението Ви не може да се шифрова, тъй като контактите Ви не са обявили публичните си ключове.\n\nМоля, помолете ги да инсталират и настроят OpenPGP. + Общи + Приемане на файлове + Автоматично приемане на файлове с размер, по-малък от… + Притурки + Известие + Вибрация + Вибриране при получаване на ново съобщение + Известие чрез светодиода + Мигане на индикаторния светодиод при получаване на ново съобщение + Тон на звънене + Звук за известията + Звук за известията при получаване на нови съобщения + Звук за входящи обаждания + Период на пренебрегване + Продължителност на времето, през което известията се заглушават, след като бъде забележена дейност на някое от другите Ви устройства. + Разширени + Никога да не се изпращат доклади за сривове + Изпращайки проследявания на стека, Вие помагате на разработката + Потвърждаване на съобщенията + Така контактите Ви ще разбират, че сте получили и прочели съобщенията им + Забраняване на снимките на екрана + Скриване на съдържанието на приложенията от превключвателя на приложения и блокиране на снимките на екрана + Потр. интерфейс + Възникна грешка в OpenKeychain. + Неправилен ключ за шифроване. + Приемане + Възникна грешка + Грешка + Вашият профил + Изпращане на актуализации за присъствието + Получаване на актуализации за присъствието + Питане за актуализации за присъствието + Изберете снимка + Заснемане + Предварително позволяване на абониране при заявка + Избраният файл не е изображение + Файлът с изображението не може да бъде преобразуван + Файлът не е открит + Обща В/И грешка. Може би нямате достатъчно свободно място? + Приложението, което използвахте, за да изберете това изображение, не предоставя нужните права за прочитането му.\n\nИзползвайте друг файлов мениджър, за да изберете изображение. + Приложението, което използвахте, за да споделите този файл, не предоставя нужните правомощия. + Непознат + Временно деактивиран + На линия + Свързване\u2026 + Извън линия + Неупълномощен + Сървърът не е открит + Няма връзка + Неуспешна регистрация + Потребителското име е заето + Регистрацията е завършена + Регистрацията не се поддържа от сървъра + Неправилен регистрационен идентификатор + Договарянето чрез TLS беше неуспешно + Домейнът не може да се провери + Нарушение на политиката + Несъвместим сървър + Поточна грешка + Проблем при отварянето на потока + Нешифровано + OTR + OpenPGP + OMEMO + Изтриване на профила + Временно деактивиране + Публикуване на аватар + Публикуване на публичния OpenPGP ключ + Премахване на публичния OpenPGP ключ + Наистина ли искате да премахнете своя публичен OpenPGP ключ от известяването си за присъствие?\nКонтактите Ви вече няма да могат да Ви изпращат съобщение, шифровани чрез OpenPGP. + Публичният OpenPGP ключ е публикуван. + Активиране на профила + Наистина ли искате това? + Изтриването на профила Ви ще изтрие и цялата история на разговорите Ви + Запис на глас + XMPP адрес + Блокиране на XMPP адрес + username@example.com + Парола + Това не е правилен XMPP адрес + Няма достатъчно памет. Изображението е твърде голямо. + Искате ли да добавите %s към адресния си указател? + Инф. за сървъра + XEP-0313: Управление на архива на съобщенията + XEP-0280: Копия на съобщенията + XEP-0352: Показания за състоянието на клиента + XEP-0191: Команда за блокиране + XEP-0237: Поддържане на версия на списъка + XEP-0198: Управление на потоците + XEP-0215: Откриване на външни услуги + XEP-0163: PEP (Аватари / OMEMO) + XEP-0363: Качване на файл през HTTP + XEP-0357: Изпращане + налично + не е налично + Липсват обявления за публичен ключ + последно видян току-що + последно видян преди една минута + последно видян преди %d минути + последно видян преди час + последно видян преди %d часа + последно видян преди ден + последно видян преди %d дни + Шифровано съобщение. Моля, инсталирайте OpenKeychain, за да го дешифровате. + Открити са нови съобщения, шифровани чрез OpenPGP + Ид. на OpenPGP ключа + Отпечатък OMEMO + Отпечатък v\\OMEMO + Отпечатък OMEMO (източник на съобщението) + v\\Отпечатък OMEMO (източник на съобщението) + Други устройства + Доверяване на отпечатъци OMEMO + Изтегляне на ключове… + Готово + Дешифроване + Отметки + Търсене + Въведете контакт + Изтриване на контакта + Преглед на подр. за контакта + Блокиране на контакт + Деблокиране на контакт + Създаване + Избиране + Контактът вече съществува + Присъединяване + канал@беседа.сървър.com/псевдоним + канал@беседа.сървър.com + Запазване като отметка + Изтриване на отметка + Унищожаване на груповия разговор + Унищожаване на канала + Наистина ли искате да унищожите този групов разговор?\n\nВнимание: Груповият разговор ще бъде премахнат от сървъра. + Наистина ли искате да унищожите този групов канал?\n\nВнимание: Груповият канал ще бъде напълно премахнат от сървъра. + Груповият разговор не може да бъде унищожен + Каналът не може да бъде унищожен + Редактиране на темата на груповия разговор + Тема + Присъединяване в групов разговор… + Напускане + Контактът е добавен във Вашия списък от контакти + Добавяне обратно + %s е прочел до тук + %s човека са прочели до тук + %1$s и още %2$d човека са прочели до тук + Всички са прочели до тук + Публикуване + Докоснете аватара, за да изберете снимка от галерията + Публикуване… + Сървърът отказа Вашето публикуване + Снимката Ви не може да бъде преобразувана + Аватарът не може да бъде запазен на диска + (Или задръжте, за да върнете началното) + Сървърът Ви не поддържа публикуване на аватари + прошепна + на %s + Изпращане на лично съобщение до %s + Свързване + Този профил вече съществува + Следващо + Установена сесия + Пропускане + Изключване на известията + Включване + Груповият разговор изисква парола + Въведете парола + Моля, първо помолете контакта за актуализации на присъствието му.\n\nТова ще бъде използвано, за да се провери какво приложение използва контактът. + Поискване сега + Пренебрегване + Внимание: Изпращането на това без съвместни актуализации на присъствието може да доведе до неочаквани проблеми.\n\nПогледнете подробностите за контакта, за да проверите дали сте абониран за актуализации на присъствието. + Сигурност + Позволяване на поправянето на съобщения + Позволяване на контактите да редактират съобщенията си след като са ги изпратили. + Експертни настройки + Моля, бъдете внимателни с тези + Относно %s + Тихи часове + Начало + Край + Включване на тихите часове + Известията ще бъдат заглушени по време на тихите часове + Други + Отпечатъкът OMEMO е копиран + Достъпът Ви до този групов разговор е забранен + Този групов разговор е само за членове + Ограничение на ресурса + Бяхте изритан(а) от този групов разговор + Груповия разговор приключи + Вече не участвате в този групов разговор + използвайки профила %s + Проверяване на %s на HTTP сървъра + Не сте свързани. Опитайте отново по-късно + Проверете размера на %s + Проверете размера на %1$s на %2$s + Настройки за съобщенята + Цитат + Поставяне като цитат + Копиране на оригиналния адрес + Повторно изпращане + Адрес на файла + Копиране на адреса + XMPP-адресът е копиран + Съобщението за грешка е копирано + уеб адрес + Сканиране на 2-измерен баркод + Показване на 2-измерен баркод + Показване на списъка с блокирани + Подробности за профила + Потвърждаване + Повторен опит + Услуга на преден план + Предотвратява прекъсването на връзката Ви от операционната система + Създаване на резервно копие + Резервните копия ще се пазят в %s + Създаване на резервни копия + Резервното копие е създадено + Файловете на резервното копие бяха запазени в %s + Възстановяване от резервно копие + Възстановяването от резервно копие е завършено + Не забравяйте да включите профила. + Изберете файл + Получаване на %1$s (%2$d%% завършено) + Сваляне на %s + Изтриване на %s + файл + Отваряне на %s + изпращане (%1$d%% завършено) + Файлът се подготвя за споделяне + %s е предложен за сваляне + Отказ на прехвърлянето + файлът не може да бъде споделен + изпращането на файла е отменено + Файлът е изтрит + Няма намерено приложение за отваряне на файла + Няма намерено приложение за отваряне на връзката + Няма намерено приложение за преглед на контакта + Динамични етикети + Показване на етикети, предназначени само за четене под контактите + Включване на известията + Не е открит сървър за груповия разговор + Груповият разговор не може да бъде създаден + Аватар на профила + Копиране на отпечатъка OMEMO + Повторно създаване на ключа OMEMO + Премахване на устройствата + Наистина ли искате да премахнете всички останали устройства от обявлението OMEMO? Следващия път, когато устройствата Ви се свържат, те ще обявят себе си отново, но може да не получат съобщенията, изпратени междувременно. + Няма ключове, които могат да бъдат използвани за този контакт.\nОт сървъра не могат да бъдат изтеглени нови ключове. Възможно е да има проблем със сървъра на контакта Ви. + Няма ключове, които могат да бъдат използвани за този контакт.\nУверете се, че и двамата имате абонамент за присъствието. + Нещо се обърка + Получаване на историята от сървъра + Няма повече история на сървъра + Актуализиране… + Паролата е променена! + Паролата не може да бъде променена + Промяна на паролата + Текуща парола + Нова парола + Паролата не може да е празна + Активиране на всички профили + Деактивиране на всички профили + Изпълнение на действието с + Няма принадлежност + Извън линия + Отхвърлен + Член + Разширен режим + Даване на правомощия на член + Премахване на правомощията на член + Даване на правомощия на администратор + Отмяна на администраторските права + Даване на правомощия на собственик + Премахване на правомощията на собственик + Премахване от груповия разговор + Премахване от канала + Принадлежността на %s не може да бъде променена + Забраняване на достъпа до груповия разговор + Забраняване на достъпа до канала + Опитвате се да премахнете%s от публичен канал. Единственият начин да направите това е да блокирате завинаги потребителя. + Забраняване на достъпа сега + Ролята на %s не може да бъде променена + Настройка на частен групов разговор + Настройка на публичен групов разговор + Частно, само за членове + Нека XMPP адресите бъдат видими за всички + Нека каналът да се модерира + Вие не участвате + Настройките на груповия разговор бяха променени! + Настройките на груповия разговор не могат да бъдат променени + Никога + До отмяна + Отлагане + Отговаряне + Отбелязване като прочетено + Въвеждане + Enter изпраща + Използвайте клавиша Enter, за да изпратите съобщение. Винаги може да използвате Ctrl+Enter за изпращане на съобщение, дори тази настройка да е изключена. + Показване на клавиша Enter + Смяна на клавиша за емотикони с клавиша Enter + аудио + видео + изображение + векторна графика + PDF документ + Приложение за Андроид + Контакт + Аватарът беше публикуван! + Изпращане на %s + Предлагане на %s + Скриване на тези извън линия + %s пише… + %s спря да пише + %s пишат… + %s спряха да пишат + Известия за писането + Така контактите Ви ще разбират, когато им пишете съобщения + Изпращане на местоположението + Показване на местоположението + Няма намерено приложение за показване на местоположението + Местоположение + Conversation се затвори + Напуснахте частния групов разговор + Напуснахте публичния канал + Да не се вярва на системните сертификати + Всички сертификати трябва да бъдат одобрени на ръка + Премахване на сертификатите + Изтриване на сертификатите, одобрени на ръка + Няма сертификати, одобрени на ръка + Премахване на сертификатите + Изтриване на избраните + Отказ + + %d сертификат е изтрит + %d сертификата са изтрити + + Замяна на бутона „Изпращане“ с бързо действие + Бързо действие + Нищо + Използвани наскоро + Изберете бързо действие + Търсене в контактите + Търсене в отметките + Изпращане на лично съобщение + %1$s напусна груповия разговор + Потребителско име + Потребителско име + Това не е правилно потребителско име + Неуспешно сваляне: Сървърът не е открит + Неуспешно сваляне: Файлът не е открит + Неуспешно сваляне: Неуспешна връзка със сървъра + Неуспешно сваляне: Файлът не може да бъде записан + Мрежата на Тор е недостъпна + Грешка при свързване + Сървърът не отговаря за този домейн + Повредено + Присъствие + Отсъстващ при заключено устройство + Показване като „отсъстващ“, когато устройството е заключено + Зает в тих режим + Показване като „зает“, когато устройството е в тих режим + Тих режим при режим на вибриране + Показване като „зает“, когато устройството е на вибрация + Разширени настройки за връзката + Показване на настройките за сървър и порт при установка на профил + xmpp.example.com + Влизане със сертификат + Сертификатът не може да бъде прочетен + Настройки за архивирането + Настройки за архивирането на сървъра + Получаване на настройките за архивирането. Моля, изчакайте… + Настройките за архивирането не могат да бъдат получени + Проверката е задължителна + Въведете текста от горното изображение + Недоверен верижен сертификат + XMPP адресът не съответства на сертификата + Подновяване на сертификата + Грешка при получаването на ключа за OMEMO! + Ключът за OMEMO беше потвърден със сертификат! + Устройството Ви не поддържа избраните клиентски сертификати! + Връзка + Свързване през Тор + Всички връзки да минават през мрежата на Тор. Изисква Орбот + Име на сървър + Порт + Адрес на сървър или .onion + Това не е правилен номер на порт + Това не е правилно име на сървър + %1$d от %2$d свързани профила + + %d съобщение + %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 + Създаване на профил + Използване на собствен доставчик + Изберете потребителското си име + Ръчна промяна на присъствието + Задайте присъствието си, когато редактирате съобщението за състоянието си. + Съобщение за състоянието + Свободен за разговор + На линия + Отсъстващ + Недостъпен + Зает + Беше създадена сигурна парола + Устройството Ви не поддържа изключването на оптимизациите за използването на батерията + Неуспешна регистрация. Опитайте отново по-късно + Неуспешна регистрация: паролата е твърде слаба + Изберете участниците + Създаване на групов разговор… + Канене отново + Изключване + Кратко + Средно + Дълго + Информиране за използването + Позволява на контактите Ви да знаят кога използвате Conversations + Поверителност + Тема + Изберете цветовата схема + Автоматично + Светла + Тъмна + Зелен фон + Получените съобщения ще бъдат на зелен фон + Свързването с OpenKeychain е невъзможно + Това устройство вече не се използва + Компютър + Мобилен телефон + Таблет + Браузър + Конзола + Изисква се плащане + Дайте разрешение за достъп до Интернет + Аз + Контакт моли за абонамент за присъствието + Позволяване + Няма позволение за достъп до %s + Отдалеченият сървър не е намерен + Времето за изчакване на отдалечения сървър изтече + Профилът не може да бъде обновен + Докладване този XMPP адрес за спам. + Изтриване на идентификаторите OMEMO + Пресъздайте своите ключове OMEMO. Всички Ваши контакти ще трябва да Ви потвърдят отново. Използвайте това само в краен случай. + Изтриване на избраните ключове. + Трябва да бъдете свързан(а), за да публикувате аватара си. + Показване на грешка + Съобщение за грешка + Пестенето на данни е включено + Операционната Ви система не позволява на %1$s да се свързва с Интернет когато работи на заден фон. За да получавате известия за новите съобщения, трябва да дадете на %1$s неограничен достъп когато пестенето на данни е включено.\n%1$s ще продължи да се опитва да записва данните когато е възможно. + Устройството Ви не поддържа изключването на пестенето на данни за %1$s. + Не може да се създаде временен файл + Това устройство е потвърдено + Копиране на отпечатъка + Потвърдили сте всички ключове OMEMO, които притежавате + Баркодът не съдържа отпечатъци за този разговор. + Потвърдени отпечатъци + Използвайте камерата, за да сканирате баркода на контакт + Моля, изчакайте получаването на ключовете + Споделяне като баркод + Споделяне като адрес на XMPP + Споделяне като връзка в Интернет + Доверяване на сляпо преди потвърждение + Нови устройства на непотвърдени контакти автоматично получават доверие, но нови устройства на потвърдени контакти изискват ръчно потвърждаване. + Доверени на сляпо ключове OMEMO, което означава, че това може да е някой друг, или че някой може да е получил неправомерен достъп. + Неприети + Грешен 2-измерен баркод + Изчистване на папката с кеша (използвана от камерата) + Изчистване на кеша + Изчистване на личното място за съхранение + Изчистване на мястото, където се съхраняват личните файлове. (Те могат да бъдат повторно изтеглени от сървъра.) + Последвах тази връзка от доверено място + На път сте да потвърдите ключовете OMEMO на %1$s, след като сте щракнали връзка. Това е безопасна операция, само ако сте последвали тази връзка от доверено място, където само %2$s би могъл да я публикува. + Потвърждаване на ключове OMEMO + Показване на неактивните + Скриване на неактивните + Сваляне на доверието + Наистина ли искате да заличите потвърждението на това устройство?\nТова устройство и съобщенията от него ще бъдат отбелязани като „недоверени“. + + %d секунда + %d секунди + + + %d минута + %d минути + + + %d час + %d часа + + + %d ден + %d дни + + + %d седмица + %d седмици + + + %d месец + %d месеца + + Автоматично изтриване на съобщенията + Автоматично изтриване на съобщенията от това устройство, които са по-стари от зададеното време. + Шифроване на съобщението + Съобщенията не се изтеглят поради местния период на задържане. + Компресиране на видеото + Съответстващите разговори са затворени. + Контактът е блокиран. + Известия от непознати + Известяване за съобщения и обаждания от непознати. + Получено е съобщение от непознат + Блокиране на непознатия + Блокиране на целия домейн + на линия в момента + Повторен опит за дешифроване + Грешка в сесията + Механизмът на SASL е понижен + Сървърът изисква регистриране чрез уеб сайт + Отваряне на уеб сайта + Няма намерено приложение за отваряне на уеб сайта + Изскачащи известия + Показване на изскачащи известия + Днес + Вчера + Проверка на името на сървъра чрез DNSSEC + Сървърните сертификати, които съдържат проверено име на сървъра, се смятат за потвърдени + Сертификатът не съдържа XMPP адрес + частично + Запис на видео + Копиране в буфера + Съобщението е копирано + Съобщение + Личните съобщения са изключени + Защитени приложения + Ако искате да продължите да получавате известия дори когато екранът е заключен, трябва да добавите „Conversations“ към списъка от защитени приложения. + Приемане на непознатия сертификат? + Сървърният сертификат не е подписан от познат център за сертификация. + Приемане на несъвпадащото име на сървъра? + Сървърът не може да се удостовери като „%s“. Сертификатът важи само за: + Искате ли да се свържете въпреки това? + Подробности за сертификата: + Веднъж + Сканирането на QR-код се нуждае от достъп до камерата + Превъртане до дъното + Превъртане надолу след изпращане на съобщение + Редактиране на съобщението за състоянието + Редактиране на съобщението за състоянието + Изключване на шифроването + %1$s не може да изпраща шифровани съобщения до %2$s. Възможно е Вашият контакт да използва остарял сървър или клиент, който не може да работи с OMEMO. + Неуспешно получаване на списъка с устройства + Неуспешно получаване на ключове за шифроване + Съвет: В някои случаи това може да се оправи, ако се добавите един друг в списъците си с контакти. + Наистина ли искате да изключите шифроването чрез OMEMO за този разговор?\nТова ще позволи на администратора Ви да чете съобщенията Ви, но пък най-вероятно е единственият начин за общуване с хората, използващи стари клиенти. + Изключване сега + Чернова: + Шифроване OMEMO + OMEMO ще се използва винаги за частни групови разговори и такива с един събеседник. + OMEMO ще се използва по подразбиране за новите разговори. + OMEMO ще трябва да се включва изрично за новите разговори. + Създаване на пряк път + Размер на шрифта + Относителен размер на шрифта в приложението. + ВКЛ по подразбиране + ИЗКЛ по подразбиране + Малък + Среден + Голям + Съобщението не е било шифровано за това устройство. + Съобщението OMEMO не може да бъде дешифровано. + отмяна + Споделянето на местоположението е изключено + Фиксиране на позицията + Разфиксиране на позицията + Копиране на местоположението + Споделяне на местоположението + Напътствия + Споделяне на местоположението + Показване на местоположението + Споделяне + Записът не може да започне + Моля, изчакайте… + Дайте на %1$s разрешение за достъп до микрофона + Търсене в съобщенията + GIF + Преглед на разговора + Разширение за споделяне на местоположението + Използване на разширението за споделяне на местоположението вместо вградената карта + Копиране на уеб адрес + Копиране на XMPP адрес + Споделяне на файлове през HTTP за S3 + Директно търсене + На екрана за „Започване на разговор“ да се отваря клавиатурата и да се поставя курсорът в полето за търсене + Аватар на груповия разговор + Сървърът не поддържа аватари за груповите разговори + Само собственикът може да променя аватара на груповия разговор + Име на контакта + Псевдоним + Име + Въвеждането на име не е задължитално + Име на груповия разговор + Този групов разговор е унищожен + Записът не може да бъде запазен + Услуга на преден план + Тази категория известия се използва за показване на постоянно известие, което показва, че %1$s работи. + Информация за състоянието + Проблеми с връзката + Тази категория известия се използва за показване на известие, в случай че има проблем със свързването с профил. + Съобщения + Обаждания + Съобщения + Входящи обаждания + Изходящи обаждания + Тихи съобщения + Тази категория известия се използва за показване на известия, които не бива да изпълняват звук. Това може да се използва, например, докато използвате друго устройство (по време на Период на пренебрегване). + Неуспешни доставяния + Настройки на известията за съобщения + Настройки на известията за обаждания + Важност, звук, вибрация + Компресия на видеото + Преглед на медийното съдържание + Участници + Разглеждане на медийното съдържание + Файлът е пропуснат поради нарушение на сигурността. + Качество на видеото + По-ниското качество означава, че файловете ще са по-малки + Средно (360p) + Високо (720p) + отказано + Вече пишете чернова на съобщение. + Функционалността не е реализирана + Грешен код на държава + Изберете държава + тел. номер + Потвърдете телефонния си номер + Quicksy ще Ви изпрати мобилно съобщение (може да се приложат таксите на мобилния оператор), за да се потвърди телефонния Ви номер. Въведете кода на държавата си и телефонния си номер: +
%s

Правилен ли е той, или искате да го промените?]]>
+ %s не е правилен телефонен номер. + Моля, въведете телефонния си номер. + Търсене на държави + Потвърждаване на %s + %s.]]> + Изпратихме Ви още едно мобилно съобщение с 6-цифрен код. + Моля, въведете 6-цифрения код по-долу. + Повторно изпращане на съобщението + Повторно изпращане на съобщението (%s) + Моля, изчакайте (%s) + назад + Вероятният код беше копиран автоматично. + Моля, въведете 6-цифрения си код. + Наистина ли искате да прекратите процеса на регистрация? + Да + Не + Потвърждаване… + Изискване на мобилно съобщение… + Въведеният код е неправилен. + Кодът, който Ви изпратихме, е с изтекла давност. + Неизвестна мрежова грешка. + Непознат отговор от сървъра. + Свързването със сървъра е невъзможно. + Установяването на защитена връзка е невъзможно. + Сървърът не може да бъде намерен. + Нещо се обърка при обработването на заявката Ви. + Неправилно въведени данни + Временно недостъпно. Опитайте отново по-късно. + Няма връзка с мрежата. + Моля, опитайте отново след %s + Имате времево ограничение + Твърде много опити + Използвате остаряла версия на приложението. + Обновяване + Този телефонен номер в момента се използва на друго устройство. + Моля, въведете името си, за да могат хората, които Ви нямат в адресните си указатели, да знаят кой сте. + Името Ви + Въведете името си + Използвайте бутона за редактиране, за да въведете името си. + Отхвърляне на заявката + Инсталиране на Orbot + Пускане на Orbot + Няма инсталирано приложение за инсталиране на приложения. + Този канал ще направи Вашия XMPP-адрес публичен + е-книга + Оригинално (некомпресирано) + Отваряне с… + Профилна снимка за Conversations + Изберете профил + Възстановяване от резервно копие + Възстановяване + Въведете паролата си за профила %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/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml new file mode 100644 index 000000000..bd426b737 --- /dev/null +++ b/app/src/main/res/values-bn-rIN/strings.xml @@ -0,0 +1,118 @@ + + + সেটিংস + নতুন কথোপকথন + অ্যকাউন্টগুলো নিয়ন্ত্রণ করা যাক + অ্যকাউন্টটি নিয়ন্ত্রণ করা যাক + কথা বন্ধ করা যাক + বিশদ বিবরণ + গ্রুপ চ্যাটের বিশদ বিবরণ + চ্যনেলের বিশদ বিবরণ + একটা অ্যকাউন্ট তৈরী করা যাক + নামটা বদল করা যাক + অ্যড্রেসবুকে সংরক্ষণ করা যাক + তালিকা থেকে মুছে ফেলা যাক + এই ব্যক্তিকে ব্লক্ করা যাক + ব্লকটা সরিয়ে ফেলা যাক + পুরো domain-টাই ব্লক করা হোক + Domainটি আর ব্লক্ করার দরকার নেই + ব্যক্তিটিকে ব্লক্ করা যাক + ব্যক্তিটিকে ব্লক্ করার আর দরকার নেই + অ্যকাউন্টগুলো নিয়ন্ত্রণ করা যাক + সেটিংস + Conversations-এর মাধ্যমে share করা হোক + কথোপকথন শুরু করা যাক + Contact নির্বাচন করুন + Contact সমূহ নির্বাচন করুন + অ্যকাউন্টের মাধ্যমে share করা যাক + তালিকা ব্লক্ করা হোক + এক্ষুণি + এক মিনিট আগে + %d মিনিট আগে + + %dটাই কথোপকথন পড়া বাকি + + + %dকথোেকথন পড়া হয়নি + + + পাঠানো হচ্ছে... + অপেক্ষা করুন, সাঙ্কেতিক সন্দেশ পঠিত হচ্ছে... + OpenPGP দ্বারা তৈরী সাঙ্কেতিক সন্দেশ + নামটা অন্য কেউ ব্যবহার করছেন + নামটা সঠিক নয় + নিয়ন্ত্রক + মালিক + নির্ধারক + অংশগ্রহণকারী + অতিথি + আপনি কি আপনার পরিচিতি তালিকা থেকে %s-কে অপসারণ করতে চান? এই যোগাযোগের সাথে কথোপকথনগুলি সরানো হবে না। + %s-কে বার্তা পাঠানো থেকে ব্লক করতে চান? + আপনি কি %s-কে আনব্লক করতে চান এবং তাদের আপনাকে বার্তা পাঠানোর অনুমতি দিতে চান? + ব্যক্তিটিকে ব্লক্ করা হয়েছে + ব্লক্ করা আছে + সার্ভারে একটি নতুন অ্যকাউন্ট খোলা যাক + সার্ভারে পাসওয়ার্ড বদলে ফেলা যাক + শেয়ার করা হোক... + কথোপকথন শুরু করা যাক + ব্যক্তিকে আমন্ত্রণ জানানো হোক + আমন্ত্রণ + পরিচিত মানুষজন + পরিচিত ব্যক্তি + না, থাক। + সেট করা যাক + যোগ করা যাক + বদলানো যাক + মুছে ফেলা যাক + ব্লক্ করা যাক + ব্লক্ তুলে নেওয়া যাক + সংরক্ষিত করা যাক + ঠিক আছে। + %1$sঅপ্রত্যাশিতভাবে থেমে গেল + পাঠানো যাক + দ্বিতীয়বার জিগ্গাসা করার দরকার নেই + অ্যকাউন্টের সাথে যোগাযোগ করা যাচ্ছে না + বেশ কয়েকটা অ্যকাউন্টের সাথে যোগাযোগ করা যাচ্ছে না + এটা ছুয়েঁ নিজের অ্যকাউন্টগুলো নিয়ন্ত্রণ করা যায় + ফাইল আটকে দেওয়া যাক + এই কনট্যাক্টটা আগে পাওয়া যাচ্ছিল না, তালিকায়ে যোগ করে দেওয়া যাক? + কন্ট্যাক্টটি যোগ করা যাক + পাঠানো সম্ভব হয়নি + ছবিটা পাঠানোর জন্য তৈরী হচ্ছে + ছবিগুলি পাঠানোর জন্য তৈরী করা হচ্ছে + ফাইলগুলো শেয়ার করা হচ্ছে, অপেক্ষা করুন + প্রতিলিপি মুছে ফেলা যাক + Conversation-এর সব প্রতিলিপি মুছে ফেলা যাক + এই কথোপকথনের সবকটি বার্তাই কি মুছে ফেলতে চান?\n‌\nসতর্ক থাকবেন: সার্ভার বা অন্য যন্ত্রে থাকা বার্তা কিন্তু অপরিআর্তিতই থাকবে। + ফাইলটি মুছে ফেলা হোক + এই কথোপকথনটি পরে সমাপ্ত করা হোক + যন্ত্র নির্বাচন করা যাক + অসাঙ্কেতিক বার্তাই পাঠানো হোক + বার্তা পাঠানো হোক + %s-কে বার্তা পাঠানো হোক + OMEMO সাঙ্কেতিক বার্তা পাঠানো হোক + v\\OMEMO সাঙ্কেতিক বার্তা পাঠানো হোক + OpenPGP সাঙ্কেতিক বার্তা পাঠানো হোক + নতুন নাম ব্যবহার করা হচ্ছে + এনক্রিপ্ট না করেই পাঠানো হোক + ডিক্রিপ্ট করা যায়নি। হয়তো আপনার কাছে সঠিক Private Key নেই। + রিস্টার্ট্ + ইনস্টল্ + OpenKeychain ইনস্টল্ করতে হবে + প্রস্তাব দেওয়া হচ্ছে... + অপেক্ষা করা হচ্ছে... + কোনো OpenPGP Key খুঁজে পাওয়া যায়নি + বুকমার্ক করা যেগুলি + খোঁজা যাক + এই ব্যক্তিকে ব্লক্ করা যাক + ব্লকটা সরিয়ে ফেলা যাক + পরিচিত ব্যক্তি + না, থাক। + পরিচিত ব্যক্তিদের মধ্যে খোঁজা যাক + বার্তাগুলির মধ্যে খোঁজা যাক + সরাসরিভাবেই খোঁজা যাক + পাবলিক চ্যানেলে যোগ দেওয়া যাক + ব্যক্তিগত গ্রুপ চ্যাট তৈরি করুন + পাবলিক চ্যানেল তৈরি করা যাক + বর্তমান চ্যানেলগুলির মধ্যে থেকে খোঁজা যাক + diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml new file mode 100644 index 000000000..d5421b742 --- /dev/null +++ b/app/src/main/res/values-ca/strings.xml @@ -0,0 +1,942 @@ + + + Preferències + Nova conversa + Gestiona els comptes + Administrar compte + Conversa pròxima + Detalls del contacte + Detalls del xat de grup + Detalls del canal + Afegeix un compte + Edita el nom + Afegeix a la llibreta d\'adreces + Elimina de la llista de contactes + Bloqueja el contacte + Desbloqueja el contacte + Bloqueja el domini + Desbloqueja el domini + Bloquejar participant + Desbloquejar participant + Gestiona els comptes + Configuració + Compartir amb Conversations + Comença una conversa + Tria el contacte + Seleccionar contactes + Comparteix mitjançant compte + Llista de bloqueig + Ara + fa 1 min + fa %d mins + + %dconverses no llegides + + + %d converses no llegides + + + enviant… + Desxifrant el missatge. Espereu… + Missatge xifrat amb OpenPGP + El sobrenom ja està en ús + El sobrenom és invàlid + Administrador + Propietari + Moderador + Participant + Visitant + Vols eliminar a %s de la teva llista de contactes? Les converses amb aquest contacte no seran eliminades. + Voleu impedir que %s us pugui enviar missatges? + Us agradaria desbloquejar %s i permetre que us enviï missatges? + Voleu bloquejar tots els contactes de %s? + Voleu desbloquejar tots el contactes de %s? + Contacte bloquejat + Bloquejats + Vols esborrar %s com a marcador? Les converses amb aquest marcador no seran eliminades. + Registra un compte nou al servidor + Canvia la contrasenya al servidor + Comparteix amb… + Comença la conversa + Convida un contacte + Convida + Contactes + Contacte + Cancel·la + Aplica + Afegeix + Edita + Elimina + Bloqueja + Desbloqueja + Desa + D\'acord + Envia ara + No preguntis de nou + No es va poder connectar al compte + No es va poder connectar-se a múltiples comptes + Prem per a administrar els teus comptes + Envia un fitxer + Afegir aquest contacte perdut a la teva llista de contactes? + Afegeix un contacte + L\'enviament ha fallat + Preparant-se per a enviar la imatge + Preparant-se per a enviar la imatge + S\'estan compartint els fitxers. Espereu… + Neteja l\'historial + Neteja l\'historial de la conversa + Vols esborrar tots els missatges d\'aquesta conversa?\n\n Advertiment: Això no influirà en els missatges emmagatzemats en altres dispositius o servidors. + Eliminar fitxer + Estàs segur que vols esborrar aquest fitxer?\n\n Advertiment: Això no eliminarà les còpies d\'aquest fitxer que estiguin emmagatzemades en altres dispositius o servidors. + Tanca aquesta conversa després + Tria el dispositiu + Envia un missatge no xifrat + Envia un missatge + Envia un missatge a %s + Envia un missatge xifrat amb OMEMO + Envia un missatge xifrat amb v\\OMEMO + Envia un missatge xifrat amb OpenPGP + Nou sobrenom en ús + Envia sense xifrar + El desxiframent ha fallat. Potser no teniu la clau privada apropiada. + OpenKeychain + Reinicia + Instal·la + Instal·leu l\'OpenKeychain + s\'està oferint… + s\'està esperant… + No s\'ha trobat cap clau OpenPGP + No es va poder xifrar el teu missatge perquè el teu contacte no està anunciant la seva clau pública.\n\n Si us plau, demana-li al seu contacte que configuri OpenPGP. + No s\'ha trobat cap clau OpenPGP + No es pot encriptar el teu missatge perquè els teus contactes no anuncien les seves claus públiques. Si us plau, demana\'ls que configurin OpenPGP. + General + Accepta els fitxers + Accepta fitxers automàticament amb una mida menor a… + Fitxers adjunts + Notificació + Vibra + Vibra quan arribi un missatge nou + Notificació LED + Fes que la notificació lumínica parpellegi quan arribi un missatge nou + To de trucada + So de notificació + So de notificació per als nous missatges + Període de gràcia + La durada de les notificacions se silencia després de detectar activitat en un dels seus altres dispositius. + Avançat + Mai enviïs informes d\'errors + En enviar els rastres de la pila estàs ajudant al desenvolupament + Confirma els missatges + Feu saber als vostres contactes quan heu rebut i llegit els seus missatges + UI + OpenKeychain va produir un error. + Clau dolenta per encriptar. + Accepta + S\'ha produït un error + Error + El vostre compte + Envia actualitzacions de presència + Rep actualitzacions de presència + Demana actualitzacions de presència + Tria una imatge + Fes una fotografia + Accepta les peticions de subscripció preventivament + El fitxer triat no és una imatge + No es va poder convertir l\'arxiu d\'imatge + No s\'ha trobat el fitxer + Error d\'E/S general. Pot ser que us hagueu quedat sense espai d\'emmagatzematge. + L\'aplicació que vas usar per a seleccionar aquesta imatge no va proporcionar suficients permisos per a llegir l\'arxiu.\n\nUtilitza un gestor d\'arxius diferent per a triar una imatge. + Desconegut + Inhabilitat temporalment + En línia + S\'està connectant\u2026 + Fora de línia + No autoritzat + No s\'ha trobat el servidor + Sense connectivitat + El registre ha fallat + El nom d\'usuari ja està en ús + S\'ha completat el registre + El registre no està suportat pel servidor + Token de registre invàlid + S\'ha produït un error en la negociació de TLS + Violació de la política + Servidor incompatible + Error de transmissió + Error d\'obertura de la transmissió + Sense xifrar + OTR + OpenPGP + OMEMO + Elimina el compte + Inhabilita temporalment + Publica l\'avatar + Publica la clau pública d\'OpenPGP + Elimina la clau pública d\'OpenPGP + Esteu segur que voleu eliminar la vostra clau pública d\'OpenPGP de l\'anunci de la vostra presència?\nEls vostres contactes ja no podran enviar missatges xifrats amb OpenPGP. + S\'ha publicat la clau pública de OpenPGP. + Habilita el compte + N\'esteu segur? + En esborrar el teu compte s\'esborra tot l\'historial de converses + Enregistra veu + Adreça XMPP + Bloquejar la direcció XMPP + username@example.com + Contrasenya + Aquesta no és una direcció XMPP vàlida + Fora de la memòria. Imatge massa gran + Voleu afegir a %s a la vostra llibreta d\'adreces? + Informació del servidor + XEP-0313: Gestió d\'arxivat de missatges + XEP-0280: Duplicat de missatges + XEP-0352: Indicació de l\'estat del client + XEP-0191: Ordre de bloqueig + XEP-0237: Versionat de la llista de contactes + XEP-0198: Gestió del flux de dades + XEP-0215: Descobriment de servei extern + XEP-0163: Protocol d\'esdeveniments personals (Avatars / OMEMO) + XEP-0363: Pujada de fitxers per HTTP + XEP-0357: Notificacions de tramesa automàtica + Disponible + No disponible + No es troben anuncis de clau pública + darrera connexió: ara mateix + vist per última vegada fa un minut + darrera connexió: fa %d minuts + vist per última vegada fa una hora + darrera connexió: fa %d hores + vist per última vegada fa un dia + darrera connexió: fa %d dies + Missatge encriptat. Si us plau, instal·la OpenKeychain per a desencriptar-ho. + S\'han trobat nous missatges xifrats OpenPGP + Identificador de la clau d\'OpenPGP + Empremta digital OMEMO + Empremta digital v\\OMEMO + Altres dispositius + Confia en les empremtes digitals d\'OMEMO + S\'estan obtenint les claus… + Fet + Desxifra + Marcadors + Cerca + Introduïu un contacte + Suprimeix el contacte + Veure els detalls del contacte + Bloqueja el contacte + Desbloqueja el contacte + Crea + Seleccioneu + El contacte ja existeix + Entra + channel@conference.example.com/nick + channel@conference.example.com + Desa com a marcador + Elimina dels marcadors + Destrueix el xat en grup + Destrueix el canal + Estàs segur que vols destruir aquest xat en grup?\n\nAdvertiment: El xat de grup serà eliminat per complet en el servidor. + Estàs segur que vols destruir aquest canal públic?\n\nAdvertiment: El canal serà completament eliminat en el servidor. + No es va poder destruir el xat del grup + No es va poder destruir el canal + Edita el tema del xat de grup + Assumpte + S\'està unint al xat de grup… + Surt + S\'ha afegit el contacte a la llista de contactes + Afegeix de nou + %s ha llegit fins aquí + %s han llegit fins aquí + %1$s +%2$d uns altres han llegit fins a aquest punt + Tothom ha llegit fins a aquí + Publica + Premi l\'avatar per a seleccionar una imatge de la galeria + S\'està publicant… + El servidor ha rebutjat la vostra publicació + No s\'ha pogut convertir la imatge + No s\'ha pogut desar l\'avatar al disc + (O feu un toc llarg per restablir al valor predeterminat) + El seu servidor no suporta la publicació d\'avatars + xiuxiuejat + a %s + Envia un missatge privat a %s + Connecta + Aquest compte ja existeix + Següent + Sessió establerta + Salta + Inhabilita les notificacions + Habilita + El xat de grup requereix contrasenya + Introduïu la contrasenya + Si us plau, sol·liciti primer les actualitzacions de presència al seu contacte.\n\nAixò s\'usarà per a determinar quina aplicació de xat està usant el teu contacte. + Sol·licita ara + Ignora + Advertiment:: Enviar això sense actualitzacions de presència mútua podria causar problemes inesperats.\n\n Vagi a \"Dades de contacte\" per a verificar les seves subscripcions de presència. + Seguretat + Permet la correcció de missatges + Permet que els contactes editin de manera retroactiva els missatges + Configuració d\'experts + Aneu amb cura + Sobre 1%s + Hores de silenci + Hora d\'inici + Hora de finalització + Habilitar hores de silenci + Les notificacions seràn silenciades a les hores de silenci + Altres + Empremta digital de OMEMO copiada en el portapapers + Estàs prohibit en aquest xat de grup + Aquest xat en grup només és de membres + Limitació de recursos + Heu estat expulsat d\'aquest xat de grup + S\'ha tancat el xat de grup + Ja no ets en aquest xat de grup + Utlitzant el compte %s + allotjat en %s + S\'està verificant %s al host HTTP + No estàs connectat. Intenta-ho més tard + Verificació de la mida de %s + Verificació de la mida de %1$s en %2$s + Opcions del missatge + Cita + Pegar com a cita + Copiar la URL original + Envia una altra vegada + URL del fitxer + URL copiada al portapapers + Adreça XMPP copiada en el porta-retalls + Missatge d\'error copiat al portapapers + Adreça Web + Escaneja el codi de barres 2D + Mostra el codi de barres 2D + Mostra la llista de bloqueig + Detalls del compte + Confirmar + Intenta una altra vegada + Servei de primer pla + Evitar que el sistema operatiu desconnecti la connexió + Crea una còpia de seguretat + Els arxius de suport s\'emmagatzemaran en %s + Creant arxius de còpia de seguretat + La còpia de seguretat s\'ha creat + Els arxius de suport han estat emmagatzemats en %s + Restaurant la còpia de seguretat + La còpia de seguretat s\'ha restaurat + No oblidis habilitar el compte. + Tria un fitxer + Rebent %1$s (%2$d%% completat) + Descargat %s + Suprimeix a %s + Fitxer + Obert %s + Enviant (%1$d%% completat) + Preparant-se per a compartir l\'arxiu + %s ofert per descarregar + Transmissió cancelada + no es va poder compartir l\'arxiu + transmissió d\'arxius cancel·lada + Arxius eliminats + No s\'ha trobat cap aplicació per a obrir l\'arxiu + No s\'ha trobat cap aplicació per a obrir l\'enllaç + No s\'ha trobat cap aplicació per a veure el contacte + Etiquetes dinàmiques + Mostra etiquetes de nomès lectura per sota dels noms dels contactes + Habilitar notificació + No s\'ha trobat cap servidor de xat de grup + No s\'ha pogut crear un xat de grup + Avatar del compte + Copieu l\'empremta digital OMEMO al porta-retalls + Regenerar la clau OMEMO + Esborra els dispositius + Estàs segur que vols esborrar tots els altres dispositius de l\'anunci de OMEMO? La pròxima vegada que els teus dispositius es connectin, tornaran a anunciar-se, però pot ser que no rebin els missatges enviats mentrestant. + No hi ha claus disponibles per a aquest contacte.\nNo s\'han pogut obtenir noves claus del servidor. Potser hi ha algun problema amb el servidor del teu contacte? + No hi ha claus utilitzables per a aquest contacte.\NAsseguri\'s que tots dos tenen subscripció de presència. + Alguna cosa ha anat malament + Anar a cercar la història als servidors + No hi ha més histories al servidor + Actualitzant + Contrasenya canviada + No s\'ha pogut canviar la contrasenya + Cambiar contrasenya + Contrasenya actual + Nova contrasenya + La contrasenya no pot estar buida + Habilitar tots els comptes + Deshabilitar tots els comptes + Realitzar l\'acció amb… + Cap afiliació + Fora de línia + Outcast + Membre + Mode avançat + Concedir privilegis als membres + Revocar els privilegis dels membres + Admetre privilegis d\'administrador + Rebocar privilegis d\'administrador + Concedir privilegis al propietari + Revocar els privilegis del propietari + Suprimeix del xat de grup + Eliminar del canal + No s\'ha pogut canviar l\'afiliació del %s + Prohibició del xat en grup + Prohibició del canal + Estàs intentant eliminar a %s d\'un canal públic. L\'única manera de fer-ho és prohibir a aquest usuari per sempre. + Banejat ara + No s\'ha pogut canviar les regles de %s + Configuració del xat de grup privat + Configuració del canal públic + Privat, només membres + Fer que les direccions XMPP siguin visibles per a qualsevol + Fer que el canal sigui moderat + No esteu participant + S\'han modificat les opcions de xat en grup. + No s\'han pogut modificar les opcions de xat de grup + Mai + Fins nou avís + Posposa + Respon + Marcar com llegit + Entrada + Entra per enviar + Utilitzi la tecla Intro per a enviar el missatge. Sempre pots usar Ctrl+Intro per a enviar un missatge, fins i tot si aquesta opció està desactivada. + Mostra el botó enter + Canviar la clau dels emoticones per un botó d\'entrada + audio + video + imatge + Document PDF + Aplicació d\'Android + Contacte + L\'avatar ha sigut publicat! + Enviant %s + Oferint %s + Amaga el fora de línia + %s està escrivint… + %s ha deixat d\'escriure + %sestà escrivint… + %sha deixat d\'escriure + Notificacions d\'escriptura + Feu saber als vostres contactes quan els escriviu missatges + Enviar localització + Mostrar localització + No s\'ha trobat cap aplicació per a mostrar la ubicació + Localització + Conversa tancada + Deixar el xat de grup privat + Canal públic de l\'esquerra + No confiar en les CAs del sistema + Tots els certificats han de ser aprovats manualment + Eliminar certificats + Esborrar certificats aprovats manualment + No hi ha certificats aprovats manualment + Esborrar certificats + Esborrar selecció + Cancel·lar + + %d certificat esborrat + %d certificats esborrats + + Substituir el botó \"Enviar\" per una acció ràpida + Acció ràpida + Cap + Ús més recent + Trieu una acció ràpida + Buscar contactes + Buscar favorits + Envia un missatge privat + %1$s ha abandonat el xat de grup + Nom d\'usuari + Nom d\'usuari + Aquest no és un nom d\'usuari vàlid + S\'ha produït un error de baixada: el servidor no s\'ha trobat + S\'ha produït un error de baixada: no s\'ha trobat el fitxer + S\'ha produït un error en la baixada: no s\'ha pogut connectar al servidor + S\'ha produït un error en la baixada: no s\'ha pogut escriure el fitxer + La xarxa Tor no està disponible + Vincular el error + El servidor no és responsable d\'aquest domini + Trencat + Disponibilitat + Ocupat en manera silenciosa + Mostrar com ocupat quan el dispositiu està en manera silenciosa + Tracteu de vibrar al modo silenciós + Mostrar com ocupat quan el dispositiu està en vibració + Configuració de connexió estesa + Mostra el nom de la màquina i la configuració del port quan configureu un compte + xmpp.example.com + Inici de sessió amb certificat + No s\'ha pogut analitzar el certificat + Arxivant preferències + Preferències d\'arxivat al servidor + S\'estan obtenint les preferències d\'arxivat. Espereu… + No s\'han pogut recuperar les preferències d\'arxiu + Es requereix CAPTCHA + Introduïu el text de la imatge de dalt + Cadena de certificats no fiable + La direcció XMPP no coincideix amb el certificat + Renova el certificat + S\'ha produït un error en obtenir la clau OMEMO!. + Clau OMEMO verificada amb certificat! + El vostre dispositiu no admet la selecció de certificats de client. + Connexió + Connectar mitjançant Tor + Tunelar totes les connexions a través de la xarxa Tor. Requereix Orbot + Nom del \"Host\" + Port + Servidor- or .onion-address + Aquest no és un número de port vàlid + Aquest no és un nom de host vàlid + %1$d of %2$d comptes connectats + + %dmissatge + %d messages + + Carregueu més missatges + Arxiu compartit amb %s + Imatge compartida amb %s + Imatges compartides amb %s + Text compartit amb %s + Sincronitza amb contactes +
No guardarem una còpia d\'aquests números de telèfon.\n\nPer a més informació llegeixi la nostra política de privadesa

Ara se us demanarà que concediu permís per accedir als vostres contactes.]]>
+ Notifica a tots els missatges + Notifica només quan s\'esmenta + S\'han desactivat les notificacions + S\'han pausat les notificacions + Compressió d\'imatge + Suggeriment: Utilitzi \"Triar arxiu\" en lloc de \"Triar imatge\" per a enviar imatges individuals sense comprimir, independentment d\'aquesta configuració. + Sempre + Només imatges grans + Optimitzacions de la bateria habilitades + Desactivar + L\'àrea seleccionada és massa gran + (Sense comptes activats) + Aquest camp és obligatori + Corregeix el missatge + Envia el missatge corregit + Ja has validat l\'empremta digital d\'aquesta persona de manera segura per a confirmar la seva confiança. En seleccionar \"Fet\" només estàs confirmant que %s forma part d\'aquest xat de grup. + Heu desactivat aquest compte + Error de seguretat: Accés invàlid a un arxiu! + No s\'ha trobat cap aplicació per a compartir URI + Comparteix l\'URI amb… +
Si es registra amb el seu número de telèfon i Quicksy,--basant-se en els números de telèfon de la seva agenda--li suggereix automàticament possibles contactes.

En inscriure\'s, accepta nostra política de privacitat.]]>
+ Acceptar i continuar + S\'ha establert una guia per a la creació de comptes en conversations.im.¹\nEn triar conversations.im com a proveïdor podràs comunicar-te amb usuaris d\'altres proveïdors donant-los la teva adreça XMPP completa. + La seva adreça XMPP completa serà: %s + Crear compte + Utilitza el meu propi proveïdor + Tria el teu nom d\'usuari + Gestioneu la disponibilitat manualment + Establir la vostra disponibilitat en editar el vostre missatge d\'estat. + Missatge d\'estat + Lliure per xatejar + En línia + Lluny + No disponible + Ocupat + S\'ha generat una contrasenya segura + El dispositiu no admet l\'exclusió de l\'optimització de la bateria + Error de registre: torna-ho a provar més tard + Ha fallat la inscripció: la contrasenya és massa feble + Tria els participants + S\'està creant el xat en grup… + Torna a convidar + Inhabilitar + Curt + Mitjà + Llarg + Ús de la radiodifusió + Permetre que els seus contactes sàpiguen quan usa Conversations + Privadesa + Tema + Seleccioneu la paleta de colors + Automàtic + Clar + Fosc + Fons verd + Utilitzeu fons verd per als missatges rebuts + No s\'ha pogut connectar amb OpenKeychain + Aquest dispositiu ja no està en ús + Ordinador + Telèfon mòbil + Tablet + Navegador web + Consola + Cal fer pagament + Concedir el permís d\'ús d\'Internet + Jo + El contacte demana la subscripció a la presència + Permetre + No hi ha permís per accedir %s + El servidor remot no s\'ha trobat + Temps d\'espera del servidor remot + No s\'ha pogut actualitzar el compte + Denunciar aquesta adreça XMPP per spam. + Elimineu les identitats OMEMO + Regenera les teves claus OMEMO. Tots els teus contactes hauran de verificar-te de nou. Utilitza això només com a últim recurs. + Elimineu les tecles seleccionades + Cal que us connecteu per publicar el vostre avatar. + Mostra el missatge d\'error + Missatge d\'error + S\'ha activat el protector de dades + No s\'ha pogut crear l\'arxiu temporal + S\'ha verificat aquest dispositiu + Copieu l\'empremta digital + El codi de barres no conté empremtes dactilars per a aquesta conversa. + Empremtes digitals verificades + Utilitzeu la càmera per escanejar el codi de barres d\'un contacte + Espereu que es puguin obtenir les claus + Comparteix com a codi de barres + Comparteix com a XMPP URI + Comparteix com a enllaç HTTP + Confiança cega abans de la verificació + Confiar en els nous dispositius dels contactes no verificats, però sol·licitar la confirmació manual dels nous dispositius per als contactes verificats. + Confiar cegament en les claus de OMEMO, la qual cosa significa que podria ser una altra persona o que algú podria haver intervingut. + No confiable + Codi de barres 2D no vàlid + Netejar la carpeta de caixet (utilitzada per l\'aplicació de la cambra) + Netejar la memòria cache + Netejar l\'emmagatzematge privat + Netejar emmagatzematge privat on es mantenen els fitxers (es poden tornar a descarregar del servidor) + He seguit aquest enllaç des d\'una font de confiança + Esteu a punt de verificar les claus OMEMO de %1$sdesprés de fer clic a un enllaç. Això només és segur si seguiu aquest enllaç des d\'una font de confiança on només %2$s podria haver publicat aquest enllaç. + Comproveu les claus OMEMO + Mostra inactiu + Amaga inactiu + Dispositiu no confiable + Està segur que vol eliminar la verificació d\'aquest dispositiu?\nAquest dispositiu i els missatges que provinguin d\'ell es marcaran com \"No fiable\". + + 1%d segons + %d segons + + + 1%d minuts + %d minuts + + + 1%d hores + %d hores + + + 1%d dies + %d dies + + + 1%d setmanes + %d setmanes + + + 1%d meses + %d meses + + Supressió de missatge automàtic + Suprimiu automàticament missatges d\'aquest dispositiu que són més grans que el marc de temps configurat. + Xifrant el missatge + No es recullen missatges deguts al període de retenció local. + S\'està comprimint el vídeo + S\'han tancat les converses corresponents. + Contacte bloquejat. + Notificacions d\'estranys + Notificar per a missatges i trucades rebudes d\'estranys. + S\'ha rebut un missatge de un desconegut + Bloqueja al desconegut + Bloqueja tot el domini + en línia ara mateix + Torneu a provar el desxifratge + Fallo de la sessió + Mecanisme SASL degradat + El servidor requereix el registre al lloc web + Obre la pàgina web + No s\'ha trobat cap aplicació per a obrir el lloc web + Notificacions \"cap amunt\" + Mostrar les notificacions dels caps de fila + Avui + Ahir + Valideu el nom del servidor amb DNSSEC + Els certificats de servidor que contenen el nom de host validat es consideren verificats + El certificat no conté una direcció XMPP + parcial + Grava vídeo + Copiar al portapapers + Missatge copiat al portapapers + Missatge + Els missatges privats estan desactivats + Aplicacions protegides + Per continuar rebent notificacions, fins i tot quan la pantalla està apagada, heu +d\'afegir Converses a la llista d\'aplicacions protegides. + Voleu acceptar un certificat desconegut? + El certificat del servidor no està signat per una autoritat de certificació coneguda. + Voleu acceptar el nom del servidor associat? + El servidor no s\'ha pogut autenticar com \" 1%s \". El certificat només és vàlid per a: + Voleu connectar de totes maneres? + Detalls del certificat: + Un cop + L\'escàner de codi QR necessita accés a la càmera + Desplaça\'t cap a la part inferior + Desplaceu-vos cap avall després d\'enviar un missatge + Edita el missatge d\'estat + Edita el missatge d\'estat + Desactiva el xifratge + No s\'ha pogut obtenir la llista de dispositius + No s\'han pogut obtenir les claus d\'encriptació + Suggeriment: en alguns casos això es pot resoldre afegint les vostres llistes de contactes. + Estàs segur que vols desactivar el xifratge OMEMO per a aquesta conversa?\nAixò permetrà +que l\'administrador del servidor llegeixi els missatges, però pot ser l\'única forma de comunicar-se amb persones que utilitzin clients obsolets. + Desactiva ara + Esborrany: + Encriptació OMEMO + OMEMO sempre s\'utilitzarà per a xerrades individuals i privades en grup. + OMEMO s\'utilitzarà per defecte per a les noves converses. + OMEMO haurà de ser activat explícitament per a noves converses. + Crear accés directe + Grandària de la font + La grandària relativa de la font utilitzada dins de l\'aplicació. + Activat per defecte + Desactivat per defecte + Petita + Mitjana + Gran + El missatge no està xifrat per a aquest dispositiu. + No s\'ha pogut desxifrar el missatge OMEMO. + desfer + L\'ús compartit d\'ubicacions està desactivat + Posició fixa + Posició no fixada + Copiar ubicació + Compartir ubicació + Adreces + Compartir ubicació + Mostrar ubicació + Compartir + No s\'ha pogut iniciar l\'enregistrament + Si us plau, esperi… + Buscar missatges + GIF + Veure conversa + Plugin per Compartir Ubicació + Utilitzar el plugin de compartir ubicació en lloc del mapa incorporat + Copiar adreça web + Copiar la adeça XMPP + Ús compartit de fitxers HTTP per a S3 + Recerca directa + En la pantalla \'Iniciar conversa\' obri el teclat i col·loqui el cursor en el camp de recerca + Avatar de xat en grup + El host no suporta avatars de xat de grup + Només el propietari pot canviar l\'avatar del xat en grup + Nom de contacte + Sobrenom + Nom + Proporcionar un nom és opcional + Nom del xat en grup + Aquest xat en grup ha estat destruït + No s\'ha pogut guardar l\'enregistrament + Servei de primer pla + Informació d\'estat + Problemes de connectivitat + Aquesta categoria de notificació s\'utilitza per a mostrar una notificació en cas que hi hagi un problema de connexió a un compte. + Missatges + Tracades + Missatges + Trucades entrants + Trucades en curs + Missatges silenciosos + Aquest grup de notificacions s\'utilitza per a mostrar notificacions que no han d\'activar cap so. Per exemple, quan estan actives en un altre dispositiu (període de gràcia). + Lliuraments fallits + Configuració de la notificació de missatges + Configuració de la notificació de trucades entrants + Importància, So, Vibració + Compressió de vídeo + Veure mitjans + Participants + Navegador de mitjans + Arxiu omès per violació de seguretat. + Qualitat del vídeo + Una menor qualitat significa arxius més petits + Mitjà (360p) + Alt (720p) + cancel·lat + Ja està redactant un missatge. + Funció no implementada + Codi de país invàlid + Triï un país + número de telèfon + Verifiqui el seu número de telèfon + Quicksy enviarà un missatge SMS (poden aplicar-se càrrecs de l\'operador) per a verificar el teu número de telèfon. Introdueix el codi del teu país i el número de telèfon: +
%s

Està bé, o vol editar el número?]]>
+ %s no és un número de telèfon vàlid. + Si us plau, introdueixi el seu número de telèfon. + Buscar països + Verificar %s + %s.]]> + Li hem enviat un altre SMS amb un codi de 6 dígits. + Si us plau, introdueixi el pin de 6 dígits a continuació. + Reexpedir SMS + Reexpedir SMS (%s) + Si us plau, esperi (%s) + tornar + Pegar automàticament un possible pin des del portapapers. + Si us plau, introdueixi el seu pin de 6 dígits. + Està segur que vol avortar el procediment de registre? + Si + No + Verificant... + Sol·licitud de SMS... + El pin que has introduït és incorrecte. + El pin que li hem enviat ha caducat. + Error de xarxa desconegut. + Resposta desconeguda del servidor. + No s\'ha pogut connectar amb el servidor. + No s\'ha pogut establir una connexió segura. + No s\'ha pogut trobar el servidor. + Alguna cosa va sortir malament en processar la seva sol·licitud. + Entrada d\'usuari no vàlida + No està disponible temporalment. Torna a intentar-ho més tard. + No hi ha connexió a la xarxa. + Si us plau, intenti-ho de nou en %s + La seva velocitat està limitada + Massa intents + Estàs utilitzant una versió desactualizada d\'aquesta aplicació. + Actualitzar + Aquest número de telèfon està actualment connectat amb un altre dispositiu. + Si us plau, introdueixi el seu nom perquè la gent, que no li té en la seva llibreta d\'adreces, sàpiga qui és vostè. + El seu nom + Introdueixi el seu nom + Utilitzi el botó d\'edició per a establir el seu nom. + Rebutjar la sol·licitud + Instal·lar Orbot + Iniciar Orbot + No hi ha cap aplicació de venda instal·lada. + Aquest canal farà pública la seva adreça XMPP + Llibre electrònic + Original (sense comprimir) + Obrir amb... + Foto de perfil de Conversations + Triï el compte + Restaurar còpia de seguretat + Restaurar + Introdueixi la contrasenya del compte %s per a restaurar la còpia de seguretat. + No utilitzi la funció de restauració de la còpia de seguretat per a intentar clonar (executar simultàniament) una instal·lació. La restauració d\'una còpia de seguretat només està pensada per a migracions o en cas que hagis perdut el dispositiu original. + No s\'ha pogut restaurar la còpia de seguretat. + No s\'ha pogut desxifrar la còpia de seguretat. La contrasenya és correcta? + Còpia de seguretat i restauració + Introdueixi la direcció XMPP + Crear un xat de grup + Unir-se a un canal públic + Crear un xat de grup privat + Crear un canal públic + Nom del canal + Adreça XMPP + Indiqui un nom per al canal + Indiqui una direcció XMPP + Aquesta és una direcció XMPP. Si us plau, proporcioni un nom. + Creant un canal públic... + Aquest canal ja existeix + T\'has unit a un canal existent + No s\'ha pogut guardar la configuració del canal + Permetre que qualsevol pugui editar el tema + Permetre que qualsevol convidi a uns altres + Qualsevol pot editar el tema. + Els propietaris poden editar el tema. + Els administradors poden editar el tema. + Els propietaris poden convidar a uns altres. + Qualsevol pot convidar a uns altres. + Les adreces XMPP són visibles per als administradors. + Les adreces XMPP són visibles per a qualsevol. + Aquest canal públic no té participants. Convida als teus contactes o utilitza el botó de compartir per a distribuir la seva adreça XMPP. + Aquest xat de grup privat no té participants. + Gestionar els privilegis + Buscar participants + Arxiu massa gran + Adjuntar + Descobreix canals + Buscar canals + Possible violació de la privacitat. + search.jabber.network.

L\'ús d\'aquesta funció transmetrà la seva adreça IP i els termes de cerca a aquest servei. Consulti la seva Política de Privacitat per a obtenir més informació.]]>
+ Ja tinc un compte + Afegir compte existent + Registrar un nou compte + Això sembla una direcció de domini + Afegir de totes maneres + Això sembla una direcció de canal + Compartir arxius de còpia de seguretat + Còpia de seguretat de Conversations + Esdeveniment + Obrir la còpia de seguretat + L\'arxiu seleccionat no és un arxiu de còpia de seguretat de Conversations + Aquest compte ja està configurada + Si us plau, introdueixi la contrasenya d\'aquest compte + No s\'ha pogut realitzar aquesta acció + Unir-se al canal públic... + L\'aplicació per a compartir no va donar permís per a accedir a aquest arxiu. + + jabber.network + Servidor local + La majoria dels usuaris haurien de triar \'jabber.network\' per a obtenir millors suggeriments de tot l\'ecosistema públic de XMPP. + Mètode de descobriment de canals + Còpia de seguretat + Sobre + Si us plau, habiliti un compte + Fer una trucada + Trucada entrant + Trucada de vídeo entrant + Connectant + Connectat + Acceptant la trucada + Finalitzant la trucada + Resposta + Descartar + Descobrir dispositius + Sonant + Ocupat + No es va poder establir la trucada + Connexió perduda + Trucada rebutjada + Fallada de l\'aplicació + Penjar + Trucada en curs + Trucada de vídeo en curs + Desactivar Tor per a fer trucades + Trucada entrant + Trucada entrant · %s + Trucada sortint + Trucada sortint · %s + Trucada perduda + Trucada de veu + Trucada de vídeo + Ajuda + Passar a la conversa + El seu micròfon no està disponible + Només pots tenir una trucada alhora. + Tornar a la trucada en curs + No es pot canviar de càmera + Enclavar en la part superior + Desenclavar de dalt + Ruta GPX + No s\'ha pogut corregir el missatge + Totes les converses + Aquesta conversa + El seu avatar + Avatar per a %s + Encriptat amb OMEMO + Encriptat amb OpenPGP + No encriptat + Sortir + Gravar la bústia de veu + Reproduir àudio + Pausar l\'àudio + Afegir contacte, crear o unir-se a un xat de grup, o descobrir canals + + View %1$d Participant + Veure %1$d Participants + + + Alguns missatges no han pogut ser lliurats + Alguns missatges no han pogut ser lliurats + + Lliuraments fallits + Més opcions + No s\'ha trobat cap aplicació + Convidar a Conversations + No es pot processar la invitació + El servidor no admet la generació d\'invitacions + Cap compte actiu admet aquesta funció +
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml new file mode 100644 index 000000000..d39965222 --- /dev/null +++ b/app/src/main/res/values-cs/strings.xml @@ -0,0 +1,978 @@ + + + Nastavení + Nová konverzace + Nastavení účtů + Nastavení účtu + Zavřít konverzaci + Detaily kontaktu + Detaily skupinového chatu + Detaily kanálu + Přidat účet + Upravit jméno + Přidat do adresáře + Smazat ze seznamu + Zablokovat kontakt + Odblokovat kontakt + Zablokovat doménu + Odblokovat doménu + Blokovat účastníka + Odblokovat účatníka + Nastavení účtů + Nastavení + Sdílet s konverzací + Začít konverzaci + Vybrat kontakt + Vybrat kontakty + Sdílet pomocí účtu + Seznam blokovaných + právě teď + před minutou + před %d minutami + + %d nepřečtená konverzace + + + %d nepřečtené konverzace + + + %d nepřečtených konverzací + + + %d nepřečtených konverzací + + + odesílám… + Dešifrování zprávy. Chvíli strpení… + OpenPGP šifrovaná zpráva + Přezdívka se již používá + Neplatná přezdívka + Administrátor + Vlastník + Moderátor + Účastník + Návštěvník + Přejete si odstranit %s ze seznamu kontaktů? Předešlé rozhovory nebudou odstraněny. + Chcete zablokovat příjem zpráv od %s? + Chcete odblokovat příjem zpráv od %s? + Zablokovat všechny kontakty z %s? + Odblokovat všechny kontakty z %s? + Kontakt zablokován + Zablokovaný + Přejete si odstranit %s ze záložek? Předešlé rozhovory pod záložkou nebudou odstraněny. + Registrovat nový účet na serveru + Změnit heslo na serveru + Sdílet s… + Začít konverzaci + Pozvat kontakt + Pozvat + Kontakty + Kontakt + Zrušit + Nastavit + Přidat + Upravit + Smazat + Zablokovat + Odblokovat + Uložit + OK + %1$s přestal reagovat + Zasíláním detailů o důvodu selhání z Vašeho XMPP účtu pomůžete dalšímu vývoji %1$s. + Odeslat teď + Již se neptat + Nelze se připojit k účtu + Nebylo možné se připojit k několika účtům + Ťukněte pro nastavení účtů + Přiložit soubor + Přidat chybějící kontakt do seznamu kontaktů? + Přidat kontakt + doručení selhalo + Připravuji odeslání obrázku + Připravuji odeslání obrázků + Sdílení souborů. Chvíli strpení… + Smazat historii + Smaže historii konverzací + Opravdu chcete smazat všechny zprávy v této konverzace?\n\nVarováníToto neovlivní zprávy uložené na jiných zařízeních či serverech. + Smazat soubor + Opravdu chcete smazat tento soubor?\n\nVarováníToto neovlivní kopie uložené na jiných zařízeních či serverech. + Poté zavřít tuto konverzaci + Vybrat přístroj + Odeslat nešifrovanou zprávu + Odeslat zprávu + Odeslat zprávu pro %s + Poslat OMEMO šifrovanou zprávu + Odeslat v\\OMEMO šifrovanou zprávu + Poslat OpenPGP šifrovanou zprávu + Přezdívka změněna + Poslat nešifrované + Zašifrování se nezdařilo. Možná nemáte správný privátní klíč. + OpenKeychain + OpenKeychain k šifrování a dešifrování zpráv a ke správě Vašich veřejných klíčů.

OpenKeychain je vydána pod licencí GPLv3+ a dostupná na F-Droid nebo Google Play.

(Po instalaci, prosím, restartujte%1$s.)]]>
+ Restartovat + Instalovat + Nainstalujte prosím OpenKeychain + nabízí… + čekám… + Nebyl nalezen žádný OpenPGP klíč + Není možné zašifrovat zprávy, protože kontakt neoznamuje svůj veřejný klíč.\n\nPožádejte kontakt, aby si nastavil OpenPGP. + Nebyly nalezeny žádné OpenPGP klíče + Není možné zašifrovat zprávy, protože kontakty neoznamují svůj veřejný klíč.\n\nPožádejte je, aby si nastavili OpenPGP. + Obecné + Přijímat soubory + Automaticky přijímat soubory menší než… + Přílohy + Upozornění + Vibrovat + Vibrovat při přijetí nové zprávy + LED upozornění + Blikat při přijetí nové zprávy + Vyzváněcí tón + Zvuk upozornění + Zvuk upozornění na nové zprávy + Vyzváněcí tón pro příchozí hovory + Časová lhůta + Časová lhůta po kterou bude Conversations v tichém režimu při zaznamenání aktivity na jiném přístroji + Rozšířené + Neodesílat detaily o pádu aplikace + Zasíláním detailů o důvodu selhání pomůžete dalšímu vývoji + Potvrzovat zprávy + Nechat kontaky vědět kdy jste dostali a přečetli jejich zprávy + UI + Chyba OpenKeychain. + Chybný klíč pro šifrování. + Přijmout + Došlo k chybě + Chyba + Váš účet + Zasílat změny stavu + Přijímat změny stavu + Zažádat o změny stavu + Vybrat obrázek + Vyfotit obrázek + Aktivně povolovat vyžádání změn stavu + Vybraný soubor není obrázek + Nebylo možné převést obrázek + 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 + Připojuji\u2026 + Offline + Nepřihlášen + Server nenalezen + Žádné připojení + Registrace selhala + Uživatelské jméno se již používá + Registrace dokončena + 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 + Chyba při otevírání proudu + Nešifrováno + OTR + OpenPGP + OMEMO + Smazat účet + Dočasně vypnout + Zveřejnit avatar + Zveřejnit OpenPGP klíč + Odstranit veřejný klíč OpenPGP + Skutečně chcete odstranit Váš současný veřejný OpenPGP klíč?\nVaše kontakty Vám nebudou moci nadále posílat zprávy šifrované pomocí OpenPGP. + OpenPGP veřejný klíč zveřejněn. + Povolit účet + Jste si jisti? + Smazáním Vašeho účtu dojde k vymazání celé Vaší historie konverzací. + Nahrát hlas + Adresa XMPP + Blokovat XMPP adresu + jmeno@server.cz + Heslo + Toto není platná XMPP adresa + Nedostatek paměti. Obrázek je příliš velký + Chcete přidat %s do svého adresáře? + Údaje serveru + XEP-0313: MAM + XEP-0280: Kopie zpráv + XEP-0352: Zobrazování stavu klienta + XEP-0191: Příkaz blokování + XEP-0237: Verzování seznamu + XEP-0198: Nastavení proudu + XEP-0163: PEP (Avatars / OMEMO) + XEP-0363: HTTP File Upload + XEP-0357: Push + dostupný + nedostupný + Chybí oznámení o veřejném klíči + právě spatřen + naposledy spatřen před minutou + naposledy spatřen před %d minutami + naposledy spatřen před hodinou + naposledy spatřen před %d hodinami + naposledy spatřen včera + naposledy spatřen před %d dny + Šifrovaná zpráva. Nainstalujte OpenKeychain pro její dešifrování. + Nalezeny nové OpenPGP šifrované zprávy + OpenPGP ID klíče + OMEMO otisk + v\\OMEMO otisk + OMEMO otisk (původce zprávy) + v\\OMEMO otisk (původce zprávy) + Ostatní přístroje + Věřit OMEMO otiskům + Získávání klíčů… + Hotovo + Dešifrovat + Záložky + Hledat + Vložit kontakt + Smazat kontakt + Zobrazit detaily kontaktu + Zablokovat kontakt + Odblokovat kontakt + Vytvořit + Vybrat + Kontakt již existuje + Vstoupit + kanál@konference.server.cz/jméno + kanál@konference.server.cz + Uložit jako záložku + Smazat záložku + Zrušit skupinový chat + Zrušit kanál + Skutečně chcete zrušit skupinový chat?\n\nVarování: Skupinový chat bude zcela odstraněn ze serveru. + Skutečně chcete zrušit veřejný kanál?\n\nVarování: Kanál bude zcela odstraněn ze serveru. + Nebylo možné zrušit skupinový chat + Nebylo možné zrušit kanál + Upravit předmět skupinového chatu + Téma + Připojuji se ke skupinovému chatu… + Odejít + Kontakt přidán do seznamu + Opět přidat + %s dočetl(a) až sem + %s dočetli až sem + %1$s +%2$d ostatní(ch) dočetli až sem + Všichni dočetli až sem + Zveřejnit + Ťuknutím na avatar vyberete obrázek z galerie + Zveřejňuji… + Server odmítl toto zveřejnění + Nebylo možné převést Váš obrázek + Nepodařilo se uložit avatar na disk + (Stisknout dlouze pro obnovení výchozího stavu) + Váš server nepodporuje zveřejňování avataru + šeptem + pro %s + Zaslat soukromou zprávu pro %s + Připojit + Tento účet již existuje + Další + Sezení vytvořeno + Přeskočit + Vypnout upozornění + Povolit + Požadováno heslo ke skupinovému chatu + Vložit heslo + Nejdříve, prosím, od kontaktu vyžádejte zasílání informací o změně stavu.\n\nTo bude využito k identifikaci aplikace, kterou kontakt používá. + Ihned vyžádat + Ignorovat + Varování: Odeslání bez povolení vzájemného informování o změně stavu může způsobit nečekané potíže.\n\nJděte do \"Detaily kontaktu\" a ověřte nastavení aktualizace stavu. + Zabezpečení + Povolit opravu zpráv + Povolí kontaktům zpětné upravování jejich zpráv + Expertní nastavení + S tímto zacházejte velmi opatrně + O %s + Tichý režim + Odkdy + Dokdy + Povolit tichý režim + Upozornění budou během tichého režimu ztlumena + Další + OMEMO otisk zkopírován do schránky + Byl(a) jste blokován(a) v této skupině + Tento skupinový chat je pouze pro registrované členy + Omezení zdrojů + Byl(a) jste vyloučen(a) z této skupiny + Skupinový chat byl ukončen + Již nejste členem tohoto skupinového chatu + za použití účtu %s + hostován na %s + Ověřuji %s na HTTP hostiteli + Bez připojení. Zkus znovu později + Ověřit %s velikost + Kontrola %1$s velikosti na %2$s + Možnosti zpráv + Citovat + Vložit jako citaci + Kopírovat originální URL + Poslat znovu + URL souboru + Adresa URL zkopírována do schránky + Adresa XMPP zkopírována do schránky + Chybové hlášení zkopírováno do schránky + webová adresa + Skenovat 2D kód + Zobrazit 2D kód + Zobrazit seznam blokovaných + Detaily účtu + Potvrdit + Zkusit znovu + Služba na popředí + Zamezit operačnímu systému v ukončení připojení + Vytvořit zálohu + Soubory zálohy budou uloženy do %s + Vytvářím soubory zálohy + Záloha byla vytvořena + Soubory zálohy byly uloženy do %s + Obnovuji zálohu + Záloha obnovena + Nezapomeňte povolit účet + Vybrat soubor + Přijímám %1$s (%2$d%% dokončeno) + Stáhnout %s + Smazat %s + soubor + Otevřít %s + odesílám (%1$d%% přeneseno) + Připravuji sdílení souboru + %s nabídnuto ke stažení + Zrušit přenos + nebylo možné sdílet soubor + přenos souboru byl zrušen + Soubor byl smazán + Nebyla nalezena aplikace umožňující otevření souboru + Nebyla nalezena aplikace umožňující otevření odkazu + Nebyla nalezena aplikace umožňující zobrazení kontaktu + Dynamické tagy + Zobrazit tagy pro čtení pod kontakty + Povolit upozornění + Žádný server pro skupinový chat nebyl nalezen + Nebylo možné vytvořit skupinový chat + Avatar účtu + Zkopírovat OMEMO otisk do schránky + Znovu vytvořit OMEMO klíč + Smazat přístroje + Opravdu chcete vymazat ostatní přístroje z OMEMO upozornění? Až se příště tyto přístroje připojí, znovu se ohlásí, ale pravděpodobně neobdrží zprávy odeslané v mezičase mezi přihlášeními. + Pro tento kontakt nejsou dostupné žádné použitelné klíče.\nNebylo možné získat nové klíče ze serveru. Možná je něco v nepořádku se serverem kontaktu? + Pro tento kontakt nejsou dostupné žádné klíče.\nUjistěte se, že oba máte zapnuté zasílání informací o změně stavu. + Něco se pokazilo + Načíst historii ze serveru + Na serveru není žádná další historie + Aktualizuji… + Heslo změněno! + Nelze změnit heslo + Změnit heslo + Současné heslo + Nové heslo + Heslo nesmí být prázdné + Povolit všechny účty + Vypnout všechny účty + Provést akci s + Nepřidružený + Offline + Vyloučený + Člen + Pokročilý mód + Udělit oprávnění člena + Odebrat oprávnění člena + Povolit administrátorská oprávnění + Odebrat administrátorská oprávnění + Udělit práva vlastníka + Odebrat práva vlastníka + Odebrat ze skupinového chatu + Odebrat z kanálu + Nelze změnit připojení uživatele %s + Blokovat ve skupinovém chatu + Blokovat v kanálu + Pokoušíte se odstranit %s z veřejného kanálu. Jediný způsob, jak toho docílit, je zablokovat tohoto uživatele navždy. + Vypovědět + Nelze změnit roli uživatele %s + Nastavení soukromých skupinových chatů + Nastavení veřejných kanálů + Soukromé, pouze pro členy + Ukázat XMPP adresy všem + Nastavit kanál jako moderovaný + Neúčastníte se + Nastavení skupinového chatu změněno! + 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 + Enter odesílá + Odeslat klávesou Enter. Vždy můžete zprávy odeslat pomocí Ctrl+Enter, i když tato možnost není povolena. + Zobrazit klávesu enter + Změnit klávesu emotikon na klávesu enter + audio + video + obrázek + dokument PDF + Aplikace pro Android + Kontakt + Avatar byl zveřejněn! + Odesílám %s + Nabízím %s + Skrýt offline + %s píše… + %s přestal(a) psát + %s píší… + %s přestali psát + Upozornění při psaní + Nechat kontaky vědět když jim píšete zprávu + Poslat pozici + Zobrazit pozici + Nebyla nalezena aplikace pro zobrazení pozice + 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 + Smazat ručně povolené certifikáty + Žádné ručně povolené certifikáty + Odstranit certifikáty + Smazat výběr + Zrušit + + %d certifikát smazán + %d certifikáty smazány + %d certifikátů smazáno + %d certifikátů smazáno + + Nahradit tlačítko odeslání rychlou akcí + Rychlá akce + Žádná + Naposledy použitá + Vybrat rychlou akci + Prohledat kontakty + Prohledat záložky + Poslat soukromou zprávu + %1$s opustil(a) skupinový chat + Uživatelské jméno + Uživatelské jméno + Toto není platné uživatelské jméno + Stahování selhalo: Server nenalezen + Stahování selhalo: Soubor nenalezen + Stahování selhalo: Nelze se připojit k hostu + 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í + Při uzamčeném zařízení nastaví váš stav na \"pryč\" + Nedostupný při vypnutém zvuku + Při ztišeném vyzvánění označí váš stav jako \"nedostupný\" + Vibrační mód brát stejně jako tichý + Při nastavení pouze na vibrace označí váš stav jako \"nedostupný\" + Rozšířená nastavení připojení + Zobrazovat nastavení hostname a port při vytváření účtu + xmpp.server.cz + Přihlásit se pomocí certifikátu + Nelze analyzovat certifikát + Nastavení archivace + Nastavení archivace na serveru + Získávání nastavení archivace. Chvíli strpení… + Nelze získat nastavení archivace + Vyžadována CAPTCHA + Zadejte text z obrázku výše + Adresa XMPP nesouhlasí s certifikátem + Obnovit certifikát + Chyba získání OMEMO klíče! + OMEMO klíč ověřen certifikátem! + Tento přístroj nepodporuje výběr klientského certifikátu! + Připojení + Připojit přes Tor + 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 + + %d zpráva + %d zprávy + %d zpráv + %d zpráv + + Načíst více zpráv + Soubor sdílen s %s + Obrázek sdílen s %s + Obrázky sdíleny s %s + Text sdílen s %s + Povolit %1$s přístup k externímu úložišti + Povolit %1$s přístup ke kameře + Synchronizovat s kontakty + %1$s požaduje přístup k Vašim kontaktům za účelem spárování s Vašimi XMPP kontakty.\nU kontaktů se pak zobrazí celé jméno a avatar.\n\n%1$s bude kontakty pouze číst a párovat místně v zařízení, aniž by došlo k nahrání těchto dat na server. +
Tyto kontaktní údaje nebudeme kopírovat a ukládat.\n\nVíce informací najdete v našich zásadách pro ochranu osobních údajů.

Nyní budete požádáni o udělení přístupu k Vašim kontaktům.]]>
+ Upozorňovat na všechny zprávy + Upozornit pouze, když mě někdo zmíní + Upozornění vypnuta + Upozornění pozastavena + Komprese obrázků + Tip: Pokud použijete \"Vybrat soubor\" místo \"Vybrat obrázek\", můžete poslat nekomprimovaný obrázek bez ohledu na toto nastavení. + Vždy + Pouze pro velké obrázky + Povolena optimalizace využití baterie + Vaše zařízení používá agresivní optimalizaci výdrže baterie pro %1$s, což může vést k opožděným upozorněním nebo dokonce ke ztrátě některých zpráv.\nDoporučujeme optimalizaci vypnout. + Vaše zařízení používá agresivní optimalizaci výdrže baterie pro %1$s, což může vést k opožděným upozorněním nebo dokonce ke ztrátě některých zpráv.\nNyní budete vyzváni k jejímu vypnutí. + Vypnout + Vybraný obsah je příliš dlouhý + (Žádné aktivované účty) + Toto pole je vyžadováno + Opravit zprávu + 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ů.]]>
+ Souhlasit a pokračovat + Průvodce je nastaven, aby vytvořil účet na serveru conversations.im.¹\nPokud si vyberete conversations.im jako svého poskytovatele, budete moci komunikovat s uživateli u ostatních poskytovatelů, budou-li mít vaši celou XMPP adresu. + Vaše celá XMPP adresa: %s + Vytvořit účet + Použít vlastního provozovatele + Zadejte své uživatelské jméno + Spravovat viditelnost ručně + Nastavit viditelnost při úpravě statusové zprávy + Stavová zpráva + Volný pro chat + Online + Pryč + Nedostupný + Zaneprázdněný + Bylo vygenerováno bezpečné heslo + Tento přístroj nepodporuje vypnutí možnosti optimalizace využití baterie + Registrace selhala: Zkuste znovu později + Registrace selhala: Příliš slabé heslo + Vybrat účastníky + Vytvářím skupinový chat… + Pozvat znovu + Vypnout + Krátký + Střední + Dlouhý + Informovat o používání + Tato možnost dává vědět Vašim kontaktům, kdy používáte Conversations + Soukromí + Vzhled + Vybrat paletu barev + Automaticky + Světlý vzhled + Tmavý vzhled + Zelené pozadí + Použít zelené pozadí pro přijaté zprávy + Nelze se spojit s OpenKeychain + Tento přístoj již není používán + Počítač + Mobil + Tablet + Prohlížeč + Konzole + Vyžadována platba + Udělte povolení pro přístup na Internet + + Kontakt žádá informace o změnách stavu + Povolit + Chybí oprávnění přistupovat k %s + Vzdálený server nebyl nalezen + Vypršel čas spojení se vzdáleným serverem + Nelze aktualizovat účet + Nahlásit tuto XMPP adresu kvůli odesílání spamu. + Smazat OMEMO identity + Znovu vygenerovat OMEMO klíče. Vyžaduje potvrzení od všech vašich kontaktů. Použijte pouze jako poslední řešení. + Smazat vybrané klíče + Pro zveřejnění svého avatara musíte být online. + Zobrazit chybovou zprávu + Chybová zpráva + Zapnuta úspora dat + Váš operační systém zabraňuje aplikaci %1$s v přístupu na Internet, pokud tato běží na pozadí. Pro příjem upozornění na nové zprávy musíte %1$s povolit neomezený přístup při zapnuté úspoře dat.\n%1$s se bude i přesto snažit omezovat přenos dat. + Tento přístroj nepodporuje vypnutí úspory dat pro aplikaci %1$s. + Nebylo možné vytvořit dočasný soubor + Tento přístroj byl ověřen + Kopírovat identifikátor + Oveřil(a) jste všechny OMEMO klíče, které vlastníte. + Kód neobsahuje otisk pro tuto konverzaci. + Ověřené otisky + Naskenovat kód kontaktu pomocí fotoaparátu + Prosím, počkejte na získání klíčů + Sdílet jako čárový kód + Sdílet jako XMPP URI + Sdílet jako HTTP odkaz + Slepě důvěřovat před ověřením + Důvěřovat novým zařízením neověřených kontaktů, ale požadovat ruční potvrzení nových zařízení u ověřených kontaktů. + Nedůvěryhodný + Neplatný 2D kód + Vyčistit složku dočasných souborů (užitých aplikací fotoaparátu) + Vyčistit dočasné soubory + Vyčistit soukromé úložiště + Vyčistit úložiště souborů (Mohou být znovu staženy ze serveru) + Tento odkaz pochází z důvěryhodného zdroje + Kliknutím na odkaz se chystáte ověřit OMEMO klíče patřící %1$s. To je bezpečné jedině tehdy, pokud jste odkaz získali z důvěryhodného zdroje, kdy pouze %2$s mohl tento odkaz zveřejnit. + Ověřit OMEMO klíče + Zobrazit neaktivní + Skrýt neaktivní + Odebrat z důvěryhodných + Jste si jisti, že chcete odebrat ověření tomuto zařízení?\nZařízení a příchozí zprávy z něj budou označeny jako \"Nedůvěryhodné\". + + %d vteřina + %d vteřiny + %d vteřin + %d vteřin + + + %d minuta + %d minut + %d minut + %d minut + + + %d hodina + %d hodiny + %d hodin + %d hodin + + + %d den + %d dny + %d dnů + %d dnů + + + %d týden + %d týdny + %d týdnů + %d týdnů + + + %d měsíc + %d měsíce + %d měsíců + %d měsíců + + Automatické mazání zpráv + Automaticky z tohoto zařízení mazat zprávy, které jsou starší, než je nastaveno. + Šifruji zprávu + Komprimuji video + Odpovídající konverzace uzavřena. + Kontakt zablokován. + Upozornění od neznámých + Upozornit na zprávy a hovory od neznámých kontaktů. + Přijata zpráva od neznámého kontaktu + Zablokovat neznámý kontakt + Zablokovat celou doménu + právě teď online + Zkusit znovu dešifrovat + Chyba sezení + Degradovaný SASL mechanismus + Server požaduje registraci přes webovou stránku + Otevřít webovou stránku + Nebyla nalezena aplikace umožňující otevření webové stránky + Heads-up upozornění + Zobrazit heads-up upozornění + Dnes + Včera + Ověřit název hostitele pomocí DNSSEC + Certifikáty serverů obsahující ověřený název hostitele jsou považovány za ověřené + Certifikát neobsahuje XMPP adresu + částečný + Nahrát video + Kopírovat do schránky + Zpráva zkopírována do schránky + Zpráva + Soukromé zprávy jsou zakázány + Chráněné aplikace + Abyste mohli dostávat upozornění i při vypnuté obrazovce, musíte přidat Conversations mezi chráněné aplikace. + Přijmout neznámý certifikát? + Certifikát není podepsaný žádnou známou certifikační autoritou. + Přijmout nesouhlasící jméno serveru? + Server se nemohl prokázat jako \"%s\". Certifikát je platný pouze pro: + Chcete se přesto připojit? + Detaily certifikátu: + Jednou + Skener kódů QR potřebuje přístup k fotoaparátu + Posunout na konec + Posunout na konec po odeslání zprávy + Upravit stavovou zprávu + Upravit stavovou zprávu + Zakázat šifrování + %1$s nemohl odeslat šifrované zprávy pro %2$s. To může být způsobeno tím, že kontakt používá zastaralý server nebo klient, který nepodporuje OMEMO šifrování. + Nelze získat seznam zařízení + Nelze získat šifrovací klíče + Tip: V některých případech může být řešení vzájemné přidání kontaktů do seznamu kontaktů. + Opravdu chcete vypnout OMEMO šifrování pro tuto konverzaci?\nTím umožníte správci Vašeho serveru číst Vaše zprávy. Zároveň to však může být jediný způsob, jak komunikovat s kontakty, které používají zastaralé verze klientů. + Vypnout hned + Koncept: + OMEMO šifrování + OMEMO bude vždy použito k šifrování zpráv v jednotlivých konverzacích i v soukromých skupinách. + OMEMO bude použito jako výchozí pro nové konverzace. + OMEMO bude nutné zapnout ručně pro každou každou novou konverzaci. + Vytvořit zástupce + Velikost písma + Relativní velikost písma v aplikaci + Zapnuto jako výchozí + Vypnuto jako výchozí + Malé + Střední + Velké + Zpráva nebyla pro toto zařízení zašifrována. + Chyba při dešifrování OMEMO zprávy. + zpět + Sdílení polohy je vypnuto + Kopírovat pozici + Sdílet pozici + Pokyny + Sdílet pozici + Zobrazit pozici + Sdílet + Nebylo možné zahájit nahrávání + Chvíli strpení… + Povolit %1$s přístup k mikrofonu + Prohledat zprávy + GIF + Zobrazit konverzaci + Plugin pro sdílení pozice + 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 + Hostitel nepodporuje avatary pro skupinový chat + Pouze vlastník může změnit avatar skupinového chatu + Jméno kontaktu + Přezdívka + Jméno + Poskytnutí jména je nepovinné + Jméno skupinového chatu + Tento skupinový chat byl zrušen + Nebylo možné uložit nahrávku + Služba na popředí + Tato kategorie upozornění zobrazuje stálou notifikaci, že aplikace %1$s je spuštěná. + Informace o stavu + Problémy s připojením + Tato kategorie upozornění zobrazuje notifikaci v případě problémů s připojením k účtu. + Zprávy + Hovory + Zprávy + Příchozí hovory + Probíhající hovory + Tiché zprávy + Kategorie upozornění, která nejsou doprovázena žádným zvukem. Například když jste aktivní na jiném zařízení (ochranná doba). + Neúspěšné přenosy + Nastavení upozornění na zprávy + Nastavení upozornění na příchozí hovory + Důležitost, Zvuk, Vibrace + Komprese videa + Zobrazit média + Účastníci + Prohlížeč médií + Soubor byl vynechán kvůli porušení bezpečnosti. + Kvalita videa + Nižší kvalita znamená menší soubory + Střední (360p) + Vysoká (720p) + zrušeno + Již máte rozepsaný koncept zprávy. + Funkce není implemetována + Neplatný kód země + Vyberte zemi + telefonní číslo + Ověřte své telefonní číslo + Quicksy Vám pošle zprávu SMS (mohou Vám být účtovány poplatky dle tarifu) k ověření Vašeho telefonního čísla. Zadejte kód země a telefonní číslo: +
%s

. Je číslo v pořádku, nebo ho chete upravit?]]>
+ %s není platné telefonní číslo. + Prosíme, zadejte své telefonní číslo. + Hledat zemi + Ověřit %s + %s.]]> + Poslali jsme Vám další SMS se 6místným kódem. + Prosím, vložte 6místný pin. + Poslat SMS znovu + Poslat SMS znovu (%s) + Chvíli strpení (%s) + zpět + Automaticky vložen pravděpodobný pin ze schránky. + Prosím, vložte svůj 6místný pin. + Opravdu si přejete přerušit registraci? + Ano + Ne + Ověřuji… + Pin, který jste zadali, je nesprávný. + Pin, který jsme Vám poslali, vypršel. + Neznámá chyba sítě. + Neznámá odpověď serveru. + Nebylo možné se připojit k serveru. + Nebylo možné navázat zabezpečené spojení. + Server nenalezen. + Něco se pokazilo při zpracovávání Vašeho požadavku. + Dočasně nedostupné. Zkuste to později. + Žádné připojení k síti. + Prosíme, zkuste to znovu za %s + Příliš mnoho pokusů + Používáte zastaralou verzi této aplikace. + Aktualizovat + Toto telefonní číslo je již přihlášeno z jiného zařízení. + Prosíme, vložte své jméno, aby ostatní, kteří Vás nemají v seznamu kontaktů, věděli, kdo jste. + Vaše jméno + Vložte své jméno + Pro nastavení jména klepněte na Upravit. + Odmítnout žádost + Instalovat Orbot + Spustit Orbot + Není nainstalován žádný správce aplikací. + Tento kanál zveřejní Vaši XMPP adresu + Originální (nekomprimováno) + Otevřít pomocí… + Nastavit profilový obrázek + Vybrat účet + Obnovit ze zálohy + Obnovit + Pro obnovení ze zálohy zadejte heslo k účtu %s. + Nepoužívejte funkci obnovy ze zálohy pro současný běh více instalací. Obnova ze zálohy je určena pouze pro případ přenosu na jinou instalaci nebo pokud došlo ke ztrátě původního zařízení. + Nebylo možné obnovit zálohu. + Nebylo možné dešifrovat zálohu. Zadal(a) jste správné heslo? + Záloha & Obnova + Zadejte XMPP adresu + Vytvořit skupinový chat + Připojit se k veřejnému kanálu + Vytvořit soukromý skupinový chat + Vytvořit veřejný kanál + Jméno kanálu + Adresa XMPP + Prosím, zadejte název kanálu + Zadejte XMPP adresu + Toto je XMPP adresa. Prosím, zadejte jméno. + Vytváření veřejného kanálu… + Tento kanál již existuje + Připojil(a) jste se k existujícímu kanálu + Nebylo možné uložit nastavení kanálu + Povolit komukoli změnit téma + Povolit komukoli pozvat další účastníky + Kdokoli může změnit téma. + Vlastníci mohou měnit téma. + Správci mohou měnit téma. + Vlastníci mohou pozvat další účastníky. + Kdokoli může pozvat další účastníky. + Správci mohou vidět XMPP adresy. + Kdokoli může vidět XMPP adresy. + Tento veřejný kanál nemá žádné účastníky. Pozvěte své kontakty nebo sdílejte XMPP adresu kanálu pomocí tlačítka Sdílet. + Tento soukromý skupinový chat nemá žádné účastníky. + Spravovat oprávnění + Hledat účastníky + Soubor je příliš velký + Přiložit + Najít kanály + Prohledat kanály + Možné porušení soukromí + search.jabber.network.

Používání této služby odešle vaši IP adresu a vyhledávaný termín této službě. Pro více informací konzultujte jejich Zásady ochrany osobních údajů.]]>
+ Již mám účet + Přidat existující účet + Vytvořit nový účet + Toto vypadá jako adresa domény + Přesto přidat + 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 + Prosím, zadejte heslo k tomuto účtu + Nebylo možné vykonat tuto akci + Připojit se k veřejnému kanálu… + Sdílející aplikace neudělila dostatečná oprávnění pro přístup k souboru. + + Místní server + Většině uživatelů doporučujeme použít \'jabber.network\' kvůli lepším návrhům z celého veřejného XMPP ekosystému. + Metoda objevování kanálů + Záloha + O + Prosíme, povolte účet + Volat + Příchozí hovor + Příchozí videohovor + Připojuji + Připojeno + Přijímám hovor + Ukončuji hovor + Přijmout + Odmítnout + Vyhledávám zařízení + Vyzvánění + Zaneprázdněný + Hovor nebylo možné spojit + Spojení ztraceno + Chyba aplikace + Zavěsit + Probíhající hovor + Probíhající videohovor + Zakázat hovory přes Tor + Příchozí hovor + Příchozí hovor · %s + Zmeškané volání · %s + Odchozí hovor + Odchozí hovor · %s + Zmeškané volání + Hovor + Videohovor + Nápověda + Přepnout na konverzaci + Váš mikrofon je nedostupný + V jednu chvíli může probíhat pouze jeden hovor. + Návrat k probíhajícímu hovoru + Nebylo možné přepnout kameru + Připnout nahoru + Odepnout shora + GPX trasa + Nebylo možné opravit zprávu + Všechny konverzace + Tato konverzace + Váš avatar + Avatar uživatele %s + Šifrováno pomocí OMEMO + Šifrováno pomocí OpenPGP + Nešifrováno + Ukončit + Nahrát hlasovou zprávu + Přehrát audio + Pozastavit audio + Přidat kontakt, vytvořit nebo se připojit ke skupinovému chatu nebo vyhledat kanály + + Ukázat %1$d účastníka + Ukázat %1$d účastníky + Ukázat %1$d účastníků + Ukázat %1$d účastníků + + + Zpráva nemohla být doručena + Několik zpráv nemohlo být doručeny + Některé zprávy nemohly být doručeny + Některé zprávy nemohly být doručeny + + Neúspěšné přenosy + 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/app/src/main/res/values-da-rDK/strings.xml b/app/src/main/res/values-da-rDK/strings.xml new file mode 100644 index 000000000..032876c1a --- /dev/null +++ b/app/src/main/res/values-da-rDK/strings.xml @@ -0,0 +1,1003 @@ + + + Indstillinger + Ny samtale + Håndter konti + Håndter konto + Afslut samtale + Kontaktdeltaljer + Gruppechat detaljer + Kanaldetaljer + Tilføj konto + Rediger navn + Tilføj til adressebog + Slet fra liste + Blokerer kontakt + Frigiv kontakt + Blokerer domæne + Frigiv domæne + Bloker deltager + Frigiv deltager + Håndter konti + Indstillinger + Del med Conversation + Start Conversation + Vælg kontakt + Vælg kontakter + Del via konto + Blokeringsliste + lige nu + 1 minut siden + %d minutter siden + + %d ulæst samtale + + + %d ulæst samtaler + + + sender… + Dekrypter besked. Vent venligst… + OpenPGP krypteret besked + Kaldenavn er allerede i brug + Ugyldig kaldenavn + Administrator + Ejer + Moderator + Deltager + Gæst + Vil du gerne fjerne %s fra din kontaktliste? Samtaler med denne kontakt vil ikke blive slettet. + Vil du bloker %s i at sende dig beskeder? + Vil du frigive %s og tillade dem at sende dig beskeder? + Bloker alle kontakter fra %s? + Frigiv alle kontakter fra %s? + Kontakt blokeret + Blokeret + Vil du gerne slette %s som et bogmærke? Samtaler med dette bogmærke vil ikke blive slettet. + Register ny konto på server + Ændr adgangskode på server + Del med… + Start samtale + Inviter kontakt + Inviter + Kontakter + Kontakt + Annuller + Indstil + Tilføj + Rediger + Slet + Bloker + Frigiv + Gem + OK + %1$s er kørt fast + Brugen af din XMPP-konto til at sende \"stack traces\" hjælper den løbende udvikling af %1$s. + Send nu + Spørg ikke igen + Kunne ikke forbinde til konto + Kunne ikke forbinde til flere konti + Tryk for at håndter dine konti + Vedhæft fil + Tilføj den manglede kontakt til din kontaktliste? + Tilføj kontakt + levering mislykkedes + Gør klar til at sende billede + Gør klar til at sende billeder + Deler filer. Vent venligst… + Ryd historik + Ryd samtalehistorik + Vil du slette alle beskeder i denne samtale?\n\nAdvarsel: Dette vil ikke påvirke beskeder gemt på andre enheder eller servere. + Slet fil + Er du sikker på, at du vil slette denne fil?\n\nAdvarsel: Dette sletter ikke kopier af denne fil, der er gemt på andre enheder eller servere. + Luk efterfølgende denne samtale + Vælg enhed + Send ukrypteret besked + Send besked + Send besked til %s + Send OMEMO-krypteret besked + Send v\\OMEMO-krypteret besked + Send OpenPGP krypteret besked + Nyt kaldenavn i brug + Send ukrypteret + Dekryptering mislykkes. Måske du ikke har den rette private nøgle. + OpenKeychain + OpenKeychain til at kryptere og dekryptere beskeder og håndtere dine offentlige nøgler.

Det er licenseret under GPLv3+ og tilgængelig på F-Droid og Google Play.

(Genstart %1$s bagefter.)]]>
+ Genstart + Installer + Installer venligst OpenKeychain + tilbyder… + venter… + Ingen Open PGP nøgler fundet + Kunne ikke kryptere din besked, fordi din kontakt ikke annoncerer deres offentlige nøgle.\n\nBed din kontakt om at konfigurere OpenPGP. + Ingen OpenPGP nøgler fundet + Kunne ikke kryptere din besked, fordi din kontakt ikke annoncerer deres offentlige nøgle.\n\n1Bed dem om at opsætte OpenPGP. + Generel + Accepter filer + Accepter automatisk filer mindre end… + Vedhæftninger + Notifikation + Vibrer + Vibrer ved nye beskeder + LED Notifikation + Blik notifikationslys når en ny besked kommer + Ringetone + Notifikationslyd + Lydnotifikation for nye beskeder + Ringetone for indkommende opkald + Fredningsperiode + Tidsintervallet hvor notifikationer er lydløs efter at have registreret aktivitet på en af dine andre enheder. + Advanceret + Send aldrig fejlrapporter + 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 + Accepter + Der er sket en fejl + Fejl + Din konto + Send nærværsopdateringer + Modtag nærværsopdateringer + Bed om nærværsopdateringer + Vælg billede + Tag billede + Giv forebyggende anmodning om abonnement + Den valgte fil er ikke et billede + Kunne ikke konverter billedefil + 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 + Forbinder\u2026 + Offline + Uautoriseret + Server ikke fundet + Ingen forbindelse + Registrering fejlede + Brugernavn er optaget + Registrering fuldført + Registrering er ikke understøttet af server + Ugyldig registreringstoken + TLS forhandling mislykkedes + Domæne kan ikke verificeres + Brud på retningslinjer + Inkompatibel server + Inkompatibel klient + Strømfejl + Fejl ved streamåbning + Ukrypteret + OTR + OpenPGP + OMEMO + Slet konto + Deaktiver midlertidigt + Offentliggør avatar + Offentliggør OpenPGP offentlig nøgle + Fjern OpenPGP offentlig nøgle + Er du sikker på, at du vil fjerne din OpenPGP-nøgle fra din nærværsmeddelelse?\nDine kontakter kan ikke længere sende dig OpenPGP-krypterede meddelelser. + OpenPGP offentlig nøgle er offentliggjort + Aktiver konto + Er du sikker? + Sletning af din konto sletter hele din samtalehistorik + Optag lyd + XMPP-adresse + Bloker XMPP-adresse + brugernavn@domæne.dk + Adgangskode + Dette er ikke en gyldig XMPP-adresse + Kørt tør for hukommelse. Billedet for stort + Vil du tilføje %s til din adressebog? + Server info + XEP-0313: MAM + XEP-0280: Message Carbons + XEP-0352: Client State Indication + XEP-0191: Blocking Command + XEP-0237: Roster Versioning + XEP-0198: Stream Management + XEP-0215: External Service Discovery + XEP-0163: PEP (Avatars / OMEMO) + XEP-0363: HTTP File Upload + XEP-0357: Push + understøttet + utilgængelig + Ingen meddelelser om offentlige nøgler + sidst set lige nu + sidst set for et minut siden + sidst set for %d minutter siden + sidst set for en time siden + sidst set %d time siden + sidst set for en dag siden + sidst set %d dage siden + Krypteret besked. Installer venligst OpenKeychain for dekryptere den. + Ny OpenPGP krypteret beskeder fundet + OpenPGP nøgle ID + OMEMO-fingeraftryk + v\\OMEMO-fingeraftryk + OMEMO-fingeraftryk (beskedoprindelse) + v\\OMEMO-fingeraftryk (beskedoprindelse) + Andre enheder + Stol på OMEMO-fingeraftryk + Henter nøgler… + Færdig + Dekrypter + Bogmærker + Find + Indtast kontakt + Slet kontakt + Vis kontaktdetaljer + Bloker kontakt + Frigiv kontakt + Opret + Vælg + Denne kontakt findes allerede + Deltag + kanal@konference.domæne.dk/kaldenavn + kanal@konference.domæne.dk + Gem som bogmærke + Slet bogmærke + Slet gruppechat + Slet kanal + Er du sikker på du vil slette denne gruppechat?\n\n Advarsel: Gruppechatten fjernes fuldstændigt på serveren. + Er du sikker på, at du vil slette denne offentlige kanal?\n\nAdvarsel: Kanalen fjernes fuldstændigt på serveren. + Kunne ikke slette gruppechat + Kunne ikke slette kanal + Rediger titel på gruppechat + Emne + Deltager i gruppechat… + Forlad + Kontakt tilføjede dig til kontaktliste + Tilføj tilbage + %s har læst hertil + %s har læst hertil + %1$s +%2$d andre har læst hertil + Alle har læst hertil + Offentliggør + Tryk på avatar for at vælge billede fra galleri + Offentliggørelse… + Serveren afviste din offentliggørelse + Kunne ikke konverter dit billede + Kunne ikke gemme avatar til disk + (Eller lang tryk for at gendan standard) + Din server understøtter ikke offentliggørelse af avatarer + hviskede + til %s + Send privat besked til %s + Forbind + Denne konto findes allerede + Næste + Session etableret + Skip + Deaktiver notifikationer + Aktiver + Gruppechat kræver adgangskode + Indtast adgangskode + Bed først om nærværsopdateringer fra din kontakt.\n\nDette bruges til at bestemme, hvilken chat-app din kontakt bruger. + Anmod nu + Ignore + Advarsel: Afsendelse af dette uden gensidig nærværsopdatering kan forårsage uventede problemer.\n\nGå til \"Kontaktdetaljer\" for at bekræfte dine nærværsabonnementer. + Sikkerhed + Tillad rettelse af beskeder + Tillad dine kontakter at redigere deres beskeder med tilbagevirkende kraft + Ekspertindstillinger + Være forsigtig med at ændre i disse + Om %s + Stilletid + Starttidspunkt + Sluttidspunkt + Aktiver stilletid + Notifikationer vil være lydløs under stilletid + Andre + Synkroniser bogmærker + Indstil \"autojoin\"-flag, når du går ind i eller forlader en MUC, og reager på ændringer foretaget af andre klienter. + OMEMO-fingeraftryk kopieret til udklipsholder + Du er udelukket fra denne gruppechat + Denne gruppechat er kun for medlemmer + Ressourcebegrænsning + Du er blevet smidt ud af denne gruppechat + Gruppechatten er lukket ned + Du er ikke længere i denne gruppechat + Du forlod denne gruppechat af tekniske årsager + anvender konto %s + hostet på %s + Tjekker %s på HTTP vært + Du er ikke forbundet. Prøv igen senere + Tjek %s størrelse + Tjek %1$s størrelse på %2$s + Beskedvalg + Citat + Indsæt som citat + Kopier original URL + Send igen + Fil URL + Kopieret URL til udklipsholder + Kopieret XMPP-adresse til udklipsholder + Kopieret fejlmeddelelse til udklipsholder + webadresse + Skan 2D stregkode + Vis 2D stregkode + Vis blokeringsliste + Kontodetaljer + Bekræft + Prøv igen + Forgrundstjeneste + Forhindre operativsystemet i at afbryde din forbindelse + Opret backup + Backup filer vil blive gemt i %s + Opretter backup filer + Din backup er oprettet + Backup filerne er blevet gem i %s + Gendan backup + Din backup er blevet gendannet + Glem ikke at aktivere kontoen. + Vælg fil + Modtager %1$s (%2$d%% fuldført) + Download %s + Slet %s + fil + Åben %s + sender (%1$d%% fuldført) + Gør klar til at dele fil + %s kan downloades + Annuller overførsel + kunne ikke dele fil + fil overførsel annulleret + Fil slettet + Ingen app fundet der kan åbne filen + Ingen app fundet der kan åbne link + Ingen app fundet der kan vise kontakt + Dynamiske Mærker + Vis skrivebeskyttet mærker under kontakter + Aktiver notifikationer + Ingen gruppechat server fundet + Kunne ikke oprette gruppechat + Konto avatar + Kopier OMEMO-fingeraftryk til udklipsholder + Gendan OMEMO-nøgle + Ryd enheder + Er du sikker på, at du vil rydde alle andre enheder fra OMEMO-meddelelsen? Næste gang dine enheder opretter forbindelse, annoncerer de sig selv, men de modtager muligvis ikke beskeder sendt i mellemtiden. + Der er ingen brugbare nøgler til rådighed for denne kontakt.\nKunne ikke hente nye nøgler fra serveren. Måske er der noget galt med din kontakts server? + Der er ingen tilgængelige nøgler til denne kontakt.\nSørg for, at I begge har nærværsabonnement. + Noget gik galt + Henter historik fra server + Der er ikke mere historik på server + Opdater… + Adgangskode ændret! + Kunne ikke ændre adgangskode + Ændr adgangskode + Nuværende adgangskode + Ny adgangskode + Adgangskode kan ikke være tomt + Aktiver alle konti + Deaktiver alle konti + Udfør handling med + Ingen tilknytning + Offline + Udstødt + Medlem + Avanceret tilstand + Giv medlemsrettigheder + Tilbagekald medlemsrettigheder + Giv administratorrettigheder + Tilbagekald administratorrettigheder + Giv ejerrettigheder + Tilbagekald ejerrettigheder + Fjern fra gruppechat + Fjern fra kanal + Kunne ikke ændre tilknytning til %s + Forbyd fra gruppechat + Forbyd fra kanal + Du prøver at fjerne %s fra en offentlig kanal. Den eneste måde at gøre dette på er at forbyde brugeren for altid. + Forbyd nu + Kunne ikke ændre rollen for %s + Privat gruppechatkonfiguration + Konfiguration af offentlig kanal + Privat, kun medlemmer + Gør XMPP-adresser synlig for alle + Moderere kanal + Du deltager ikke + Ændrede gruppechat valg! + Kunne ikke ændre valg for gruppechat + Aldrig + Indtil videre + Udsæt + Svar + Marker som læst + Input + Enter er send + Brug Enter taste til at send besked. Du kan altid bruge Ctrl+Enter til at sende besked, selv om dette valg slået fra. + Vis enter taste + Skift humørikonetasten til en enter-tast + lyd + video + billede + vektorgrafik + multimediefil + PDF dokument + Android App + Kontakt + Avatar er blevet offentliggjort + Sender %s + Tilbyder %s + Skjul offline + %s skriver… + %s skriver ikke mere + %s skriver… + %s har stoppet skrivning + Indtastningsnotifikation + Lad dine kontakter vide når du skriver beskeder til dem + Send placering + Vis placering + Ingen app fundet der kan vise placering + Placering + Samtale afsluttet + Forlod privat gruppechat + Forlod offentlig kanal + Stol ikke på system-CA\'er + Alle certifikater skal godkendes manuelt + Fjern certifikater + Slet manuelt godkendt certifikater + Ingen manuelt godkendt certifikater + Fjern certifikater + Slet valgt + Annuller + + %d certifikat slettet + %d certifikater slettet + + Erstat \"Send\" knap med hurtig handling + Hurtig handling + Ingen + Senest brugt + Vælg hurtig handling + Find kontakter + Find bogmærker + Send privat besked + %1$s har forladt gruppechatten + Brugernavn + Brugernavn + Dette er ikke et gyldigt brugernavn + Download mislykkes: Server ikke fundet + Download mislykkes: Fil ikke fundet + Download mislykkes: Kunne ikke forbinde til vært + Download mislykkes: Kunne ikke skrive til fil + Download mislykkes: Ugyldig fil + TOR netværk er utilgængelig + Bind fejl + Serveren er ikke ansvarlig for dette domæne + Brudt + Tilgængelighed + Ude når enhed er låst + Vis som Ude når enheden er låst + Optaget i lydløs tilstand + Vis som Optaget når enhed er i lydløs tilstand + Behandl vibration som lydløs tilstand + Vis som Optaget når enhed er på vibration + Udvidede forbindelsesindstillinger + Vis værtsnavn og port indstillinger under opsætning af en konto + xmpp.domæne.dk + Log ind med certifikat + Kunne ikke analysere certifikatet + Arkiveringsindstillinger + Arkiveringsindstillinger på serversiden + Henter arkiveringsindstillinger. Vent venligst… + Kunne ikke hente arkiveringsindstillinger + CAPTCHA påkrævet + Indtast teksten fra billedet herover + Utroværdig certifikatkæde + XMPP-adresse matcher ikke certifikatet + Forny certifikat + Fejl ved hentning af OMEMO-nøgle! + Bekræftet OMEMO-nøgler med certifikat! + Din enhed understøtter ikke valget af klientcertifikater! + Forbindelse + Forbind via TOR + Send alle forbindelser gennem Tor-netværket. Kræver Orbot + Værtsnavn + Port + Server- eller onion-adresse + Dette er ikke en gyldigt port-nummer + Dette er ikke et gyldigt værtsnavn + %1$d af %2$d konti forbundet + + %d besked + %d beskeder + + Indlæs flere beskeder + Fil delt med %s + Billede delt med %s + Billeder delt med %s + Tekst delt med %s + Giv %1$s adgang til ekstern lagerplads + Giv %1$s adgang til kameraet + Synkroniser med kontakter + %1$s ønsker tilladelse til at få adgang til din adressebog for at matche den med din XMPP kontaktliste.\nDette vil vise dine kontakters fulde navne og avatarer.\n\n%1$s læser kun din adressebog og matcher den lokalt uden at uploade noget til din server. +
Vi gemmer ikke en kopi af disse telefonnumre.\n\nFor mere information, læs vores privatlivspolitik.

Du vil bedes nu om at give tilladelse til at få adgang til dine kontakter.]]>
+ Underret ved alle beskeder + Underret kun når nævnt + Notifikationer deaktiveret + Notifikationer pauseret + Billedekompression + Tip: Brug ‘Vælg fil’ i stedet for ‘Vælg billede’ for at sende individuelle billeder ukomprimeret uanset denne indstilling. + Altid + Kun store billeder + Batterioptimering aktiveret + Din enhed anvender kraftig batterioptimeringer for %1$s som kan føre til forsinkede notifikationer eller tab af beskeder.\nDet er anbefalet at slå dem fra. + Din enhed anvender kraftig batterioptimeringer for %1$s som kan føre til forsinkede notifikationer eller tab af beskeder.\n\nDu bliver nu bedt om at deaktivere dem. + Deaktiver + Det valgte område er for stort + (Ingen aktiverede konti) + Dette felt er påkrævet + Ret besked + Send rettet besked + Denne persons fingeraftryk er allerede bekræftet som sikkert. Ved at vælge “Udført” acceptere du bare, at %s er en del af denne gruppechat. + Du har deaktiveret denne konto + Sikkerhedsfejl: Ugyldig filadgang! + Ingen app fundet der kan dele URL + Del URL med… +
Du tilmelder dig med dit telefonnummer, og Quicksy vil automatisk - baseret på telefonnumre i din adressebog - foreslå mulige kontakter til dig.

Når du tilmelder dig, accepterer du vores privatlispolitik.]]>
+ Accepter og fortsætte + En guide er oprettet til kontooprettelse på conversations.im.¹\nNår du vælger conversations.im som udbyder, kan du kommunikere med brugere fra andre udbydere ved at give dem din fulde XMPP-adresse. + Din fulde XMPP-adresse vil blive: %s + Opret konto + Brug min egen udbyder + Vælg dit brugernavn + Håndter tilgængelighed manuelt + Indstil din tilgængelighed, når du redigerer din statusbesked. + Statusbesked + Gratis for Chat + Online + Ude + Ikke tilgængelig + Optaget + Der er genereret en sikker adgangskode + Enheden understøtter ikke fravalg af batterioptimering + Registrering mislykkes: Prøv igen senere + Registring mislykkes: Adgangskode for svag + Vælg deltager + Opretter gruppechat… + Inviter igen + Deaktiver + Kort + Mellem + Lang + Udsend brug af app + Lad dine kontakter vide når du bruger Conversations + Privatliv + Tema + Vælg farvepalette + Automatisk + Lys + Mørk + Grøn baggrund + Brug grøn baggrund til modtaget beskeder + Kunne ikke forbinde til OpenKeychain + Denne enhed er ikke længere i brug + Computer + Mobiltelefon + Tablet + Webbrowser + Konsol + Betaling påkrævet + Giv tilladelse til at bruge Internettet + Mig + Kontakt beder om nærværsabonnement + Tillad + Ingen adgangstilladelse til %s + Fjernserver ikke fundet + Fjernserver timeout + Kunne ikke opdatere konto + Reporter denne XMPP-adresse for spamming. + Slet OMEMO-identiteter + Gendan dine OMEMO-nøgler. Alle dine kontakter skal bekræfte dig igen. Brug kun dette som en sidste udvej. + Slet valgte nøgler + Du skal være forbundet for at offentliggøre dit avatar. + Vis fejlbesked + Fejlbesked + Datasparer aktiveret + Dit operativsystem begrænser %1$s adgangen til Internettet i baggrunden. For at modtage notifikationer om nye beskeder, skal du tillade %1$s ubegrænset adgang, når \"Datasparer\" er aktiveret. \n%1$s vil stadig gøre en indsats for at gemme data, når det er muligt. + Din enhed understøtter ikke deaktivering af Databesparelse for %1$s. + Kunne ikke oprette midlertidig fil + Den enhed er blevet bekræftet + Kopier fingeraftryk + Du har bekræftet alle OMEMO-nøglerne i din besiddelse + Stregkoden indeholder ingen fingeraftryk for denne samtale. + Bekræft fingeraftryk + Brug kameraet til at skanne en kontakt’s stregkode + Vent til nøglerne hentes + Del som stregkode + Del som XMPP URL + Del som HTTP link + Stol blindt før bekræftelse + Stol på nye enheder fra ubekræftede kontakter, men prompt manuelt bekræftelse af nye enheder for bekræftede kontakter. + Autobekræftet OMEMO nøgler, hvilket betyder de kan være en anden eller en kan have indtastet info. + Utroværdig + Ugyldig 2D stregkode + Tøm cache mappe (brugt af kamera app) + Tøm cache + Tøm privat lagerplads + Tøm privat lagerplads, hvor filer opbevares (De kan downloades igen fra serveren) + Jeg fulgte dette link fra en pålidelig kilde + Du er ved bekræfte OMEMO-nøgler af %1$s efter du har klikket på et link. Dette er kun sikkert, hvis du fulgte linket fra troværdig kilde hvor kun %2$s kunne have offentliggjort dette link. + Du er ved at bekræfte dine OMEMO-nøgler til din konto. Det er kun sikkert, hvis du fulgte linket fra en troværdig kilde, hvor kun du kan have offentliggjort dette link. + Fortsæt + Beskræft OMEMO-nøgler + Vis inaktive + Skjul inaktive + Stol ikke på enhed + Er du sikker på, at du vil fjerne bekræftelsen på denne enhed?\nDenne enhed og meddelelser fra den vil blive markeret som \"upålidelig\". + + %d sekund + %d sekunder + + + %d minut + %d minutter + + + %d time + %d timer + + + %d dag + %d dage + + + %d uge + %d uger + + + %d måned + %d måneder + + Automatisk sletning af besked + Slet automatisk meddelelser fra denne enhed, der er ældre end den konfigurerede tidsramme. + Krypter beskeden + Henter ikke meddelelser på grund af lokal opbevaringsperiode. + Komprimerer video + Tilsvarende samtaler lukket. + Kontakt blokeret. + Notifikationer fra fremmede + Underret ved beskeder og opkald modtaget fra fremmede. + Modtaget besked fra fremmed + Bloker fremmed + Bloker hele domænet + online lige nu + Prøv dekryptering igen + Sessionsfejl + Nedgraderet SASL-mekanisme + Server kræver registrering på hjemmeside + Åben hjemmeside + Ingen app fundet der kan åbne hjemmeside + Forhåndsvisning af notifikationer + Vis forhåndsvisning af notifikationer + I dag + I går + Bekræft værtsnavn med DNSSEC + Servercertifikater, der indeholder det validerede værtsnavn, betragtes som bekræftede + Certifikat indeholder ikke en XMPP-adresse + delvis + Optag video + Kopier til udklipsholder + Besked kopieret til udklipsholder + Besked + Private beskeder er deaktiveret + Beskyttet apps + For at modtage underretninger, selv når skærmen er slukket, skal du tilføje Conversations til listen over beskyttede apps. + Accepter ukendt certifikat? + Serverens certifikat er ikke underskrevet af en kendt Certifikat Autoritet. + Accepter fejlbehæftet servernavn? + Server kunne ikke godkendes som \"%s\". Certifikatet er kun gyldig for: + Vil du fortsætte alligevel? + Certifikatdetaljer: + En gang + QR kode skanner kræver adgang til kameraet + Rul til bunden + Rul ned efter afsendelse af besked + Rediger statusbesked + Rediger statusbesked + Deaktiver kryptering + %1$s kan ikke sende krypterede meddelelser til %2$s. Dette kan skyldes din kontakt bruger en forældet server eller klient, der ikke kan håndtere OMEMO. + Kunne ikke hente enhedsliste + Kunne ikke hente krypteringsnøgler + Tip: I nogle tilfælde kan dette løses ved at tilføje hinanden dine kontaktlister. + Er du sikker på, at du vil deaktivere OMEMO-kryptering til denne samtale?\nDette giver din serveradministrator mulighed for at læse dine meddelelser, men det er muligvis den eneste måde at kommunikere med folk, der bruger forældede klienter. + Deaktiver nu + Udkast: + OMEMO-kryptering + OMEMO vil altid blive brugt for en-til-en og private gruppechats + OMEMO vil blive brugt som standard for nye samtaler. + OMEMO skal være tændt udtrykkeligt for nye samtaler. + Opret genvej + Skriftstørrelse + Den relative skriftstørrelse brugt inde i appen. + Til som standard + Fra som standard + Lille + Mellem + Stor + Besked var ikke krypteret på denne enhed. + Dekryptering af OMEMO-besked mislykkes. + fortryd + Deling af placering er deaktiveret + Fastgør position + Frigør position + Kopier placering + Del placering + Retninger + Del placering + Vis placering + Del + Kunne ikke starte optagelse + Vent venligst… + Giv %1$s adgang til mikrofonen + Find beskeder + GIF + Vis samtale + Del placeringsplugin + Brug Plugin for delingsplacering i stedet for det indbyggede kort + Kopier webadresse + Kopier XMPP-adresse + HTTP fildeling for S3 + Direkte søgning + På skærmbillede \'Start samtale\' skal du åbne tastaturet og placere markøren i søgefeltet + Gruppechat avatar + Vært understøtter ikke gruppechat avatarer + Kun ejeren kan ændre gruppechat avatar + Kontaktnavn + Kaldenavn + Navn + Angivelse af navn er valgfrit + Gruppechat navn + Denne gruppechat er blevet slettet + Kinne ikke gemme optagelse + Forgrundstjeneste + Denne notifikationskategori bruges til at vise en permanent notifikation, der angiver, at %1$s kører. + Status Information + Forbindelsesproblemer + Denne notifikationskategori bruges til at vise en notifikation, hvis der er et problem med at oprette forbindelse til en konto. + Beskeder + Opkald + Beskeder + Indkommende opkald + Udgående opkald + Mistet opkald + Lydløse beskeder + Denne notifikationsgruppe bruges til at vise notifikationer, der ikke bør udløse nogen lyd. For eksempel når du er aktiv på en anden enhed (Fredningsperiode). + Mislykkede leverancer + Notifikationsindstilling for besked + Notifikationsindstilling for indgående opkald + Vigtighed, lyd, vibrere + Video kompression + Vis medie + Deltagere + Mediebrowser + Fil udeladt på grund af sikkerhedsovertrædelse. + Videokvalitet + Lavere kvalitet betyder mindre filer + Mellem (360p) + Høj (720p) + annulleret + Du er allerede ved at udarbejde en besked. + Funktionen ikke implementeret + Ugyldig landekode + Vælg et land + telefonnummer + Bekræft dit telefonnummer + Quicksy sender en SMS-besked (operatørgebyrer kan forekomme) for at bekræfte dit telefonnummer. Indtast din landekode og dit telefonnummer: +
%s

er det OK, eller vil du ændre telefonnummeret?]]>
+ %s er ikke et gyldigt telefonnummer. + Indtast venligst din telefonnummer. + Find lande + Bekræft %s + %s.]]> + Vi har sendt dig en ny SMS med en 6 cifret pinkode. + Indtast den 6 cifret pinkode herunder. + Send SMS igen + Send SMS igen (%s) + Vent venligst (%s) + Tilbage + Indsat automatisk mulig pinkode fra udklipsholder. + Indtast venligst din 6 cifret pinkode. + Er du sikker på at du afbryde registreringsproceduren? + Ja + Nej + Bekræfter… + Anmoder SMS… + Den indtastet pinkode er forkert. + Den sendte pinkode er udløbet. + Ukendt netværksfejl. + Ukendt respons fra server. + Kunne ikke få forbindelse til server. + Kunne ikke etablere en sikker forbindelse. + Kunne ikke finde server. + Noget gik galt ved behandlingen af din anmodning. + Ugyldig brugerindtastning + Midlertidig utilgængelig. Prøv igen senere. + Ingen netværksforbindelse + Prøv venligst igen om %s + Du er begrænset + For mange forsøg + Du bruger en ældre version af denne app. + Opdater + Dette telefonnummer er i øjeblikket logget ind med en anden enhed. + Indtast dit navn for at lade folk, der ikke har dig i deres adressebog, vide, hvem du er. + Dit navn + Indtast dit navn + Brug redigeringsknappen for at instille dit navn + Afvis anmodning + Installer Orbot + Start Orbot + Ingen markedsapp installeret. + Denne kanal vil offentliggør din XMPP-adresse + e-bog + Original (ukomprimeret) + Åbn med… + Conversations profilbillede + Vælg konto + Gendan backup + Gendan + Indtast din adgangskode til kontoen %s for at gendanne backuppen. + Brug ikke gendannelsessikkerhedsfunktionen i et forsøg på at klone (køre samtidigt) en installation. Gendannelse af en backup er kun beregnet til migreringer, eller hvis du har mistet den originale enhed. + Kunne ikke gendan backup + Kunne ikke dekryptere backup. Er adgangskoden korrekt? + Backup & Gendan + Indtast XMPP-adresse + Opret gruppechat + Deltag i offentlig kanal + Opret privat gruppechat + Opret offentlig kanal + Kanalnavn + XMPP-adresse + Angiv venligst et navn til kanalen + Angiv venligst en XMPP-adresse + Dette er en XMPP-adresse. Angiv venligst et navn + Opret offentlig kanal… + Denne kanal eksister allerede + Du sluttede dig til en eksisterende kanal + Kunne ikke gemme kanalkonfiguration + Tillad enhver at redigere emnet + Tillad alle at invitere andre + Alle kan redigere titlen + Ejere kan redigere emnet. + Administrator kan redigere emnet. + Ejere kan invitere andre. + Alle kan invitere andre. + XMPP-adresser er synlig for administratorerne. + XMPP-adresser er synlige for alle. + Denne offentlige kanal har ingen deltagere. Inviter dine kontakter eller brug deleknappen til distribuere dens XMPP-adresse + Denne private gruppechat har ingen medlemmer. + Administrer rettigheder + Find deltagere + Fil for stor + Vedhæft + Find kanaler + Find kanaler + Risiko for krænkelse af privatlivet! + søg.jabber.netværk.

Brug af denne funktion sender din IP-adresse og søgetermer til denne service. Se deres Privatlivspolitik for mere information.]]>
+ Jeg har allerede en konto + Tilføj eksisterende konto + Registrer ny konto + Dette ligner en domæne adresse + Tilføj alligvel + Dette ligner en kanal adresse + Del backup filer + Conversations backup + Begivenhed + Åben backup + Den fil du valgte er ikke en Conversations backup fil + Denne konto er allerede oprettet + Indtast adgangskoden til denne konto + Kunne ikke udføre denne handling + Deltag i offentlig kanal… + Dele-appen gav ikke tilladelse til at få adgang til denne fil. + + jabber.netværk + Lokal server + De fleste brugere bør vælge \'jabber.netværk\' for bedst mulige forslag fra hele det offentlige XMPP-økosystem. + Metode for kanalsøgning + Backup + Om + Aktiver venligst en konto + Lav opkald + Indkommende opkald + Indkommende videoopkald + Skift til videoopkald + Tilføje yderligere spor? + Forbinder + Forbundet + Forbinder igen + Accepter opkald + Afslut opkald + Svar + Afvis + Find enheder + Ringer + Optaget + Kunne ikke forbinde opkald + Forbindelsen tabt + Tilbagetrukket opkald + App fejl + Bekræftelsesproblem + Læg på + Udgående opkald + Igangværende videoopkald + Forbinder igen opkald + Forbinder igen videoopkald + Deaktiver TOR for at lave opkald + Indkommende opkald + Indkommende opkald · %s + Mistet opkald · %s + Udgående opkald + Udgående opkald · %s + Mistet opkald + + %1$d mistet opkald fra %2$s + %1$d mistet opkald fra %2$s + + + %d mistet opkald + %d mistet opkald + + + %1$d mistet opkald fra %2$d kontakt + %1$d mistet opkald fra %2$d kontakter + + Lydopkald + Videoopkald + Hjælp + Skift til samtale + Din mikrofon er utilgængelig + Du kan kun have et opkald af gangen. + Returner til igangværende opkald + Kunne ikke skifte kamera + Fastgør til top + Frigør fra top + GPX spor + Kunne ikke rette besked + Alle samtaler + Denne samtale + Dit avatar + Avatar for %s + Krypteret med OMEMO + Krypteret med OpenPGP + Ikke krypteret + Afslut + Optag telefonsvarer + Afspil lyd + Pauser lyd + Tilføj kontakt, opret eller deltag i gruppechat, eller find kanaler + + Vis %1$d deltager + Vis %1$d deltagere + + + En besked kunne ikke leveres + Nogle beskeder kunne ikke leveres + + Mislykkede leverancer + Flere valg + Intet program fundet + Inviter til Conversations + Kunne ikke analysere invitation + 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 + Kontoregistrering er ikke understøttet + Ingen XMPP-adresse fundet + Midlertidig godkendelsesfejl + Slet avatar + Opkald er deaktiveret ved brug af Tor + Skift til video + Afvis skift til video anmodning + +
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml new file mode 100644 index 000000000..e51229cc2 --- /dev/null +++ b/app/src/main/res/values-de/strings.xml @@ -0,0 +1,1010 @@ + + + Einstellungen + Neue Unterhaltung + Konten verwalten + Konto verwalten + Unterhaltung beenden + Kontaktdetails + Gruppenchatdetails + Channeldetails + Konto hinzufügen + Namen bearbeiten + Zum Telefonbuch hinzufügen + Aus Kontaktliste entfernen + Kontakt sperren + Kontakt entsperren + Domain sperren + Domain entsperren + Teilnehmer sperren + Teilnehmer entsperren + Konten verwalten + Einstellungen + Mit Unterhaltung teilen + Unterhaltung beginnen + Kontakt auswählen + Kontakte auswählen + Teilen über Konto + Sperrliste + gerade + vor einer Minute + vor %d Minuten + + %d ungelesene Unterhaltung + %d ungelesene Unterhaltungen + + senden… + Nachricht wird entschlüsselt. Bitte warten… + OpenPGP-verschlüsselte Nachricht + Nickname wird bereits verwendet + Ungültiger Nickname + Administrator + Eigentümer + Moderator + Teilnehmer + Besucher + Möchtest du %s von deiner Kontaktliste entfernen? Unterhaltungen mit diesem Kontakt werden dabei nicht entfernt. + Möchtest du %s sperren und keine Nachrichten mehr erhalten? + Möchtest du %s entsperren und wieder Nachrichten empfangen? + Alle Kontakte von %s sperren? + Alle Kontakte von %s entsperren? + Kontakt gesperrt + Gesperrt + Möchtest du %s als Lesezeichen entfernen? Unterhaltungen mit diesem Lesezeichen werden dabei nicht entfernt. + Neues Konto auf Server erstellen + Passwort ändern + Teilen mit… + Unterhaltung beginnen + Kontakt einladen + Einladen + Kontakte + Kontakt + Abbrechen + Einstellen + Hinzufügen + Bearbeiten + Entfernen + Sperren + Entsperren + Speichern + OK + %1$s ist abgestürzt + Die Verwendung deines XMPP-Kontos zum Einsenden von Absturzberichten hilft bei der Weiterentwicklung von %1$s. + Jetzt abschicken + Nie mehr nachfragen + Verbindung zum 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? + Kontakt hinzufügen + Zustellung fehlgeschlagen + Vorbereitung zum Senden des Bildes + Vorbereitung zum Senden der Bilder + Teile Dateien. Bitte warten… + Verlauf löschen + Unterhaltungsverlauf löschen + Möchtest du alle Nachrichten in dieser Unterhaltung löschen?\n\nAchtung: Dies beeinflusst nicht Nachrichten, die auf anderen Geräten oder Servern gespeichert sind. + Datei löschen + Bist du sicher, dass du diese Datei löschen möchtest\? +\n +\nAchtung: Dies löscht keine Kopien dieser Datei, die auf anderen Geräten oder Servern gespeichert sind. + Diese Unterhaltung danach beenden + Gerät auswählen + Unverschlüsselt schreiben… + Nachricht senden + Nachricht an %s senden + OMEMO-verschlüsselt schreiben… + v\\OMEMO-verschlüsselte Nachricht senden + OpenPGP-verschlüsselt schreiben… + Neuer Nickname wird jetzt verwendet + Unverschlüsselt senden + Entschlüsselung fehlgeschlagen. Vielleicht hast du nicht den richtigen privaten Schlüssel. + OpenKeychain + OpenKeychain, um Nachrichten zu ver- und entschlüsseln und um deine Schlüssel zu verwalten.

Es ist GPLv3+ lizenziert und über F-Droid und Google Play verfügbar.

(Bitte starte %1$s danach neu.)]]>
+ Neu starten + Installieren + Bitte OpenKeychain installieren + angeboten… + warten… + Kein OpenPGP-Schlüssel gefunden + 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. + Allgemein + Dateien annehmen + Dateien automatisch annehmen, die kleiner sind als … + Anhänge + Benachrichtigung + Vibrieren + Vibrieren bei Erhalt einer neuen Nachricht + LED Benachrichtigung + Blinke bei Erhalt einer neuen Nachricht + Klingelton + Benachrichtigungston + Benachrichtigungston für neue Nachrichten + Klingelton für eingehende Anrufe + Schonfrist + Die Zeitspanne, in der Benachrichtigungen nach der Erkennung von Aktivitäten auf einem deiner anderen Geräte unterdrückt werden. + Erweitert + Niemals Absturzberichte senden + 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. + Annehmen + Ein Fehler ist aufgetreten + Fehler + Dein Konto + Online-Status senden + Online-Status empfangen + Online-Status anfragen + Bild auswählen + Bild aufnehmen + Statusanfragen vorab erlauben + Die ausgewählte Datei ist kein Bild + 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 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 + Online + Verbinde\u2026 + Offline + Ungültige Zugangsdaten + Server nicht gefunden + Keine Internetverbindung + Registrierung fehlgeschlagen + Benutzername wird bereits verwendet + Registrierung abgeschlossen + Registrierung wird vom Server nicht unterstützt + Ungültiger Registrierungstoken + TLS-Aushandlung fehlgeschlagen + Domain nicht überprüfbar + Verstoß gegen die Richtlinien + Inkompatibler Server + Inkompatibler Client + Stream-Fehler + Fehler beim Öffnen des Streams + Unverschlüsselt + OTR + OpenPGP + OMEMO + Konto löschen + Vorübergehend abschalten + Profilbild veröffentlichen + Ö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. + Konto aktivieren + Bist du dir sicher? + Die Löschung deines Kontos löscht deinen gesamten Gesprächsverlauf + Sprache aufzeichnen + XMPP-Adresse + XMPP-Adresse sperren + benutzer@domain.de + Passwort + Ungültige XMPP-Adresse + Zu wenig Speicher vorhanden. Bild ist zu groß + Möchtest du %s in dein Telefonbuch hinzufügen\? + Server-Info + XEP-0313: MAM + XEP-0280: Message Carbons + XEP-0352: CSI + XEP-0191: Blocking Command + XEP-0237: Roster Versioning + XEP-0198: Stream Management + XEP-0215: External Service Discovery + XEP-0163: PEP (Avatare/OMEMO) + XEP-0363: HTTP File Upload + XEP-0357: Push + ja + nein + Öffentlicher Schlüssel fehlt + gerade eben noch gesehen + vor einer Minute gesehen + vor %d Minuten gesehen + vor einer Stunde gesehen + vor %d Stunden gesehen + vor einem Tag gesehen + vor %d Tagen gesehen + Verschlüsselte Nachricht. Bitte installiere OpenKeychain, um sie zu entschlüsseln. + Neue verschlüsselte OpenPGP-Nachricht gefunden + OpenPGP Schlüssel-ID + OMEMO-Fingerabdruck + v\\OMEMO-Fingerabdruck + OMEMO-Fingerabdruck des verwendeten Gerätes + v\\OMEMO-Fingerabdruck des verwendeten Gerätes + Andere Geräte + OMEMO-Fingerabdruck vertrauen + Schlüssel werden abgerufen… + Fertig + Entschlüsseln + Lesezeichen + Suchen + Kontakt eingeben + Kontakt löschen + Kontaktdetails anzeigen + Kontakt sperren + Kontakt entsperren + Erstellen + Auswählen + Der Kontakt existiert bereits + Beitreten + channel@conference.domain.de/Nickname + channel@conference.domain.de + Als Lesezeichen speichern + Lesezeichen löschen + Gruppenchat löschen + Channel löschen + Bist du sicher, dass du diesen Gruppenchat löschen willst?\n\nAchtung: Der Gruppenchat wird dabei vollständig auf dem Server gelöscht. + Bist du sicher, dass du diesen öffentlichen Channel löschen willst?\n\nAchtung: Der Channel wird dabei vollständig auf dem Server gelöscht. + Gruppenchat konnte nicht gelöscht werden + Channel konnte nicht gelöscht werden + Gruppenchatthema bearbeiten + Thema + Gruppenchat beigetreten… + Verlassen + 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 + Profilbild antippen, um ein Bild aus der Galerie auszuwählen + Veröffentliche… + Server hat die Veröffentlichung des Profilbildes abgelehnt + Bild konnte nicht konvertiert werden + Profilbild kann nicht gespeichert werden + (Oder klicke lange, um den Standard wiederherzustellen) + Dein Server unterstützt die Veröffentlichung von Profilbildern nicht + private Nachricht: + an %s + Private Nachricht an %s senden + Verbinden + Dieses Konto existiert bereits + Weiter + Sitzung wiederhergestellt + Überspringen + Benachrichtigungen deaktivieren + Aktivieren + Gruppenchat ist passwortgeschützt + Passwort eingeben + Bitte zuerst den Online-Status von deinem Kontakt anfragen.\n\nDies wird verwendet, um festzustellen, welche Chat-App dein Kontakt nutzt. + Jetzt anfordern + Ignorieren + Achtung: Ohne gegenseitige Kenntnis des Online-Status kann es zu unerwarteten Problemen kommen. +\n +\nGehe zu \"Kontaktdetails\", um deine Einstellungen zu überprüfen. + Sicherheit + Nachrichtenkorrektur erlauben + Erlaube deinen Kontakten das nachträgliche Korrigieren ihrer Nachrichten + Experteneinstellungen + Hier bitte vorsichtig sein + Über %s + Ruhige Stunden + Beginn + Ende + Ruhige Stunden aktivieren + Benachrichtigungen sind während der ruhigen Stunden stumm + Sonstiges + Lesezeichen synchronisieren + Setzt das \"Autojoin\"-Kennzeichen beim Betreten oder Verlassen eines Gruppenchats/Channels und reagiert auf Änderungen durch andere Clients. + OMEMO-Fingerabdruck in die Zwischenablage kopiert + Du wurdest aus diesem Gruppenchat ausgeschlossen + Dieser Gruppenchat ist nur für Mitglieder + Ressourcenbeschränkung + Du wurdest aus diesem Gruppchat geworfen + Gruppenchat wurde geschlossen + Du bist nicht länger in diesem Gruppenchat + Du hast diesen Gruppenchat aus technischen Gründen verlassen + verwende Konto %s + gehostet bei %s + %s auf HTTP-Host wird überprüft + 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 + Zitat + Als Zitat einfügen + Original-URL kopieren + Erneut senden + Datei-URL + URL in die Zwischenablage kopiert + XMPP-Adresse in Zwischenablage kopiert + Fehlermeldung in Zwischenablage kopiert + Internetadresse + Barcode scannen + Barcode anzeigen + Sperrliste anzeigen + Kontodetails + Bestätigen + Erneut versuchen + Vordergrunddienst + Verhindert, dass das Betriebssystem deine Verbindung unterbricht + Sicherung erstellen + Sicherungsdateien werden gespeichert in %s + Erstelle Sicherungsdateien + Deine Sicherung wurde erstellt + Die Sicherungsdateien wurden gespeichert in %s + Stelle Sicherung wieder her + Deine Sicherung wurde wiederhergestellt + Vergiss nicht, das Konto zu aktivieren. + Datei auswählen + Empfange %1$s (%2$d%% abgeschlossen) + %s herunterladen + %s löschen + Datei + %s öffnen + Senden (%1$d%% abgeschlossen) + Vorbereitung zum Teilen der Datei + %s zum Herunterladen angeboten + Übertragung abbrechen + Datei konnte nicht geteilt werden + Dateiübertragung abgebrochen + Datei gelöscht + Keine App zum Öffnen der Datei gefunden + Keine App zum Öffnen des Links gefunden + Keine App zum Anzeigen des Kontaktes gefunden + Dynamische Tags + Schreibgeschütze Tags unterhalb der Kontakte anzeigen + Benachrichtigungen aktivieren + Keinen Gruppenchatserver gefunden + Gruppenchat konnte nicht erstellt werden + Konto-Profilbild + OMEMO-Fingerabdruck in Zwischenablage kopieren + OMEMO-Schlüssel erneuern + Geräte entfernen + Bist du sicher, dass du alle anderen Geräte aus der OMEMO-Bekanntmachung entfernen willst? Die Bekanntmachung wird bei der nächsten Verbindung erneuert aber möglicherweise werden keine zwischenzeitlich gesendeten Nachrichten empfangen. + 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 ihr beide gegenseitig den Online-Status aktiviert habt. + Etwas ist schief gelaufen + Lade Chatverlauf vom Server + Keine weiteren Nachrichten vorhanden + Aktualisieren… + Passwort geändert! + Passwort konnte nicht geändert werden + Passwort ändern + Aktuelles Passwort + Neues Passwort + Passwort kann nicht leer sein + Alle Konten aktivieren + Alle Konten abschalten + Aktion durchführen mit + Keine Zugehörigkeit + Offline + Ausgeschlossen + Mitglied + Erweiterter Modus + Mitgliederrechte gewähren + Mitgliederrechte entziehen + Administratorrechte gewähren + Administratorrechte entziehen + Eigentümerrechte gewähren + Eigentümerrechte entziehen + Aus Gruppenchat entfernen + Aus Channel entfernen + Zugehörigkeit von %s konnte nicht verändert werden + Vom Gruppenchat ausschließen + Vom Channel ausschließen + Du versuchst %s aus einem öffentlichen Channel zu entfernen. Die einzige Möglichkeit, dies dauerhaft zu tun, ist den Kontakt auszuschließen. + Kontakt ausschließen + Rolle von %s konnte nicht geändert werden + Einstellungen für private Gruppenchats + Einstellungen für öffentlichen Channel + Privat, nur Mitglieder + XMPP-Adressen für alle sichtbar machen + Channel wird moderiert + Du bist kein Mitglied + Gruppenchatoptionen wurden geändert! + Gruppenchatoptionen konnten nicht geändert werden + Niemals + Bis auf Weiteres + Schlummern + Antworten + Als gelesen markieren + Eingabe + Eingabetaste sendet Nachricht + Nutze die Eingabetaste zum Versenden einer Nachricht. Strg+Eingabetaste sendet die Nachricht unabhängig von dieser Einstellung. + Zeige Eingabetaste + Emoji-Taste durch Eingabetaste ersetzen + Audio + Video + Bild + Vektorgrafik + Multimediadatei + PDF-Dokument + Android App + Kontakt + Profilbild wurde veröffentlicht! + %s wird gesendet + %s wird angeboten + Offline verstecken + %s schreibt… + %s schreibt nicht mehr + %s schreiben… + %s schreiben nicht mehr + Tipp-Benachrichtigung + Informiere deine Kontakte, wenn du eine Nachricht schreibst + Standort senden + Standort anzeigen + Keine App für die Standortanzeige gefunden + Standort + Unterhaltung beendet + Privaten Gruppenchat verlassen + Öffentlichen Channel verlassen + Zertifizierungsstellen nicht vertrauen + Alle Zertifikate müssen manuell bestätigt werden + Zertifikate löschen + Manuell bestätigte Zertifikate löschen + Keine manuell bestätigten Zertifikate + Zertifikate löschen + Auswahl löschen + Abbrechen + + %d Zertifikat gelöscht + %d Zertifikate gelöscht + + Ersetze \"Senden\"-Schaltfläche durch Schnell-Tasten + Schnell-Tasten + Keine + Zuletzt verwendet + Wähle Schnell-Taste + Kontakte durchsuchen + Lesezeichen durchsuchen + Private Nachricht senden + %1$s hat den Gruppenchat verlassen + Benutzername + Benutzername + Ungültiger Benutzername + Download fehlgeschlagen: Server nicht gefunden + Download fehlgeschlagen: Datei nicht gefunden + Download fehlgeschlagen: Keine Verbindung zum Host + Download fehlgeschlagen: Datei konnte nicht gespeichert werden + Download fehlgeschlagen: Ungültige Datei + Tor-Netzwerk nicht verfügbar + Verbindungsfehler + Der Server ist nicht für diese Domain verantwortlich + Fehlerhaft + Status + Abwesend bei gesperrtem Gerät + Als abwesend anzeigen, wenn das Gerät gesperrt ist + Beschäftigt im lautlosen Modus + Als Beschäftigt anzeigen, wenn sich das Gerät im lautlosen Modus befindet + Vibration als Lautlos behandeln + Als Beschäftigt anzeigen, wenn das Gerät auf Vibration eingestellt ist + Erweiterte Verbindungseinstellungen + Hostname- und Port-Optionen bei Kontoeinrichtung anzeigen + xmpp.domain.de + Mit Zertifikat anmelden + Zertifikat konnte nicht verarbeitet werden + Archivierungseinstellungen + Archivierungseinstellungen des Servers + Archivierungseinstellungen werden abgerufen. Bitte warten… + Archivierungseinstellungen konnten nicht abgerufen werden + CAPTCHA erforderlich + Gib den Text aus dem Bild oben ein + Nicht vertrauenswürdige Zertifikatskette + XMPP-Adresse stimmt nicht dem Zertifikat überein + Zertifikat erneuern + Kann OMEMO-Schlüssel nicht empfangen! + OMEMO-Schlüssel mit Zertifikat überprüft! + Dein Gerät unterstützt das Auswählen von Client-Zertifikaten nicht! + Verbindung + Über Tor verbinden + Alle Verbindungen über das Tor-Netzwerk tunneln. Benötigt Orbot + Hostname + Port + Server- oder .onion-Adresse + Dies ist keine gültige Port-Nummer + Dies ist kein gültiger Hostname + %1$d von %2$d Konten verbunden + + %d Nachricht + %d Nachrichten + + Weitere Nachrichten laden + Datei mit %s geteilt + Bild mit %s geteilt + Bilder mit %s geteilt + Text mit %s geteilt + %1$s den Zugriff auf den externen Speicher gewähren + %1$s den Zugriff auf die Kamera gewähren + Mit Kontakten synchronisieren + %1$s möchte die Erlaubnis, auf deine Kontakte zuzugreifen, um sie mit deiner XMPP-Kontaktliste abzugleichen.\nDadurch werden die vollständigen Namen und Profilbilder deiner Kontakte angezeigt.\n\n%1$s liest nur dein Adressbuch und gleicht es lokal ab, ohne dass etwas auf deinen Server hochgeladen wird. +
Wir werden keine Kopie dieser Telefonnummern speichern.\n\nFür weitere Informationen lies unsere Datenschutzerklärung.

Du wirst nun gefragt, ob du den Zugriff auf deine Kontakte erlauben möchtest.]]>
+ Bei allen Nachrichten benachrichtigen + Nur benachrichtigen, wenn ich erwähnt werde + Benachrichtigungen deaktiviert + Benachrichtigungen pausiert + Bilder komprimieren + Tipp: Nutze ‘Datei auswählen’ statt ‘Bild auswählen’ um ein einzelnes Bild, unabhängig von dieser Einstellung, unkomprimiert zu übertragen. + Immer + Nur große Bilder + Akkuoptimierung aktiv + Dein Gerät verwendet Akkuoptimierungen für %1$s, welche verspätete Benachrichtigungen oder Nachrichtenverlust verursachen können.\nEs wird empfohlen, sie zu deaktivieren. + Dein Gerät verwendet Akkuoptimierungen für %1$s, welche verspätete Benachrichtigungen oder Nachrichtenverlust verursachen können.\n\nDu wirst nun gefragt, sie zu deaktivieren. + Deaktivieren + Der ausgewählte Bereich ist zu groß + (Keine aktivierten Konten) + Dieses Feld ist erforderlich + Nachricht korrigieren + Korrigierte Nachricht senden + Du hast den Fingerabdruck dieser Person bereits sicher verifiziert, um das Vertrauen zu bestätigen. Durch Auswählen von \"Fertig\" bestätigst du, dass %s Teil dieses Gruppenchats ist. + Du hast dieses Konto deaktiviert + Sicherheitsfehler: Dateizugriff nicht erlaubt! + Keine App zum Teilen der URI gefunden + Teile URI mit… +
Du registrierst dich mit deiner Telefonnummer und Quicksy wird automatisch auf der Grundlage der Telefonnummern in deinem Adressbuch mögliche Kontakte vorschlagen.

Mit der Anmeldung erklärst du dich mit unserer Datenschutzerklärung einverstanden.]]>
+ Zustimmen und fortfahren + Ein Guide hilft bei der Kontoerstellung auf conversations.im. +\nWenn du conversations.im als Provider wählst, kannst du mit Nutzern anderer Anbieter kommunizieren, indem du ihnen deine vollständige XMPP-Adresse gibst. + Deine vollständige XMPP-Adresse lautet: %s + Konto erstellen + Nutze eigenen Provider + Wähle deinen Benutzernamen + Status manuell ändern + Lege deinen Status fest, wenn du deine Statusnachricht bearbeitest. + Statusnachricht + Frei zum Chatten + Online + Weg + Nicht verfügbar + Beschäftigt + Ein sicheres Passwort wurde erstellt + Dein Gerät unterstützt nicht das Ausschalten der Akkuoptimierung + Registrierung fehlgeschlagen: Bitte später versuchen + Registrierung fehlgeschlagen: Passwort zu schwach + Teilnehmer wählen + Erstelle Gruppenchat… + Erneut einladen + Deaktivieren + Kurz + Mittel + Lang + Benutzeraktivität veröffentlichen + Informiert deine Kontakte, wenn du Conversations nutzt + Privatsphäre + Design + Wähle die Farbpalette aus + Automatisch + Hell + Dunkel + Grüner Hintergrund + Für empfangene Nachrichten grünen Hintergrund verwenden + Verbindung zu OpenKeychain konnte nicht hergestellt werden + Dieses Gerät ist nicht länger in Benutzung + Computer + Mobiltelefon + Tablet + Webbrowser + Kommandozeile + Bezahlung erforderlich + Berechtigung zur Nutzung des Internets erteilen + Ich + Kontakt möchte Online-Status abbonieren + Erlauben + Keine Berechtigung um auf %s zuzugreifen + Remote-Server nicht gefunden + Zeitüberschreitung des Remote-Servers + Konto konnte nicht aktualisiert werden + Diese XMPP-Adresse als Spammer melden. + OMEMO-Identitäten zurücksetzen + Erzeuge neue OMEMO-Schlüssel. Alle deine Kontakte müssen sie erneut verifizieren. Verwende dies nur als letztes Mittel. + Ausgewählte Schlüssel löschen + Du musst verbunden sein, um deinen Profilbild zu veröffentlichen. + Zeige Fehlermeldung + Fehlermeldung + Datensparmodus aktiv + Dein Betriebssystem verhindert, dass %1$s im Hintergrund auf das Internet zugreift. Um Benachrichtigungen erhalten zu können, solltest du %1$s den Zugang erlauben, wenn der Datensparmodus aktiv ist.\n%1$s wird dennoch versuchen, so viele Daten wie möglich einzusparen. + Dein Gerät unterstützt den Datensparmodus für %1$s nicht. + Temporäre Datei konnte nicht erstellt werden + Dieses Gerät wurde überprüft + Fingerabdruck kopieren + 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 die Kamera, um Barcodes deiner Kontakte zu scannen + Bitte warten, bis die Schlüssel abgerufen werden + Als Barcode teilen + Als XMPP-URI teilen + Als HTTP Link teilen + Blind vertrauen vor der Überprüfung + Neuen Geräten von nicht verifizierten Kontakten vertrauen, aber bei verifizierten Kontakten eine manuelle Bestätigung der neuen Geräte verlangen. + Blind vertraute OMEMO-Schlüssel bedeutet, dass es sich um eine andere Person handeln könnte oder dass jemand sie abgehört haben könnte. + Nicht vertraut + Ungültiger Barcode + Cache-Ordner löschen (von der Kamera-App genutzt) + Lösche Cache + Lösche privaten Speicher + Lösche privaten Speicher, in dem die Dateien gespeichert werden (sie können erneut vom Server heruntergeladen werden) + Ich habe diesen Link aus einer vertrauenswürdigen Quelle erhalten + Du bist dabei, die OMEMO-Schlüssel von %1$s nach dem Klick auf diesen Link zu überprüfen. Dies ist nur sicher, wenn du diesen Link von einer vertrauenswürdigen Quelle erhalten hast, der nur von %2$s veröffentlicht werden konnte. + Du bist dabei, die OMEMO-Schlüssel deines eigenen Kontos zu verifizieren. Dies ist nur sicher, wenn du diesem Link aus einer vertrauenswürdigen Quelle gefolgt bist, bei der nur du diesen Link veröffentlicht haben kannst. + Weiter + Überprüfe OMEMO-Schlüssel + Inaktive anzeigen + Inaktive ausblenden + Gerät nicht mehr vertrauen + Bist du sicher, dass du die vorhandene Überprüfung dieses Gerätes löschen willst?\nDieses Gerät und gesendete Nachrichten von diesem Gerät werden als \"nicht vertrauenswürdig\" markiert. + + %d Sekunde + %d Sekunden + + + %d Minute + %d Minuten + + + %d Stunde + %d Stunden + + + %d Tag + %d Tage + + + %d Woche + %d Wochen + + + %d Monat + %d Monate + + Automatische Nachrichtenlöschung + Lösche Nachrichten automatisch, wenn diese älter als die eingestellte Zeit sind. + Nachricht wird verschlüsselt + Nachrichten werden aufgrund der eingestellten lokalen Speicherfrist nicht abgerufen. + Video wird komprimiert + Zugehörige Unterhaltung beendet. + Kontakt gesperrt. + Benachrichtigungen von Unbekannten + Benachrichtigen bei Erhalt von Nachrichten und Anrufen von Unbekannten. + Erhaltene Nachricht von einem Unbekannten + Unbekannten sperren + Gesamte Domain sperren + aktuell online + Entschlüsseln wiederholen + Sitzungsfehler + SASL-Mechanismus zurückgestuft + Der Server benötigt eine Registrierung auf einer Website + Website öffnen + Keine App zum Öffnen der Website gefunden + Benachrichtigungsvorschau + Benachrichtigungsvorschau anzeigen + Heute + Gestern + Überprüfe den Hostnamen mit DNSSEC + Serverzertifikate, die den überprüften Hostnamen enthalten, werden als verifiziert betrachtet + Zertifikat enthält keine XMPP-Adresse + teilweise + Video aufnehmen + In die Zwischenablage kopieren + Nachricht in die Zwischenablage kopiert + Nachricht + Private Nachrichten sind deaktiviert + Geschützte Apps + Um weiterhin Benachrichtigungen zu erhalten, auch wenn der Bildschirm ausgeschaltet ist, musst du Conversations zur Liste der geschützten Apps hinzufügen. + Unbekanntes Zertifikat akzeptieren? + Das Serverzertifikat wurde nicht von einer bekannten Zertifizierungsstelle signiert. + Nicht übereinstimmenden Servernamen akzeptieren? + Server konnte sich nicht als \"%s\" authentifizieren. Das Zertifikat ist nur gültig für: + Möchtest du trotzdem eine Verbindung herstellen? + Zertifikatdetails: + Einmalig + Der QR-Codescanner benötigt Zugriff auf die Kamera + Nach unten scrollen + Nach dem Senden einer Nachricht nach unten scrollen + Statusnachricht bearbeiten + Statusnachricht bearbeiten + Verschlüsselung deaktivieren + %1$s ist nicht in der Lage, verschlüsselte Nachrichten an %2$s zu senden. Dies kann daran liegen, dass dein Kontakt einen veralteten Server oder Client verwendet, der mit OMEMO nicht umgehen kann. + Geräteliste konnte nicht abgerufen werden + Verschlüsselungsschlüssel konnte nicht abgerufen werden + Tipp: In manchen Fällen kann dies behoben werden, indem man sich gegenseitig zu den Kontaktlisten hinzufügt. + Bist du sicher, dass du die OMEMO-Verschlüsselung für dieses Gespräch deaktivieren möchtest?\nDies wird es deinem Serveradministrator ermöglichen, deine Nachrichten zu lesen, aber es könnte die einzige Möglichkeit sein, mit Personen zu kommunizieren, die veraltete Clients verwenden. + Jetzt deaktivieren + Entwurf: + OMEMO-Verschlüsselung + OMEMO wird immer für persönliche Unterhaltungen und private Gruppenchats verwendet. + OMEMO wird standardmäßig für neue Unterhaltungen verwendet. + OMEMO muss für neue Unterhaltungen explizit eingeschaltet werden. + Verknüpfung erstellen + Schriftgröße + Die verwendete relative Schriftgröße innerhalb der App. + Standardmäßig eingeschaltet + Standardmäßig ausgeschaltet + Klein + Mittel + Groß + Die Nachricht wurde nicht für dieses Gerät verschlüsselt. + OMEMO-Nachricht konnte nicht entschlüsselt werden. + rückgängig + Standort teilen ist deaktiviert + Position fixieren + Position lösen + Standort kopieren + Standort teilen + Wegbeschreibungen + Standort teilen + Standort anzeigen + Teilen + Aufnahme konnte nicht gestartet werden + Bitte warten… + %1$s den Zugriff auf das Mikrofon gewähren + Nachrichten durchsuchen + GIF + Unterhaltung anzeigen + Plugin zum Standort teilen + Verwende das Plugin zum Teilen des Standorts anstatt der integrierten Karte + Internetadresse kopieren + XMPP-Adresse kopieren + HTTP-Dateifreigabe für S3 + Direkte Suche + Beim Dialog \'Unterhaltung beginnen\' Tastatur öffnen und den Cursor im Suchfeld platzieren + Gruppenchat-Profilbild + Host unterstützt keine Gruppenchat-Profilbilder + Nur der Eigentümer kann das Gruppenchat-Profilbild ändern + Kontaktname + Nickname + Name + Angabe eines Namens ist optional + Gruppenchatname + Dieser Gruppenchat wurde gelöscht + Aufnahme konnte nicht gespeichert werden + Vordergrunddienst + Diese Benachrichtigungsart wird verwendet, um eine permanente Benachrichtigung anzuzeigen, die anzeigt, dass %1$s gerade ausgeführt wird. + Statusinformation + Verbindungsprobleme + Diese Benachrichtigungsart wird verwendet, um eine Benachrichtigung anzuzeigen, falls es ein Problem bei der Verbindung zu einem Konto gibt. + Nachrichten + Anrufe + Nachrichten + Eingehende Anrufe + Laufende Anrufe + Entgangene Anrufe + Lautlose Nachrichten + Diese Benachrichtigungsart wird verwendet, um Benachrichtigungen anzuzeigen, die keinen Ton auslösen sollen. Zum Beispiel, wenn du auf einem anderen Gerät aktiv bist (Schonfrist). + Fehlgeschlagene Zustellungen + Benachrichtigungseinstellungen + Anrufeinstellungen + Wichtigkeit, Klang, Vibrationen + Video komprimieren + Medien anzeigen + Teilnehmer + Medienbrowser + Datei wurde aufgrund von Sicherheitsverletzungen ausgelassen. + Videoqualität + Geringere Qualität ermöglicht kleinere Dateien + Mittel (360p) + Hoch (720p) + abgebrochen + Du bist bereits dabei, eine Nachricht zu verfassen. + Funktion nicht implementiert + Ungültige Landesvorwahl + Land auswählen + Telefonnummer + Telefonnummer überprüfen + Quicksy sendet eine SMS-Nachricht (es können Gebühren anfallen), um deine Telefonnummer zu überprüfen. Gib deine Landesvorwahl und Telefonnummer ein: +
%s

überprüfen. Ist diese in Ordnung oder möchtest du die Nummer bearbeiten?]]>
+ %s ist keine gültige Telefonnummer. + Gib bitte deine Telefonnummer ein. + Land suchen + Überprüfe %s + %s geschickt.]]> + Wir haben dir eine weitere SMS mit einem 6-stelligen Code geschickt. + Gib bitte die 6-stellige PIN unten ein. + SMS erneut versenden + SMS erneut versenden (%s) + Bitte warten (%s) + zurück + Mögliche PIN aus der Zwischenablage automatisch eingefügt. + Gib bitte deine 6-stellige PIN ein. + Bist du sicher, dass du den Registrierungsprozess abbrechen willst? + Ja + Nein + Überprüfen… + SMS anfordern… + Die eingegebene PIN ist falsch. + Die von uns zugesandte PIN ist abgelaufen. + Unbekannter Netzwerkfehler. + Unbekannte Rückmeldung vom Server. + Verbindung zum Server konnte nicht hergestellt werden. + Sichere Verbindung konnte nicht hergestellt werden. + Server konnte nicht gefunden werden. + Etwas ist bei der Bearbeitung deiner Anfrage schief gelaufen. + Ungültige Benutzereingabe + Vorübergehend nicht verfügbar. Versuche es später noch einmal. + Keine Netzwerkverbindung. + Bitte versuche es erneut in %s + Du bist limitiert + Zu viele Versuche + Du verwendest eine veraltete Version dieser App. + Update + Diese Telefonnummer ist derzeit mit einem anderen Gerät verknüpft. + Bitte gib deinen Namen ein und lass Leute, die dich nicht in ihren Adressbüchern haben, wissen, wer du bist. + Dein Name + Deinen Namen eingeben + Benutze die \"Bearbeiten\"-Schaltfläche, um deinen Namen festzulegen. + Anfrage ablehnen + Orbot installieren + Orbot starten + Kein App-Store installiert. + Dieser Channel wird deine XMPP-Adresse veröffentlichen + E-Book + Original (unkomprimiert) + Öffnen mit… + Conversations Profilbild + Konto auswählen + Sicherung wiederherstellen + Wiederherstellung + Gib dein Passwort für das Konto %s ein, um die Sicherung wiederherzustellen. + Benutze die Sicherungsfunktion nicht, um eine Installation zu klonen (gleichzeitig auszuführen). Die Wiederherstellung einer Sicherung ist nur für Migrationen oder für den Fall gedacht, dass du das ursprüngliche Gerät verloren hast. + Sicherung konnte nicht wiederhergestellt werden. + Sicherung konnte nicht entschlüsselt werden. Ist das Passwort korrekt? + Sicherung & Wiederherstellung + XMPP-Adresse eingeben + Gruppenchat erstellen + Öffentlichen Channel beitreten + Privaten Gruppenchat erstellen + Öffentlichen Channel erstellen + Channelname + XMPP-Adresse + Bitte einen Namen für den Channel eingeben + Bitte eine XMPP-Adresse eingeben + Dies ist eine XMPP-Adresse. Bitte einen Namen eingeben. + Öffentlichen Channel erstellen… + Dieser Channel existiert bereits + Du bist einem bestehenden Channel beigetreten + Channeleinstellung konnte nicht gespeichert werden + Erlaubt es jedem, das Thema zu bearbeiten + Erlaubt es jedem, andere einzuladen + Alle können das Thema bearbeiten. + Eigentümer können das Thema bearbeiten. + Administratoren können das Thema bearbeiten. + Eigentümer können andere einladen. + Alle können andere einladen. + XMPP-Adressen sind für Administratoren sichtbar. + XMPP-Adressen sind für alle sichtbar. + Dieser öffentliche Channel hat keine Teilnehmer. Lade deine Kontakte ein oder benutze die \"Teilen\"-Schaltfläche, um die XMPP-Adresse zu verteilen. + Dieser private Gruppenchat hat keine Teilnehmer. + Rechte verwalten + Teilnehmer suchen + Datei ist zu groß + Hinzufügen + Channels entdecken + Channels suchen + Mögliche Datenschutzverletzung! + search.jabber.network.

Wenn du diese Funktion verwendest, werden deine IP-Adresse und deine Suchbegriffe an diesen Dienst übertragen. Weitere Informationen findest du in der Datenschutzerklärung.]]>
+ Ich habe bereits ein Konto + Vorhandenes Konto hinzufügen + Neues Konto erstellen + Dies sieht aus wie eine Domain-Adresse + Trotzdem hinzufügen + Dies sieht aus wie eine Channel-Adresse + Sicherungsdateien teilen + Sicherung für Conversations + Ereignis + Sicherung öffnen + Die von dir ausgewählte Datei ist keine Sicherungsdatei von Conversations + Dieses Konto wurde bereits eingerichtet + Bitte gib das Passwort für dieses Konto ein + Diese Aktion konnte nicht durchgeführt werden + Öffentlichen Channel beitreten… + Die teilende App hat keine Berechtigung für den Zugriff auf diese Datei erteilt. + + jabber.network + Lokaler Server + Die meisten Benutzer sollten hier ‘jabber.network’ auswählen, um bessere Vorschläge aus dem gesamten, öffentlichen XMPP-Ökosystem zu bekommen. + Channelsuchmethode + Sicherung + Über + Bitte aktiviere ein Konto + Anrufen + Eingehender Anruf + Eingehender Videoanruf + Umschalten auf Videoanruf? + Zusätzliche Audiospuren hinzufügen? + Verbinden + Verbunden + Erneut verbinden + Anruf annehmen + Anruf beenden + Annehmen + Ablehnen + Entdecke Geräte + Klingelt + Besetzt + Verbindungsaufbau fehlgeschlagen + Verbindung unterbrochen + Anruf zurückgenommen + App-Fehler + Verifikationsproblem + Auflegen + Laufender Anruf + Laufender Videoanruf + Anruf erneut verbinden + Videoanruf erneut verbinden + Deaktiviere Tor, um Anrufe zu tätigen + Eingehender Anruf + Eingehender Anruf · %s + Entgangener Anruf · %s + Ausgehender Anruf + Ausgehender Anruf · %s + Entgangener Anruf + + %1$d entgangener Anruf von %2$s + %1$d entgangene Anrufe von %2$s + + + %d entgangener Anruf + %d entgangene Anrufe + + + %1$d entgangener Anruf von %2$d Kontakt + %1$d entgangene Anrufe von %2$d Kontakten + + Audioanruf + Videoanruf + Hilfe + Zur Unterhaltung wechseln + Dein Mikrofon ist nicht verfügbar + Du kannst immer nur einen Anruf zur gleichen Zeit machen. + Zurück zum laufenden Aufruf + Kamera konnte nicht gewechselt werden + Oben anheften + Von oben ablösen + GPX-Strecke + Nachricht konnte nicht korrigiert werden + Alle Unterhaltungen + Diese Unterhaltung + Dein Profilbild + Profilbild für %s + Verschlüsselt mit OMEMO + Verschlüsselt mit OpenPGP + Nicht verschlüsselt + Beenden + Sprachnachricht aufzeichnen + Audio abspielen + Audio anhalten + Kontakt hinzufügen, Gruppenchat erstellen oder beitreten oder Channels entdecken + + %1$d Teilnehmer anzeigen + %1$d Teilnehmer anzeigen + + + Eine Nachricht konnte nicht zugestellt werden + Einige Nachrichten konnten nicht zugestellt werden + + Fehlgeschlagene Zustellungen + Weitere Optionen + Keine Anwendung gefunden + Einladung zu Conversations + Einladung kann nicht verarbeitet werden + Server unterstützt keine Generierung von Einladungen + Keine aktiven Konten unterstützen diese Funktion + Die Sicherung wurde gestartet. Du bekommst eine Benachrichtigung, sobald sie fertig ist. + Video kann nicht aktiviert werden. + Textdokument + Kontoregistrierungen werden nicht unterstützt + Keine XMPP-Adresse gefunden + Temporärer Authentifizierungsfehler + Profilbild löschen + Anrufe sind bei der Verwendung von Tor deaktiviert + Umschalten auf Video + Umschalten auf Video ablehnen + XMPP-Konto + Push-Server + Ein selbst gewählter Push-Server, der Push-Nachrichten über XMPP an dein Gerät weiterleitet. + Kein (deaktiviert) + UnifiedPush Verteiler + Das Konto, über das Push-Nachrichten empfangen werden sollen. +
\ No newline at end of file diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml new file mode 100644 index 000000000..88750efe5 --- /dev/null +++ b/app/src/main/res/values-el/strings.xml @@ -0,0 +1,976 @@ + + + Ρυθμίσεις + Νέα συζήτηση + Διαχείριση λογαριασμών + Διαχείριση λογαριασμού + Τερματισμός συζήτησης + Λεπτομέρειες επαφής + Λεπτομέρειες ομαδικής συζήτησης + Λεπτομέρειες καναλιού + Προσθήκη λογαριασμού + Επεξεργασία ονόματος + Προσθήκη στο βιβλίο διευθύνσεων + Διαγραφή από τη λίστα επαφών + Αποκλεισμός επαφής + Άρση αποκλεισμού επαφής + Αποκλεισμός τομέα + Άρση αποκλεισμού τομέα + Αποκλεισμός συμμετέχοντα + Άρση αποκλεισμού συμμετέχοντα + Διαχείριση Λογαριασμών + Ρυθμίσεις + Διαμοιρασμός με Συζήτηση + Έναρξη Συζήτησης + Επιλογή επαφής + Επιλογή επαφών + Διαμοιρασμός μέσω λογαριασμού + Αποκλεισμός λίστας + μόλις τώρα + πριν από 1 λεπτό + πριν από %d λεπτά + + %d μη αναγνωσμένη συζήτηση + + + %d μη αναγνωσμένες συζητήσεις + + + αποστολή... + Αποκρυπτογράφηση μηνύματος. Παρακαλώ περιμένετε... + Κρυπτογραφημένο μήνυμα OpenPGP + Το ψευδώνυμο είναι ήδη σε χρήση + Μη έγκυρο ψευδώνυμο + Διαχειριστής + Κάτοχος + Συντονιστής + Συμμετέχων + Επισκέπτης + Θα θέλατε να αφαιρέσετε τον/την %s από τη λίστα επαφών σας; Οι Συζητήσεις με αυτή την επαφή δεν θα αφαιρεθούν. + Θέλετε να αποκλείσετε την επαφή %s από το να σας στέλνει μηνύματα; + Θέλετε να κάνετε άρση αποκλεισμού και να επιτρέψετε στην επαφή %s να σας στέλνει μηνύματα; + Αποκλεισμός όλων των επαφών από το %s; + Άρση αποκλεισμού όλων των επαφών από το %s; + Η επαφή αποκλείστηκε + Αποκλεισμένος + Θέλετε να αφαιρέσετε το %s από σελιδοδείκτη; Οι συζητήσεις που σχετίζονται με αυτόν τον σελιδοδείκτη δεν θα αφαιρεθούν. + Εγγραφή νέου λογαριασμού στον διακομιστή + Αλλαγή συνθηματικού στον διακομιστή + Διαμοιρασμός με... + Έναρξη συζήτησης + Πρόσκληση επαφής + Πρόσκληση + Επαφές + Επαφή + Ακύρωση + Ορισμός + Προσθήκη + Επεξεργασία + Αφαίρεση + Αποκλεισμός + Άρση αποκλεισμού + Αποθήκευση + Εντάξει + Το %1$s έχει τερματιστεί απρόσμενα + Χρησιμοποιώνας τον λογαριασμό XMPP σας για να στέιλετε ίχνη στοίβας βοηθάτε την συνεχιζόμενη ανάπτυξη του %1$s. + Αποστολή τώρα + Χωρίς ερώτηση την επόμενη φορά + Δεν ήταν δυνατή η σύνδεση στον λογαριασμό + Δεν ήταν δυνατή η σύνδεση σε πολλαπλούς λογαριασμούς + Επιλέξτε για διαχείριση των λογαριασμών σας + Επισύναψη αρχείου + Προσθήκη της επαφής αυτής στη λίστα επαφών σας; + Προσθήκη επαφής + η αποστολή απέτυχε + Προετοιμασία εικόνας για αποστολή + Προετοιμασία εικόνων για μεταφορά + Διαμοιρασμός αρχείων. Παρακαλώ περιμένετε... + Καθαρισμός ιστορικού + Καθαρισμός ιστορικού Συζήτησης + Θέλετε να διαγράψετε όλα τα μηνύματα αυτής της συζήτησης;\n\nΠροσοχή: Αυτή η ενέργεια δεν θα επηρεάσει μηνύματα που είναι αποθηκευμένα σε άλλες συσκευές ή εξυπηρετητές. + Διαγραφή αρχείου + Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το αρχείο;\n\nΠροσοχή: Αυτή η ενέργεια δεν θα διαγράψει αντίγραφα αυτού του αρχείου που είναι αποθηκευμένα σε άλλες συσκευές ή εξυπηρετητές. + Κλείσιμο της συζήτησης αμέσως μετά + Επιλογή συσκευής + Αποστολή μη κρυπτογραφημένου μηνύματος + Αποστολή μηνύματος + Αποστολή μηνύματος στον/στην %s + Αποστολή κρυπτογραφημένου μηνύματος OMEMO + Αποστολή v\\κρυπτογραφημένου μηνύματος OMEMO + Αποστολή κρυπτογραφημένου μηνύματος OpenPGP + Χρήση νέου ψευδωνύμου + Αποστολή χωρίς κρυπτογράφηση + Η αποκρυπτογράφηση απέτυχε. Ίσως δεν κατέχετε το σωστό ιδιωτικό κλειδί. + OpenKeychain + OpenKeychain για να κρυπτογραφήσει και αποκρυπτογραφήσει μηνύματα και να διαχειριστεί τα δημόσια κλειδιά σας.

Η άδειά του βασίζεται στο GPLv3+ και είναι διαθέσιμο στο F-Droid και το Google Play.

(Παρακαλώ επανεκκινήστε το %1$s μετά.)]]>
+ Επανεκκίνηση + Εγκατάσταση + Παρακαλώ εγκαταστήστε το OpenKeychain + προσφορά... + αναμονή... + Δεν βρέθηκε κλειδί OpenPGP + Δεν ήταν δυνατή η κρυπτογράφηση του μηνύματός σας γιατί η επαφή σας δεν ανακοινώνει το δημόσιο κλειδί της.\n\nΠαρακαλώ ζητήστε από την επαφή σας να ρυθμίσει το OpenPGP. + Δεν βρέθηκαν κλειδιά OpenPGP + Δεν ήταν δυνατή η κρυπτογράφηση του μηνύματός σας γιατί οι επαφές σας δεν ανακοινώνουν το δημόσιο κλειδί τους.\n\nΠαρακαλώ ζητήστε από τις επαφές σας να εγκαταστήσουν το OpenPGP. + Γενικά + Αποδοχή αρχείων + Αυτόματη αποδοχή αρχείων μικρότερων από... + Συνημμένα + Ειδοποίηση + Δόνηση + Δόνηση όταν δέχεστε νέο μήνυμα + Ειδοποίηση LED + Ειδοποίηση μέσω εναλλαγής LED κατά τη λήψη νέου μηνύματος + Κουδούνισμα + Ήχος ειδοποίησης + Ήχος ειδοποίησης για νέα μηνύματα + Ήχος κουδουνίσματος για εισερχόμενες κλήσεις + Περίοδος Χάριτος + Ο χρόνος σίγασης ειδοποιήσεων αφότου ανιχνευθεί δραστηριότητα σε μια από τις άλλες συσκευές σας. + Για προχωρημένους + Να μην αποστέλλονται αναφορές λαθών + Στέλνοντας ίχνη στοίβας βοηθάτε την συνεχόμενη ανάπτυξη + Επιβεβαίωση μηνυμάτων + Επιτρέψτε στις επαφές σας να γνωρίζουν όταν έχετε λάβει και διαβάσει τα μηνύματά τους + Αποτροπή στιγμιοτύπων οθόνης + Απόκρυψη περιεχομένων εφαρμογής στην εναλλαγή εφαρμογών και αποκλεισμός στιγμιοτύπων οθόνης + Διεπαφή χρήστη + Το OpenKeychain ανέφερε κάποιο σφάλμα. + Σφάλμα στο κλειδί κρυπτογράφησης. + Αποδοχή + Έχει συμβεί κάποιο σφάλμα + Σφάλμα + Ο λογαριασμός σας + Αποστολή ενημερώσεων παρουσίας + Λήψη ενημερώσεων παρουσίας + Παράκληση για ενημερώσεις παρουσίας + Επιλογή εικόνας + Λήψη εικόνας + Ερήμην χορήγηση αίτησης συνδρομής + Το αρχείο που επιλέξατε δεν είναι εικόνα + Σφάλμα κατά τη μετατροπή του αρχείου εικόνας + Το αρχείο δεν βρέθηκε + Γενικό σφάλμα εισόδου/εξόδου. Ίσως δεν έχετε ελεύθερο χώρο αποθήκευσης; + Η εφαρμογή που χρησιμοποιήσατε για να επιλέξετε αυτή την εικόνα δεν παραχώρησε αρκετά δικαιώματα για την ανάγνωση του αρχείου.\n\nΧρησιμοποιήστε διαφορετικό διαχειριστή αρχείων για να επιλέξετε μια εικόνα + Η εφαρμογή που χρησιμοποιήσατε για να διαμοιραστείτε αυτό το αρχείο δεν παρείχε αρκετά δικαιώματα. + Άγνωστο + Προσωρινά απενεργοποιημένο + Σε σύνδεση + Σύνδεση\u2026 + Εκτός σύνδεσης + Χωρίς εξουσιοδότηση + Δεν βρέθηκε ο διακομιστής + Χωρίς σύνδεση + Η εγγραφή απέτυχε + Το όνομα χρησιμοποιείται ήδη + Ολοκλήρωση εγγραφής + Ο διακομιστής δεν υποστηρίζει εγγραφή + Άκυρο κουπόνι εγγραφής + Αποτυχία διαπραγμάτευσης TLS + Ο τομέας δεν είναι επαληθεύσιμος + Παραβίαση κανονισμού + Μη συμβατός διακομιστής + Σφάλμα μετάδοσης + Σφάλμα στην εκκίνηση μετάδοσης + Μη κρυπτογραφημένο + OTR + OpenPGP + OMEMO + Διαγραφή λογαριασμού + Προσωρινή απενεργοποίηση + Δημοσίευση εικόνας προφίλ + Δημοσίευση του δημόσιου κλειδιού OpenPGP + Διαγραφή δημόσιου κλειδιού OpenPGP + Είστε βέβαιοι ότι θέλετε να αφαιρέσετε το δημόσιο κλειδί σας OpenPGP από την ανακοίνωση παρουσίας σας;\nΟι επαφές σας δεν θα είναι πλέον δυνατόν να σας στείλουν κρυπτογραφημένα μηνύματα με OpenPGP. + Το δημόσιο κλειδί OpenPGP έχει δημοσιευτεί. + Ενεργοποίηση λογαριασμού + Είστε βέβαιοι; + Η διαγραφή του λογαριασμού σας διαγράφει όλο το ιστορικό συζητήσεών σας + Εγγραφή φωνής + Διεύθυνση XMPP + Αποκλεισμός διεύθυνσης XMPP + username@example.com + Συνθηματικό + Μη έγκυρη διεύθυνση XMPP + Πλήρης μνήμη. Η εικόνα είναι πολύ μεγάλη + Θέλετε να προσθέσετε τον/την %s στο βιβλίο διευθύνσεών σας? + Πληροφορίες διακομιστή + XEP-0313: Διαχείριση αρχείου μηνυμάτων + XEP-0280: Αντίγραφα μηνυμάτων + XEP-0352: Ένδειξη κατάστασης πελάτη + XEP-0191: Εντολή αποκλεισμού + XEP-0237: Διατήρηση εκδόσεων λίστας επαφών + XEP-0198: Διαχείριση ροών + XEP-0215: Εύρεση εξωτερικών υπηρεσιών + XEP-0163: Πρωτόκολλο προσωπικών συμβάντων (εικόνες προφίλ / ΟΜΕΜΟ) + XEP-0363: Μεταφόρτωση αρχείου με πρωτόκολλο HTTP + XEP-0357: Push + διαθέσιμος + μη διαθέσιμος + Ελλειπείς ανακοινώσεις δημοσίων κλειδιών + συνδέθηκε τελευταία φορά μόλις τώρα + τελευταία σύνδεση πριν από ένα λεπτό + τελευταία σύνδεση πριν από %d λεπτά + τελευταία σύνδεση πριν από μία ώρα + τελευταία σύνδεση πριν από %d ώρες + τελευταία σύνδεση πριν από μία μέρα + τελευταία σύνδεση πριν από %d μέρες + Κρυπτογραφημένο μήνυμα. Παρακαλώ εγκαταστήστε το OpenKeychain για αποκρυπτογράφηση. + Βρέθηκαν νέα μηνύματα κρυπτογραφημένα με OpenPGP + Ταυτότητα κλειδιού OpenPGP + Αποτύπωμα OMEMO + v\\Αποτύπωμα OMEMO + Αποτύπωμα OMEMO (πηγή μηνύματος) + v\\Αποτύπωμα OMEMO (πηγή μηνύματος) + Άλλες συσκευές + Επαλήθευση των αποτυπωμάτων OMEMO + Μεταφόρτωση κλειδιών... + Έγινε + Αποκρυπτογράφηση + Σελιδοδείκτες + Αναζήτηση + Εισαγωγή επαφής + Διαγραφή επαφής + Λεπτομέρειες επαφής + Αποκλεισμός επαφής + Άρση αποκλεισμού επαφής + Δημιουργία + Επιλογή + Η επαφή υπάρχει ήδη + Συμμετοχή + channel@conference.example.com/nick + channel@conference.example.com + Αποθήκευση σαν σελιδοδείκτη + Διαγραφή σελιδοδείκτη + Διαγραφή ομαδικής συζήτησης + Διαγραφή καναλιού + Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτή την ομαδική συζήτηση;\n\nΠροσοχή:Η ομαδική συζήτηση θα διαγραφεί οριστικά από τον διακομιστή. + Είστε βέβαιοι ότι θέλετε να καταστρέψετε αυτό το δημόσιο κανάλι;\n\nΠροσοχή:Το κανάλι θα διαγραφεί πλήρως από τον διακομιστή. + Δεν ήταν δυνατή η διαγραφή της ομαδικής συζήτησης + Δεν ήταν δυνατή η διαγραφή του καναλιού + Επεξεργασία θέματος ομαδικής συζήτησης + Θέμα + Εισαγωγή σε ομαδική συζήτηση... + Έξοδος + Η επαφή σάς πρόσθεσε στην λίστα επαφών + Προσθήκη επίσης + Ο/Η %s έχει διαβάσει μέχρι αυτό το σημείο + Οι χρήστες %s έχουν διαβάσει μέχρι αυτό το σημείο + Ο χρήστης %1$s +%2$d ακόμα έχουν διαβάσει μέχρι αυτό το σημείο + Όλοι έχουν διαβάσει μέχρι αυτό το σημείο + Δημοσίευση + Επιλέξτε την εικόνα προφίλ για να διαλέξετε εικόνα από την έκθεση + Δημοσίευση... + Ο διακομιστής απέρριψε την δημοσίευσή σας + Δεν ήταν δυνατή η μετατροπή της εικόνας σας + Αδύνατη η αποθήκευση της εικόνας προφίλ στο δίσκο + (Ή πατήστε παρατεταμένα για να επιστρέψετε στο αρχικό) + Ο διακομιστής σας δεν υποστηρίζει την δημοσίευση εικονών προφίλ + ψιθύρισε + στο %s + Αποστολή ιδιωτικού μηνύματος στην επαφή %s + Σύνδεση + Αυτός ο λογαριασμός υπάρχει ήδη + Επόμενος + Σύσταση συνεδρίας + Παράλειψη + Απενεργοποίηση ειδοποιήσεων + Ενεργοποίηση + Η ομαδική συζήτηση απαιτεί συνθηματικό + Εισαγωγή συνθηματικού + Παρακαλώ αιτηθείτε ενημερώσεις παρουσίας από την επαφή σας πρώτα.\n\nΑυτό θα χρησιμοποιηθεί για να ταυτοποιηθεί το πρόγραμμα-πελάτης που χρησιμοποιεί η επαφή σας.. + Αίτηση τώρα + Αγνόηση + Προειδοποίηση: Η αποστολή αυτού χωρίς αμφίδρομες ενημερώσεις παρουσίας μπορεί να προκαλέσει απροσδόκητα προβλήματα.\n\nΠηγαίνετε στις \"Λεπτομέρειες επαφής\" για να επαληθεύσετε τις συνδρομές παρουσίας σας. + Ασφάλεια + Να επιτρέπεται η διόρθωση μηνυμάτων + Να επιτρέπεται οι επαφές σας να διορθώνουν τα μηνύματά τους αναδρομικά + Ρυθμίσεις για προχωρημένους + Παρακαλώ να είστε προσεκτικοί με αυτά + Περί %s + Ώρες ησυχίας + Ώρα έναρξης + Ώρα λήξης + Ενεργοποίηση ωρών ησυχίας + Οι ειδοποιήσεις θα σιγαστούν κατά τις ώρες ησυχίας + Άλλο + Το αποτύπωμα OMEMO αντιγράφηκε στο πρόχειρο + Είστε αποκλεισμένοι από αυτή την ομαδική συζήτηση + Αυτή η ομαδική συζήτηση είναι μόνο για μέλη + Περιορισμός πόρων + Έχετε διωχθεί από αυτή την ομαδική συζήτηση + Η ομαδική συζήτηση έχει τερματιστεί + Δεν είστε πλέον μέλος αυτής της ομαδικής συζήτησης + χρήση λογαριασμού %s + φιλοξενείται στο %s + Έλεγχος %s στον διακομιστή HTTP + Δεν είστε συνδεμένοι. Δοκιμάστε ξανά αργότερα + Ελέγξτε το μέγεθος του %s + Ελέγξτε το μέγεθος του %1$s στο %2$s + Επιλογές μηνυμάτων + Παράθεση + Επικόλληση ως παράθεση + Αντιγραφή αρχικής διεύθυνσης URL + Αποστολή ξανά + Διεύθυνση URL αρχείου + Η διεύθυνση URL αντιγράφηκε στο πρόχειρο + Η διεύθυνση XMPP αντιγράφηκε στο πρόχειρο + Το μήνυμα λάθους αντιγράφηκε στο πρόχειρο + διεύθυνση ιστού + Σάρωση 2D γραμμοκώδικα + Εμφάνιση 2D γραμμοκώδικα + Εμφάνιση λίστας αποκλεισμού + Λεπτομέρειες λογαριασμού + Επιβεβαίωση + Επανάληψη + Υπηρεσία στο προσκήνιο + Αποτρέπει τον τερματισμό της σύνδεσης από το λειτουργικό σύστημα + Δημιουργία αντιγράφου ασφαλείας + Τα αντίγραφα ασφαλείας θα αποθηκεύονται στο %s + Δημιουργία αντιγράφων ασφαλείας + Το αντίγραφο ασφαλείας σας έχει δημιουργηθεί + Τα αρχεία του αντιγράφου ασφαλείας έχουν αποθηκευτεί στο %s + Γίνεται επαναφορά αντιγράφου ασφαλείας + Έχει γίνει επαναφορά του αντιγράφου ασφαλείας σας + Μην παραλείψετε να ενεργοποιήσετε τον λογαριασμό. + Επιλογή αρχείου + Λήψη %1$s (ολοκληρώθηκε %2$d%%) + Μεταφόρτωση του %s + Διαγραφή του %s + αρχείο + Άνοιγμα του %s + αποστολή (ολοκλήρωση %1$d%%) + Προετοιμασία του αρχείου για διαμοιρασμό + Το %s προσφέρθηκε για μεταφόρτωση + Ακύρωση μετάδοσης + ο διαμοιρασμός του αρχείου απέτυχε + η μεταφορά αρχείου ακυρώθηκε + Το αρχείο διαγράφηκε + Δεν βρέθηκε εφαρμογή για να ανοίξει το αρχείο + Δεν βρέθηκε εφαρμογή για να ανοίξει τον σύνδεσμο + Δεν βρέθηκε εφαρμογή για προβολή της επαφής + Δυναμικές ετικέτες + Εμφάνιση ετικετών μόνο για ανάγνωση κάτω από τις επαφές + Ενεργοποίηση ειδοποιήσεων + Δεν βρέθηκε διακομιστής ομαδικής συζήτησης + Η δημιουργία ομαδικής συζήτησης απέτυχε + Εικόνα προφίλ λογαριασμού + Αντιγραφή του αποτυπώματος OMEMO στο πρόχειρο + Αναδημιουργία κλειδιού OMEMO + Καθαρισμός συσκευών + Είστε βέβαιοι ότι θέλετε να αφαιρέσετε όλες τις άλλες συσκευές από την αναγγελία OMEMO; Την επόμενη φορά που θα συνδεθούν οι συσκευές σας θα αναγγείλουν την παρουσία τους ξανά, αλλά είναι πιθανό να μην λάβουν τυχόν μηνύματα που θα αποσταλούν στο ενδιάμεσο. + Δεν υπάρχουν διαθέσιμα χρήσιμα κλειδιά για αυτή την επαφή.\nΗ μεταφόρτωση νέων κλειδιών από τον διακομιστή ήταν ανεπιτυχής. Ίσως υπάρχει κάποιο πρόβλημα με τον διακομιστή της επαφής σας; + Δεν υπάρχουν διαθέσιμα χρήσιμα κλειδιά για αυτή την επαφή.\nΒεβαιωθείτε ότι έχετε συνδρομή αμοιβαίας παρουσίας. + Κάτι πήγε στραβά + Ανάκτηση ιστορικού από τον διακομιστή + Δεν υπάρχει άλλο ιστορικό στον διακομιστή + Ενημέρωση... + Επιτυχής αλλαγή συνθηματικού! + Δεν ήταν δυνατή η αλλαγή του συνθηματικού + Αλλαγή συνθηματικού + Τρέχον συνθηματικό + Νέο συνθηματικό + Το συνθηματικό δεν μπορεί να είναι κενό + Ενεργοποίηση όλων των λογαριασμών + Απενεργοποίηση όλων των λογαριασμών + Εκτέλεση ενέργειας με + Χωρίς δεσμό + Εκτός σύνδεσης + Απόκληρος + Μέλος + Κατάσταση για προχωρημένους + Απόδοση δικαιωμάτων μέλους + Ανάκληση δικαιωμάτων μέλους + Απόδοση δικαιωμάτων διαχειριστή + Ανάκληση δικαιωμάτων διαχειριστή + Απόδοση δικαιωμάτων ιδιοκτήτη + Ανάκληση δικαιωμάτων κατόχου + Αφαίρεση από την ομάδική συζήτηση + Αφαίρεση από το κανάλι + Δεν ήταν δυνατή η αλλαγή του δεσμού της επαφής %s + Αποκλεισμός από την ομαδική συζήτηση + Αποκλεισμός από το κανάλι + Προσπαθείτε να αφαιρέσετε τον χρήστη %s από ένα δημόσιο κανάλι. Ο μόνος τρόπος να γίνει αυτό είναι να αποκλείσετε τον χρήστη για πάντα. + Αποκλεισμός τώρα + Δεν ήταν δυνατή η αλλαγή ρόλου της επαφής %s + Ρύθμιση συζήτησης ιδιωτικής ομάδας + Ρύθμιση δημοσίου καναλιού + Ιδιωτική, μόνο για μέλη + Ορατότητα διευθύνσεων XMPP από όλους + Ορισμός καναλιού ως συντονιζόμενο + Δεν συμμετέχετε + Μεταβολή των επιλογών ομαδικής συζήτησης! + Δεν ήταν δυνατή η μεταβολή των επιλογών ομαδικής συζήτησης + Ποτέ + Μέχρι νεωτέρας + Αναβολή + Απάντηση + Σημείωμα ως αναγνωσμένο + Είσοδος + Αποστολή με το πλήκτρο Enter + Χρήση του πλήκτρου Enter για την αποστολή μηνύματος. Μπορείτε πάντα να χρησιμοποιείτε τον συνδυασμό Ctrl+Enter για να στείλετε μήνυμα, ακόμα και αν αυτή η επιλογή είναι απενεργοποιημένη. + Εμφάνιση του πλήκτρου Enter + Αλλαγή του πλήκτρου emoticons σε πλήκτρο Enter + ήχος + βίντεο + εικόνα + διανυσματικά γραφικά + έγγραφο PDF + Εφαρμογή Android + Επαφή + Η εικόνα προφίλ έχει δημοσιευτεί! + Αποστολή του %s + Προσφορά του %s + Απόκρυψη των εκτός σύνδεσης + Ο/Η %s πληκτρολογεί... + Ο/Η %s σταμάτησε να πληκτρολογεί + Οι %s πληκτρολογούν... + Οι %s σταμάτησαν να πληκτρολογούν + Ειδοποιήσεις πληκτρολόγησης + Επιτρέψτε στις επαφές σας να γνωρίζουν πότε γράφετε μηνύματα προς αυτές + Αποστολή τοποθεσίας + Εμφάνιση τοποθεσίας + Δεν βρέθηκε εφαρμογή για την απεικόνιση τοποθεσίας + Τοποθεσία + Η συζήτηση έκλεισε + Αποχώρησε από την συζήτηση ιδιωτικής ομάδας + Αποχώρησε από το δημόσιο κανάλι + Μη έμπιστες αρχές πιστοποίησης συστήματος + Όλα τα πιστοποιητικά πρέπει να εγκριθούν χειροκίνητα + Αφαίρεση πιστοποιητικών + Διαγραφή των χειροκίνητα επιβεβαιωμένων πιστοποιητικών + Δεν υπάρχουν χειροκίνητα επιβεβαιωμένα πιστοποιητικα + Αφαίρεση πιστοποιητικών + Διαγραφή επιλογής + Ακύρωση + + %d πιστοποιητικό διαγράφηκε + %d πιστοποιητικά διαγράφηκαν + + Αντικατάσταση του κουμπιού \"Αποστολή\" με γρήγορη ενέργεια + Γρήγορη Ενέργεια + Κανένα + Πιο πρόσφατα χρησιμοποιημένη + Επιλογή γρήγορης ενέργειας + Αναζήτηση επαφών + Αναζήτηση σελιδοδεικτών + Αποστολή ιδιωτικού μηνύματος + Ο/Η %1$s αποχώρησε από την ομαδική συζήτηση + Όνομα χρήστη + Όνομα χρήστη + Αυτό δεν είναι έγκυρο όνομα χρήστη + Η μεταφόρτωση απέτυχε: Δεν βρέθηκε ο διακομιστής + Η μεταφόρτωση απέτυχε: Δεν βρέθηκε το αρχείο + Η μεταφόρτωση απέτυχε: Δεν ήταν δυνατή η σύνδεση + Η μεταφόρτωση απέτυχε: Αποτυχία εγγραφής αρχείου + Το δίκτυο Tor δεν είναι διαθέσιμο + Αποτυχία διασύνδεσης + Ο διακομιστής δεν είναι υπεύθυνος για αυτόν τον τομέα + Χαλασμένος + Διαθεσιμότητα + Εκτός χρήσης όταν η οθόνη είναι κλειδωμένη + Εμφάνιση παρουσίας ως εκτός χρήσης όταν η συσκευή κλειδώνεται + Απασχολημένος/η όταν βρίσκεται σε σιωπηρή λειτουργία + Σημειώνει την παρουσία σας ως Απασχολημένος/η όταν η συσκευή είναι σε κατάσταση σιωπής + Χρήση της κατάστασης δόνησης ως σιωπηρή κατάσταση + Σημειώνει την παρουσία σας ως Απασχολημένος/η όταν η συσκευή είναι σε κατάσταση δόνησης + Αναλυτικότερες ρυθμίσεις σύνδεσης + Εμφάνιση ονόματος μηχανήματος και ρυθμίσεων θύρας όταν ρυθμίζεται νέος λογαριασμός + xmpp.example.com + Σύνδεση με πιστοποιητικό + Αδυναμία ανάγνωσης πιστοποιητικού + Επιλογές αρχειοθέτησης + Επιλογές αρχειοθέτησης στον διακομιστή + Μεταφόρτωση επιλογών αρχειοθέτησης. Παρακαλώ περιμένετε... + Αδυναμία μεταφόρτωσης ρυθμίσεων αρχειοθέτησης + Απαραίτηση η χρήση CAPTCHA + Εισάγετε το κείμενο από την παραπάνω εικόνα + Η αλυσίδα του πιστοποιητικού δεν είναι έμπιστη + Η διεύθυνση XMPP δεν ταιριάζει με το πιστοποιητικό + Ανανέωση πιστοποιητικού + Σφάλμα μεταφόρτωσης κλειδιού OMEMO! + Επαληθεύτηκε το κλειδί OMEMO με πιστοποιητικό! + Η συσκευή σας δεν υποστηρίζει την επιλογή πιστοποιητικών πελάτη! + Σύνδεση + Σύνδεση μέσω Tor + Δρομολόγηση όλων των συνδέσεων μέσω του δικτύου Tor. Απαιτεί τη χρήση Orbot + Όνομα μηχανήματος + Θύρα + Διεύθυνση διακομιστή ή .onion + Ο αριθμός θύρας δεν είναι έγκυρος + Μη έγκυρο όνομα μηχανήματος + Συνδέθηκαν %1$d από %2$d λογαριασμοί + + %d μήνυμα + %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 είναι μέλος αυτής της ομαδικής συζήτησης. + Έχετε απενεργοποιήσει αυτόν τον λογαριασμό + Σφάλμα ασφάλειας: Μη έγκυρη πρόσβαση σε αρχείο! + Δεν βρέθηκε εφαρμογή για να μοιραστείτε την διεύθυνση URI + Διαμοιρασμός της διεύθυνσης URI με... +
Εγγράφεστε με τον τηλεφωνικό σας αριθμό και το Quicksy αυτόματα — με βάση τους τηλεφωνικούς αριθμούς στο βιβλίο διευθύνσεών σας — προτείνει πιθανές επαφές για εσάς.

Με την εγγραφή σας συμφωνείτε με την πολιτική απορρήτου μας.]]>
+ Συμφωνώ και προχωρήστε + Θα καθοδηγηθείτε στη διαδικασία δημιουργίας ενός λογαριασμού στο conversations.im.¹\nΕπιλέγοντας το conversations.im ως πάροχο θα μπορείτε να επικοινωνείτε με χρήστες άλλων παρόχων δίνοντάς τους την πλήρη διεύθυνση XMPP σας. + Η πλήρης ταυτότητα XMPP σας θα είναι: %s + Δημιουργία λογαριασμού + Χρήση του δικού μου παρόχου + Επιλογή ονόματος χρήστη + Διαχείριση της παρουσίας χειροκίνητα + Ορίστε την διαθεσιμότητά σας όταν διορθώνετε το μήνυμα κατάστασής σας. + Μήνυμα κατάστασης + Ελεύθερος για συνομιλία + Σε σύνδεση + Εκτός χρήσης + Μη διαθέσιμος + Απασχολημένος + Ένα ασφαλές συνθηματικό έχει δημιουργηθεί + Η συσκευή σας δεν υποστηρίζει την απενεργοποίηση βελτιστοποίησης χρήσης μπαταρίας + Η εγγραφή απέτυχε: Προσπαθήστε αργότερα + Η εγγραφή απέτυχε: Το συνθηματικό είναι πολύ ασθενές + Επιλογή συμμετεχόντων + Δημιουργία ομαδικής συζήτησης... + Πρόσκληση ξανά + Απενεργοποίηση + Μικρός + Μεσαίος + Μεγάλος + Δημοσιοποίηση της τελευταίας χρήσης + Επιτρέψτε στις επαφές σας να γνωρίζουν πότε χρησιμοποιείτε το Conversations + Ιδιωτικότητα + Θέμα + Επιλογή παλέτας χρωμάτων + Αυτόματο + Ανοιχτόχρωμο + Σκουρόχρωμο + Πράσινο φόντο + Χρήση πράσινου φόντου για εισερχόμενα μηνύματα + Αδυναμία σύνδεσης στο OpenKeychain + Αυτή η συσκευή δεν χρησιμοποιείται πλέον + Υπολογιστής + Κινητό τηλέφωνο + Ταμπλέτα + Περιηγητής ιστού + Κονσόλα + Απαιτείται πληρωμή + Απόδοση δικαιώματος χρήσης Internet + Εγώ + Η επαφή ζητά συνδρομή σε υπηρεσία παρουσίας + Να επιτραπεί + Δεν υπάρχει δικαίωμα για πρόσβαση στο %s + Δεν βρέθηκε ο απομακρυσμένος διακομιστής + Λήξη χρόνου για τον απομακρυσμένο διακομιστή + Αδυναμία ενημέρωσης λογαριασμού + Αναφέρετε για αυτή την ταυτότητα XMPP ότι στέλνει ανεπιθύμητα μηνύματα. + Διαγραφή ταυτοτήτων OMEMO + Αναδημιουργία των κλειδιών OMEMO. Όλες οι επαφές σας θα πρέπει να σας επαληθεύσουν πάλι. Χρησιμοποιήστε το μόνο ως τελευταία λύση. + Διαγραφή επιλεγμένων κλειδιών + Πρέπει να είστε συνδεμένοι για να δημοσιεύσετε το avatar σας. + Εμφάνιση μηνύματος λάθους + Μήνυμα λάθους + Ενεργοποίηση μείωσης χρήσης δεδομένων + Το λειτουργικό σας σύστημα περιορίζει το %1$s από το να συνδέεται στο Internet όταν βρίσκεται στο παρασκήνιο. Για να λαμβάνετε ειδοποιήσεις νέων μηνυμάτων πρέπει να επιτρέψετε στο %1$s να έχει απεριόριστη πρόσβαση όταν ενεργοποιείται η μείωση χρήσης δεδομένων.\nΤο %1$s θα προσπαθεί να περιορίσει τη χρήση δεδομένων όταν είναι δυνατό. + Η συσκευή σας δεν υποστηρίζει την απενεργοποίηση μείωσης χρήσης δεδομένων για το %1$s. + Αδυναμία δημιουργίας προσωρινού αρχείου + Αυτή η συσκευή έχει επαληθευτεί + Αντιγραφή αποτυπώματος + Έχετε επαληθεύσει όλα τα κλειδιά OMEMO που κατέχετε + Ο γραμμοκώδικας δεν περιέχει αποτυπώματα για την συζήτηση αυτή. + Επαληθευμένα αποτυπώματα + Χρήση της φωτογραφικής μηχανής για την σάρωση του γραμμοκώδικα της επαφής + Παρακαλώ περιμένετε την μεταφόρτωση των κλειδιών + Διαμοιρασμός ως γραμμοκώδικα + Διαμοιρασμός ως διεύθυνση URI XMPP + Διαμοιρασμός ως σύνδεσμος HTTP + Τυφλή εμπιστοσύνη πριν την επαλήθευση + Αυτόματη εμπιστοσύνη σε όλες τις νέες συσκευές επαφών που δεν έχουν επαληθευτεί παλιότερα, και παροχή χειροκίνητης επιβεβαίωσης κάθε φορά που μια επαληθευμένη επαφή προσθέτει μια νέα συσκευή. + Κλειδιά OMEMO με τυφλή εμπιστοσύνη, που σημαίνει ότι μπορεί να είναι κάποιος άλλος ή κάποιος τρίτος μπορεί να έχει αποκτήσει πρόσβαση. + Μη έμπιστος + Άκυρος γραμμοκώδικας 2D + Καθαρισμός κρυφού φακέλου (χρησιμοποείται από την εφαρμογή φωτογραφικής μηχανής) + Καθαρισμός κρυφής μνήμης + Καθαρισμός ιδιωτικής αποθήκευσης + Καθαρισμός ιδιωτικού χώρου όπου αποθηκεύονται αρχεία (Μπορούν να μεταφορτωθούν ξανά από τον διακομιστή) + Ακολούθησα αυτόν τον σύνδεσμο από μια έμπιστη πηγή + Πρόκειται να επαληθεύσετε τα κλειδιά OMEMO της επαφής %1$s ακολουθώντας έναν σύνδεσμο. Αυτό είναι ασφαλές μόνο αν ακολουθήσατε τον σύνδεσμο από μια έμπιστη πηγή όπου μόνο η επαφή %2$s μπορεί να τον δημοσίευσε. + Πρόκειται να επαληθεύσετε τα κλειδιά OMEMO του δικού σας λογαριασμού. Αυτό είναι ασφαλές μόνο αν ακολουθήσατε τον σύνδεσμο από έμπιστη πηγή, όπου μόνο εσείς μπορεί να δημοσιεύσατε τον σύνδεσμο. + Συνέχεια + Επιβεβαίωση κλειδιών OMEMO + Εμφάνιση ανενεργών + Απόκρυψη ανενεργών + Αναίρεση εμπιστοσύνης συσκευής + Είστε βέβαιοι ότι θέλετε να αφαιρέσετε την επαλήθευση για αυτή τη συσκευή;\nΑυτή η συσκευή και μηνύματα που έρχονται από αυτή τη συσκευή θα σημειωθούν ως μη έμπιστα. + + %d δευτερόλεπτο + %d δευτερόλεπτα + + + %d λεπτό + %d λεπτά + + + %d ώρα + %d ώρες + + + %d ημέρα + %d ημέρες + + + %d εβδομάδα + %d εβδομάδες + + + %d μήνας + %d μήνες + + Αυτόματη διαγραφή μηνυμάτων + Αυτόματη διαγραφή μηνυμάτων από αυτή την συσκευή που είναι παλιότερα από την ρυθμισμένη χρονική περίοδο. + Κρυπτογράφηση μηνύματος + Χωρίς μεταφόρτωση μηνυμάτων λόγω τοπικού χρόνου διατήρησης. + Συμπίεση βίντεο + Οι αντίστοιχες συζητήσεις έκλεισαν. + Η επαφή αποκλείστηκε. + Ειδοποιήσεις από άγνωστους + Ειδοποίηση για μηνύματα και κλήσεις που προέρχονται από άγνωστους. + Λήψη μηνύματος από άγνωστο + Αποκλεισμός αγνώστου + Αποκλεισμός ολόκληρου τομέα + Σε σύνδεση αυτή τη στιγμή + Επανάληψη αποκρυπτογράφησης + Σφάλμα συνεδρίας + Υποβάθμιση του μηχανισμού SASL + Ο διακομιστής απαιτεί εγγραφή σε ιστοσελίδα + Άνοιγμα ιστοσελίδας + Δεν βρέθηκε εφαρμογή για να ανοίξει την ιστοσελίδα + Αναδυόμενες ειδοποιήσεις + Εμφάνιση αναδυόμενων ειδοποιήσεων + Σήμερα + Χτες + Επαλήθευση ονόματος μηχανήματος με χρήση DNSSEC + Τα πιστοποιητικά διακομιστή που περιέχουν το επικυρωμένο όνομα μηχανήματος θεωρούνται επαληθευμένα + Το πιστοποιητικό δεν περιέχει ταυτότητα XMPP + μερικώς + Εγγραφή βίντεο + Αντιγραφή στο πρόχειρο + Το μήνυμα αντιγράφηκε στο πρόχειρο + Μήνυμα + Τα ιδιωτικά μηνύματα είναι απενεργοποιημένα + Προστατευμένες εφαρμογές + Για να συνεχίσετε να λαμβάνετε ειδοποιήσεις, ακόμα κι όταν η οθόνη είναι σβηστή, χρειάζεται να προσθέσετε το Conversations στον κατάλογο με τις προστατευμένες εφαρμογές. + Αποδοχή άγνωστου πιστοποιητικού; + Το πιστοποιητικό του διακομιστή δεν είναι υπογεγραμμένο από κάποια γνωστή Αρχή Πιστοποίησης. + Αποδοχή αναντίστοιχου ονόματος διακομιστή; + Ο διακομιστής δεν ταυτοποιείται ως \"%s\". Το πιστοποιητικό είναι έγκυρο μόνο για: + Θέλετε να συνδεθείτε έτσι κι αλλιώς; + Λεπτομέρειες πιστοποιητικού: + Μια φορά + Ο σαρωτής κώδικα QR χρειάζεται πρόσβαση στην φωτογραφική μηχανή + Κύλιση στο τέλος + Κύλιση στο τέλος μετά από αποστολή μηνύματος + Διόρθωση μηνύματος κατάστασης + Διόρθωση μηνύματος κατάστασης + Απενεργοποίηση κρυπτογράφησης + Το %1$s αδυνατεί να στείλει κρυπτογραφημένα μηνύματα στην επαφή %2$s . Αυτό μπορεί να συμβαίνει γιατί η επαφή σας χρησιμοποιεί παλιότερο διακομιστή ή πρόγραμμα που δε μπορεί να χειριστεί κρυπτογράφηση OMEMO. + Αδύναμια μεταφόρτωσης λίστας συσκευών + Αδυναμία ανάσυρσης κλειδιών κρυπτογράφησης + Συμβουλή: Σε κάποιες περιπτώσεις αυτό μπορεί να διορθωθεί αν αμοιβαία προστεθείτε στους καταλόγους επαφών σας. + Είστε βέβαιοι ότι θέλετε να απενεργοποιήσετε την κρυπτογράφηση OMEMO για αυτή τη συζήτηση;\nΑυτή η ενέργεια θα επιτρέψει στον διαχειριστή του διακομιστή να αναγνώσει τα μηνύματά σας, αλλά ίσως είναι ο μόνος τρόπος να επικοινωνήσετε με επαφές που χρησιμοποιούν παλιότερα προγράμματα. + Απενεργοποίηση τώρα + Πρόχειρο: + Κρυπτογράφηση OMEMO + Η κρυπτογράφηση OMEMO θα χρησιμοποιείται πάντα για έναν προς έναν συζητήσεις και ιδιωτικές ομαδικές συζητήσεις. + Η κρυπτογράφηση OMEMO θα προεπιλέγεται για νέες συζητήσεις. + Η κρυπτογράφηση OMEMO θα πρέπει να επιλεγεί χειροκίνητα για νέες συζητήσεις. + Δημιουργία συντόμευσης + Μέγεθος γραμματοσειράς + Το σχετικό μέγεθος γραμματοσειράς που χρησιμοποιείται στην εφαρμογή. + Ενεργοποιημένο από προεπιλογή + Απενεργοποιημένο από προεπιλογή + Μικρό + Μεσαίο + Μεγάλο + Το μήνυμα δεν κρυπτογραφήθηκε για αυτή τη συσκευή. + Αποτυχία αποκρυπτογράφησης μηνύματος OMEMO. + αναίρεση + Ο διαμοιρασμός τοποθεσίας είναι απενεργοποιημένος + Σταθεροποίηση θέσης + Αποσταθεροποίηση θέσης + Αντιγραφή τοποθεσίας + Διαμοιρασμός τοποθεσίας + Οδηγίες + Διαμοιρασμός τοποθεσίας + Εμφάνιση τοποθεσίας + Διαμοιρασμός + Αδυναμία έναρξης εγγραφής + Παρακαλώ περιμένετε... + Απόδοση δικαιώματος στο %1$s για πρόσβαση στο μικρόφωνο + Αναζήτηση μηνυμάτων + GIF + Εμφάνιση συζήτησης + Πρόσθετο διαμοιρασμού τοποθεσίας + Χρήση του πρόσθετου διαμοιρασμού τοποθεσίας αντί για τον ενσωματωμένο χάρτη + Αντιγραφή διεύθυνσης ιστού + Αντιγραφή ταυτότητας XMPP + Διαμοιρασμός αρχείων μέσω HTTP για S3 + Άμεση αναζήτηση + Άνοιγμα πληκτρολογίου και τοποθέτηση του δείκτη στο πεδίο αναζήτησης στην οθόνη \'Έναρξη συζήτησης\' + Εικόνα ομαδικής συζήτησης + Ο κεντρικός υπολογιστής δεν υποστηρίζει εικόνες προφίλ σε ομαδικές συζητήσεις + Μόνο ο ιδιοκτήτης μπορεί να αλλάξει την εικόνα προφίλ μιας ομαδικής συζήτησης + Όνομα επαφής + Ψευδώνυμο + Όνομα + Η παροχή ονόματος είναι προαιρετική + Όνομα ομαδικής συζήτησης + Αυτή η ομαδική συζήτηση έχει καταστραφεί + Αδυναμία αποθήκευσης εγγραφής + Υπηρεσία στο προσκήνιο + Αυτή η κατηγορία ειδοποιήσεων χρησιμοποιείται για την εμφάνιση μιας μόνιμης ειδοποίησης που δείχνει πως εκτελείται το %1$s. + Πληροφορίες κατάστασης + Προβλήματα συνδεσιμότητας + Αυτή η κατηγορία ειδοποιήσεων χρησιμοποιείται για να εμφανίσει μια ειδοποίηση σε περίπτωση που υπάρχει πρόβλημα σύνδεσης σε κάποιον λογαριασμό. + Μηνύματα + Κλήσεις + Μηνύματα + Εισερχόμενες κλήσεις + Κλήσεις σε εξέλιξη + Σιωπηρά μηνύματα + Αυτή η κατηγορία ειδοποιήσεων χρησιμοποιείται για να εμφανίσει ειδοποιήσεις που δεν θα έπρεπε να παράγουν ήχο. Για παράδειγμα όταν κάποιος είναι ενεργός σε άλλη συσκευή (περίοδος χάριτος). + Αποτυχημένες διανομές + Ρυθμίσεις ειδοποίησης μηνυμάτων + Ρυθμίσεις ειδοποίησης εισερχόμενων κλήσεων + Σημασία, Ήχος, Δόνηση + Συμπίεση βίντεο + Εμφάνιση μέσου + Συμμετέχοντες + Περιηγητης μέσων + Το αρχείο παραλείπεται λόγω παραβίασης ασφάλειας. + Ποιότητα βίντεο + Μικρότερη ποιότητα σημαίνει μικρότερα αρχεία + Μέση (360p) + Υψηλή (720p) + ακυρώθηκε + Συνθέτετε ήδη ένα μήνυμα. + Δεν έχει υλοποιηθεί ακόμα + Άκυρος κωδικός χώρας + Επιλέξτε χώρα + τηλεφωνικός αριθμός + Επαληθεύστε τον τηλεφωνικό σας αριθμό + Το Quicksy θα στείλει ένα μήνυμα SMS (πιθανή χρέωση από τον πάροχο) για να επαληθεύσει τον τηλεφωνικό σας αριθμό. Εισάγετε τον κωδικό χώρας και τον αριθμό τηλεφώνου: +
%s

Είναι εντάξει ή θα θέλατε να διορθώσετε τον αριθμό;]]>
+ Ο %s δεν είναι έγκυρος τηλεφωνικός αριθμός. + Παρακαλώ εισάγετε τον τηλεφωνικό σας αριθμό. + Αναζήτηση χωρών + Επαλήθευση %s + %s.]]> + Σας έχει αποσταλεί άλλο ένα SMS με κωδικό 6 ψηφίων. + Παρακαλώ εισάγετε τον κωδικό 6 ψηφίων παρακάτω. + Αποστολή SMS ξανά + Αποστολή SMS ξανά (%s) + Παρακαλώ περιμένετε (%s) + πίσω + Αυτόματη επικόλληση πιθανού κωδικού από το πρόχειρο + Παρακαλώ εισάγετε τον κωδικό σας 6 ψηφίων. + Είστε βέβαιοι ότι θέλετε να ακυρώσετε την διαδικασία εγγραφής; + Ναι + Όχι + Επαλήθευση... + Αίτηση SMS... + Ο κωδικός που εισάγατε δεν είναι σωστός. + Ο κωδικός που σας έχει σταλεί έχει λήξει. + Άγνωστο σφάλμα δικτύου. + Άγνωστη απάντηση από τον διακομιστή. + Αδυναμία σύνδεσης στον διακομιστή. + Αδυναμία δημιουργίας ασφαλούς σύνδεσης. + Αδυναμία εύρεσης του διακομιστή. + Κάτι δεν πήγε καλά κατά την εξυπηρέτηση της αίτησής σας. + Μη έγκυρη εισαγωγή + Προσωρινά μη διαθέσιμο. Προσπαθήστε αργότερα. + Χωρίς σύνδεση δικτύου. + Παρακαλώ προσπαθήστε ξανά σε %s + Σας έχει επιβληθεί περιορισμός ροής + Υπερβολικά πολλές προσπάθειες + Χρησιμοποιείτε μια παλιά έκδοση της εφαρμογής. + Ενημέρωση + Αυτός ο τηλεφωνικός αριθμός χρησιμοποιείται σε άλλη συσκευή αυτή τη στιγμή. + Παρακαλώ εισάγετε το όνομά σας για να επιτρέψετε σε χρήστες, που δεν σας έχουν καταγράψει στα βιβλία διευθύνσεών τους, να γνωρίζουν ποιος/α είστε. + Το όνομά σας + Εισάγετε το όνομά σας + Χρησιμοποιήστε το κουμπί διόρθωσης για να ορίσετε το όνομά σας. + Απόρριψη αίτησης + Εγκατάσταση Orbot + Εκκίνηση Orbot + Δεν υπάρχει εγκατεστημένη εφαργμογή διαχείρισης εφαρμογών. + Αυτή η ομαδική συζήτηση θα κάνει την ταυτότητά XMPP σας δημόσια + ηλεκτρονικό βιβλίο + Αρχικό (μη συμπιεσμένο) + Άνοιγμα με... + Φωτογραφία προφίλ του Conversations + Επιλογή λογαριασμού + Επαναφορά αντιγράφου ασφαλείας + Επαναφορά + Εισάγετε τον κωδικό σας για τον λογαριασμό %s για να επαναφέρετε το αντίγραφο ασφαλείας. + Μην χρησιμοποιείτε τη λειτουργία επαναφοράς αντιγράφων ασφαλείας για να κλωνοποιήσετε (ταυτόχρονη εκτέλεση) μια εγκατάσταση. Η επαναφορά αντιγράφου ασφαλείας προσφέρεται μόνο για μεταφορές ή σε περίπτωση που έχετε χάσει την αρχική συσκευή. + Αδυναμία επαναφοράς αντιγράφου ασφαλείας. + Αδυναμία αποκρυπτογράφησης του αντιγράφου ασφαλείας. Είναι ο κωδικός σωστός; + Δημιουργία & Επαναφορά + Εισάγετε τη διεύθυνση XMPP + Δημιουργία ομαδικής συζήτησης + Είσοδος σε δημόσιο κανάλι + Δημιουργία συζήτησης ιδιωτικής ομάδας + Δημιουργία δημόσιου καναλιού + Όνομα καναλιού + Διεύθυνση XMPP + Παρακαλώ εισάγετε ένα όνομα για το κανάλι + Παρακαλώ εισάγετε μια διεύθυνση XMPP + Αυτή είναι μια διεύθυνση XMPP. Παρακαλώ εισάγετε ένα όνομα. + Δημιουργία δημόσιου καναλιού... + Αυτό το κανάλι υπάρχει ήδη + Έχετε εισέρθει σε ένα προϋπάρχον κανάλι + Αδυναμία ορισμού ρυθμίσεων καναλιού + Αλλαγή θέματος από οποιονδήποτε + Πρόσκληση άλλων χρηστών από οποιονδήποτε + Οποιοσδήποτε μπορεί να αλλάξει το θέμα. + Οι ιδιοκτήτες μπορούν να αλλάξουν το θέμα. + Οι διαχειριστές μπορούν να αλλάξουν το θέμα. + Οι κάτοχοι μπορούν να προσκαλούν άλλους χρήστες. + Οποιοσδήποτε μπορεί να προσκαλεί άλλους χρήστες. + Οι διευθύνσεις XMPP είναι ορατές στους διαχειριστές. + Οι διευθύνσεις XMPP είναι ορατές σε όλους. + Αυτό το δημόσιο κανάλι δεν έχει συμμετέχοντες. Προσκαλέστε τις επαφές σας ή χρησιμοποιήστε το κουμπί διαμοιρασμού για να διαδώσετε τη διεύθυνση XMPP του. + Αυτή η συζήτηση ιδιωτικής ομάδας δεν έχει συμμετέχοντες. + Διαχείριση δικαιωμάτων + Αναζήτηση συμμετεχόντων + Το αρχείο είναι πολύ μεγάλο + Επισύναψη + Εύρεση καναλιών + Αναζήτηση καναλιών + Πιθανή παραβίαση ιδιωτικότητας! + search.jabber.network.

Χρησιμοποιώντας αυτή τη λειτουργία θα μεταβιβαστεί η διεύθυνση IP σας και οι όροι αναζήτησης σε αυτή την υπηρεσία. Δείτε την Πολιτική Ιδιωτικότητας της για περισσότερες πληροφορίες.]]>
+ Έχω ήδη λογαριασμό + Προσθήκη υπάρχοντος λογαριασμού + Εγγραφή νέου λογαριασμού + Αυτό μοιάζει με διεύθυνση τομέα + Προσθήκη έτσι κι αλλιώς + Αυτό μοιάζει με διεύθυνση καναλιού + Διαμοιρασμός αντιγράφων ασφαλείας + Αντίγραφο ασφαλείας Conversations + Γεγονός + Άνοιγμα αντιγράφου ασφαλείας + Το αρχείο που επιλέξατε δεν είναι αντίγραφο ασφαλείας του Conversations + Αυτός ο λογαριασμός έχει προστεθεί ήδη + Παρακαλώ εισάγετε τον κωδικό για αυτό το λογαριασμό + Αδυναμία εκτέλεσης αυτής της λειτουργίας + Είσοδος σε δημόσιο κανάλι... + Η εφαρμογή από την οποία έγινε διαμοίραση δεν έδωσε δικαιώματα πρόσβασης στο αρχείο. + + jabber.network + Τοπικός διακομιστής + Οι περισσότεροι χρήστες πρέπει να επιλέξουν ‘jabber.network’ για καλύτερες προτάσεις από το σύνολο του οικοσυστήματος XMPP. + Μέθοδος εύρεσης καναλιού + Αντίγραφο ασφαλείας + Σχετικά με + Παρακαλώ ενεργοποιήστε έναν λογαριασμό + Νέα κλήση + Εισερχόμενη κλήση + Εισερχόμενη βιντεοκλήση + Γίνεται σύνδεση + Συνδέθηκε + Επανασύνδεση + Αποδοχή κλήσης + Τερματισμός κλήσης + Απάντηση + Παράβλεψη + Εύρεση συσκευών + Κουδούνισμα + Απασχολημένος + Αδυναμία σύνδεσης κλήσης + Απώλεια σύνδεσης + Αποσυρμένη κλήση + Αποτυχία εφαρμογής + Πρόβλημα επαλήθευσης + Τερματισμός κλήσης + Κλήση σε εξέλιξη + Βιντεοκλήση σε εξέλιξη + Επανασύνδεση κλήσης + Επανασύνδεση βίντεοκλήσης + Απενεργοποίηστε το Tor για να κάνετε κλήσεις + Εισερχόμενη κλήση + Εισερχόμενη κλήση · %s + Αναπάντηση κλήση · %s + Εξερχόμενη κλήση + Εξερχόμενη κλήση · %s + Αναπάντηση κλήση + Κλήση ήχου + Βιντεοκλήση + Βοήθεια + Εναλλαγή στη συζήτηση + Το μικρόφωνο δεν είναι διαθέσιμο + Μπορείτε να κάνετε μόνο μια κλήση τη φορά. + Επιστροφή στην κλήση σε εξέλιξη + Αδυναμία εναλλαγής κάμερας + Καρφίτσωμα στην κορυφή + Ξεκαρφίτσωμα από την κορυφή + Ίχνος GPX + Αδυναμία διόρθωσης μηνύματος + Όλες οι συζητήσεις + Αυτή η συζήτηση + Η φωτογραφία προφίλ σας + Φωτογραφία προφίλ του/της %s + Κρυπτογραφημένη με OMEMO + Κρυπτογραφημένη με OpenPGP + Χωρίς κρυπτογράφηση + Έξοδος + Ηχογράφηση μηνύματος τηλεφωνητή + Αναπαραγωγή ήχου + Παύση ήχου + Προσθήκη επαφής, δημιουργία ή είσοδος σε ομαδική συζήτηση, ή εύρεση καναλιών + + Εμφάνιση %1$d συμμετέχοντα + Εμφάνιση %1$d συμμετεχόντων + + + Κάποιο μήνυμα δεν ήταν δυνατό να παραδοθεί + Κάποια μηνύματα δεν ήταν δυνατό να παραδοθούν + + Αποτυχημένες παραδόσεις + Περισσότερες επιλογές + Δεν βρέθηκε εφαρμογή + Πρόσκληση στο Conversations + Αδυναμία ανάγνωσης πρόσκλησης + Ο διακομιστής δεν υποστηρίζει την δημιουργία προσκλήσεων + Κανένας από τους ενεργούς λογαριασμούς δεν υποστηρίζει αυτό το χαρακτηριστικό + Το αντίγραφο ασφαλείας δημιουργείται. Θα λάβετε ειδοποίηση όταν ολοκληρωθεί. + Αδυναμία ενεργοποίησης βίντεο. + Έγγραφο απλού κειμένου + Δεν υποστηρίζονται εγγραφές λογαριασμών + Δεν βρέθηκε διεύθυνση XMPP +
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml new file mode 100644 index 000000000..1f37df176 --- /dev/null +++ b/app/src/main/res/values-es/strings.xml @@ -0,0 +1,1024 @@ + + + Ajustes + Nueva conversación + Gestionar cuentas + Gestionar cuenta + Cerrar conversación + Detalles del contacto + Detalles de conversación + Detalles del canal + Añadir cuenta + Editar contacto + Añadir a contactos + Eliminar contacto de la lista + Bloquear contacto + Desbloquear contacto + Bloquear dominio + Desbloquear dominio + Bloquear participante + Desbloquear participante + Gestionar Cuentas + Ajustes + Compartir con Conversación + Nueva Conversación + Seleccionar Contacto + Seleccionar Contactos + Compartir via cuenta + Lista contactos bloqueados + ahora + hace 1 min + hace %d min + + %d conversación sin leer + %dconversaciones sin leer + %dconversaciones sin leer + + enviando… + Descifrando el mensaje. Espere por favor… + Mensaje cifrado con OpenPGP + El apodo ya está en uso + Apodo inválido + Administrador + Propietario + Moderador + Participante + Visitante + ¿Quieres eliminar a %s de tu lista de contactos? Las conversaciones con este contacto no se eliminarán. + ¿Quieres bloquear a %s para que no pueda enviarte mensajes? + ¿Quieres desbloquear a %s y permitirle que te envíe mensajes? + ¿Bloquear todos los contactos de %s? + ¿Desbloquear todos los contatos de %s? + Contacto bloqueado + Bloqueado + ¿Quieres eliminar %s de tus marcadores? Las conversaciones con este marcador no serán eliminadas. + Registrar nueva cuenta en servidor + Cambiar contraseña en servidor + Compartir con… + Comenzar conversación + Invitar a contacto + Invitar + Contactos + Contacto + Cancelar + Establecer + Añadir + Editar + Eliminar + Bloquear + Desbloquear + Guardar + OK + %1$s se ha detenido + Usar tu cuenta XMPP para enviar trazas de error ayuda al desarrollo de %1$s. + Enviar ahora + No preguntar de nuevo + No se ha podido conectar a la cuenta + No se ha podido conectar a varias cuentas + Pulsa aquí para gestionar tus cuentas + Adjuntar + El contacto no está en tu lista. ¿Te gustaría añadirlo? + Añadir contacto + Error al enviar + Preparando para enviar imagen + Preparando para enviar imágenes + Compartiendo el archivo, por favor espere… + Limpiar historial + Limpiar historial de conversación + ¿Quieres borrar todos los mensajes de esta conversación?\n\nAviso: Esto no afectará a los mensajes guardados en otros dispositivos o servidores. + Eliminar fichero + ¿Está seguro de que desea eliminar este archivo\? +\n +\nAdvertencia: Esto no eliminará las copias de este archivo almacenadas en otros dispositivos o servidores. + Cerrar esta conversación después + Seleccionar dispositivo + Enviar mensaje sin cifrar + Enviar mensaje + Enviar mensaje a %s + Enviar mensaje cifrado con OMEMO + Enviar mensaje cifrado v\\OMEMO + Enviar mensaje cifrado con OpenPGP + El apodo ha sido modificado + Enviar sin cifrar + Falló el descifrado. Tal vez no tengas la clave privada apropiada. + OpenKeychain + OpenKeychain para cifrar y descifrar mensajes y gestionar tus claves públicas.

Está publicado bajo licencia GPLv3+ y disponible en F-Droid y Google Play.

(Por favor, reinicie %1$s después.)]]>
+ Reiniciar + Instalar + Por favor, instala OpenKeyChain + ofreciendo… + esperando… + Clave OpenPGP no encontrada + No se ha podido cifrar tu mensaje porque tu contacto no está anunciando su clave pública.\n\nPor favor, pide a tu contacto que configure OpenPGP. + Claves OpenPGP no encontradas + No se ha podido cifrar tu mensaje porque tus contactos no están anunciando sus claves públicas.\n\nPor favor, pide a tus contactos que configuren OpenPGP. + General + Aceptar archivos + De forma automática aceptar archivos menores que… + Adjuntos + Notificaciones + Vibrar + Vibra cuando llega un nuevo mensaje + Luz + La luz parpadea cuando llega un nuevo mensaje + Tono de llamada + Sonido de notificación + Sonido de notificación para nuevos mensajes + Tono para las nuevas llamadas + Periodo de gracia + El tiempo que se silencian las notificaciones después de detectar actividad en uno de sus otros dispositivos. + Avanzado + Nunca informar de errores + Al enviar los detalles sobre el error, ayudará al 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. + Aceptar + Ha ocurrido un error + Error + Tu cuenta + Enviar actualizaciones de presencia + Recibir actualizaciones de presencia + Solicitar actualizaciones de presencia + Seleccionar imagen + Hacer foto + De forma automática conceder suscripción de presencia + El archivo seleccionado no es una imagen + No se pudo comprimir el archivo de imagen + Archivo no encontrado + Error general. ¿Es posible que no tengas espacio en disco? + La aplicación que utilizó para seleccionar la imagen no tiene los permisos necesarios para ver la imagen. +\n +\nUse otro administrador de archivos para seleccionar una imagen. + La aplicación que utilizó para compartir este archivo no tiene suficientes permisos. + Desconocido + Deshabilitado temporalmente + Conectado + Conectando\u2026 + Desconectado + No autorizado + Servidor no encontrado + Sin conectividad + Error en el registro + El identificador ya está en uso + Registro completado + El servidor no soporta registros + Token de registro inválido + Error de negociación TLS + Dominio no verificable + Violación de los términos + Servidor incompatible + Cliente incompatible + Error de flujo + Error al abrir la secuencia + Sin cifrado + OTR + OpenPGP + OMEMO + Eliminar cuenta + Deshabilitar temporalmente + Imagen de perfil + Publicar clave pública OpenPGP + Eliminar la clave pública OpenPGP + ¿Estás seguro de que quieres eliminar tu clave pública OpenPGP de tu anuncio de presencia?\nTus contactos no podrán enviarte mensajes cifrados con OpenPGP. + La clave pública OpenPGP ha sido publicada. + Habilitar + ¿Estás seguro? + Si eliminas tu cuenta tu historial de conversaciones completo se perderá + Grabar audio + Dirección XMPP + Bloquear dirección XMPP + usuario@ejemplo.com + Contraseña + Esta no es una dirección XMPP válida + Sin memoria. La imagen es demasiado grande + ¿Quieres añadir a %s a tus contactos? + Información de servidor + XEP-0313: MAM + XEP-0280: Copias de los mensajes + XEP-0352: Visualización del estado del cliente + XEP-0191: Comando de bloqueo + XEP-0237: Control de las versiones de la lista de contactos + XEP-0198: Gestión del flujo de datos + XEP-0215: Detectando servicios externos + XEP-0163: PEP (Avatares / OMEMO) + XEP-0363: Carga de archivo HTTP + XEP-0357: Notificaciones automáticas + + No + Se han perdido las claves de anuncio públicas + Visto última vez ahora + visto última vez hace un minuto + Visto última vez hace %d minutos + visto última vez hace una hora + Visto última vez hace %d horas + visto última vez hace un día + Visto última vez hace %d días + Mensaje cifrado. Por favor instala OpenKeychain para descifrarlo. + Encontrado un nuevo mensaje cifrado con OpenPGP + Identificador de la clave OpenPGP + 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 + Descargando claves… + Hecho + Descifrar + Marcadores + Buscar + Introducir contacto + Eliminar contacto + Ver detalles del contacto + Bloquear contacto + Desbloquear contacto + Crear + Seleccionar + El contacto ya existe + Unirse + canal@salas.ejemplo.com/nick + canal@salas.ejemplo.com + Guardar en marcadores + Eliminar marcador + Destruir conversación en grupo + Destruir canal + ¿Estás seguro de que quieres destruir esta conversación en grupo?\n\nAviso:La conversación en grupo será eliminada completamente en el servidor. + ¿Estás seguro de que quieres destruir este canal público?\n\nAviso:El canal será eliminado completamente en el servidor. + No se ha podido destruir la conversación en grupo + No se ha podido destruir el canal + Editar asunto de la conversación + Asunto + Uniéndose a un chat de grupo… + Salir + El contacto te ha añadido a su lista de contactos + Añadir contacto + %s ha leído hasta aquí + %s han leído hasta aquí + %1$s + %2$d han leído hasta aquí + Todos han leído hasta aquí + Publicar + Pulsa la imagen de perfil para seleccionar una imagen de la galería + Publicando… + El servidor rechazó la publicación + No se ha podido convertir su imagen + No se ha podido guardar la imagen de perfil en disco + (O pulsación prolongada para volver a tu imagen de la agenda) + Tu servidor no soporta la publicación de imágenes de perfil + en privado + en privado para %s + Enviar mensaje privado a %s + Conectar + Esta cuenta ya existe + Siguiente + Sesión establecida + Omitir + Deshabilitar notificaciones + Habilitar + Esta conversación en grupo requiere contraseña + Introduce la contraseña + Por favor, solicita la actualización de presencia a tu contacto primero.\n\nEsto se usará para determinar qué aplicación de mensajería está usando tu contacto. + Solicitar ahora + Ignorar + Aviso: Si envías esto sin actualización de presencia mutua con tu contacto se podrían producir problemas inesperados.\n\nVe a “Detalles del contacto” para verificar las actualizaciones de presencia. + Seguridad + Corrección de mensaje + Permitir a tus contactos editar mensajes previamente enviados + Opciones para expertos + Por favor, cuidado con estas opciones + Acerca de %s + Horario de silencio + Hora de comienzo + Hora de fin + Habilitar horario de silencio + Las notificaciones serán silenciadas durante el horario de silencio + Otros + Sincronizar marcadores + Establecer la opción \"unirse automáticamente\" cuando entras o sales de un MUC y reaccionar a las modificaciones realizadas por otros clientes. + Huella digital OMEMO copiada al portapapeles + Tu entrada a esta conversación en grupo ha sido prohibida + Esta conversación en grupo es solo para miembros + Limitación de recursos + Has sido expulsado de esta conversación + La conversación en grupo ha sido cerrada + Ya no estás dentro de esta conversación en grupo + Abandonaste esta conversación de grupo por motivos técnicos + Usando cuenta %s + alojado en %s + Comprobando %s en servidor HTTP + No estás conectado. Inténtalo más tarde + Comprobar tamaño de %s + Comprobar tamaño de %1$s en %2$s + Opciones de mensaje + Citar + Pegar como cita + Copiar URL original + Volver a enviar + URL de archivo + URL copiada al portapapeles + Dirección XMPP copiada al portapapeles + Mensaje de error copiado al portapapeles + dirección web + Escanear código QR + Mostrar código QR + Mostrar contactos bloqueados + Detalles de la cuenta + Confirmar + Intentar de nuevo + Servicio en primer plano + Mantener el servicio en primer plano previene que el sistema cierre la conexión + Crear una copia de respaldo + Los ficheros de respaldo serán almacenados en %s + Creando los ficheros de respaldo + Tu copia de respaldo ha sido creada + Los ficheros de respaldo han sido almacenados en %s + Restaurando copia de respaldo + Tu copia de respaldo ha sido restaurada + No olvides activar la cuenta. + Seleccionar archivo + Recibiendo %1$s (%2$d%% completado) + Descargar %s + Eliminar %s + archivo + Abrir %s + Enviando (%1$d%% completado) + Preparando para compartir archivo + %s ofrecido para descarga + Cancelar transferencia + no se ha podido compartir el archivo + transferencia del fichero cancelada + Archivo eliminado + No se ha encontrado ninguna aplicación para abrir el archivo + No se ha encontrado aplicación para abrir el link + No se ha encontrado aplicación para ver el contacto + Etiquetas dinámicas + Mostrar información en forma de etiquetas debajo de los contactos + Habilitar notificaciones + No se ha encontrado el servidor de la conversación en grupo + No se ha podido crear la conversación en grupo + Imagen de perfil + Copiar huella digital OMEMO al portapapeles + Regenerar clave OMEMO + Limpiar dispositivos + ¿Estás seguro de que quieres limpiar todos los otros dispositivos del anuncio OMEMO? La próxima vez que tus dispositivos conecten, tendrán que volver a anunciarse, pero podrían no recibir los mensajes enviados mientras tanto. + No hay claves disponibles para este contacto.\nNo se ha podido obtener nuevas claves del servidor. ¿Es posible que haya algún problema con el servidor de tu contacto? + No hay claves disponibles para este contacto.\nAsegúrate que tenéis actualizaciones de presencia mutua. + Se produjo un error + Buscando historial en el servidor + No hay más historial en el servidor + Actualizando… + ¡Contraseña cambiada! + No se puede cambiar la contraseña + Cambiar contraseña + Contraseña actual + Nueva contraseña + La contraseña no puede ser vacía + Habilitar todas las cuentas + Deshabilitar todas las cuentas + Realizar acción con + Sin afiliación + Desconectado + Rechazado + Miembro + Modo avanzado + Conceder privilegios de miembro + Revocar privilegios de miembro + Conceder privilegios de administrador + Revocar privilegios de administrador + Conceder privilegios de propietario + Revocar privilegios de propietario + Expulsar de la conversación + Eliminar del canal + No se puede cambiar la afiliación de %s + Prohibir entrada en la conversación + Prohibir entrada al canal + Estás intentando eliminar a %s de un canal público. La única manera de hacerlo es prohibir su entrada para siempre. + Prohibir ahora + No se puede cambiar el rol de %s + Configuración de conversación en grupo privada + Configuración del canal público + Privada, solo miembros + Hacer las direcciones XMPP visibles para todos + Hacer que el canal sea moderado + No estás participando + ¡Modificadas las opciones de la conversación! + No se pueden modificar las opciones de la conversación + Nunca + Hasta nuevo aviso + Silenciar + Responder + Marcar como leído + Entrada + Intro para enviar + Utilizar la tecla Enter para enviar un mensaje. Siempre puedes usar Ctrl+Enter para enviar un mensaje, incluso si esta opción está deshabilitada. + Mostrar tecla Intro + Cambiar la tecla de emoticonos por la tecla Intro + audio + vídeo + imagen + gráfico de vectores + archivo multimedia + documento PDF + Android App + Contacto + ¡La imagen de perfil ha sido publicada! + Enviando %s + Ofreciendo %s + Ocultar desconectados + %s está escribiendo… + %s ha dejado de escribir + %s están escribiendo… + %s han dejado de escribir + Notificación de escritura + Permitir a tus contactos saber cuando estás escribiendo un mensaje + Enviar ubicación + Mostrar ubicación + No se ha encontrado ninguna aplicación para mostrar la ubicación + Ubicación + Conversación cerrada + Dejar la conversación en grupo + Dejar el canal público + No confiar en los CAs del sistema + Todos los certificados deben ser aprobados manualmente + Eliminar certificados + Eliminar manualmente certificados aceptados + No aceptar certificados manualmente + Eliminar certificados + Eliminar seleccionados + Cancelar + + %d certificado eliminado + %d certificados eliminados + %d certificados eliminados + + Cambiar el botón de “Enviar” por el botón de acción rápida + Acción rápida + Ninguna + Usada más recientemente + Elegir acción rápida + Buscar contactos + Buscar marcadores + Enviar mensaje privado + %1$s ha dejado la conversación + Usuario + Usuario + Esto no es un usuario válido + Error al descargar: Servidor no encontrado + Error al descargar: Archivo no encontrado + Error al descargar: No se ha podido conectar con el servidor + Falló la descarga: No se puede escribir el fichero + Error al descargar: Archivo no válido + La red Tor no está disponible + Fallo de enlace + 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 + Mostrar como Ocupado cuando el dispositivo esté en modo vibración + Opciones de conexión + Mostrar el hostname y el puerto cuando se está creando una cuenta + xmpp.ejemplo.com + Iniciar sesión con certificado + No se ha podido leer el certificado + Preferencias de archivado + Preferencias de archivado en servidor + Recuperando la configuración del archivo. Espere por favor… + No se ha podido conseguir las preferencias de archivado + Captcha requerido + Introduce el texto de la imagen de arriba + El certificado no es de confianza + La dirección XMPP no coincide con el certificado + Renovar certificado + ¡Error buscando clave OMEMO! + ¡Clave OMEMO con certificado verificada! + ¡Tu dispositivo no soporta la elección de certificados de cliente! + Conexión + Conectar via Tor + Todas las conexiones se realizan a través de la red TOR. Requiere Orbot + Hostname + Puerto + Dirección del servidor o .onion + Éste no es un número de puerto válido + Éste no es un hostame válido + %1$d de %2$d cuentas conectadas + + %d mensaje + %d mensajes + %d mensajes + + Cargar más mensajes + Archivo compartido con %s + Imagen compartida con %s + Imágenes compartidas con %s + Texto compartido con %s + Permitir a %1$s acceder al almacenamiento externo + Permitir a %1$s acceder a la cámara + Sincronizar contactos + %1$s quiere permiso para acceder a tu agenda de contactos y cruzarla con tu lista de contactos de XMPP.\nEsto permitirá mostrar el nombre completo y los avatares de tus contactos.\n\n%1$s solo leerá tu agenda de contactos y la cruzará localmente sin subir nada a tu servidor. +
No guardaremos una copia de estos números de teléfono.\n\nPara más información puedes leer nuestra política de privacidad.

El sistema te preguntará ahora para conceder los permisos de acceso a tus contactos del móvil.]]>
+ Notificar para todos los mensajes + Notificar solo cuando eres mencionado + Notificaciones deshabilitadas + Notificaciones pausadas + Compresión de imagen + Pista: Usa \'Seleccionar archivo\' en lugar de \'Seleccionar imagen\' para enviar imágenes individuales sin comprimir con independencia de los ajustes. + Siempre + Solo imágenes de gran tamaño + Optimizaciones de uso de batería habilitadas + Tu dispositivo está empleando severas optimizaciones del uso de batería por parte de %1$s, lo cual puede hacer que las notificaciones se retrasen o incluso que los mensajes se pierdan.\nEs recomendable deshabilitarlas. + Tu dispositivo está empleando severas optimizaciones del uso de batería por parte de %1$s, lo cual puede hacer que las notificaciones se retrasen o incluso que los mensajes se pierdan.\n\nA continuación se te preguntará si quiere deshabilitarlas. + Deshabilitar + El área seleccionada es demasiado grande + (No hay cuentas activas) + Este campo es requerido + Corregir mensaje + Enviar mensaje corregido + Ya has validado la huella digital de esta persona de forma segura confirmando su confianza. Seleccionando \'Hecho\', estás confirmando que %s es parte de esta conversación en grupo. + Has deshabilitado esta cuenta + Error de seguridad: ¡Acceso a archivo inválido! + No se ha encontrado ninguna aplicación para compartir la URI + Compartir URI con… + Quicksy es un derivado del popular cliente XMPP Conversations con detección automática de contactos.<br><br>El registro se realiza con tu número de teléfono y Quicksy automáticamente—basado en los teléfonos de tu agenda de contactos—te sugerirá posibles contactos.<br><br>Registrándote en Quicksy aceptas nuestra <a href=https://quicksy.im/#privacy>política de privacidad</a>. + Aceptar y continuar + Una guía te ayudará en el proceso de creación de la cuenta en conversations.im. +\nCuando selecciones conversations.im como proveedor podrás comunicarte con usuarios de otros servidores proporcionándoles tu dirección XMPP completa. + Tu dirección XMPP completa será: %s + Crear cuenta + Usar otro proveedor de mi elección + Elige tu nombre de usuario + Gestionar disponibilidad manualmente + Establezca su disponibilidad cuando edite su mensaje de estado. + Mensaje de estado + Libre para hablar + Disponible + Ausente + No disponible + Ocupado + Se ha generado una contraseña segura + Tu dispositivo no soporta la opción de optimización de batería + El registro falló. Prueba de nuevo más tarde + Error en el registro: La contraseña es demasiado débil + Elige a los participantes + Creando un chat de grupo… + Invitar de nuevo + Deshabilitar + Corto + Medio + Largo + Uso de difusión + Permite que tus contactos sepan cuando usas Conversations + Privacidad + Tema + Selecciona el color de la paleta + Automático + Claro + Oscuro + Fondo verde + Usar fondo verde para mensajes recibidos + No se ha podido conectar a OpenKeychain + Este dispositivo ya no está en uso + Ordenador + Teléfono móvil + Tableta + Navegador + Consola + Pago requerido + Otorgue permiso de acceso a internet + Yo + El contacto solicita ver tus actualizaciones de estado + Permitir + Sin permiso de acceso a %s + Servidor no encontrado + Tiempo de espera agotado al servidor remoto + No se ha podido actualizar la cuenta + Reporta esta dirección XMPP como spam. + Eliminar identidades OMEMO + Regenerar tus clave OMEMO. Todos tus contactos tendrán que verificarte de nuevo. Usa esta opción como último recurso. + Eliminar claves seleccionadas + Necesitas estar conectado para publicar un avatar. + Mostrar mensaje de error + Mensaje de error + Optimización de datos habilitado + Tu sistema operativo está restringiendo a %1$s el acceso a Internet cuando está en segundo plano. Para recibir notificaciones de nuevos mensajes deberías permitir a %1$s un acceso sin restricciones cuando la optimización de datos está habilitada.\n%1$s se esforzará igualmente en ahorrar datos cuando sea posible. + Tu dispositivo no soporta la opción de deshabilitar la optimización de datos para %1$s. + No se ha podido crear el archivo temporal + Este dispositivo ha sido verificado + Copiar huella digital + Has verificado todas las huellas digitales OMEMO en tu posesión + El código QR no contiene huellas digitales para esta conversación. + Huellas digitales verificadas + Usa la cámara para escanear el código QR del contacto + Por favor, espera a que las claves sean recuperadas + Compartir como código QR + Compartir como XMPP URI + Compartir como link HTTP + Confianza ciega antes de verificación + Confiar en los nuevos dispositivos de tus contactos no verificados, pero solicitar confirmación manual para los nuevos dispositivos de tus contactos verificados. + Confiar ciegamente en las claves OMEMO, lo que significa que tus contactos podrían ser otra persona o alguien podría haber intervenido. + No confiables + Código QR inválido + Limpiar caché de datos (usado por la aplicación de la cámara) + Limpiar caché + Limpiar datos privados + Limpiar datos privados de ficheros descargados (Pueden volver a descargarse desde el servidor) + Enlace desde una fuente de confianza + Está a punto de verificar las claves OMEMO de %1$s después de hacer clic en un enlace. Esto solo es seguro si siguió este enlace desde una fuente confiable donde solo %2$s podría haber publicado este enlace. + Está a punto de verificar las claves OMEMO de su propia cuenta. Esto solamente es seguro si ha seguido este enlace desde una fuente segura, donde solo usted lo haya publicado. + Continuar + Verificar claves OMEMO + Mostrar inactivos + Ocultar inactivos + Desconfiar de este dispositivo + ¿Estás seguro de que quieres eliminar la verificación de este dispositivo?\nEste dispositivo y los mensajes que lleguen desde allí serán marcados como \"No confiables\". + + %d segundo + %d segundos + %d segundos + + + %d minuto + %d minutos + %d minutos + + + %d hora + %d horas + %d horas + + + %d día + %d días + %d días + + + %d semana + %d semanas + %d semanas + + + %d mes + %d meses + %d meses + + Borrado automático de mensajes + Automáticamente borrar mensajes del dispositivo más antiguos que el configurado en el marco de tiempo. + Cifrando mensaje + No buscar mensajes más antiguos que el establecido en el marco de tiempo. + Comprimiendo video + Conversación correspondiente cerrada. + Contacto bloqueado. + Notificaciones de desconocidos + Notificar de nuevos mensajes y llamadas recibidas de contactos desconocidos. + Mensaje recibido de un contacto desconocido + Bloquear desconocido + Bloquear el dominio completo + Conectado ahora mismo + Reintentar descifrado + Fallo de sesión + Mecanismo SASL degradado + El servidor requiere registro en su página web + Abrir página web + No se ha encontrado aplicación para abrir el sitio web + Notificaciones emergentes + Mostrar ventana emergente al recibir una notificación + Hoy + Ayer + Validar hostname con DNSSEC + Los certificados del servidor que contienen el hostname validado son considerados verificados + El certificado no contiene una dirección XMPP completa + Parcial + Grabar video + Copiar al portapapeles + Mensaje copiado en el portapapeles + Mensaje + Los mensajes privados están deshabilitados + Aplicaciones protegidas + Para seguir recibiendo notificaciones, aunque la pantalla esté apagada, tienes que añadir Conversaciones a la lista de aplicaciones protegidas. + ¿Aceptar certificado desconocido? + El certificado del servidor no está firmado por una Autoridad Certificadora conocida. + ¿Aceptar nombre del servidor no coincidente? + El servidor no pudo autenticarse como \"%s\". El certificado es solo válido para: + ¿Quieres conectar de todas formas? + Detalles del Certificado: + Una vez + El escáner de código QR necesita acceso a la cámara + Desplazarse hasta abajo + Desplazarse hasta abajo después de mandar un mensaje + Editar Mensaje de Estado + Editar mensaje de estado + Deshabilitar cifrado + %1$s no puede enviar mensajes cifrados a %2$s. Esto puede deberse a que tu contacto está usando un servidor o un cliente desactualizado que no puede manejar las claves OMEMO. + No se ha podido conseguir la lista de dispositivos + No se han podido conseguir las claves de cifrado + Consejo: En algunas ocasiones esto puede corregirse agregando a tu contacto a tu lista de contactos. Tu contacto deberá asegurarse también que estás en su lista de contactos. + ¿Estás seguro de que quieres deshabilitar el cifrado OMEMO para esta conversación?\nEsto permitiría al administrador de tu servidor leer tus mensajes, aunque esta podría ser la única via de comunicación con personas que usen clientes desactualizados. + Deshabilitar ahora + Borrador: + Cifrado OMEMO + OMEMO siempre será usado para conversaciones uno a uno y en conversaciones en grupo privadas. + OMEMO será usado por defecto para nuevas conversaciones. + OMEMO tendrá que ser explícitamente activado para nuevas conversaciones. + Crear acceso directo + Tamaño de fuente + Tamaño de fuente usado en la aplicación. + Activo por defecto + Desactivado por defecto + Pequeño + Mediano + Grande + El mensaje no fue cifrado para este dispositivo. + Error al descifrar el mensaje cifrado con OMEMO. + deshacer + Compartir ubicación está deshabilitado + Fijar posición + Desfijar posición + Copiar ubicación + Compatir Ubicación + Direcciones + Compartir ubicación + Mostrar ubicación + Compartir + No se ha podido empezar la grabación + Espere por favor… + Permitir a %1$s acceder al micrófono + Buscar mensajes + GIF + Ver conversación + Plugin para Compartir Ubicación + Usar el Plugin Compartir Ubicación en lugar del propio de la aplicación + Copiar dirección web + Copiar dirección XMPP + Compartición de Archivos mediante S3 + Búsqueda directa + En la pantalla de \'Nueva Conversación\' abrir el teclado y poner el cursor en el campo de búsqueda + Avatar de la conversación en grupo + El servidor no soporta avatares en conversaciones en grupo + Solo el propietario de la conversación puede cambiar el avatar + Nombre del contacto + Apodo + Nombre + Añadir un nombre es opcional + Nombre de la Conversación en grupo + Esta conversación en grupo ha sido destruida + No se ha podido guardar la grabación + Servicio en primer plano + Esta categoría de notificación se usa para mostrar una notificación permantente indicando que %1$s está ejecutándose. + Información de estado + Problemas de conectividad + Esta categoría de notificación se usa para mostrar una notificación en caso de que exista un problema conectándose a una cuenta. + Mensajes + Llamadas + Mensajes + Llamadas entrantes + Llamadas salientes + Llamadas perdidas + Mensajes sin sonido + Este grupo de notificaciones se usa para mostrar notificaciones que no deberían emitir ningún sonido. Por ejemplo, cuando estás activo en otro dispositivo (periodo de gracia). + Envíos fallidos + Ajustes de notificación de mensajes + Ajustes de notificación de llamadas + Importancia, Sonido, Vibración + Compresión de video + Ver galería + Participantes + Galería + El archivo se omitió debido a una violación de seguridad. + Calidad del video + Calidad más baja indica archivos más pequeños + Medio (360p) + Alta (720p) + cancelado + Ya estás redactando un mensaje. + Funcionalidad no implementada + Código de país no válido + Elige un país + número de teléfono + Verifica tu número de teléfono + Quicksy te enviará un mensaje SMS (se podrían aplicar cargos) para verificar tu número de teléfono. Introduce tu código de país y tu número de teléfono: +
%s

¿Quieres continuar o te gustaría modificar el número?]]>
+ %sno es un número válido. + Por favor, introduce tu número de teléfono. + Buscar países + Verificar %s + %s.]]> + Hemos enviado otro mensaje SMS con un código de 6 dígitos. + Por favor, introduce el código de 6 dígitos abajo. + Reenviar SMS + Reenviar SMS (%s) + Por favor, espera (%s) + atrás + Automáticamente pegar el posible pin del portapapeles. + Por favor, introduce tu código de 6 dígitos. + ¿Estás seguro de que quieres abortar el proceso de registro? + + No + Verificando… + Solicitando un mensaje de texto… + El código que has introducido no es correcto. + El código que te hemos enviado ha expirado. + Error desconocido de red. + Respuesta de servidor desconocida. + No se ha podido conectar al servidor. + No se ha podido establecer una conexión segura. + No se ha podido encontrar el servidor. + Algo fue mal procesando tu solicitud. + Entrada de usuario no válida + Temporalmente no disponible. Inténtalo más tarde. + No hay conexión de red. + Por favor, inténtalo en %s + Tienes una tasa limitada + Demasiados intentos + Estás usando una versión desactualizada de esta aplicación. + Actualizar + Este teléfono está registrado en otro dispositivo. + Por favor, introduce tu nombre para dejar que las personas que no están en tu agenda de contactos sepan quien eres. + Tu nombre + Introduce tu nombre + Usa este botón para establecer tu nombre. + Rechazar petición + Instalar Orbot + Iniciar Orbot + No hay tienda de aplicaciones instalada. + Este canal hará tu dirección XMPP visible públicamente + e-book + Original (sin comprimir) + Abrir con… + Foto del perfil de conversaciones + Elige una cuenta + Restaurar copia de respaldo + Restaurar + Introduce tu contraseña para la cuenta %s para restaurar la copia de respaldo. + No utilices la opción de restaurar una copia de respaldo para clonar (ejecutar simultáneamente) una instalación. Restaurar una copia de respaldo se debe utilizar solo para migraciones o en caso de que hayas perdido el dispositivo original. + No se ha podido restaurar la copia de respaldo. + No se ha podido descifrar la copia de respaldo. ¿Es la contraseña correcta? + Respaldar & Restaurar + Introduce dirección XMPP + Crear una conversación en grupo + Unirse a canal público + Crear una conversación en grupo privada + Crear un canal público + Nombre del canal + Dirección XMPP + Por favor, proporciona un nombre para el canal + Por favor, proporciona una dirección XMPP + Esta es una dirección XMPP. Introduce un nombre. + Creando un canal público… + Esta canal ya existe + Te has unido a un canal existente + No se ha podido guardar la configuración del canal + Permitir a cualquiera editar el asunto + Permitir a cualquiera invitar a otros contactos + Cualquiera puede editar el tema. + Los propietarios pueden editar el asunto. + Los administradores pueden editar el asunto. + Los propietarios pueden invitar a otros contactos. + Todos pueden invitar a otros contactos. + Las direcciones XMPP son visibles para los administradores. + Las direcciones XMPP son visibles para todos. + Este canal público no tiene participantes. Invita a tus contactos o usa el botón para distribuir la dirección XMPP del canal. + Esta conversación en grupo privada no tiene participantes. + Gestionar privilegios + Buscar participantes + Archivo demasiado grande + Adjuntar + Descubrir canales + Buscar canales + ¡Posible violación de privacidad! + search.jabbercat.org
.

Usando esta funcionalidad transmitirás tu dirección IP y los términos buscados a este servicio. Ver su Política de Privacidad para más información.]]>
+ Ya tengo una cuenta + Añadir una cuenta existente + Registrar una cuenta nueva + Esto parece una dirección de dominio + Añadir de todas formas + Esto parece una dirección de un canal + Compartir ficheros de respaldo + Respaldo de Conversations + Evento + Abrir respaldo + El fichero seleccionado no es un respaldo de Conversations + Esta cuenta ya fue configurada + Por favor ingrese la contraseña para esta cuenta + No se ha podido realizar esta acción + Uniéndose a un canal público… + La aplicación de compartir no concedió permisos para acceder a este fichero. + + jabber.network + Servidor local + La mayoría de los usuarios deberían elegir \'jabber.network\' para mejores sugerencias de todo el ecosistema público XMPP. + Método para la búsqueda de Canales + Copia de respaldo + Acerca de + Por favor, habilita una cuenta + Hacer una llamada + Llamada entrante + Videollamada entrante + ¿Cambiar a videollamada? + ¿Añadir pistas adicionales? + Conectando + Conectado + Reconectando + Aceptar llamada + Terminar llamada + Contestar + Descartar + Localizando dispositivos + Llamando + Ocupado + No se ha podido realizar la llamada + Conexión perdida + Llamada rechazada + Fallo en la aplicación + Problema de verificación + Colgar + Llamada saliente + Video llamada saliente + Reconectando llamada + Reconectando video llamada + Deshabilitar Tor para hacer llamadas + Llamada entrante + Llamada entrante · %s + Llamada perdida · %s + Llamada saliente + Video llamada saliente · %s + Llamada perdida + + %1$d llamada perdida de %2$s + %1$d llamadas perdidas de %2$s + %1$d llamadas perdidas de %2$s + + + %d llamada perdida + %d llamadas perdidas + %d llamadas perdidas + + + %1$d llamadas perdidas de %2$d contacto + %1$d llamadas perdidas de %2$d contacto + %1$d llamadas perdidas de %2$d contactos + + Audio llamada + Video llamada + Ayuda + Cambiar a conversación + Tu micrófono no está disponible + Solo se puede hacer una llamada a la vez. + Volver a la llamada en curso + No se ha podido cambiar de cámara + Fijar en la parte superior + Desfijar de la parte superior + Recorrido GPX + No se pudo corregir el mensaje + Todas las conversaciones + Esta conversación + Tu imagen de perfil + Imagen de perfil de %s + Encriptado con OMEMO + Encriptado con OpenPGP + Sin encriptar + Salir + Grabar mensaje de voz + Reproducir audio + Pausar audio + Añadir contacto, crear o unirse a un grupo de chat, o descubrir canales + + Ver %1$d Participante + Ver %1$d Participantes + Ver %1$d Participantes + + + Un mensaje no se ha podido entregar + Algunos mensajes no se han podido entregar + Algunos mensajes no se han podido entregar + + Envíos fallidos + Más opciones + No se ha encontrado aplicación + Invitar a Conversations + 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 + Los registros de cuenta no están soportados + Dirección XMPP no encontrada + Fallo temporal de autenticación + Eliminar imagen de perfil + Las llamadas están deshabilitadas cuando se usa Tor + Cambiar a vídeo + Rechazar petición de cambiar a vídeo + Distribuidor de UnifiedPush + Cuenta XMPP + La cuenta a través de la cual se recibirán los mensajes push. + Servidor push + Un servidor push elegido por el usuario para transmitir mensajes push a través de XMPP a su dispositivo. + Ninguno (desactivado) + \ No newline at end of file diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml new file mode 100644 index 000000000..cfd94fcb3 --- /dev/null +++ b/app/src/main/res/values-eu/strings.xml @@ -0,0 +1,758 @@ + + + Ezarpenak + Elkarrizketa berria + Kontuak kudeatu + Kontua kudeatu + Kontaktuaren xehetasunak + Taldearen xehetasunak + Kanalaren xehetasunak + Kontua gehitu + Izena editatu + Helbideen liburura gehitu + Zerrendatik ezabatu + Kontaktua blokeatu + Kontaktua desblokeatu + Domeinua blokeatu + Domeinua desblokeatu + Parte-hartzailea blokeatu + Parte-hartzaileari blokeoa kendu + Kontuak kudeatu + Ezarpenak + Elkarrizketa batekin partekatu + Elkarrizketa hasi + Kontaktua hautatu + Kontaktuak hautatu + Kontu bidez partekatu + Blokeatutakoen zerrenda + orain + min 1 lehenago + %d min lehenago + bidaltzen… + Mezua desenkriptatzen. Mesedez itxaron… + OpenPGPz enkriptatutako mezua + Ezizena erabilita dagoeneko + Ezizen ez balioduna + Administratzailea + Jabea + Moderatzailea + Parte-hartzailea + Bisitaria + %s(e)k zuri mezuak bidaltzea blokeatu nahi al duzu? + %s desblokeatu eta zuri mezuak bidali ahal izatea onartu nahi al duzu? + %s(r)en kontaktu guztiak blokeatu? + %s(r)en kontaktu guztiak desblokeatu? + Kontaktua blokeatu da + Blokeatuta + Kontu berria zerbitzarian erregistratu + Pasahitza zerbitzarian aldatu + Partekatu honekin... + Elkarrizketa hasi + Kontaktua gonbidatu + Gonbidatu + Kontaktuak + Kontaktua + Utzi + Ezarri + Gehitu + Editatu + Ezabatu + Blokeatu + Desblokeatu + Gorde + Ados + Bidali orain + Ez galdetu berriz + Fitxategia erantsi + Kontaktua gehitu + huts bidaltzerakoan + Fitxategiak partekatzen. Mesedez itxaron... + Historia garbitu + Elkarrizketa historia garbitu + Fitxategia ezabatu + Fitxategi hau ezabatu nahi al duzu?\n\nAbisua: Honek ez du beste gailu edo zerbitzarietan gordetako fitxategi honen kopiak ezabatuko. + Elkarrizketa hau jarraian itxi + Gailua aukeratu + Enkriptatu gabeko mezua bidali + Mezua bidali + %s(r)i mezua bidali + OMEMOz enkriptatutako mezua bidali + v\\OMEMOz enkriptatutako mezua bidali + OpenPGPz enkriptatutako mezua bidali + Enkriptatu gabe bidali + Desenkriptazioak huts egin du. Agian ez duzu gako pribatu egokia. + OpenKeychain + Berrabiarazi + Instalatu + Mesedez instalatu ezazu OpenKeychain + eskeintzen… + itxaroten… + Ez da OpenPGP gakorik aurkitu + Ez da OpenPGP gakorik aurkitu + Orokorrak + Fitxategiak onartu + Hurrengo tamaina baino fitxategi txikiagoak automatikoki onartu… + Eranskinak + Jakinarazpena + Dardaratu + Mezu berri bat heltzerakoan dardartu + LED jakinarazpena + Mezu berri bat heltzerakoan jakinarazpenen argia keinu egin + Dei-tonua + Grazia epea + Aurreratua + Gelditze txostenik ez bidali inoiz + Mezuak egiaztatu + Zure kontaktuak haien mezuak noiz jaso eta irakurri dituzun jakin dezaten baimendu + Erabiltzaile-interfazea + Enkriptatzeko gako okerra. + Onartu + Akats bat gertatu da + Akatsa + Zure kontua + Presentzia eguneraketak bidali + Presentzia eguneraketak jaso + Presentzia eguneraketak eskatu + Argazkia aukeratu + Argazkia egin + Prebentiboki harpidetza eskaera eman + Aukeratu duzun fitxategia ez da irudi bat + Fitxategia ez da aurkitu + Sarrera/Irteera akats orokorra. Agian biltegian lekurik gabe gelditu zara? + Ezezaguna + Aldi baterako ezgaituta + Konektatuta + Konektatzen\u2026 + Lineaz kanpo + Ez baimenduta + Zerbitzaria ez da aurkitu + Konektagarritasunik ez + Erregistroak huts egin du + Erabiltzaile izena dagoeneko erabilita + Erregistroa burutu da + TLN negoziazioak huts egin du + Politikaren urraketa + Zerbitzari ez bateragarria + Akatsa korrontean + Akatsa korrontea irekitzerakoan + Enkriptatu gabe + OTR + OpenPGP + OMEMO + Kontua ezabatu + Aldi baterako ezgaitu + Profileko argazkia argitaratu + OpenPGP gako publikoa argitaratu + OpenPGP gako publikoa kendu + Ziur zure OpenPGP gako publikoa zure presentzia eguneraketetatik kendu nahi duzula?\nZure kontaktuek ezin dizute gehiago OpenPGPz enkriptatutako mezuak bidali. + Kontua gaitu + Ziur al zaude? + Ahotsa grabatu + XMPP helbidea + XMPP helbidea blokeatu + erabiltzailea@adibidea.com + Pasahitza + Hau ez da XMPP helbide baliodun bat + %s zure helbideen liburura gehitu nahi duzu? + Zerbitzariaren informazioa + XEP-0313: MAM + XEP-0280: Message Carbons + XEP-0352: Client State Indication + XEP-0191: Blocking Command + XEP-0237: Roster Versioning + XEP-0198: Stream Management + XEP-0163: PEP (Avatars / OMEMO) + XEP-0363: HTTP File Upload + XEP-0357: Push + eskuragarri + ez eskuragarri + Gako publikoen iragarpenak faltan + azkenengoz ikusia orain + azkenengoz ikusia %d minutu lehenago + azkenengoz ikusia %d ordu lehenago + azkenengoz ikusia %d egun lehenago + OpenPGP gakoaren nortasuna + OMEMO hatz-marka + v\\OMEMO hatz-marka + Beste gailuak + OMEMO hatz-marketaz fidatu + Gakoak eskuratzen... + Eginda + Desenkriptatu + Laster-markak + Bilatu + Kontaktua sartu + Kontaktua ezabatu + Kontaktuaren xehetasunak ikusi + Kontaktua blokeatu + Kontaktua desblokeatu + Sortu + Hautatu + Kontaktua existitzen da dagoeneko + Batu + kanala@konferentzia.adibidea.eus/ezizena + kanala@konferentzia.adibidea.eus + Gorde laster-marka bezala + Laster-marka ezabatu + Taldea deuseztatu + Kanala deuseztatu + Talde hau deuseztatu nahi duzu?\n\nAbisua: Taldea guztiz ezabatuko da zerbitzaritik. + Kanal publiko hau deuseztatu nahi duzu?\n\nAbisua: Kanala zerbitzaritik guztiz kenduko da. + Ezin izan da taldea deuseztatu + Ezin izan da kanala deuseztatu + Taldearen gaia editatu + Gaia + Taldera batzen... + Alde egin + Kontaktuak bere zerrendara gehitu zaitu + Bera gehitu + %s(e)k puntu honetaraino irakurri du + %s(e)k puntu honetaraino irakurri du + Guztiek puntu honetaraino irakurri dute + Argitaratu + Argitaratzen… + Zerbitzariak zure argitarapena ukatu du + Ezin izan da profileko argazkia diskoan gorde + (Edo sakatu luze lehenetsira bueltatzeko) + xuxurlatu du + %s(r)i + %s(r)i mezu pribatua bidali + Konektatu + Kontu hau existitzen da dagoeneko + Hurrengoa + Orain ez + Jakinarazpenak ezgaitu + Gaitu + Taldeak pasahitza behar du + Sartu pasahitza + Eskatu orain + Kasurik ez egin + Segurtasuna + Mezuen zuzenketa baimendu + Zure kontaktuak haien mezuak atzeraeraginez editatzea baimendu + Adituentzako ezarpenak + Mesedez kontuz ibili hauekin + %s buruz + Ordu lasaiak + Hasiera ordua + Amaiera ordua + Ordu lasaiak gaitu + Jakinarazpenak isilaraziko dira ordu lasaiak iraun bitartean + Besteak + Talde honetara sartzea debekatuta duzu + Talde hau kideentzat da soilik + Baliabide murrizketa + Talde honetatik kanporatua izan zara + Taldea itzali egin da + Ez zaude dagoeneko talde honetan + %s kontua erabiltzen + %s egiaztatzen HTTP ostalarian + Ez zaude konektatuta. Saiatu beranduago berriz + Egiaztatu %sren neurria + Egiaztatu %1$sren neurria %2$s ostalarian + Mezuaren aukerak + Aipatu + Aipamen bezala itsatsi + Jatorrizko URLa kopiatu + Berriro bidali + Fitxategiaren URLa + URLa arbelera kopiatu da + XMPP helbidea arbelera kopiatu da + Akats mezua arbelera kopiatu da + web helbidea + 2D barra kodea eskaneatu + 2D barra kodea erakutsi + Blokeatutakoen zerrenda ikusi + Kontuaren xehetasunak + Baieztatu + Saiatu berriz + Aurreko planoko zerbitzua + Sistema eragileak zure konexioa hiltzea galarazten du + Babes-kopia sortu + Babes-kopiaren fitxategiak %s(e)n gordeko dira + Babes-kopiaren fitxategiak sortzen + Zure babes-kopia sortu da + Babes-kopiaren fitxategiak %s(e)n gorde dira + Babes-kopia berrezartzen + Zure babes-kopia berrezarri da + Ez ahaztu kontua gaitzeaz. + Fitxategia aukeratu + %1$s jasotzen (%2$d%% osatua) + %s deskargatu + Ezabatu %s + fitxategia + %s ireki + bidaltzen (%1$d%% osatua) + %s deskargatzeko eskeinita + Transmisioa utzi + fitxategiaren transmisioa utzi egin da + Etiketa dinamikoak + Irakurtzeko soilik diren etiketak erakutsi kontaktuen azpian + Jakinarazpenak gaitu + Ez da talde zerbitzaririk aurkitu + Kontuaren profileko argazkia + OMEMO hatz-marka arbelara kopiatu + OMEMO gakoa birsortu + Gailuak garbitu + Zerbait gaizki joan da + Mezuak zerbitzaritik eskuratzen + Mezu gehiagorik ez zerbitzarian + Eguneratzen... + Pasahitza aldatu da + Pasahitza ezin izan da aldatu + Pasahitza aldatu + Oraingo pasahitza + Pasahitz berria + Kontu guztiak gaitu + Kontu guztiak ezgaitu + Ekintza honekin egin + Afiliaziorik ez + Lineaz kanpo + Baztertutakoa + Kidea + Modu aurreratua + Kide baimenak eman + Kide baimenak ezezaztu + Administratzaile baimenak eman + Administratzaile baimenak ezeztatu + Jabe baimenak eman + Jabe baimenak ezezaztu + Taldetik kendu + Kanaletik kendu + %s(r)en afiliazioa ezin izan da aldatu + Taldean egotea debekatu + Kanalean egotea debekatu + Debekatu orain + %s(r)en rola ezin izan da aldatu + Talde pribatuaren konfigurazioa + Kanal publikoaren konfigurazioa + Pribatua, kideentzat soilik + XMPP helbideak edonorentzako ikusgai bihurtu + Kanala moderatua bihurtu + Ez zara parte hartzen ari + Taldearen aukerak aldatu dira + Ezin izan dira taldearen aukerak aldatu + Inoiz + abisatu arte + Beranduago jakinarazi + Erantzun + Irakurrita bezala markatu + Sarrera + Sartu teklak bidaltzen du + Sartu tekla erakutsi + Aurpegieren tekla sartu teklarekin aldatu + audioa + bideoa + irudia + PDF dokumentua + Android aplikazioa + Kontaktua + Profileko argazkia argitaratu da + %s bidaltzen + %s eskeintzen... + Lineaz kanpokoak ezkutatu + %s idazten ari da... + %s(e)k idazteari utzi dio + %s idazten ari dira… + %s idazteari utzi diote + Idazketa jakinarazpenak + Zure kontaktuak mezu berri bat noiz idazten ari zaren jakin dezaten baimendu + Kokapena partekatu + Kokapena erakutsi + Kokapena + Elkarrizketa itxi egin da + Talde pribatua utzi da + Kanal publikoa utzi da + Sistemaren CAtaz ez fidatu + Ziurtagiri guztiak eskuz onartu behar dira + Ziurtagiriak kendu + Eskuz ezabatu onartutako ziurtagiriak + Ez dago eskuz onartutako ziurtagiririk + Ziurtagiriak kendu + Aukeratutakoak ezabatu + Utzi + + Ziurtagiri %d ezabatua + %d ziurtagiri ezabatuak + + Ekintza azkarra + Bat ere ez + Azkenengo aldiz erabilitakoa + Ekintza azkarra aukeratu + Kontaktuak bilatu + Laster-marketan bilatu + Mezu pribatua bidali + Erabiltzaile izena + Erabiltzaile izena + Hau ez da erabiltzaile izen baliodun bat + Deskargak huts egin du: zerbitzaria ez da aurkitu + Deskargak huts egin du: fitxategia ez da aurkitu + Deskargak huts egin du: ezin izan da ostalarira konektatu + Deskargak huts egin du: ezin izan da fitxategia idatzi + Tor sarea ez dago eskuragarri + Estekatzeak hust egin du + Hondatuta + Eskuragarritasuna + Dardara modu isila bezala tratatu + Konexioaren ezarpen luzatuak + Ostalariaren izena eta ataka ezarpenak erakutsi kontu bat ezartzerakoan + xmpp.adibidea.com + Artxibatze hobespenak + Zerbitzariaren aldeko artxibatze hobespenak + Artxibatze hobespenak eskuratzen. Mesedez itxaron... + Sar ezazu goiko irudiko testua + XMPP helbideak ez du ziurtagiriarekin bat egiten + Ziurtagiria berriztu + Akatsa OMEMO gakoa eskuratzerakoan! + OMEMO gakoa ziurtagiriarekin egiaztatuta! + Zure gailuak ez du bezero ziurtagiriak aukeratzea onartzen! + Konexioa + Tor bidez konektatu + Konexio guztiak Tor sarean zehar igaro. Orbot behar du + Ostalariaren izena + Ataka + Hau ez da ataka zenbaki balioduna + Hau ez da ostalari izen balioduna + %2$d tik %1$d kontu konektatuta + + mezu %d + %d mezu + + Mezu gehiago kargatu + Kontaktuekin sinkronizatu + Mezu guztiak jakinarazi + Jakinarazi aipatua izaterakoan soilik + Jakinarazpenak ezgaituta + Jakinarazpenak gelditu dira + Irudiak konprimatu + Beti + Irudi handiak soilik + Bateriaren optimizazioak gaituta + Ezgaitu + Hautatutako zatia handiegia da + (Ez dago kontu aktiborik) + Datu hau beharrezkoa da + Mezua zuzendu + Mezu zuzendua bidali + Kontu hau ezgaitu duzu + URIa honekin partekatu... + Zure XMPP helbide osoa hau izango da: %s + Kontua sortu + Nire hornitzale propioa erabili + Aukeratu zure erabiltzaile izena + Kudeatu eskuragarritasuna eskuz + Ezarri zure eskuragarritasuna zure egoera mezua editatzerakoan. + Egoera mezua + Hitzegiteko aske + Konektatuta + Kanpoan + Ez eskuragarri + Lanpetuta + Pasahitz seguru bat sortu da + Zure gailuak ez du bateria optimizatzeko aukerarik ematen + Erregistroak huts egin du: saiatu berriz beranduago + Erregistroa huts egin du: pasahitza ahulegia da + Parte hartzaileak hautatu + Taldea sortzen… + Berriz gonbidatu + Ezgaitu + Laburra + Ertaina + Luzea + Pribatutasuna + Gaia + Kolore paleta hautatu + Automatikoa + Atzealde berdea + Atzealde berdea erabili jasotako mezuentzat + Gailu hau ez da gehiago erabiltzen + Ordenagailua + Mugikorra + Tableta + Web nabigatzailea + Kontsola + Ordainketa beharrezkoa da + Ni + Kontaktuak presentzia harpidetza eskatzen du + Baimendu + %s(e)ra sartzeko baimenik ez + Urruneko zerbitzaria ez da aurkitu + Denbora agortu da urruneko zerbitzarian + OMEMO nortasunak ezabatu + Hautatutako gakoak ezabatu + Konektatuta egon behar zara zure profileko argazkia argitaratzeko. + Akatsaren mezua erakutsi + Akatsaren mezua + Datuen aurreztailea gaituta + Gailu hau egiaztatu da + Hatz-marka kopiatu + Egiaztatutako hatz-markak + Kamera erabili kontaktuaren barra kodea eskaneatzeko + Mesedez itxaron gakoak ekarri arte + Barra kode bezala partekatu + XMPP URI bezala partekatu + HTTP link bezala partekatu + Egiaztatu aurreko fidagarritasun itsua + Ez fidagarria + 2D barra kodea baliogabea + Cachea garbitu + Biltegi pribatua garbitu + Fitxategiak gordetzeko erabiltzen den biltegi pribatua garbitu (zerbitzaritik berriro deskargatu daitezke) + Lotura hau jatorri fidagarri batetik jarraitu dut + %1$s(r)en OMEMO gakoak egiaztatuko dira lotura batean sakatu ondoren. Hau segurua da soilik lotura hau jatorri fidagarri batetik jarraitu baduzu eta soilik %2$s(e)k argitaratu izan ahal badu. + OMEMO gakoak egiaztatu + Ez aktiboak erakutsi + Ez aktiboak ezkutatu + Gailu ez fidagarria + + Segundu %d + %d segundu + + + Minutu %d + %d minutu + + + Ordu %d + %d ordu + + + Egun %d + %d egun + + + Aste %d + %d aste + + + Hilabete %d + %d hilabete + + Mezuen ezabatze automatikoa + Ezarritako denbora tartea baino zaharragoak diren mezuak gailu honetatik automatikoki ezabatu. + Mezua enkriptatzen + Mezurik ez eskuratzen gelditze tarte lokalarengatik. + Bideoa konprimatzen + Dagokion elkarrizketa itxi egin da. + Kontaktua blokeatu da. + Ezezagunen jakinarazpenak + Ezezagun baten mezu bat jaso duzu + Ezezaguna blokeatu + Domeinu osoa blokeatu + orain konektatuta + Saiatu berriz desenkriptatzen + Saioaren akatsa + SASL mekanismoa beheratua + Zerbitzariak webgunean izena ematea eskatzen du + Webgunea ireki + Goranzko jakinarazpenak + Gaur + Atzo + Ostalariaren izena DNSSECekin balioztatu + Ostalariaren izena balioztatuta daukaten zerbitzarien ziurtagiriak egiaztatutzat hartzen dira + Ziurtagiriak ez du XMPP helbide bat + partziala + Bideoa grabatu + Arbelera kopiatu + Mezua arbelera kopiatu da + Mezua + Mezu pribatuak ezgaituta daude + Babestutako aplikazioak + Jakinarazpenak jasotzen jarraitu nahi naduzu, baita pantaila itzalita dagoenean ere, Conversations babestutako aplikazioen zerrendan gehitu behar duzu. + Ziurtagiri ezezaguna onartu? + Zerbitzariaren ziurtagiria ez dago ezaguna den Ziurtagiri jaulkitzaile batez sinatuta. + Bat ez datorren zerbitzari izena onartu? + Zerbitzariak ezin izan du \"%s\" bezala autentifikatu. Ziurtagiria honetarako da balioduna soilik: + Konektatu nahi al duzu hala ere? + Ziurtagiriaren xehetasunak: + Behin + QR kode eskanerrak kamerara sarbidea behar du + Behera joan + Jaitsi mezu bat bidali ostean + Egoera mezua editatu + Egoera mezua editatu + Enkriptazioa ezgaitu + Lagungarria: kasu batzuetan bakoitzak bestea bere kontaktuen zerrendara gehituz konpon daiteke. + Ziur al zaude OMEMO enkriptazioa ezgaitu nahi duzula elkarrizketa honetarako?\nHonek zure zerbitzariaren administratzaileari zure mezuak irakurtzea ahalbidetuko dio, baina aplikazio zaharrak erabiltzen dituzten pertsonekin komunikatzeko modu bakarra izan daiteke. + Orain ezgaitu + Zirriborroa: + OMEMO enkriptazioa + OMEMO beti erabiliko da biren artean eta taldeko txat pribatetuetan + OMEMO elkarrizketa berrietan lehenetsi bezala erabiliko da + OMEMO esplizituki piztu beharko da elkarrizketa berrietarako + Lasterbidea sortu + Letraren neurria + Aplikazioaren barruan erabiliko den letraren neurri erlatiboa. + Piztuta lehenetsi bezala + Itzalita lehenetsi bezala + Txikia + Ertaina + Handia + Mezua ez da enkriptatu gailu honentzat. + Ezin izan da OMEMO mezua desenkriptatu. + desegin + Kokapena partekatzea ezgaituta dago + Kokapena finkatu + Kokapena askatu + Kokapena kopiatu + Kokapena partekatu + Norabideak + Kokapena partekatu + Kokapena erakutsi + Partekatu + Mesedez itxaron… + Mezuak bilatu + GIF + Elkarrizketa ikusi + Kokapena partekatzeko plugina + Erabili kokapena partekatzeko plugina mapa erabili beharrean + Web helbidea kopiatu + XMPP helbidea kopiatu + HTTP fitxategiak partekatzea S3rentzako + Bilaketa zuzena + \'Elkarrizketa hasi\' pantailan teklatua ireki eta kurtsorea bilaketa eremuan jarri + Taldearen irudia + Ostalariak ez ditu taldeen irudiak onartzen + Jabeak soilik alda dezake taldearen irudia + Kontaktuaren izena + Ezizena + Izena + Izen bat ematea hautazkoa da + Taldearen izena + Talde hau suntsitu egin da + Aurreko planoko zerbitzua + Egoeraren informazioa + Konektagarritasun arazoak + Jakinarazpen maila hau kontu batera konektatzeko arazoak daudenean jakinarazpen bat erakusteko erabiltzen da. + Mezuak + Mezuak + Mezu isilak + Jakinarazpen talde hau inolako soinurik egin beharko ez luketen jakinarazpenak erakusteko erabiltze da. Adibidez beste gailu batean aktibo zaudenean (grazia epea). + Garrantzia, soinua, dardara + Bideoen konprimatzea + Ikusi multimedia + Parte-hartzaileak + Multimedia nabigatzailea + Fitxategia alde batera utzita segurtasun hauste bategatik. + Bideoen kalitatea + Kalitate baxuagoarekin fitxategi txikiagoak lortzen dira + Ertaina (360p) + Altua (720p) + utzita + Dagoeneko mezu baten zirriborroa idazten ari zara. + Ezaugarria ez da inplementatu + Herrialde kode ez balioduna + Herrialde bat hautatu + telefono zenbakia + Zure telefono zenbakia egiaztatu + Quicksyk SMS mezu bat bidali dizu (baliteke ordaindu behar izatea) zure telefono zenbakia egiaztatzeko. +
%s

telefono zenbakia egiaztatuko dugu. Zuzena da, edo zenbakia editatu nahiko al zenuke?]]>
+ %s ez da telefono zenbaki baliodun bat. + Mesedez idatzi zure telefono zenbakia. + Herrialdeak bilatu + %s egiaztatu + %s zenbakira.]]> + Beste SMS bat bidali dizugu 6 digituzko kode batekin. + Mesedez idatzi 6 digituzko kodea behean. + SMSa birbidali + SMSa birbidali (%s) + Itxaron mesedez (%s) + atzera + Izan daitekeen kodea arbeletik automatikoki itsatsi da. + Mesedez idatzi zure 6 digituzko kodea. + Izen emate prozesua bertan behera utzi nahi duzu? + Bai + Ez + Egiaztatzen… + SMSa eskatzen… + Idatzi duzun kodea ez da zuzena. + Bidali dizugun kodea iraungi egin da. + Sareko akats ezezaguna. + Zerbitzariaren erantzun ezezaguna. + Zerbait oker joan da zure eskaera prozesatzerakoan. + Idatzitakoa ez da balioduna + Aldi baterako ez eskuragarri. Saiatu beranduago. + Ez dago sareko konexiorik. + Mesedez saiatu berriro %s barru + Mugatutako tasa duzu + Saiakera gehiegi + Aplikazio honen bertsio zahar bat erabiltzen ari zara. + Eguneratu + Telefono zenbaki hau beste gailu batean saioa hasita dauka une honetan. + Mesedez idatzi zure izena besteek, zu haien helbideen liburuan ez zaituztenek, nor zaren jakin ahal dezaten. + Zure izena + Idatzi zure izena + Erabili ezazu editatu botoia zure izena ezartzeko. + Eskaera ukatu + Orbot instalatu + Orbot abiarazi + Merkatuko aplikazioa ez instalatuta. + Kanal honek zure XMPP helbidea publikoa egingo du + e-booka + Jatorrizkoa (konprimatu gabea) + Ireki honekin… + Conversations profil argazkia + Kontua hautatu + Babes-kopia berrezarri + Berrezarri + Sartu %s kontuaren pasahitza babes-kopia berrezartzeko. + Ez erabili babes-kopiak berrezartzeko ezaugarria instalazio bat klonatzeko (aldi berean exekutatzeko). Babes-kopia bat berrezartzea migrazioetarako edo jatorrizko gailua galdu duzunerako da soilik. + Babes-kopiak egin eta berrezarri + Sartu XMPP helbidea + Talde bat sortu + Kanal publiko batean sartu + Talde pribatu bat sortu + Kanal publiko bat sortu + Kanalaren izena + XMPP helbidea + Mesedez kanalarentzako izan bat eman + Mesedez XMPP helbide bat eman + Hau XMPP helbide bat da. Mesedez izen bat eman. + Kanal publikoa sortzen… + Kanal hau existitzen da dagoeneko + Existitzen den kanal batean sartu zara + Baimendu edonor gaia aldatzea + Baimendu edonor besteak gonbidatzea + Edonor alda dezake gaia + Jabeek alda dezakete gaia + Administratzaileek alda dezakete gaia + Jabeek besteak gonbidatu ditzakete + Edonor beste batzuk gonbida ditzake + Administratzaileek XMPP helbideak ikus ditzakete. + Edonor ikus ditzake XMPP helbideak. + Kanal publiko honek ez du parte-hartzailerik. Gonbidatu itzazu kontaktuak edo erabili ezazu partekatzeko botoia kanalaren XMPP helbidea bidaltzeko. + Talde pribatu honek ez du parte-hartzailerik. + Baimenak kudeatu + Parte-hartzaileak bilatu + Fitxategia handiegia da + Erantsi + Kanalak aurkitu + Kanalak bilatu + Balizko pribatutasun urraketa! + search.jabber.network. izeneko hirugarren zerbitzu bat erabiltzen du.

Ezaugarri hau erabiltzeak zure IP helbidea eta bilatutako testua zerbitzu horretara bidaltzea dakar. Ikusi beren pribatutasun politika informazio gehiago lortzeko.]]>
+ Badaukat kontu bat dagoeneko + Gehitu existitzen den kontu bat + Kontu berria erregistratu + Honek domeinu helbide baten itxura dauka + Gehitu hala ere + Honek kanal helbide baten itxura dauka + Babes-kopia fitxategiak partekatu + Conversations babes-kopia + Gertaera + Babes-kopia ireki + Hautatu duzun fitxategia ez da Conversations babes-kopia bat + Kontu hau konfiguratuta dago jada + Mesedez idatzi ezazu kontu honetarako pasahitza + Kanal publiko batean sartu… + jabber.network + Zerbitzari lokala + Kanalak aurkitzeko modua + Babeskopia + Honi buruz + Mesedez kontu bat gaitu + Lanpetuta + + Parte-hartzaile %1$d ikusi + %1$d parte-hartzaile ikusi + +
diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml new file mode 100644 index 000000000..9ba3e4d45 --- /dev/null +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -0,0 +1,167 @@ + + + تنظیمات + مکالمه جدید + مدیریت حساب های کاربری + جزییات مخاطب + جزئیات چت گروهی + اضافه کردن حساب کاربری + تغییر نام + اضافه کردن به لیست ادرس ها + حذف از لیست نام ها + بلاک مخاطب + غیر بلاک کردن مخاطب + بلاک کردن دامنه + باز کردن دامنه + مدیریت حساب ها + تنظیمات + به اشتراک گذاری با Conversation + شروع گفتگو + انتخاب مخاطب + انتخاب مخاطبین + به اشتراک گذاری با حساب + لیست مسدود شده ها + هم اکنون + 1 دقیقه قبل + %d دقیقه قبل + در حال ارسال... + در حال رمزگشایی پیام. لطفا صبور باشید... + پیام رمز شده به وسیله OpenPGP + نام مستعار قبلا استفاده شده + نام مستعار نادرست است + مدیر + صاحب + ناظم + شرکت کننده + بازدید کننده + مخاطب مسدود شد + مسدود شده + ثبت نام حساب جدید بر روی سرور + تغییر رمز عبور بر روی سرور + به اشتراک گذاری با ... + شروع گفتگو + دعوت از مخاطب + مخاطبین + مخاطب + لغو + اضافه کردن + ویرایش + حذف + مسدود کردن + از انسداد خارج کردن + ذخیره + تایید + هم اکنون ارسال کن + دیگر هرگز نپرس + پیوست فایل + افزودن مخاطب + ارسال ناموفق بود + در حال به اشتراک گذاری فایل ها. لطفا صبور باشید... + پاک سازی تاریخچه + پاک سازی تاریخچه گفتگو ها + انتخاب دستگاه + پیام رمز نشده ارسال کن + ارسال پیام + ارسال پیام به %s + ارسال پیام رمز شده با OMEMO + ارسال پیام رمز شده با OMEMO + ارسال پیام رمز شده با OpenPGP + ارسال رمز نشده + رمزگشایی موفق نبود. شاید شما کلید محرمانه صحیح را در اختیار ندارید. + OpenKeychain + راه اندازی مجدد + نصب + لطفا OpenKeychain را نصب نمایید + ارائه ... + انتظار... + کلید OpenPGP یافت نشد + کلید های OpenPGP یافت نشدند + عمومی + پذیرفتن فایل ها + پذیرفتن خودکار فایل های کوچکتر از ... + پیوست ها + اعلان + لرزش + هنگام دریافت پیام جدید بلرز + اعلان از طریق LED + چشمک زدن چراغ اعلان هنگام رسیدن پیام جدید + آهنگ زنگ + مهلت + پیشرفته + هیچ وقت گزارش خرابی را ارسال نکن + پیام ها را تایید کن + رابط کاربری + پذیرفتن + فایلی که انتخاب نموده اید تصویر نیست + فایل پیدا نشد + ناشناخته + موقتا غیر فعال شد + آنلاین + آفلاین + غیر مجاز + سرور پیدا نشد + عدم اتصال + ثبت نام ناموفق بود + نام کاربری قبلا استفاده شده + ثبت نام به پایان رسید + برقرای ارتباط امن با شکست مواجه شد + نقض سیاست + سرور ناسازگار + خطا در ارتباط + رمز نشده + OTR + OpenPGP + OMEMO + حذف حساب کاربری + موقتا غیر فعال کن + انتشار آواتار + انتشار کلید عمومی OpenPGP + حذف کلید عمومی OpenPGP + فعال سازی حساب کاربری + آیا مطمئن هستید؟ + ضبط صدا + username@example.com + کلمه عبور + آیا می خواهید %s را به مخاطبان خود اضافه کنید؟ + مشخصات سرور + در دسترس + خارج از دسترس + آخرین بار لحظاتی قبل مشاهده شده + آخرین بار %d دقیقه قبل مشاهده شده + آخرین بار %d ساعت قبل مشاهده شده + آخرین بار %d روز قبل مشاهده شده + دیگر دستگاه ها + در حال دریافت کلید ها... + رمز گشایی + جستجو + وارد کردن مخاطب + حذف مخاطب + مشاهده جزییات مخاطب + بلاک مخاطب + غیر بلاک کردن مخاطب + ارسال مجدد + آدرس وب + آفلاین + مخاطب + نمایش موقعیت + لغو + آنلاین + متوسط + گواهی ناشناخته را بپذیر؟ + هم اکنون غیر فعال کن + سایز فونت + فعال به صورت پیش فرض + غیر فعال به صورت پیش فرض + کوچک + متوسط + بزرگ + پیام برای این دستگاه رمزگذاری نشده + کپی کردن موقعیت + به اشتراک گذاری موقعیت + به اشتراک گذاری موقعیت + نمایش موقعیت + به اشتراک گذاری + لطفا صبر کنید... + جستجو در پیام ها + مشاهده گفتگو + diff --git a/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml similarity index 100% rename from src/main/res/values-fi/strings.xml rename to app/src/main/res/values-fi/strings.xml diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 000000000..147721e5e --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,977 @@ + + + Paramètres + Nouvelle conversation + Gérer les comptes + Gérer le compte + Fermer la conversation + Détails du contact + Détails du groupe + Détails du canal + Ajouter un compte + Modifier le nom + Ajouter au carnet d\'adresses + Retirer des contacts + Bloquer le contact + Débloquer le contact + Bloquer le domaine + Débloquer le domaine + Bloquer le participant + Débloquer le participant + Gestion des comptes + Paramètres + Partager avec Conversations + Démarrer une conversation + Choisir un contact + Choisir les contacts + Partager via le compte + Bloquer la liste + À l\'instant + Il y a 1 minute + Il y a %d minutes + + %d conversation non lue + %d conversations non lues + %d conversations non lues + + Envoi… + Déchiffrement du message. Veuillez patienter… + Message chiffré avec OpenPGP + Cet identifiant est déjà utilisé + Identifiant non valide + Administrateur + Propriétaire + Modérateur + Participant + Visiteur + Voulez-vous supprimer %s de votre liste de contacts ? Les conversations associées à ce contact ne seront pas supprimées. + Voulez-vous bloquer %s pour l\'empêcher de vous envoyer des messages ? + Voulez-vous débloquer %s et lui permettre de vous envoyer des messages ? + Bloquer tous les contacts de %s ? + Débloquer tous les contacts de %s ? + Contact bloqué + Bloqué + Voulez-vous retirer %s des favoris ? La conversation associée à ce favori ne sera pas supprimée. + Créer un nouveau compte sur le serveur + Changer de mot de passe sur le serveur + Partager avec… + Démarrer une conversation + Inviter un contact + Inviter + Contacts + Contact + Annuler + Établir + Ajouter + Modifier + Supprimer + Bloquer + Débloquer + Enregistrer + OK + %1$s a planté + En utilisant votre compte XMPP pour envoyer des rapports de crash, vous aidez le développement de %1$s. + Envoyer + Ne plus me demander + Impossible de se connecter au compte. + Impossible de se connecter aux comptes. + Appuyez pour gérer vos comptes. + Joindre un fichier + Ajouter ce contact manquant à votre liste de contact ? + Ajouter un contact + Échec de l\'envoi + Préparation pour l’envoi de l\'image + Préparation pour l\'envoi des images + Partage des fichiers. Veuillez patienter… + Vider l\'historique + Vider l\'historique de la conversation + Êtes-vous sûr de vouloir supprimer tous les messages de cette conversation ?\n\n Avertissement : Cela ne supprimera pas les copies des messages qui sont stockés sur d\'autres appareils ou serveurs. + Supprimer le fichier + Êtes-vous sûr de vouloir supprimer ce fichier ?\n\n Avertissement : Cela ne supprimera pas les copies de ce fichier qui sont stockés sur d\'autres appareils ou serveurs. + Fermez cette conversation après + Choisir l\'appareil + Envoyer un message en clair + Envoyer le message + Envoyer un message à %s + Envoyer un message chiffré avec OMEMO + Envoyer un message chiffré avec \\OMEMO + Envoyer un message chiffré avec OpenPGP + Votre identifiant a été changé + Envoyer en clair + Échec du déchiffrement. Avez-vous la bonne clé privée ? + OpenKeychain + OpenKeychain pour chiffrer et déchiffrer les messages et pour gérer vos clés publiques.\n\nOpenKeychain est sous licence GPLv3 et est disponible sur F-Droid et Google Play.\n\n(Veuillez redémarrer %1$s après l\'installation de l\'application.)]]> + Redémarrer + Installer + Veuillez installer OpenKeychain + Proposition… + Patientez… + Aucune clé OpenPGP trouvée. + Impossible de chiffrer vos messages car votre contact n\'a pas communiqué sa clé publique.\n\nDemandez-lui de configurer OpenPGP. + Aucune clé OpenPGP n\'a été trouvée. + Impossible de chiffrer votre message car vos contacts ne communiquent pas leur clé publique.\n\nDemandez-leur de configurer OpenPGP. + Général + Accepter les fichiers + Accepter automatiquement les fichiers plus petits que… + Pièces jointes + Notification + Vibration + Vibrer lors de la réception d\'un message + Notification LED + Faire clignoter la LED lors de la réception d\'un message + 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é + Ne pas envoyer de rapports d\'erreurs + En envoyant des rapports de crash vous aidez le développement de Conversations + Confirmation de lecture + Informer vos contacts quand vous avez reçu et lu leurs messages + Interdire les captures d’écran + Interface + OpenKeychain a signalé une erreur. + Mauvaise clé pour le chiffrement. + Accepter + Une erreur s\'est produite + Erreur + Votre compte + Envoyer mes màj de disponibilité + Recevoir ses màj de disponibilité + Demander les màj de disponibilité + Choisir une image + Prendre une photo + Accepter par avance les demandes de publication. + Le fichier choisi n\'est pas une image + Impossible de convertir l\'image + 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 + Connexion… + Hors-ligne + Non autorisé + Impossible de trouver le serveur + Aucune connectivité + Échec de l\'inscription + Identifiant déjà utilisé + Inscription réussie + 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 + Erreur d\'ouverture du flux + Non chiffré + OTR + OpenPGP + OMEMO + Supprimer + Désactiver temporairement + Publier un avatar + Publier la clé publique OpenPGP + Supprimer la clé publique OpenPGP + Êtes-vous sûr de vouloir supprimer votre clé publique OpenPGP de votre annonce de présence ?\nVos contacts ne pourront plus vous envoyer de message chiffrés avec OpenPGP. + Clé publique OpenPGP publiée + Activer + Êtes-vous sûr ? + Supprimer votre compte effacera l\'historique de vos conversations + Enregistrer un son + Adresse XMPP + Bloquer l\'adresse XMPP + nom@exemple.com + Mot de passe + Ce n\'est pas une adresse XMPP valide + Plus de mémoire disponible. L\'image est trop volumineuse. + Voulez-vous ajouter %s à votre carnet d\'adresses ? + Infos sur le serveur + XEP-0313 : MAM + XEP-0280 : Copies carbone + XEP-0352 : Indication statut client + XEP-0191 : Commande de blocage + XEP-0237 : Révision contacts + XEP-0198 : Gestion des flux + XEP-0215 : Découverte de service externe + XEP-0163 : PEP (Avatars / OMEMO) + XEP-0363 : Envoi de fichiers via HTTP + XEP-0357 : Notifications Push + supportée + non supportée + Annonce de clé publique manquante + en ligne à l\'instant + en ligne il y a 1 minute + en ligne il y a %d minutes + en ligne il y a 1 heure + en ligne il y a %d heures + en ligne hier + en ligne il y a %d jours + Message chiffré. Veuillez installer OpenKeychain pour le déchiffrer. + Nouveaux messages chiffrés avec OpenPGP détectés. + 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… + Terminé + Déchiffrer + Favoris + Rechercher + Ajouter contact + Supprimer un contact + Afficher les détails du contact + Bloquer le contact + Débloquer le contact + Ajouter + Sélectionner + Le contact existe déjà + Rejoindre + canal@conference.example.com/surnom + canal@conference.example.com + Enregistrer comme favori + Supprimer le favori + Détruire le groupe + Détruire le canal + Voulez-vous vraiment détruire ce groupe ?\n\nAvertissement : le groupe sera complètement supprimé du serveur. + Êtes-vous sûr de vouloir détruire ce canal public\? On Avertissement: le canal sera complètement supprimé du serveur. + Impossible de détruire le groupe + Impossible de détruire le canal + Modifier le sujet du groupe + Sujet + Rejoindre le groupe + Partir + Votre correspondant vous a ajouté dans sa liste de contacts + Ajouter en retour + %s a tout lu jusqu\'ici + %s ont tout lu jusqu\'ici + %1$s+%2$d autres ont tout lu jusqu\'ici + Tout le monde a lu jusqu\'ici + Publier + Appuyer sur l\'avatar pour choisir une image depuis la galerie + Mise à jour… + Le serveur a rejeté votre publication + Impossible de convertir votre image + Impossible de stocker l\'avatar sur le disque + (Un appui long réinitialise le paramètre) + Votre serveur ne supporte pas la publication d\'avatars + chuchoté + pour %s + Envoyer un message privé à %s + Se connecter + Ce compte existe déjà + Suivant + Session établie + Passer + Désactiver les notifications + Activer + Le groupe requiert un mot de passe + Entrer le mot de passe + Veuillez demander à votre contact de partager ses mises à jour de disponibilité.\n\nElles seront utilisées pour déterminer l\'application qu\'il utilise. + Demander maintenant + Ignorer + Attention : peut poser problème si l\'un des deux correspondants n\'a pas activé les mises à jour de disponibilité.\n\nVérifiez dans « Détails du contact » que vous y avez bien souscrit. + Sécurité + Autoriser la correction + Permet à vos contacts d\'éditer leurs messages rétroactivement + Paramètres avancés + À utiliser avec précaution. + À propos de %s + Heures tranquilles + Heure de début + Heure de fin + Activer les heures tranquilles + Les notifications seront muettes pendant les heures tranquilles. + Autres + Empreinte OMEMO copiée dans le presse-papier + Vous êtes bannis de ce groupe + Ce groupe est réservé aux membres + Contrainte de ressource + Vous avez été éjecté de ce groupe + Le groupe a été fermé + Vous n\'appartenez plus à ce groupe + avec le compte %s + hébergé sur %s + Vérification de %s sur l\'hôte HTTP + Vous n\'êtes pas connecté. Essayez plus tard. + Vérification de la taille de %s + Vérification de la taille de %1$s sur %2$s + Options du message + Citation + Coller en tant que citation + Copier l\'URL + Envoyer de nouveau + URL du fichier + URL copiée dans le presse-papier + Adresse XMPP copiée dans le presse-papiers + Message d\'erreur copié dans le presse-papier + adresse internet + Scanner le code-barres 2D + Montrer le code-barres 2D + Afficher la liste des contacts bloqués + Détails du compte + Confirmer + Réessayer + Service au premier plan + Évite que le système ne ferme votre connexion. + Créer une sauvegarde + La sauvegarde sera stockée dans %s + Création des fichiers de sauvegarde + Votre sauvegarde a été créée + Les fichiers de sauvegarde ont été stockés dans %s + Restauration de la sauvegarde + Votre sauvegarde a été restaurée + N\'oubliez pas d\'activer le compte. + Choisir un fichier + Réception %1$s (%2$d%% complété) + Télécharger %s + Effacer %s + fichier + Ouvrir %s + envoi (%1$d%% complété) + Préparation pour l\'envoi du fichier + %s proposé à télécharger + Annuler l\'envoi + impossible de partager le fichier + Transfert de fichier annulé + Fichier supprimé + Aucune application disponible pour ouvrir le fichier + Aucune application disponible pour ouvrir le lien + Aucune application disponible pour afficher le contact + Tags dynamiques + Afficher des tags en lecture seule en dessous des contacts. + Activer les notifications + Serveur de groupe non trouvé + Impossible de créer le groupe + Avatar du compte + Copier l\'empreinte OMEMO dans le presse-papier + Régénérer l\'empreinte OMEMO + Supprimer les appareils + Êtes-vous sûr de vouloir supprimer les autres appareils de l\'annonce OMEMO ? Ils s\'annonceront de nouveau à leur prochaine connexion, mais ils peuvent ne pas recevoir les messages envoyés entre temps. + Aucune clé utilisable n\'est disponible pour ce contact. \nImpossible d\'obtenir de nouvelles clés depuis le serveur. Pourrait-il y avoir un problème avec le serveur de votre contact ? + Il n\'y a aucune clé utilisable disponible pour ce contact.\nAssurez-vous d\'avoir un abonnement de présence mutuelle. + Une erreur est survenue + Récupération de l\'historique sur le serveur + Plus d\'historique sur le serveur + Mise à jour… + Mot de passe modifié ! + Impossible de changer le mot de passe + Changer de mot de passe + Mot de passe actuel + Nouveau mot de passe + Le mot de passe ne peut pas être vide + Activer tous les comptes + Désactiver tous les comptes + Faire une action avec + Aucune affiliation + Hors ligne + Banni + Membre + Mode expert + Accorder des privilèges aux membres + Révoquer les privilèges des membres + Accorder des privilèges d\'administrateur + Révoquer des privilèges d\'administrateur + Accorder des privilèges de propriétaire + Révoquer les privilèges du propriétaire + Supprimer du groupe + Supprimer du canal + Impossible de changer l\'affiliation de %s + Bannir du groupe + Bannir du canal + Vous essayez de supprimer %s d\'un canal public. La seule façon de le faire est de bannir cet utilisateur pour toujours. + Bannir maintenant + Impossible de changer le rôle de %s + Configuration du groupe + Configuration du canal public + Privé, membres uniquement + Rendre les adresses XMPP visibles à tout le monde + Rendre le canal modéré + Vous ne participez pas + Options du groupe modifiées ! + Impossible de modifier les options du groupe + Jamais + Jusqu\'à nouvel ordre + Répéter les notifications + Répondre + Marquer comme lu + Saisie + Touche Entrée pour envoyer + Utilisez la touche Entrée pour envoyer un message. Vous pourrez toujours utiliser la combinaison Ctrl+Entrée pour envoyer un message, même si cette option est désactivée. + Afficher la touche Entrée + Remplacer la touche Émoticônes par une touche Entrée. + audio + vidéo + image + document PDF + Application Android + Contact + L\'avatar a été publié ! + %s en cours d\'envoi + En train de proposer un(e) %s + Cacher contacts hors-ligne + %s est en train d\'écrire… + %s a arrêté d\'écrire + %s sont en train d\'écrire… + %s ont cessé d\'écrire + Notifications d\'écriture + Informer vos contacts quand vous leur écrivez des messages + Envoyer la position + Afficher la position + Aucune application trouvée pour afficher le lieu + Position + Conversation fermée + Quitter le groupe privé + Quitte le canal public + Ne pas utiliser les CAs système + Tous les certificats doivent être approuvés manuellement. + Retirer les certificats + Supprimer les certificats approuvés manuellement. + Aucun certificat approuvé manuellement + Retirer les certificats + Supprimer la sélection + Annuler + + %d certificat supprimé + %d certificats supprimés + %d certificats supprimés + + Remplacer le bouton « Envoyer » par une action rapide + Action Rapide + Aucune + Dernière utilisée + Sélectionner l\'action rapide + Rechercher dans les contacts + Rechercher des favoris + Envoyer un message privé + %1$s a quitté le groupe + Identifiant + Identifiant + Cet identifiant n\'est pas valide + Échec du téléchargement : impossible de trouver le serveur + Échec du téléchargement : impossible de trouver le fichier + Échec du téléchargement : impossible de se connecter à l\'hôte + Échec du téléchargement : Écriture impossible + Échec du téléchargement : Fichier non valide + Réseau Tor inaccessible + La liaison a échoué + Le serveur n\'est pas responsable pour ce domaine + Détraqué + Disponibilité + Absent quand l\'appareil est verrouillé + Afficher comme étant absent lorsque l\'appareil est verrouillé + Occupé en mode silence + Occupé lorsque l\'appareil est en mode silencieux + Indisponible en mode vibreur + Occupé lorsque l\'appareil est en mode vibreur + Paramètres de connexion avancés + Montrer le nom d\'hôte et le port lors du paramétrage d\'un compte + xmpp.example.com + Se connecter avec certificat + Impossible d\'analyser le certificat + Paramètres d\'archivage + Paramètres d\'archivage du serveur + Récupération des paramètres d\'archivage en cours… + Impossible d\'obtenir les préférences d\'archivage + CAPTCHA exigé + Entrez le texte de l\'image ci-dessus + Chaîne de certificats indigne de confiance + L\'adresse XMPP ne correspond pas au certificat + Renouveler le certificat + Erreur lors de la récupération de la clé OMEMO ! + Clé OMEMO vérifiée avec un certificat ! + Votre appareil ne supporte pas la sélection de certificats client ! + Connexion + Connexion via Tor + Rediriger toutes les connexions via le réseau Tor. Nécessite Orbot. + Nom d\'hôte + Port + Adresse du serveur (ou .onion) + Ce numéro de port n\'est pas valide + Ce nom d\'hôte n\'est pas valide + %1$d compte(s) sur %2$d connecté(s) + + %d message + %d messages + %d messages + + Charger plus de messages + Fichier partagé avec %s + Image partagée avec %s + Images partagées avec %s + Texte partagé avec %s + Autoriser %1$s à accéder au stockage externe + Autoriser %1$s à accéder à la caméra + Synchroniser avec contacts +
Nous ne stockerons pas de copie de ces numéros.\n\nPour plus d\'informations, lisez notre politique de confidentialité.

maintenant être invité à donner la permission d\'accéder à vos contacts.]]>
+ Notifier pour tous les messages + Notifier seulement en cas de mention + Notifications désactivées + Notifications en pause + Compression des images + Remarque : Utiliser « Choisir un fichier » au lieu de « Choisir une image » pour envoyer des images non compressées. + Toujours + Grandes images seulement + Optimisations de batterie activées + Votre appareil applique d\'importantes optimisations de batterie pour %1$s pouvant entraîner des retards de notifications, voire des pertes de messages.\nIl est recommandé de les désactiver. + Votre appareil applique d\'importantes optimisations de batterie pour %1$s pouvant entraîner des retards de notifications, voire des pertes de messages.\nVous allez être invité à les désactiver. + Désactiver + La zone sélectionnée est trop grande + (Aucun compte activé) + Ce champ est requis + Corriger le message + Envoyer le message corrigé + Vous avez déjà validé l\'empreinte de cette personne pour accorder votre confiance. En sélectionnant « Terminé », vous confirmez simplement que %s fait partie de ce groupe. + Vous avez désactivé ce compte + Erreur de sécurité : accès fichier invalide + Aucune application disponible pour partager l\'URI + Partager l\'URI avec… +
Vous vous inscrivez avec votre numéro de téléphone et Quicksy va automatiquement, en fonction des numéros de votre carnet d’adresses, vous suggérer d’éventuels contacts.

en vous inscrivant, vous acceptez notre politique de confidentialité.]]>
+ Accepter et continuer + Nous vous guiderons tout au long du processus de création d\'un compte sur conversations.im.¹\nLorsque vous sélectionnerez conversations.im en tant que fournisseur, vous pourrez communiquer avec les utilisateurs d\'autres fournisseurs en leur fournissant votre adresse XMPP complète. + Votre adresse XMPP complète sera : %s + Créer un compte + Utiliser votre propre fournisseur + Choisissez votre nom d\'utilisateur + Gérer l\'état de disponibilité manuellement + Définir votre disponibilité lors de l\'édition de votre statut. + Message de statut + Disponible + En ligne + Absent + Non disponible + Occupé + Un mot de passe fort a été généré + Les optimisations de batterie ne peuvent pas être désactivées sur votre appareil + Échec de l\'inscription : Réessayer plus tard + Échec de l\'inscription : le mot de passe n\'est pas assez fort + Choisir les participants + Création d\'un groupe… + Inviter à nouveau + Désactiver + Courte + Moyenne + Longue + Partage de l\'utilisation + Informer vos contacts lorsque vous utilisez Conversations + Confidentialité + Thème + Choisir la palette de couleurs + Automatique + Clair + Sombre + Fond vert + Utiliser un fond vert pour les messages reçus + Impossible de se connecter à OpenKeyChain + Cet appareil n\'est plus utilisé + Ordinateur + Smartphone + Tablette + Navigateur Internet + Console + Paiement requis + Autoriser à accéder à internet + Moi + Le contact demande la notification de présence + Autoriser + Permission d\'accéder à %s refusée + Serveur distant non trouvé + Dépassement du délai du serveur distant + Impossible de mettre à jour le compte + Signaler cette adresse XMPP comme émettrice de messages indésirables. + Effacer les identités OMEMO + Régénérer vos clés OMEMO. Tous vos contacts devront vous vérifier à nouveau. À n\'utiliser qu\'en dernier recours. + Supprimer les clés sélectionnées + Vous devez être connecté pour publier votre avatar. + Afficher le message d\'erreur + Message d\'erreur + Économiseur de données activé + Votre appareil ne prend pas en charge la désactivation de l\'Économiseur de données pour %1$s. + Impossible de créer un fichier temporaire + Cet appareil a été vérifié + Copier l\'empreinte + Vous avez vérifié toutes les clés OMEMO en votre possession + Le code-barres ne contient pas d\'empreintes pour cette conversation. + Empreintes vérifiées + Utilisez l\'appareil photo pour scanner le code-barres d\'un contact + Veuillez attendre que les clés soient récupérées + Partager par code-barres + Partager par URI XMPP + Partager par lien HTTP + Faire aveuglément confiance avant vérification + Faire automatiquement confiance aux nouveaux appareils des contacts qui n\'ont pas été vérifiés auparavant mais demander une confirmation manuelle à chaque fois qu\'un contact vérifié auparavant utilise un nouvel appareil. + Les clés OMEMO ont fait l\'objet d\'une confiance aveugle, cela signifie qu\'il pourrait s\'agir de quelqu\'un d\'autre ou que quelqu\'un aurait pu intercepter l\'échange. + Non approuvée + Code-barres 2D invalide + Vide le dossier de cache (utilisé par l\'appplication caméra) + Vider le cache + Vider le stockage privé + Vide le stockage privé, où les fichiers sont conservés (ils peuvent être re-téléchargés depuis le serveur) + J\'ai obtenu ce lien d\'une source de confiance + Vous êtes sur le point de vérifier les clés OMEMO de %1$s en cliquant sur un lien. Cette procédure n\'est sécurisée que si le lien en question n\'a pu être publié que par %2$s et que vous l\'avez obtenu d\'une source digne de confiance. + Continuer + Vérifier les clés OMEMO + Afficher les comptes inactifs + Cacher les comptes inactifs + Ne plus faire confiance à cet appareil + Êtes-vous sûr de vouloir retirer la vérification pour cet appareil ?\nCet appareil et les messages qui en proviennent seront marqués comme « indignes de confiance ». + + %d seconde + %d secondes + %d secondes + + + %d minute + %d minutes + %d minutes + + + %d heure + %d heures + %d heures + + + %d jour + %d jours + %d jours + + + %d semaine + %d semaines + %d semaines + + + %d mois + %d mois + %d mois + + Suppression messages auto + Efface automatiquement de cet appareil les messages plus anciens que l\'intervalle choisi. + Chiffrement du message en cours + Aucun message récupéré, en raison de la configuration de rétention de l\'appareil. + Compression de la vidéo en cours + Conversations correspondantes fermées. + Contact bloqué. + Notifications d\'inconnus + Notifier les messages et appels reçus d\'inconnus. + Message d\'un inconnu reçu + Bloquer l\'inconnu + Bloquer le domaine entier + En ligne actuellement + Nouvelle tentative de déchiffrement + Échec de la session + Mécanisme SASL dégradé + Le serveur nécessite une identification sur son site web + Ouvrir le site web + Aucune application disponible pour ouvrir le site web + Notifications « pop-up » + Afficher les notifications « pop-up » + Aujourd\'hui + Hier + Valider le nom de domaine avec DNSSEC + Les certificats serveur qui comportent le nom de domaine validé sont considérés comme validés + Le certificat ne contient pas d\'adresse XMPP + partiel + Enregistrer une vidéo + Copier dans le presse-papier + Message copié dans le presse-papier + Message + Les messages privés sont désactivés + Applications protégées + Pour recevoir les notifications, même lorsque l\'écran est éteint, vous devez ajouter Conversations à la liste des applications protégées. + Accepter les certificats inconnus ? + Le certificat du serveur n\'est pas signé par une Autorité de Certification connue. + Accepter un nom de serveur qui ne correspond pas ? + Le serveur n\'a pu s\'authentifier en tant que « %s ». Le certificat est valide uniquement pour : + Désirez-vous quand-même vous connecter ? + Détails du certificat : + Une fois + La lecture d\'un QR Code nécessite l\'accès à l\'appareil photo + Faire défiler l\'écran jusqu\'en bas + Faire défiler l\'écran jusqu\'en bas après avoir envoyé un message + Modifier le message de l\'état + Modifier le message de statut + Désactiver le chiffrement + %1$s n\'est pas capable d\'envoyer un message chiffré à %2$s. Ceci peut être lié au fait que votre contact utilise un serveur obsolète ou un client qui ne gère par OMEMO. + Impossible de récupérer la liste des appareils + Impossible de récupérer les clés de chiffrement + Indication : Dans certains cas, cela peut être résolu en vous ajoutant respectivement dans votre liste de contacts. + Êtes-vous sûr de vouloir désactiver le chiffrement OMEMO pour cette discussion ?\nCeci permettra à l\'administrateur de votre serveur de lire vos messages, mais cela peut être le seul moyen de communiquer avec des personnes utilisant un vieux client. + Désactiver maintenant + Brouillon : + Chiffrement OMEMO + OMEMO sera toujours utilisé pour des discussions à deux ou les groupes privés. + OMEMO sera utilisé par défaut pour les nouvelles discussions. + OMEMO devra être activé manuellement pour chaque nouvelle discussion + Créer un raccourci + Taille de police + La taille de police relative utilisée dans l\'application. + Activé par défaut + Désactivé par défaut + Petite + Moyenne + Grande + Le message n\'était pas chiffré pour cet appareil. + Échec de déchiffrement du message OMEMO. + annuler + Le partage de positionnement est désactivé. + Figer la position + Débloquer la position + Copier la position + Partager la position + Instructions + Partager la position + Afficher la position + Partager + Impossible de démarrer l\'enregistrement + Veuillez patienter… + Autoriser %1$s à accéder au microphone + Rechercher dans les messages + GIF + Voir la conversation + Plugin de partage de localisation + Utilisez le plugin Share Location à la place de la carte intégrée + Copier l\'adresse internet + Copier l\'adresse XMPP + Partage de fichier HTTP pour S3 + Recherche directe + Sur l\'écran de démarrage de Conversation, afficher le clavier et placer le curseur sur le champ recherche + Avatar du groupe + Le serveur ne prend pas en charge les avatars pour les groupes + Seul le propriétaire peut changer l\'avatar d\'un groupe + Nom du contact + Surnom + Nom + Fournir un nom est facultatif + Nom du groupe + Ce groupe a été supprimé + Impossible de sauvegarder l\'enregistrement + Service au premier plan + Cette catégorie de notification est utilisée pour afficher une notification permanente indiquant que %1$s fonctionne. + Information sur l\'état + Problèmes de connectivité + Cette catégorie de notification est utilisée pour afficher une notification lors d\'un problème de connexion avec un compte. + Messages + Appels + Messages + Appels entrants + Appels sortants + Messages silencieux + Ce groupe de notifications est utilisé pour afficher les notifications qui ne doivent pas émettre de son. Par exemple, lorsque le son est activé sur un autre appareil (délai de grâce). + Échec lors de la livraison + Paramètres de notification des messages + Paramètres de notification d\'appels entrants + Importance, son, vibration + Compression vidéo + Voir les média + Participants + Navigateur de média + Fichier omis en raison d\'une violation de la sécurité. + Qualité des vidéos + Une qualité inférieure signifie des fichiers plus petits + Moyenne (360p) + Haute (720p) + Annulé + Vous avez déjà un brouillon de message. + Fonction non mise en oeuvre + Code pays invalide + Choisissez un pays + Numéro de téléphone + Vérifier votre numéro de téléphone + Quicksy va envoyer un message SMS (des frais opérateurs sont possibles) pour vérifier votre numéro de téléphone. Entrez votre code pays et votre No de téléphone. +
%s

. Est-ce correct ou souhaitez-vous modifier le numéro ?]]>
+ %s n\'est pas un numéro de téléphone valide. + Veuillez saisir votre numéro de téléphone. + Recherche de pays + Vérifier %s + %s.]]> + Nous vous avons envoyé un autre SMS avec un code à 6 chiffres. + Veuillez saisir ci-dessous le code PIN à 6 chiffres. + Renvoyer un SMS. + Renvoyer SMS (%s) + S\'il vous plaît, attendez (%s) + retour + Collage possible des broches possibles du presse-papiers. + Veuillez entrer votre code PIN à 6 chiffres. + Êtes-vous sûr de vouloir quitter la procédure d\'inscription ? + Oui + Non + Vérification… + Demander un SMS… + Le code PIN que vous avez entré est incorrect. + Le code PIN que nous vous avons envoyé a expiré. + Erreur réseau inconnue. + Réponse inconnue du serveur. + Impossible de se connecter au serveur. + Impossible d\'établir une connexion sécurisée. + Impossible de trouver le serveur. + Une erreur est survenue pendant le traitement de votre requête. + Entrée utilisateur incorrecte + Temporairement indisponible. Réessayez plus tard. + Pas de connexion réseau. + Veuillez réessayer dans%s + Vous êtes à taux limité + Trop de tentatives + Vous utilisez une version obsolète de cette application. + Mettre à jour + Ce numéro de téléphone est actuellement connecté avec un autre appareil. + Veuillez entrer votre nom pour que les personnes qui ne vous ont pas dans leur carnet d’adresses sachent qui vous êtes. + Votre nom + Entrez votre nom + Utilisez le bouton modifier pour définir votre nom. + Rejeter la demande + Installer Orbot + Démarrer Orbot + Aucune application de marché installée. + Ce canal rendra votre adresse XMPP publique + e-book + Original (non compressé) + Ouvrir avec… + Photo de profil pour Conversations + Choisir un compte + Restaurer la sauvegarde + Restaurer + Entrez votre mot de passe pour que le compte %s restaure la sauvegarde. + N\'utilisez pas la fonctionnalité de sauvegarde de la restauration pour tenter de cloner (exécuter simultanément) une installation. La restauration d’une sauvegarde ne concerne que les migrations ou en cas de perte du périphérique d’origine. + Impossible de restaurer la sauvegarde. + Impossible de déchiffrer la sauvegarde. Le mot de passe est-il correct ? + Sauvegarde & restauration + Entrez l\'adresse XMPP + Créer un groupe + Rejoindre le canal public + Créer un groupe privé + Créer un canal public + Nom du canal + Adresse XMPP + Veuillez donner un nom au canal + Veuillez renseigner une adresse XMPP + Ceci est une adresse XMPP. Veuillez renseigner un nom. + Création d\'un canal public… + Ce canal existe déjà + Vous avez rejoint un canal existant + Impossible de sauvegarder la configuration du canal + Autoriser quiconque à éditer le sujet + Permettre à quiconque d\'inviter d\'autres personnes + N\'importe qui peut éditer le sujet. + Les propriétaires peuvent éditer le sujet. + Les administrateurs peuvent modifier le sujet. + Les propriétaires peuvent inviter d\'autres personnes. + N\'importe qui peut inviter d\'autres personnes. + Les adresses XMPP sont visibles par les administrateurs. + Les adresses XMPP sont visibles par tous. + Ce canal public n\'a pas de participants. Invitez vos contacts ou utilisez le bouton de partage pour distribuer son adresse XMPP. + Ce groupe privé n\'a aucun participant. + Gérer les privilèges + Rechercher des participants + Fichier trop volumineux + Joindre + Découverte des canaux + Recherche des canaux + Violation possible de la confidentialité ! + search.jabber.network.

L\'utilisation de cette fonction transmettra votre adresse IP et les termes de recherche à ce service. Veuillez consulter leur Politique de confidentialité pour plus d\'information.]]>
+ J\'ai déjà un compte + Ajouter un compte existant + Enregistrer un nouveau compte + Ceci ressemble à une adresse de domaine + Ajouter quand même + Ceci ressemble à une adresse de canal + Partager les fichiers de sauvegardes + Sauvegarder les conversations + Événement + Ouvrir sauvegarde + Le fichier sélectionné n\'est pas une sauvegarde de Conversations + Ce compte a déjà été configuré + Veuillez saisir le mot de passe pour ce compte + Impossible de réaliser cette action + Rejoindre le canal public… + L\'application de partage n\'a pas accordé la permission d\'accéder à ce fichier. + + jabber.network + Serveur local + La plupart des utilisateurs devraient choisir « jabber.network » pour de meilleures suggestions provenant de l’écosystème public entier de XMPP. + Méthode de découverte des canaux + Sauvegarde + À propos + Veuillez activer votre compte + Appeler + Appel entrant + Appel vidéo entrant + Connexion en cours + Connecté + Reconnexion en cours + Accepter les appels + Fin d\'appel + Décrocher + Ignorer + Découverte des appareils + Ça sonne + Occupé + Impossible de connecter l\'appel + Connexion perdue + Appel annulé + Échec de l\'application + Vérification du problème + Raccrocher + Appel en cours + Appel vidéo en cours + En cours de reconnexion de l\'appel + En cours de reconnexion de l\'appel vidéo + Désactivez Tor afin de passer des appels + Appel entrant + Appel entrant · %s + Appel manqué · %s + Appel sortant + Appel sortant · %s + Appel manqué + Appel audio + Appel vidéo + Aide + Basculer vers la conversation + Votre micro n\'est pas disponible + Vous ne pouvez prendre qu\'un appel à la fois. + Reprendre l\'appel en cours + Impossible de changer de caméra + Épingler en haut de la liste + Détacher du haut de la liste + Trace GPX + Impossible de corriger le message + Toutes les conversations + Cette conversation + Votre avatar + Avatar pour %s + Chiffré avec OMEMO + Chiffré avec OpenPGP + Non chiffré + Quitter + Enregistrer un message vocal + Lire l\'audio + Mettre en pause l\'audio + Ajouter un contact, créer ou joindre un groupe de discussion, ou découvrir les salons + + Voir %1$d participant + Voir %1$d participants + Voir %1$d participants + + + Certains messages n\'ont pas pu être distribués + Certains messages n\'ont pu être distribués + Certains messages n\'ont pu être distribués + + Échec lors de la livraison + Plus d\'options + Aucune application trouvée + Inviter à Conversations + Impossible de lire l\'invitation + Le serveur ne prend pas en charge la génération d\'invitations + Aucun compte actif ne prend en charge cette fonctionalité + Impossible d’activer la vidéo. + La création de nouveaux comptes n’est pas prise en charge + Aucune adresse XMPP trouvée +
\ No newline at end of file diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml new file mode 100644 index 000000000..377b7da8b --- /dev/null +++ b/app/src/main/res/values-gl/strings.xml @@ -0,0 +1,1013 @@ + + + Axustes + Nova conversa + Xestionar contas + Xestionar conta + Pechar conversa + Detalles do contacto + Detalles da conversa de grupo + Detalles da canle + Engadir conta + Editar contacto + Engadir a libreta de enderezos + Eliminar contacto da lista + Bloquear contacto + Desbloquear contacto + Bloquear dominio + Desbloquear dominio + Bloquear persoa + Desbloquear persoa + Xestionar contas + Axustes + Compartir na conversa + Iniciar conversa + Escoller Contacto + Seleccionar contactos + Compartir coa conta + Lista de bloqueo + agora + Hai 1 min + hai %d minutos + + %d conversa sen ler + %d conversas sen ler + + enviando… + Descifrando a mensaxe. Agarda por favor… + Mensaxe cifrado con OpenPGP + O alcume xa está en uso + Alcume non válido + Admin + Dono + Moderador + Participante + Visitante + Desexas eliminar a %s da túa lista de contactos? As conversas con este contacto non se eliminarán. + 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 + Bloqueado + Desexas eliminar o marcador %s? As conversas deste marcador non se eliminarán. + Rexistrar nova conta no servidor + Cambiar o contrasinal no servidor + Compartir con… + Comezar conversa + Convidar contacto + Convidar + Contactos + Contacto + Cancelar + Establecer + Engadir + Editar + Eliminar + Bloquear + Desbloquear + Gardar + OK + %1$s fallou + Ao utilizar a túa conta XMPP para enviar trazas do sistema axudas ao desenvolvemento de %1$s. + Enviar agora + Non preguntar de novo + Non se puido conectar a conta + Non puido conectarse a múltiples contas + Toca para xestionar as túas contas + Adxuntar + ¿Engadir este contacto faltante a lista de contactos? + Engadir contacto + Erro ao enviar + Preparándose para enviar a imaxe + Preparándose para enviar imaxes + Compartindo ficheiros. Agarda… + Baleirar historial + Eliminar historial da conversa + ¿Queres eliminar tódalas mensaxes desta conversa\? +\n +\nAviso: Esto non lle afecta ás mensaxes gardadas noutros dispositivos ou servidores. + Eliminar ficheiro + 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 + Enviar mensaxe a 1%s + Enviar mensaxe cifrada con OMEMO + Enviar mensaxe cifrada v\\OMEMO + Enviar mensaxe cifrado con OpenPGP + Novo alcume en uso + Enviar sen cifrar + Fallou o descifrado. Quizais non teñas a chave privada apropiada. + OpenKeychain + OpenKeychain para cifrar e descifrar as mensaxes e xestionar a túas chaves públicas.

Está baixo licenza GPLv3+ e dispoñible en F-Droid e Google Play.

(Reinicia %1$s após a instalación.)]]>
+ Reiniciar + Instalar + Instala OpenKeychain por favor + ofrecendo… + agardando… + Clave OpenPGP non atopada + Non se cifrou a mensaxe porque o contacto non está publicando a súa chave pública.\n\nPídelle ao contacto que configure OpenPGP. + Non se atoparon chaves OpenPGP + Non se cifrou a mensaxe porque os contactos non están publicando a súas chaves públicas.\n\nPídelles que configuren OpenPGP. + Xeral + Aceptar ficheiros + De forma automática aceptar ficheiros menores de… + Complementos + Notificación + Vibrar + Vibra cando chega unha nova mensaxe + Notificación LED + Luz pestanexante cando chegue unha nova mensaxe + Ton de chamada + Son da notificación + Son da notificación para novas mensaxes + Ton para as chamadas entrantes + Período de graza + O tempo no que as notificacións son silenciadas tras detectar actividade nalgún dos teus outros dispositivos. + Avanzado + 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 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. + Aceptar + Produciuse un erro + Fallo + A túa conta + Enviar actualizacións de presenza + Recibir actualizacións de presenza + Solicitar actualizacións de presenza + Seleccionar imaxe + Facer foto + 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 seleccionar esta imaxe non deu permisos suficientes para ler o ficheiro. +\n +\nUsa un xestor de ficheiros diferente para escoller a imaxe. + A app que usaches para compartir este ficheiro non concedeu os permisos suficientes. + Descoñecido + Desactivado temporalmente + Conectado + Conectando\u2026 + Desconectado + Non autorizado + Servidor non atopado + Sen conectividade + Erro no rexistro + O identificador xa está en uso + Rexistro completado + O servidor non permite o rexistro + O testemuño de rexistro non é válido + Fallo a negociación TLS + Dominio non verificable + Violación da política + Servidor incompatible + Cliente non compatible + Erro de fluxo + Fallo ao abrir o fluxo + Non cifrado + OTR + OpenPGP + OMEMO + Eliminar conta + Desactivar temporalmente + Publicar avatar + Publicar chave pública OpenPGP + Eliminar a chave pública OpenPGP + Tes a certeza de que queres eliminar a túa chave pública OpenPGP do anuncio de presenza? \nOs teus contactos non poderán enviarche mensaxes cifradas con OpenPGP. + Publicada a chave pública OpenPGP. + Habilitar + Seguro? + Ao eliminar a conta eliminas todo o historial de conversas + Gravar audio + Enderezo XMPP + Bloquear enderezo XMPP + usuaria@exemplo.com + Contrasinal + Non é un enderezo XMPP válido + Memoria insuficiente. Imaxe demasiado grande + Queres engadir a %s a túa libreta de enderezos? + Info do servidor + XEP-0313: MAM + XEP-0280: Copia de mensaxes + XEP-0352: Indicativo do estado do cliente + XEP-0191: Bloqueo de ordes + XEP-0237: Roster Versioning + XEP-0198: Stream Management + XEP-0215: Descubrimento de Servizo Externo + XEP-0163: PEP (Avatars / OMEMO) + XEP-0363: HTTP File Upload + XEP-0357: Push + Dispoñible + Fallo + Anuncios de chave pública non notificados + acaba de estar dispoñible + visto hai un minuto + visto hai 1%d minutos + visto hai unha hora + visto hai 1%d horas + visto hai un día + última conexión hai 1%d días + Mensaxe cifrada. Instala OpenKeychain para descifrala. + Atoparonse novas mensaxes cifradas cn OpenPGP + ID da chave OpenPGP + Impresión dixital OMEMO + v\\impresión OMEMO + Impresión dixital OMEMO (orixe da mensaxe) + v\\Impresión dixital OMEMO (orixe da mensaxe) + Outros dispositivos + Confiar en impresións dixitais OMEMO + Obtendo chaves… + Feito + Descifrar + Marcadores + Buscar + Introducir contacto + Eliminar contacto + Ver os detalles do contacto + Bloquear contacto + Desbloquear contacto + Crear + Selecionar + Xa existe o contacto + Unirse + canle@sala.exemplo.com/alcume + canle@sala.exemplo.com + Gardar como marcador + Eliminar marcador + Destruír a conversa en grupo + Eliminar canle + 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 + Editar o tema da conversa en grupo + Asunto + Entrando na conversa en grupo… + Saír + Contacto engadido a túa lista de contactos + Volver a engadir + %s leu até aquí + %s leron até aquí + %1$s + %2$d outras leron até aquí + Todas leron até aquí + Publicar + Toca no avatar para elixir a imaxe na galería + Publicando… + O servidor rexeitou a túa publicación + Non se puido converter a imaxe + Non se puido salvar o avatar no disco + (ou pulsación longa para volver ao valor por defecto) + O servidor non soporta a publicación de avatares + murmurado + a %s + Enviar mensaxe privada a %s + Conectar + Esta conta xa existe + Seguinte + Sesión establecida + Omitir + Desactivar notificacións + Habilitar + A conversa en grupo require contrasinal + Introducir contrasinal + Primeiro solicita actualizacións de presenza aos contactos.\n\nEsto utilizarase para determinar que aplicación de chat está a utilizar o contacto. + Solicitar agora + Ignorar + Aviso: Ao enviar esto sen mutuas actualizacións de presenza podería dar problemas.\n\nVaite a \"Detalles do contacto\" para verificar as subscricións de presenza. + Seguridade + Permitir a corrección de mensaxes + Permitir aos teus contactos editar as súas mensaxes de xeito retroactivo + Axustes de experta + Por favor ten tino con estos axustes + Acerca de %s + Non molestar + Hora de inicio + Hora de finalización + Establecer horario sen notificacións + As notificacións serán silenciadas durante estas horas + Outro + Sincronizar marcadores + Poñer marca de \"autojoin\" ao entrar ou deixar unha MUC e reaccionar ás modificacións feitas desde outros clientes. + Copiouse a impresión dixital OMEMO ao portapapeis + Non podes acceder a esta conversa en grupo + Esta conversa en grupo é so para membros + Restrición do recurso + Xa foi expulsado de esta conversa en grupo + A conversa en grupo foi apagada + Xa non estás nesta conversa en grupo + Deixaches esta conversa en grupo por razóns técnicas + utilizando a conta %s + hospedado en %s + Comprobando %s no servidor HTTP + Non está conectada. Inténteo máis tarde + Comprobando o tamaño de %s + Comprobar o tamaño de %1$s en %2$s + Opcións da mensaxe + Cita + Pegar como cita + Copiar URL orixinal + Enviar de novo + URL do ficheiro + URL copiado ao portapapeis + Copiouse o enderezo XMPP ao portapapeis + Mensaxe do fallo copiado ao portapapeis + Dirección Web + Escanear código de barras 2D + Mostar código de barras 2D + Mostrar lista de bloqueo + Detalles da conta + Confirmar + Inténteo de novo + Servizo en primeiro plano + Evita que o sistema operativo corte a conexión + Crear copia de apoio + Os ficheiros de copia gardaranse en %s + Creando ficheiros de apoio + Creouse o ficheiro + Os ficheiros de apoio gardáronse en %s + Restaurando a copia + O copia foi restablecida + Non esqueza activar a conta. + Escoller ficheiro + Recibindo %1$s (%2$d %% completado) + Descargar %s + Eliminar %s + ficheiro + Abrir %s + enviando (%1$d %% completado) + Preparándose para compartir o ficheiro + Ofreceuse %s para descargar + Cancelar a transmisión + non puido compartir o ficheiro + Detívose a transmisión do ficheiro + Ficheiro eliminado + 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 + 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 + Avatar da conta + Copiar impresión OMEMO ao portapapeis + Rexenerar a chave OMEMO + Desconectar dispositivos + Tes a certeza de que queres eliminar os outros dispositivos OMEMO publicados? A próxima vez que un dos teus dispositivos se conecte, deberá volver a anunciarse, mais podería non recibir mensaxes mentras tanto. + Non hai chaves utilizables dispoñibles para este contacto.\nNon se obtiveron novas chaves do servidor. ¿Podería ser que algo fallase no teu servidor de contactos? + Non hai chaves utilizables para este contacto.\nAsegúrate de que ambas tedes subscrición de presenza activada. + Algo saíu mal + Obtendo historial desde o servidor + Non hai máis historial no servidor + Actualizando… + Mudou o contrasinal! + Non puido mudar o contrasinal + Cambiar contrasinal + Contrasinal actual + Novo contrasinal + O contrasinal non pode estar baleiro + Habilitar todas as contas + Desactivar todas as contas + Realizar a acción con + Sen afiliación + Offline + Outcast + Membro + Modo avanzado + Conceder privilexios de membresía + Retirar privilexios de membresía + Dar privilexios de administración + Revocar privilexios de administración + Conceder privilexios de propiedade + Retirar privilexios de propiedade + Eliminar da conversa en grupo + Retirar da canle + Non se puido mudar a afiliación de %s + Prohibición da conversa en grupo + Vetar na canle + Estás a intentar eliminar a %s dunha canle pública. O único xeito de facelo é prohibíndolle o acceso para sempre. + Rexeitar agora + Non se puido mudar o rol de %s + Configuración do grupo privado de conversa + Configuración da canle pública + Privada, só para membros + Facer os enderezos XMPP visibles para calquera + Establecer canle como moderada + Non estás a participar + ¡Opcións da conversa en grupo modificadas! + Non se puideron modificar as opcións da conversa en grupo + Nunca + Ate novo aviso + Pospor + Responder + Marcar como lido + Entrada + Enter envía + Usa a tecla Enter para enviar a mensaxe. Igualmente poderás usar Ctrl+Enter para enviar, incluso se esta opción está desactivada. + Mostrar a tecla Enter + Cambiar a chave de emoticonas pola tecla Enter + son + video + imaxe + gráfico de vector + ficheiro multimedia + documento PDF + App Android + Contacto + Publicouse o avatar! + Enviando %s + Ofrecendo %s + Ocultar fora de liña + %s está escribindo… + %s deixou de escribir + %s están escribindo… + %s deixaron de escribir + Notificacións de escritura + Permitelle aos teus contactos que saiban cando lles estás a escribir + Enviar localización + Mostrar localización + Non se atopou unha app para mostrar a localización + Localización + Pechouse a conversa + Deixar o grupo de conversa privada + Deixar a canle pública + Non confiar nas CAs do sistema + Todos os certificados deberán ser aprobados manualmente + Eliminar certificados + Borrar os certificados aprobados manualmente + Sen certificados aprobados manualmente + Eliminar certificados + Borrar seleción + Cancelar + + %d certificado eliminado + %d certificados eleminados + + Substituír o botón \"Enviar\" cunha acción rápida + Acción rápida + Ningunha + Utilizadas recentemente + Elixe a acción rápida + Buscar contactos + Buscar marcadores + Enviar mensaxe privada + %1$s deixou a conversa en grupo + Identificador + Identificador + Este non é un identificador válido + Fallou a descarga: non se atopou o servidor + Fallou a descarga: non se atopou o ficheiro + Fallou a descarga: Non se puido conectar ao servidor + Fallou a descarga: non se escribeu o ficheiro + Fallou a descarga: ficheiro non válido + Sen acceso a rede Tor + Fallou a ligazón + O servidor non corresponde a este dominio + Roto + Dispoñibilidade + Ausente cando o dispositivo está bloqueado + Mostrar como Ausente cando o dispositivo está bloqueado + En modolo silencioso, Ocupado + Mostrar como Ocupado se o dispositivo está en silencio + Trata a vibración como modo silencioso + Mostrar como Ocupado cando o dispositivo está en vibración + Axustes ampliados de conexión + Mostar axustes de servidor e porto cando se configura unha conta + xmpp.exemplo.com + Accede con certificado + Non se puido procesar o certificado + Gardando axustes + Axustes de gardado no servidor + Obtendo os axustes de gardado. Agarda… + Non se obtiveron os axustes gardados + Requírese o CAPTCHA + Introduza o texto da imaxe superior + Cadea do certificado sen confianza establecida + Os enderezos XMPP non concordan co certificado + Anovar certificado + Fallo obtendo a chave OMEMO! + Comprobouse a chave OMEMO co certificado! + 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 + Servidor + Porto + Enderezo do servidor ou .onion + Este non é un número válido de porto + Este non é un servidor válido + %1$d de %2$d contas conectadas + + %d mensaxe + %d mensaxes + + Cargar máis mensaxes + Ficheiro compartido con %s + Imaxe compartida con %s + Imaxes compartidas con %s + Texto compartido con %s + Permitir que %1$s acceda ao almacenaxe externo + Permitir que %1$s acceda á cámara + Sincronice con todos os contactos + %1$s quere ter permiso para acceder á túa libreta de enderezos para comparala coa lista de contactos XMPP.\nDeste xeito poderá mostrar o nome completo e avatares dos teus contactos.\n\n%1$s só utilizará de xeito local a túa lista de contactos, sen subila a ningún servidor. +
Non gardaremos unha copia desos números de teléfono.\n\nPara máis información le a nosa política de privacidade.

A continuación pediremos permiso para acceder aos contactos.]]>
+ Notificar todas as mensaxes + Notificar só cando é mencionada + Notificacións desactivadas + Notificacións pausadas + Compresión de imaxes + Truco: Utiliza \'Escoller ficheiro\' no lugar de \'Escoller imaxe\' para enviar imaxes individuais sen comprimir obviando este axuste. + Sempre + Só imaxes grandes + Optimizacións de batería activadas + O dispositivo está facendo optimizacións de batería fortes para %1$s e podería retrasar as notificacións ou incluso perder mensaxes.\nÉ recomendable desactivalas. + O dispositivo está facendo fortes optimizacións de batería para %1$s e podería causar retrasos nas notificacións e incluso perda de mensaxes\n\nAgora ímosche pedir que as desactives. + Desactivar + O área selecionada é demasiado grande + (Contas non activadas) + Este campo é requerido + Correxir mensaxe + Enviar mensaxe correxida + Xa validaches as impresións dixitais destas persoas de xeito seguro para confiar nelas. Ao escoller \"Feito\" estás simplemente confirmando que %s é parte desta conversa en grupo. + Desactivou esta conta + Fallo de seguridade: Acceso non válido ao ficheiro! + Non se atopou unha app para compartir URI + Compartir URI con… +
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 teu enderezo XMPP completo será: %s + Crear conta + Utilizar o meu propio proveedor + Elixe un nome de usuaria + Xestionar a dispoñibilidade manualmente + Configura a túa dispoñibilidade ao editar a mensaxe de estado. + Mensaxe de estado + Dispoñible para conversar + En liña + Fóra + Non dipoñible + Ocupada + Xerouse un contrasinal seguro + 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 conversa en grupo… + Convidar de novo + Desactivar + Breve + Medio + Longo + Publicar utilización + Permitelle aos teus contactos saber cando estás a usar Conversations + Privacidade + Decorado + Elixe a gama de cores + Automático + Claro + Escuro + Fondo verde + Utilizar fondo verde para mensaxes recibidas + Non se puido conectar con OpenKeychain + Este dispositivo xa non está en uso + Computadora + Teléfono móbil + Tableta + Navegador web + Consola + Pagamento requerido + Conceder permiso para usar Internet + Eu + O contacto pide a suscrición de presenza + Permitir + Se permiso para acceder %s + Non se atopa o servidor remoto + Espera do servidor remoto + Non se actualizou a conta + Denuncia esta conta XMPP por facer spam. + 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 + Debes ter conexión para publicar o teu avatar. + Mostrar mensaxe do fallo + Mensaxe de fallo + Aforrador de datos habilitado + O teu sistema operativo está limitando o acceso a internet en segundo plano para %1$s. Para recibir notificacións de novas mensaxes deberías permitir que %1$s teña acceso sen restricións cando o \"Economizador de datos\" está activo.\n%1$s continuará aforrando datos cando fose posible. + O dispositivo non ten soporte para desactivar o aforrador de datos para %1$s. + Non se puido crear o ficheiro temporal + Este dispositivo foi verificado + Copiar impresión dixital + Verificaches tódalas chaves OMEMO da túa propiedade + O código de barras non contén impresións dixitais para esta conversa. + Impresións dixitais verificadas + Utilice a cámara para escanear o código de barras do contacto + Por favor agarde mentras se obteñen as chaves + Compartir como código de barras + Compartir como URI XMPP + Compartir como ligazón HTTP + Confianza cega antes da verificación + Confiar en dispositivos novos de contactos non verificados, pero solicitar confirmación manual de novos dispositivos para contactos verificados. + Chaves OMEMO de confianza cega, significa que podería ser calquera outra persoa ou algunha impostora. + Non confiables + Código de barras 2D non válido + Baleirar o cartafol da caché (utilizado pola cámara) + 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 + 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. + Vas verificar as chaves OMEMO da túa propia conta. Esto só é seguro se seguiches esta ligazón desde unha orixe segura onde só tí poderías ter publicado esta ligazón. + Continuar + Validar chaves OMEMO + Mostrar 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\". + + %d segundo + %d segundos + + + 1%d minutos + %d minutos + + + %d hora + %d horas + + + %ddias + %d dias + + + %dsemanas + %d semanas + + + %d mes + %d meses + + Borrado automático de mensaxes + Borrar mensaxes de xeito automático de este dispositivo que anteriores ao marco temporal configurado. + Cifrando mensaxe + Sen obtención de mensaxes debido ao período de retención local. + Comprimindo vídeo + Conversas correpondentes pechadas. + Contacto bloqueado. + Notificacións de estraños + Notificar as mensaxes e chamadas recibidas por parte de extraños. + Mensaxe recibida de un estraño + Bloquear estraño + Bloquear o dominio ao completo + En liña neste momento + Volver a intentar o descifrado + Fallo na sesión + Mecanismo SASL desactualizado + O servidor require rexistro no sitio web + Abrir sitio web + Non se atopou app para abrir sitio web + Notificacións Heads-up + Mostrar notificacións emerxentes + Hoxe + Onte + Validar servidor con DNSSEC + Os certificados de servidor que conteñen o nome de servidor validado considéranse verificados + O certificado non contén un enderezo XMPP + parcial + Gravar vídeo + Copiar ao portapapeis + Mensaxe copiada ao portapapeis + Mensaxe + As mensaxes privadas están desactivadas + Apps protexidos + Para seguir recibindo notificacións, incluso cando a pantalla está apagada, tes que engadir Conversations á lista de apps protexidas. + ¿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: + Queres conectarte de todos os xeitos? + Detalles do certificado: + Unha vez + O escaner de código QR necesita acceso á cámara + Desprazarse ata a parte inferior + Desprazarse cara abaixo logo de enviar unha mensaxe + Editar a Mensaxe de Estado + Editar a mensaxe de estado + Desactivar a encriptación + %1$s non pode enviar mensaxes cifradas a %2$s. Podería deberse a que o teu contacto utiliza un servidor sen actualizar ou un cliente que non pode xestionar OMEMO. + Non se obtivo a lista de dispositivos + Non se obtiveron as chaves de cifrado + Suxestión: Nalgúns casos, isto pode solucionarse engadíndovos mutuamente as vosas listas de contactos. + Tes a certeza de querer desactivar o cifrado OMEMO para esta conversa\? +\nIsto permitirá á administración do teu servidor ler as túas mensaxes, pero pode ser a única forma de comunicarse con persoas que usan clientes obsoletos. + Desactivar agora + Borrador: + Cifrado OMEMO + OMEMO sempre se utilizará para charlas individuais e privadas en grupo. + 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 letra + O tamaño relativo da letra que utiliza a app. + Activado por defecto + Desactivado por defecto + Pequena + Mediana + Grande + A mensaxe non foi cifrada para este disposivivo. + Fallo ao descifrar a mensaxe OMEMO. + desfacer + Compartir Localización está desactivado + Fixar posición + Soltar posición + Copiar Localización + Compartir Localización + Direccións + Compartir localización + Mostrar localización + Compartir + Non comezou a gravación + Por favor, agarda… + Permitir que %1$s acceda ao micrófono + Buscar mensaxes + GIF + Ver conversa + Complemento para Compartir Localización + Utiliza o Complemento para Compartir Localización no lugar do mapa incluído + Copiar a dirección web + Copiar enderezo XMPP + Compartición de ficheiro HTTP para S3 + Busca directa + Na pantalla \'Iniciar Conversa\' abrir teclado e pór o cursor no campo de busca + Avatar da conversa de grupo + O servidor non soporta o avatar na conversa de grupo + Só o dono pode cambiar o avatar da conversa de grupo + Nome do contacto + Alcume + Nome + Proporcionar un nome é optativo + Nome da conversa de grupo + Esta conversa de grupo foi destruída + Non se gardou a gravación + Servizo en segundo plano + Esta categoría de notificacións utilízase para mostrar unha notificación permanente indicando que %1$s está a funcionar. + Información do estado + Problemas de conexión + Esta categoría de notificación utilízase para mostrar unha notificación en caso de que houbese un problema ao conectar a conta. + Mensaxes + Chamadas + Mensaxes + Chamadas recibidas + Chamadas realizadas + Chamadas perdidas + Mensaxes acalados + Este grupo de notificacións é utilizado para mostrar notificacións que non debería activar ningún son. Por exemplo, cando está activo en outro dispositivo (Período de Graza). + Entregas fallidas + Axustes de notificación das mensaxes + Axustes da notificación de chamadas + Importancia, Son, Vibrar + Compresión de vídeo + Ver medios + Participantes + Navegador de medios + Ficheiro omitido debido a transgresión da seguridade. + Calidade de vídeo + Menor calidade significa ficheiros máis pequenos + Media (360p) + Alta (720p) + cancelada + Xa está a escribir un borrador. + Característica non implementada + Código de país non válido + Indica un país + 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 escribe o teu número de teléfono. + Buscar países + Validar %s + %s.]]> + Enviamosche outro SMS cun código de 6 díxitos. + Escribe o PIN de 6 díxitos. + Reenviar SMS + Reenviar SMS (%s) + Agarda por favor (%s) + atrás + Copiado automático desde o portapapeis. + Por favor, escribe o pin de 6 díxitos. + Seguro que quere cancelar o proceso de rexistro? + Si + Non + Validando… + Solicitando SMS… + O pin introducido non é correcto. + O pin que che enviamos caducou. + Fallo descoñecido na rede. + Resposta descoñecida desde o servidor. + Non se puido conectar co servidor. + Non se puido establecer unha conexión segura. + Non se atopou o servidor. + Algo fallou ao xestionar a túa solicitude. + Entrada da usuaria non válida + Non dispoñible temporalmente. Inténtao máis tarde. + Se conexión a rede. + Inténteo de novo en %s + Taxa de transferencia limitada + Demasiados intentos + Estás a usar unha versión desactualizada desta app. + Actualizar + Este número de teléfono está actualmente ligado a outro dispositivo. + Por favor, escribe o teu nome para permitir que a xente que non te ten na axenda de enderezos sepa quen es. + O teu nome + Escribe o teu nome + Establece o teu nome usando o botón editar. + Rexeitar solicitude + Instalar Orbot + Iniciar Orbot + 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 + Elixir conta + Restablecer copia de apoio + Restablecer + Escribe o contrasinal da conta %s para restablecer a copia. + Non utilices a función de restaurar a copia nun intento de clonar (utilizar simultaneamente) unha instalación. Restaurar unha copia só ten sentido para migrar ou en caso de perda do dispositivo orixinal. + Non se puido restaurar a copia. + Non se puido descifrar a copia. É correcto o contrasinal? + Respaldar & Restaurar + Introducir enderezo XMPP + Crear grupo de conversa + Unirse a canle pública + Crear grupo privado de conversa + Crear canle pública + Nome da canle + Enderezo XMPP + Por favor, escribe un nome para a canle + Por favor, escribe un enderezo XMPP + Esto é un enderezo XMPP. Por favor, escribe un nome. + Creando canle pública… + Esta canle xa existe + Entraches nunha canle existente + Non se gardaron os axustes da canle + Permitir que calquera cambie o asunto + Permitir que calquera poida convidar + Calquera pode editar o asunto. + As propietarias poden editar o asunto. + Admins poden editar o asunto. + Propietarias poden convidar a outras. + Calquera pode convidar a outras. + Os enderezos XMPP son visibles para a administración. + Os enderezos XMPP son visibles para calquera. + Esta canle pública non ten participantes. Convida aos teus contactos ou utiliza o botón compartir para distribuír o teu enderezo XMPP. + Este grupo privado non ten participantes. + Xestionar privilexios + Buscar participantes + Ficheiro demasiado grande + Anexar + Descubrir canles + Buscar canles + Posible intrusión na privacidade! + search.jabber.network.

Ao utilizar esta función transmitirás o teu enderezo IP e termos de busca a ese servizo. Le a súa Política de Privacidade para máis información.]]>
+ Xa teño unha conta + Engadir conta existente + Rexistrar unha nova conta + Esto semella un enderezo de dominio + Engadir igualmente + Esto semella o enderezo dunha canle + Compartir ficheiros de apoio + Respaldar Conversations + Evento + Abrir copia de apoio + O ficheiro seleccionado non é un ficheiro de apoio Conversations + Esta conta xa foi configurada + Introduza o contrasinal de esta conta + Non se puido completar a acción + Unirse a canle pública… + A aplicación que comparte non proporciona permiso para acceder ao ficheiro. + + jabber.network + Servidor local + A maioría das usuarias debería escoller \'jabber.network\' para obter mellores suxestións desde o ecosistema público XMPP. + Método de descubrimento de canles + Copia de apoio + Acerca de + Activa unha conta por favor + Facer unha chamada + Chamada entrante + Videochamada entrante + Cambiar a unha chamada de vídeo? + Engadir pistas adicionais? + Conectando + Conectado + Reconectando + Aceptando a chamada + Rematando a chamada + Responder + Rexeitar + Atopando dispositivos + Sonando + Ocupado + Non se pode establecer a chamada + Perdeuse a conexión + Chamada cortada + Fallo na aplicación + Problema na verificación + Colgar + Chamada en curso + Videochamada en curso + Reconectando a chamada + Reconectando a videochamada + Desactivar Tor para facer chamadas + Chamada entrante + Conversa de · %s + Chamada perdida · %s + Chamada realizada + Conversa de · %s + Chamada perdida + + %1$d chamada perdida de %2$s + %1$d chamadas perdidas de %2$s + + + %d chamada perdida + %d chamadas perdidas + + + %1$d chamadas perdidas de %2$d contacto + %1$d chamadas perdidas de %2$d contactos + + Chamada de audio + Chamada de vídeo + Axuda + Ir á conversa + O micrófono non está dispoñible + Só podes manter unha chamada en cada momento. + Volver á chamada activa + Non se puido activar a cámara + Fixar enriba + Desafixar de enriba + Ruta GPX + No se pode correxir a mensaxe + Todas as conversas + Esta conversa + O teu avatar + Avatar para %s + Cifrado con OMEMO + Cifrado con OpenPGP + Sen cifrar + Saír + Gravar correo de voz + Reproducir audio + Pausar audio + Engade un contacto, crea o únete a unha conversa en grupo ou descubre canles + + Ver %1$d Participante + Ver %1$d Participantes + + + Unha mensaxe non se entregou + Algunhas mensaxes non se entregaron + + Entregas fallidas + Máis opcións + Non se atopa unha aplicación + Convida a Conversations + Non se puido enviar o convite + O servidor non soporta a creación de convites + 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 + Non está permitido o rexistro de novas contas + Non se atopa un enderezo XMPP + Fallo temporal da autenticación + Eliminar avatar + As chamadas están desactivadas cando usas Tor + Cambiar a vídeo + Rexeitar a solicitude para cambiar a vídeo + Distribuidor UnifiedPush + Conta XMPP + A conta a través da cal se recibirán as mensaxes push. + Servidor Push + O servidor elexido pola usuaria para obter as mensaxes push a través de XMPP. + Ningún (desactivado) +
\ No newline at end of file diff --git a/app/src/main/res/values-hi-rIN/strings.xml b/app/src/main/res/values-hi-rIN/strings.xml new file mode 100644 index 000000000..c757504ac --- /dev/null +++ b/app/src/main/res/values-hi-rIN/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml new file mode 100644 index 000000000..2ee852279 --- /dev/null +++ b/app/src/main/res/values-hr/strings.xml @@ -0,0 +1,90 @@ + + + Postavke + Novi razgovor + Upravljanje računima + Upravljaj računom + Zatvori razgovor + Kontakt podaci + Pojedinosti grupnog razgovora + Detalji kanala + Dodaj račun + Uredi ime + Dodaj u adresar + Izbriši s popisa + Blokiraj kontakt + Odblokiraj kontakt + Blokiraj domenu + Odblokiraj domenu + Blokiraj sudionika + Deblokiraj sudionika + Upravljanje računima + Postavke + Dijeli s Conversation + Započni razgovor + Odaberite Kontakt + Odaberite kontakte + Dijeli putem računa + Lista blokiranih + upravo sad + prije 1 min + prije %d min + + %d nepročitan razgovor + + + %d nepročitanih razgovora + + + %d nepročitani razgovori + + + slanje… + Dešifriranje poruke. Molimo pričekajte… + OpenPGP šifrirana poruka + Nadimak je već u upotrebi + Nevažeći nadimak + Admin + Vlasnik + Moderator + Sudionik + Posjetitelj + Želite li ukloniti %s s popisa kontakata? Razgovori s ovim kontaktom neće biti uklonjeni. + Želite li blokirati %s da vam šalje poruke? + Želite li deblokirati %s i dopustiti im da vam šalju poruke? + Blokirati sve kontakte iz %s? + Deblokirati sve kontakte iz %s? + Kontakt blokiran + Blokiran + Želite li ukloniti %s kao oznaku? Razgovori s ovom knjižnom oznakom neće biti uklonjeni. + Registrirajte novi račun na poslužitelju + Promjena lozinke na poslužitelju + Podijeli s… + Započni razgovor + Pozovi kontakt + Pozovi + Kontakti + Kontakt + Otkazati + Dodati + Uredi + Obriši + Blok + Odblokiraj + Sačuvaj + Ok + Pošalji sada + Nikad više ne pitaj + Nije moguće povezati se s računom + Nije moguće povezati se s više računa + Dodirnite za upravljanje svojim računima + Priložite datoteku + Dodati ovaj kontakt koji nedostaje na popis kontakata? + Dodaj kontakt + dostava nije uspjela + Priprema za slanje slike + Priprema za slanje slika + Dijeljenje datoteka. Molimo pričekajte… + Obriši povijest + Obriši povijest razgovora + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml new file mode 100644 index 000000000..5ad16d2e9 --- /dev/null +++ b/app/src/main/res/values-hu/strings.xml @@ -0,0 +1,901 @@ + + + Beállítások + Új beszélgetés + Fiókok kezelése + Fiók kezelése + Beszélgetés bezárása + Partner részletei + Csoportos csevegés részletei + Csatorna részletei + Fiók hozzáadása + Név szerkesztése + Hozzáadás a címjegyzékhez + Törlés a névsorból + Partner tiltása + Partner tiltásának feloldása + Tartomány tiltása + Tartomány tiltásának feloldása + Résztvevő tiltása + Résztvevő tiltásának feloldása + Fiókok kezelése + Beállítások + Megosztás a Conversations alkalmazással + Beszélgetés indítása + Partner kiválasztása + Partnerek kiválasztása + Megosztás fiókon keresztül + Tiltólista + éppen most + 1 perce + %d perce + + %d olvasatlan beszélgetés + + + %d olvasatlan beszélgetés + + + küldés… + Üzenet visszafejtése. Kérem várjon… + OpenPGP-vel titkosított üzenet + A becenév már használatban van + Érvénytelen becenév + Adminisztrátor + Tulajdonos + Moderátor + Résztvevő + Látogató + Szeretné eltávolítani %s partnerét a partnerlistájából? Beszélgetései ezzel a partnerrel nem lesznek eltávolítva. + Szeretné tiltani %s partnert, hogy ne tudjon üzeneteket küldeni? + Szeretné feloldani %s partner tiltását és lehetővé tenni számára az üzenetek küldését? + %s összes partnerét tiltja? + %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… + Beszélgetés indítása + Partner meghívása + Meghívás + Partnerek + Partner + Mégse + Beállítás + Hozzáadás + Szerkesztés + Törlés + Tiltás + 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 + Nem sikerült kapcsolódni több fiókhoz + Koppintson a fiókjai kezeléséhez + Fájl csatolása + Hozzáadja ezt a hiányzó partnert a partnerlistájához? + Partner hozzáadása + kézbesítés sikertelen + Kép előkészítése az átvitelhez + Képek előkészítése az átvitelhez + Fájlok megosztása. Kérem várjon… + Előzmények törlése + Beszélgetés előzményeinek törlése + Biztosan törölni szeretné az összes üzenetet a beszélgetésen belül?\n\nFigyelmeztetés: Ez nem fogja érinteni a más eszközökön vagy kiszolgálókon tárolt üzeneteket. + Fájl törlése + Biztosan törölni szeretné ezt a fájlt?\n\nFigyelmeztetés: Ez nem fogja törölni a fájlnak azon másolatait, amelyek más eszközökön vagy kiszolgálókon vannak tárolva. + A beszélgetés bezárása azután + Eszköz kiválasztása + Titkosítatlan üzenet küldése + Üzenet küldése + Üzenet küldése %s számára + OMEMO titkosítású üzenet küldése + v\\OMEMO titkosítású üzenet küldése + OpenPGP titkosítású üzenet küldése + Új becenév használva + 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 + felajánlás… + várakozás… + Nem található OpenPGP kulcs + Nem lehet titkosítani az üzenetét, mert a partnere nem közölte a nyilvános kulcsát.\n\nKérje meg a partnerét, hogy állítsa be az OpenPGP-t. + Nem találhatók OpenPGP kulcsok + Nem lehet titkosítani az üzenetét, mert a partnerei nem közölték a nyilvános kulcsukat.\n\nKérje meg a partnereit, hogy állítsák be az OpenPGP-t. + Általános + Fájlok fogadása + Fájlok automatikus fogadása, amelyek kisebbek mint… + Mellékletek + Értesítés + Rezgés + Rezegés új üzenet érkezésekor + LED értesítés + Értesítési fény villogása új üzenet érkezésekor + Csengőhang + Értesítési hang + Értesítési hang új üzeneteknél + Csengőhang bejövő hívásnál + Türelmi idő + Az időtartam, amíg az értesítések némítva vannak az egyéb eszközei egyikén történt tevékenység észlelése után. + Speciális + Sose küldjön összeomlási jelentéseket + 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 + Hiba + Az Ön fiókja + Jelenlétfrissítések küldése + Jelenlétfrissítések fogadása + Jelenlétfrissítések kérése + Fénykép kiválasztása + Fénykép készítése + Feliratkozási kérelem előzetes engedélyezése + A kiválasztott fájl nem kép + 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ő + Kapcsolódás\u2026 + Kilépett + Nem engedélyezett + A kiszolgáló nem található + Nincs kapcsolat + Regisztráció sikertelen + A felhasználónév már használatban van + Regisztráció befejezve + 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 + Adatfolyam megnyitási hiba + Titkosítatlan + OTR + OpenPGP + OMEMO + Fiók törlése + Letiltás átmenetileg + Profilkép közzététele + OpenPGP nyilvános kulcs közzététele + OpenPGP nyilvános kulcs eltávolítása + Biztosan el szeretné távolítani az OpenPGP nyilvános kulcsát a jelenléti közleményéből?\nA partnerei többé nem lesznek képesek OpenPGP titkosítású üzeneteket küldeni Önnek. + 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 + felhasznalonev@pelda.hu + Jelszó + Ez nem érvényes XMPP-cím + Nincs elég memória. A kép túl nagy + Hozzá szeretné adni %s partnert a címjegyzékéhez? + Kiszolgáló-információk + XEP-0313: MAM + XEP-0280: Üzenetindigók + XEP-0352: Kliensállapot-jelzés + XEP-0191: Tiltóparancs + XEP-0237: Névsorverziózás + XEP-0198: Adatfolyam-kezelés + XEP-0215: Külső szolgáltatás felderítése + XEP-0163: PEP (profilképek / OMEMO) + XEP-0363: HTTP fájlfeltöltés + XEP-0357: Leküldés + elérhető + nem érhető el + Hiányzó nyilvános kulcs bejelentései + éppen most volt aktív + egy perce volt aktív + %d perce volt aktív + egy órája volt aktív + %d órája volt aktív + 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 + Más eszközök + Megbízás az OMEMO ujjlenyomatokban + Kulcsok lekérése… + Kész + Visszafejtés + Könyvjelzők + Keresés + Partner megadása + Partner törlése + Partner részleteinek megtekintése + Partner tiltása + Partner tiltásának feloldása + Létrehozás + Kiválasztás + A partner már létezik + Csatlakozás + csatorna@konferencia.pelda.hu/becenev + csatorna@konferencia.pelda.hu + Mentés könyvjelzőként + Könyvjelző törlése + Csoportos csevegés megszüntetése + Csatorna megszüntetése + Biztosan meg szeretné szüntetni ezt a csoportos csevegést?\n\nFigyelmeztetés: A csoportos csevegés teljesen el lesz távolítva a kiszolgálóról. + Biztosan meg szeretné szüntetni ezt a nyilvános csatornát?\n\nFigyelmeztetés: A csatorna teljesen el lesz távolítva a kiszolgálóról. + Nem sikerült megszüntetni a csoportos csevegést + Nem sikerült megszüntetni a csatornát + Csoportos csevegés tárgyának szerkesztése + Téma + Csatlakozás csoportos csevegéshez… + Kilépés + A partner hozzáadta Önt a partnerlistához + Én is hozzáadom + %s elolvasta eddig a pontig + %selolvasta eddig a pontig + Mindenki elolvasta eddig a pontig + Közzététel + Érintse meg a profilképet egy fénykép kiválasztásához a galériából + Közzététel… + A kiszolgáló elutasította a közzétételt + Nem sikerült átalakítani a képet + Nem sikerült elmenteni a profilképet a lemezre + (Vagy nyomja hosszan az alapértelmezett visszaállításához) + A kiszolgáló nem támogatja a profilképek közzétételét + suttogva + %s részére + Személyes üzenet küldése %s részére + Kapcsolódás + Ez a fiók már létezik + Következő + Munkafolyamat létrehozva + Kihagyás + Értesítések letiltása + Engedélyezés + A csoportos csevegés jelszót igényel + Adja meg a jelszót + Kérés most + Mellőzés + Biztonság + Üzenetjavítás engedélyezése + Engedélyezés a partnereknek, hogy visszamenőlegesen szerkesszék az üzeneteiket + Szakértő beállítások + Legyen óvatos ezekkel + A %s névjegye + Csendes órák + Kezdési idő + Befejezési idő + Csendes órák engedélyezése + Az értesítések el lesznek némítva a csendes órák alatt + Egyéb + OMEMO ujjlenyomat a vágólapra lett másolva + Ki van tiltva ebből a csoportos csevegésből + Ez a csoportos csevegés csak tagoknak szól + Erőforráskényszer + Ki lett rúgva ebből a csoportos csevegésből + A csoportos csevegés le lett állítva + Többé már nincs ebben a csoportos csevegésben + használt fiók: %s + kiszolgálva itt: %s + %s ellenőrzése a HTTP gépen + Nincs kapcsolódva. Próbálja meg később újra + %s méretének ellenőrzése + %1$s méretének ellenőrzése itt: %2$s + Üzenet beállításai + Idézet + Beillesztés idézetként + Eredeti URL másolása + Küldés újra + Fájl URL-je + Az URL a vágólapra másolva + Az XMPP-cím a vágólapra másolva + A hibaüzenet a vágólapra másolva + webcím + 2D vonalkód beolvasása + 2D vonalkód megjelenítése + Tiltólista megjelenítése + Fiók részletei + Megerősítés + Próbálja újra + Előtér szolgáltatás + Megakadályozza az operációs rendszert abban, hogy kilője a kapcsolatát + Biztonsági mentés léterhozása + A biztonsági mentés fájljai itt lesznek tárolva: %s + Biztonsági mentés fájljainak létrehozása + A biztonsági mentés létrehozva + A biztonsági mentés fájljai itt lettek eltárolva: %s + Biztonsági mentés visszaállítása + A biztonsági mentés vissza lett állítva + Ne felejtse el engedélyezni a fiókot. + Fájl kiválasztása + %1$s fogadása (%2$d%% kész) + %s letöltése + %s törlése + fájl + %s megnyitása + küldés (%1$d%% kész) + Fájl előkészítése a megosztáshoz + %s felajánlva letöltésre + Átvitel megszakítása + a fájlmegosztás sikertelen + a fájlátvitel megszakítva + A fájl törölve lett + Nem található alkalmazás a fájl megnyitásához + Nem található alkalmazás a hivatkozás megnyitásához + Nem található alkalmazás a partner megtekintéséhez + Dinamikus címkék + Csak olvasható címkék megjelenítése a partnerek alatt + Értesítések engedélyezése + Nem található csoportos csevegés kiszolgáló + A csoportos csevegés létrehozása nem sikerült! + Fiók profilképe + OMEMO ujjlenyomat másolása a vágólapra + OMEMO kulcs újra előállítása + Eszközök törlése + Valami hiba történt + Előzmények lekérése a kiszolgálóról + Nincs több előzmény a kiszolgálón + Frissítés… + Jelszó megváltoztatva! + Nem sikerült megváltoztatni a jelszót + Jelszó megváltoztatása + Jelenlegi jelszó + Új jelszó + A jelszó nem lehet üres + Összes fiók engedélyezése + Összes fiók letiltása + Művelet végrehajtása ezzel + Nincs hovatartozás + Kilépett + Kiközösített + Tag + Speciális mód + Tagi jogosultságok megadása + Tagi jogosultságok visszavonása + Adminisztrátori jogosultságok megadása + Adminisztrátor jogosultságok visszavonása + Tulajdonosi jogosultságok megadása + Tulajdonosi jogosultságok visszavonása + Eltávolítás a csoportos csevegésből + Eltávolítás a csatornából + Nem sikerült %s hovatartozását megváltoztatni + Kitiltás a csoportos csevegésből + Kitiltás a csatornából + Megpróbálta %s eltávolítását egy nyilvános csatornából. Ennek egyetlen módja, ha örökre kitiltja a felhasználót. + Kitiltás most + Nem sikerült %s szerepét megváltoztatni + Személyes csoportos csevegés beállításai + Nyilvános csatorna beállításai + Személyes, csak tagoknak + XMPP-címek láthatóvá tétele bárki számára + Csatorna moderálttá tétele + Ön nem résztvevő + Csoportos csevegés beállításai módosítva! + Nem sikerült módosítani a csoportos csevegés beállításait + Soha + További értesítésig + Szundi + Válasz + Megjelölés olvasottként + Bevitel + Küldés enterrel + Enter billentyű megjelenítése + Hangulatjelek billentyű megváltoztatása az enter billentyűre + hang + videó + kép + vektorgrafika + PDF-dokumentum + Android alkalmazás + Partner + A profilkép közzé lett téve! + %s küldése + %s felajánlása + Kilépettek elrejtése + %s éppen ír… + %s abbahagyta az írást + %s éppen ír… + %s abbahagyta az írást + Gépelési értesítések + Tudassa a partnereivel, hogy mikor ír nekik üzeneteket + Hely küldése + Hely megjelenítése + Nem található alkalmazás a hely megjelenítéséhez + Hely + A beszélgetés bezárva + Kilépett a személyes csoportos csevegésből + Kilépett a nyilvános csatornából + Ne bízzon meg a rendszer hitelesítésszolgáltatóiban + Az összes tanúsítványt kézzel kell jóváhagyni + Tanúsítványok eltávolítása + Kézzel jóváhagyott tanúsítványok törlése + Nincsenek kézzel jóváhagyott tanúsítványok + Tanúsítványok eltávolítása + Kijelölés törlése + Mégse + + %d tanúsítvány törölve + %d tanúsítvány törölve + + Küldés gomb cseréje gyors művelettel + Gyors művelet + Nincs + Legutóbb használt + Gyors művelet kiválasztása + Partnerek keresése + Könyvjelzők keresése + Személyes üzenet küldése + %1$s elhagyta a csoportos csevegést + Felhasználónév + Felhasználónév + Ez nem érvényes felhasználónév + Letöltés sikertelen: a kiszolgáló nem található + Letöltés sikertelen: a fájl nem található + Letöltés sikertelen: nem sikerült kapcsolódni a géphez + Letöltés sikertelen: nem sikerült írni a fájlt + A Tor hálózat nem érhető el + Kötési hiba + 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 + xmpp.example.com + Bejelentkezés tanúsítvánnyal + Nem sikerült elemezni a tanúsítványt + Archiválási beállítások + Kiszolgáló oldali archiválási beállítások + Archiválási beállítások lekérése. Kérem várjon… + Nem sikerült lekérni az archiválási beállításokat + CAPTCHA szükséges + Írja be a fenti képen lévő szöveget + Nem megbízható tanúsítványlánc + Az XMPP-cím nem egyezik a tanúsítvánnyal + Tanúsítvány megújítása + Hiba történt az OMEMO kulcs lekérésekor! + Ellenőrzött OMEMO kulcs tanúsítvánnyal! + A készüléke nem támogatja az ügyféltanúsítványok kiválasztását! + Kapcsolat + Kapcsolódás a Tor hálózaton keresztül + Az összes kapcsolat átvezetése a Tor hálózaton. Az Orbot alkalmazás szükséges hozzá + Gépnév + Port + Ez nem érvényes portszám + Ez nem érvényes gépnév + %1$d/%2$d fiók kapcsolódott + + %d üzenet + %d üzenet + + További üzenetek betöltése + %s partnerrel megosztott fájl + %s partnerrel megosztott kép + %s partnerrel megosztott képek + %s partnerrel megosztott szöveg + Szinkronizálás a partnerekkel +
A telefonszámok másolatát nem fogjuk eltárolni.\n\nTovábbi információkért olvassa el az adatvédelmi irányelveinket.

Most arra fogják kérni, hogy adjon jogosultságot a névjegyek eléréséhez.]]>
+ Értesítés az összes üzenetről + Csak akkor értesítsen, ha megemlítik + Értesítések letiltva + Értesítések szüneteltetve + Képtömörítés + Tipp: használja a „Fájl kiválasztása” lehetőséget a „Fénykép kiválasztása” helyett az egyes képek tömörítetlen küldéséhez ettől a beállítástól függetlenül. + Mindig + Csak nagy képek + Akkumulátor-optimalizációk engedélyezve + Letiltás + A kijelölt terület túl nagy + (Nincsenek aktivált fiókok) + Ez a mező kötelező + Üzenet javítása + Javított üzenet küldése + Letiltotta ezt a fiókot + Biztonsági hiba: érvénytelen fájlhozzáférés + Nem található alkalmazás az URI megosztásához + URI megosztása ezzel… +
Regisztráljon a telefonszámával, és a Quicksy automatikusan – a címjegyzékében szereplő telefonszámok alapján – javaslatot tesz a lehetséges partnerekre.

A regisztrációval elfogadja azadatvédelmi irányelveinket.]]>
+ Elfogadás és folytatás + A teljes XMPP-címe ez lesz: %s + Fiók létrehozása + Saját szolgáltató használata + Válasszon felhasználónevet + Elérhetőség kezelése kézzel + Az elérhetőségének beállítása az állapotüzenet szerkesztésekor. + Állapotüzenet + Csevegésre kész + Elérhető + Távol + Nem érhető el + Elfoglalt + Egy biztonságos jelszó lett előállítva + Az eszköze nem támogatja az akkumulátor-optimalizálás kikapcsolását + Regisztráció sikertelen: próbálja meg később újra + Regisztráció sikertelen: a jelszó túl gyenge + Résztvevők kiválasztása + Csoportos csevegés létrehozása… + Meghívás újra + Letiltás + Rövid + Közepes + Hosszú + Üzenetszórás használata + Tudassa partnereivel, hogy mikor használja a Conversations-t + Adatvédelem + Téma + Színpaletta kiválasztása + Automatikus + Világos + Sötét + Zöld háttér + Zöld háttér használata a fogadott üzenetekhez + Ez az eszköz többé nincs használatban + Számítógép + Mobiltelefon + Táblagép + Webböngésző + Konzol + Fizetés szükséges + Engedélyezze az internet használatát + Én + A partner jelenlét-feliratkozást kér + Engedélyezés + Nincs jogosultság hozzáférni ehhez: %s + A távoli kiszolgáló nem található + Távoli kiszolgáló időtúllépés + Nem sikerült frissíteni a fiókot + Jelentse ezt a Jabber-címet spamküldés miatt. + OMEMO személyazonosságok törlése + Kijelölt kulcsok törlése + Kapcsolódva kell lennie a profilkép közzétételéhez. + Hibaüzenet megjelenítése + Hibaüzenet + Adatsporolás engedélyezve + Nem sikerült létrehozni átmeneti fájlt + Ez a készülék ellenőrizve lett + Ujjlenyomat másolása + Ellenőrzött ujjlenyomatok + Használja a kamerát egy partner vonalkódjának beolvasásához + Várjon a kulcsok lekérésére + Megosztás vonalkódként + Megosztás XMPP URI-ként + Megosztás HTTP hivatkozásként + Vak bizalom ellenőrzés előtt + Nem megbízható + Érvénytelen 2D vonalkód + Gyorsítótármappa törlése (Kamera alkalmazás által használt) + Gyorsítótár törlése + Személyes tárhely törlése + Személyes tárhely törlése, ahol a fájlok vannak (Ezek újra letölthetők a kiszolgálóról) + Egy megbízható forrásból követtem ezt a hivatkozást + %1$s OMEMO kulcsainak ellenőrzésére készül egy hivatkozásra kattintás után. Ez csak akkor biztonságos, ha ezt a hivatkozást megbízható forrásból követte, ahol csak %2$s tudta közzétenni a hivatkozást. + OMEMO kulcsok ellenőrzése + Inaktívak megjelenítése + Inaktívak elrejtése + Ne bízzon meg az eszközben + + %d másodperc + %d másodperc + + + %d perc + %d perc + + + %d óra + %d óra + + + %d nap + %d nap + + + %d hét + %d hét + + + %d hónap + %d hónap + + Automatikus üzenettörlés + Üzenetek automatikus törlése erről az eszközről, amelyek régebbiek a beállított időkeretnél. + Üzenet titkosítása + Nincs üzenetletöltés a helyi megőrzési időszak miatt. + Videó tömörítése + A megfelelő beszélgetések lezárultak. + Partner tiltva. + Értesítések idegenektől + Értesítés az idegenektől fogadott üzenetekről és hívásokról. + Üzenet érkezett egy idegentől + Idegen tiltása + Teljes tartomány tiltása + éppen most elérhető + Visszafejtés újrapróbálása + Munkamenethiba + Csökkentett SASL mechanizmus + A kiszolgáló a weboldalon történő regisztrációt igényli + Weboldal megnyitása + Nem található alkalmazás a weboldal megnyitásához + Figyelmeztető értesítések + Figyelmeztető értesítések megjelenítése + Ma + Tegnap + Gépnév ellenőrzése DNSSEC használatával + Az ellenőrzött gépnevet tartalmazó kiszolgálótanúsítványok ellenőrzöttként lesznek figyelembe véve + A tanúsítvány nem tartalmaz XMPP-címet + részleges + Videó rögzítése + Másolás a vágólapra + Üzenet a vágólapra másolva + Üzenet + A személyes üzenetek le vannak tiltva + Védett alkalmazások + Ha akkor is szeretne értesítéseket kapni, amikor a kijelző ki van kapcsolva, hozzá kell adnia a Conversations alkalmazást a védett alkalmazások listájához. + Elfogadja az ismeretlen tanúsítványt? + A kiszolgáló tanúsítványa nincs aláírva egy ismert hitelesítésszolgáltató által. + Elfogadja a nem egyező kiszolgálónevet? + A kiszolgáló nem tudta hitelesíteni mint „%s”. A tanúsítvány csak az alábbiakra érvényes: + Mindenképp szeretne csatlakozni? + Tanúsítvány részletei: + Egyszer + A QR-kód olvasónak kamera-hozzáférésre van szüksége + Görgessen az aljára + Görgessen le egy üzenet elküldése után + Állapotüzenet szerkesztése + Állapotüzenet szerkesztése + Titkosítás letiltása + Nem sikerült lekérni az eszközlistát + Nem sikerült lekérni a titkosítási kulcsokat + Tipp: bizonyos esetekben ez megoldható azzal, hogy hozzáadják egymást a partnerlistákhoz. + Biztosan le szeretné tiltani az OMEMO titkosítást ennél a beszélgetésnél?\nEz lehetővé fogja tenni a kiszolgáló rendszergazdájának, hogy elolvassa az üzeneteket, de ez lehet az egyetlen módja annak, hogy kommunikáljon az elavult programokat használó emberekkel. + Letiltás most + Vázlat: + OMEMO titkosítás + Mindig az OMEMO lesz használva az egymással történő üzenetváltásokhoz és a személyes csoportos csevegésekhez. + Az OMEMO lesz alapértelmezetten használva az új beszélgetésekhez. + Az OMEMO-t kifejezetten be kell majd kapcsolni az új beszélgetésekhez. + Gyorsbillentyű létrehozása + Betűméret + Az alkalmazáson belül használt relatív betűméret. + Alapértelmezetten be + Alapértelmezetten ki + Kicsi + Közepes + Nagy + Az üzenet nem volt titkosított ehhez az eszközhöz. + Nem sikerült az OMEMO üzenet visszafejtése. + visszavonás + A helymegosztás le van tiltva + Helyzet rögzítése + Helyzet feloldása + Hely másolása + Hely megosztása + Irányok + Hely megosztása + Hely megjelenítése + Megosztás + Nem sikerült elindítani a rögzítést + Kérem várjon… + Üzenetek keresése + GIF + Beszélgetés megtekintése + Helymegosztás bővítmény + A helymegosztás bővítmény használata a beépített térkép helyett + Webcím másolása + XMPP-cím másolása + HTTP fájlmegosztás S3-hoz + Közvetlen keresés + A „Beszélgetés indítása” képernyőn nyissa meg a billentyűzetet, és helyezze a kurzort a keresőmezőbe + Csoportos csevegés profilképe + A gép nem támogatja a csoportos csevegés profilképeket + Csak a tulajdonos tudja megváltoztatni a csoportos csevegés profilképét + Partner neve + Becenév + Név + A név megadása elhagyható + Csoportos csevegés neve + Ezt a csoportos csevegést megszüntették + Nem sikerült elmenteni a felvételt + Előtér szolgáltatás + Állapotinformációk + Kapcsolódási problémák + Ezt az értesítési kategóriát egy értesítés megjelenítéséhez használják abban az esetben, ha probléma merül fel a fiókhoz való kapcsolódásnál. + Üzenetek + Hívások + Üzenetek + Bejövő hívások + Kimenő hívások + Csendes üzenetek + Ezt az értesítési csoportot olyan értesítések megjelenítéséhez használják, amelyek nem aktiválhatnak hangot. Például ha aktívvá válik egy másik eszközön (türelmi idő). + Üzenet értesítésének beállításai + Bejövő hívások értesítésnek beállításai + Fontosság, hang, rezgés + Videó tömörítése + Média megtekintése + Résztvevők + Médiaböngésző + A fájl ki lett hagyva a biztonság megsértése miatt. + Videó minősége + Az alacsonyabb minőség kisebb fájlokat jelent + Közepes (360p) + Magas (720p) + megszakítva + Már elmentett vázlatként egy üzenetet. + A funkció nincs megvalósítva + Érvénytelen országkód + Válasszon egy országot + telefonszám + Erősítse meg a telefonszámát + A Quicksy SMS-üzenetet fog küldeni (a szolgáltató díjat számolhat fel), hogy ellenőrizze a telefonszámát. Írja be az országkódot és telefonszámát: +
%s

Ez rendben van, vagy szeretné szerkeszteni a számot?]]>
+ A(z) %s nem érvényes telefonszám. + Adja meg a telefonszámát. + Országok keresése + %s ellenőrzése + %s.]]> + Küldtünk egy 6 számjegyű kódot egy másik SMS-ben. + Adja meg az alábbi 6 számjegyű PIN-t. + SMS újraküldése + SMS újraküldése (%s) + Kérem várjon (%s) + vissza + Automatikusan beillesztett lehetséges PIN-kód a vágólapról. + Adja meg a 6 számjegyű PIN-kódot. + Biztosan meg szeretné szakítani a regisztrációs folyamatot? + Igen + Nem + Ellenőrzés… + SMS kérése… + A beírt PIN-kód helytelen. + Az általunk küldött PIN-kód lejárt. + Ismeretlen hálózati hiba. + Ismeretlen válasz a kiszolgálótól. + Nem sikerült kapcsolódni a kiszolgálóhoz. + Nem sikerült biztonságos kapcsolatot kiépíteni. + Nem sikerült megtalálni a kiszolgálót. + Valami hiba történt a kérés feldolgozásakor. + Érvénytelen felhasználói bevitel + Átmenetileg nem érhető el. Próbálja meg később újra. + Nincs hálózati kapcsolat. + Próbálja meg újra %s múlva + A sebessége korlátozott + Túl sok próbálkozás + Az alkalmazás elavult verzióját használja. + Frissítés + Ez a telefonszám jelenleg egy másik eszközzel van bejelentkezve. + Adja meg nevét, hogy azok az emberek is tudják, hogy Ön kicsoda, akiknél nincs benne a címjegyzékükben. + Az Ön neve + Adja meg nevét + Használja a szerkesztés gombot a neve beállításához. + Kérés elutasítása + Orbot telepítése + Orbot indítása + Nincs bolti alkalmazás telepítve. + Ez a csatorna nyilvánossá teszi az XMPP-címét + e-könyv + Eredeti (tömörítetlen) + Megnyitás ezzel… + Conversations profilkép + Fiók kiválasztása + Biztonsági mentés visszaállítása + Visszaállítás + Adja meg a(z) %s fiók jelszavát a biztonsági mentés visszaállításához. + Ne használja a biztonsági mentés visszaállítása funkciót egy telepítés klónozásának (egyidejűleg történő futtatásának) kísérletéhez. A biztonsági mentés visszaállítása csak költöztetésekhez való, illetve arra az esetre, ha elveszti az eredeti eszközt. + Nem sikerült visszaállítani a biztonsági mentést. + Nem sikerült visszafejteni a biztonsági mentést. Helyes a jelszó? + Biztonsági mentés és visszaállítás + Adja meg az XMPP-címet + Csoportos csevegés létrehozása + Csatlakozás nyilvános csatornához + Személyes csoportos csevegés létrehozása + Nyilvános csatorna létrehozása + Csatorna neve + XMPP-cím + Adjon egy nevet a csatornának + Adjon meg egy XMPP-címet + Ez egy XMPP-cím. Adjon meg egy nevet. + Nyilvános csatorna létrehozása… + Ez a csatorna már létezik + Csatlakozott egy meglévő csatornához + Nem sikerült elmenteni a csatorna beállításait + Bárkinek engedélyezett a téma szerkesztése + Bárkinek engedélyezett mások meghívása + Bárki szerkesztheti a témát. + A tulajdonosok szerkeszthetik a témát. + Az adminisztrátorok szerkeszthetik a témát. + A tulajdonosok meghívhatnak másokat. + Bárki meghívhat másokat. + Az XMPP-címek láthatóak az adminisztrátoroknak. + Az XMPP-címek láthatóak bárkinek. + Ennek a nyilvános csatornának nincsenek résztvevői. Hívja meg a partnereit, vagy használja a megosztás gombot a csatorna XMPP-címének terjesztéséhez. + Ennek a személyes csoportos csevegésnek nincsenek résztvevői. + Jogosultságok kezelése + Résztvevők keresése + A fájl túl nagy + Csatolás + Csatornák felderítése + Csatornák keresése + Magánélet lehetséges megsértése! + search.jabber.network nevű, harmadik féltől származó szolgáltatást használja.

A funkció használata elküldi az IP-címét és a keresési kifejezést annak a szolgáltatásnak. További információkért nézze meg az adatvédelmi irányelveiket.]]>
+ Már van fiókom + Meglévő fiók hozzáadása + Új fiók regisztrálása + Ez egy tartománycímnek tűnik + Hozzáadás mindenképp + Ez egy csatornacímnek tűnik + Biztonsági mentés fájlok megosztása + Conversations biztonsági mentés + Esemény + Biztonsági mentés megnyitása + A kiválasztott fájl nem Conversations biztonsági mentés fájl + Ez a fiók már be lett állítva + Adja meg a fiók jelszavát + Nem sikerült végrehajtani ezt a műveletet + Csatlakozás nyilvános csatornához… + A megosztási alkalmazás nem adott jogosultságot a fájl eléréséhez. + + jabber.network + Helyi kiszolgáló + A legtöbb felhasználónak a „jabber.network” lehetőséget kell választania a nyilvános XMPP ökoszisztéma egészéből származó jobb javaslatokért. + Csatornafelderítés módszere + Biztonsági mentés + Névjegy + Engedélyezzen egy fiókot + Hívás indítása + Bejövő hívás + Bejövő videohívás + Kapcsolódás + Kapcsolódva + Hívás elfogadása + Hívás befejezése + Válasz + Elutasítás + Eszközök keresése + Csörgetés + Elfoglalt + Nem sikerült kapcsolódni a híváshoz + Visszavont hívás + Alkalmazáshiba + Lerakás + Kimenő hívás + Kimenő videohívás + Tor letiltása a hívások indításához + Bejövő hívás + Bejövő hívás · %s + Kimenő hívás + Kimenő hívás · %s + Nem fogadott hívás + Hanghívás + Videohívás + A mikrofonja nem érhető el + Egyszerre csak egy hívásban vehet részt. + Visszatérés a kimenő híváshoz + Nem sikerült átváltani a kamerát + + %1$d résztvevő megtekintése + %1$d résztvevő megtekintése + +
diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml new file mode 100644 index 000000000..f2a621876 --- /dev/null +++ b/app/src/main/res/values-id/strings.xml @@ -0,0 +1,493 @@ + + + Pengaturan + Percakapan Baru + Pengaturan Akun + Pengaturan akun + Tutup percakapan + Detil Kontak + Detil percakapan grup + Detil channel + Tambah Akun + Ubah Nama + Tambahkan ke daftar kontak + Hapus dari roster + Blokir kontak + Batal blokir kontak + Blokir domain + Batal blokir domain + Blok partisipan + Buka blok partisipan + Pengaturan Akun + Pengaturan + Bagikan dengan Conversation + Mulai Percakapan + Pilih kontak + Pilih beberapa kontak + Bagikan melalui akun + Daftar blokir + sekarang + 1 min lalu + %d min lalu + + %d percakapan belum dibaca + + + mengirim... + Mendekripsi pesan. Mohon tunggu… + Pesan terenkripsi OpenPGP + Nickname ini sudah digunakan + Alias tidak valid + Administrator + Pemilik + Moderator + Peserta + Pengunjung + Apakah Anda ingin menghapus %s dari daftar kontak? Percakapan dengan kontak ini tidak akan dihapus. + Apakah Anda ingin memblokir pesan dari %s? + Apakah Anda ingin membuka blokir %s dan membolehkannya untuk mengirim pesan? + Blokir semua kontak dari %s? + Batalkan blokir semua kontak dari %s? + Kontak terblokir + Diblok + Apakah Anda ingin menghapus %s bookmark ini? Percakapan di bookmark ini tidak akan dihapus. + Daftarkan akun baru di server + Ganti password di server + Bagikan dengan... + Mulai percakapan + Undang kontak + Undang + Kontak + Kontak + Batal + Atur + Tambah + Ubah + Hapus + Blokir + Batalkan blokir + Simpan + YA + Kirim sekarang + Jangan tanya lagi + Tidak dapat terhubung ke akun + Tidak dapat terhubung ke multi akun + Klik untuk mengatur akun anda + Sisipkan berkas + Tambah kontak ini ke daftar kontak anda? + Tambah kontak + pengiriman gagal + Mempersiapkan pengiriman gambar + Mempersiapkan pengiriman beberapa gambar + Membagikan berkas. Mohon tunggu… + Bersihkan riwayat + Hapus Riwayat Percakapan + Apakah Anda ingin menghapus semua pesan dalam percakapan ini?\n\nPeringatan: Ini tidak akan mempengaruhi pesan yang tersimpan di perangkat atau server lain. + Hapus file + Anda yakin ingin menghapus file ini?\n\nPeringatan: Ini tidak akan menghapus salinan dari file ini yang disimpan di perangkat atau server lain. + Lanjutkan dengan menutup percakapan + Pilih perangkat + Kirim pesan tak-terenkripsi + Kirim pesan + Kirimkan pesan ke %s + Kirim pesan terenkripsi OMEMO + Mengirim pesan terenkripsi v\\OMEMO + Kirim pesan terenskripsi OpenPGP + Nama panggilan sudah dipakai + Kirim tidak terenkripsi + Dekripsi gagal. Mungkin Anda tidak memiliki kunci pribadi yang tepat. + OpenKeychain + Mulai ulang + Pasang + Harap install OpenKeychain + menawarkan... + menunggu... + Tidak ada kunci OpenPGP ditemukan + Tidak dapat mengenkripsi pesan karena kontak Anda tidak mengumumkan kunci publiknya.\n\nMinta kontak Anda untuk mengatur OpenPGP. + Tidak ada kunci OpenPGP ditemukan + Tidak dapat mengenkripsi pesan karena kontak Anda tidak mengumumkan kunci publiknya.\n\nMinta kontak Anda untuk mengatur OpenPGP. + Umum + Terima berkas + Otomatis menerima berkas lebih kecil dari... + Lampiran + Notif + Getar + Aktifkan getar ketika pesan masuk + Notifikasi LED + Lampu notifikasi berkedip saat ada pesan baru + Nada dering + Notifikasi suara + Notifikasi suara untuk pesan baru + Masa tenggang + Lamanya waktu notifikasi diredam setelah mendeteksi aktivitas di salah satu perangkat Anda yang lain. + Lanjutan + Jangan kirim laporan kerusakan + Dengan mengirimkan pelacakan stack, Anda membantu pengembangan + Konfirmasi Pesan + Beri tahu kontak jika Anda telah menerima dan membaca pesan mereka + UI + OpenKeychain menghasilkan kesalahan. + Kunci untuk enkripsi cacat + Terima + Sebuah kesalahan terjadi + Kesalahan + Akun anda + Kirim pembaruan kehadiran + Terima pembaruan kehadiran + Tanya untuk pembaruan kehadiran + Pilih gambar + Ambil gambar + Ijinkan permintaan berlangganan + Berkas yang anda pilih bukan gambar + Tidak bisa mengkonversi file gambar + Berkas tidak ditemukan + Kesalahan Umum I/O. Mungkin Anda kehabisan ruang penyimpanan? + Aplikasi yang digunakan untuk memilih gambar ini tidak memberikan izin yang cukup untuk membaca file.\n\nGunakan pengelola file yang berbeda untuk memilih gambar. + Tidak diketahui + Sementara dimatikan + Online + Menghubungkan\u2026 + Offline + Tidak mendapat izin + Server tidak ditemukan + Tidak ada koneksi + Registrasi gagal + Username telah digunakan + Registrasi berhasil + Server tidak mendukung registrasi + Token registrasi salah + Kegagalan negosiasi TLS + Pelanggaran kebijakan + Server tidak cocok + Kesalahan stream + Kesalahan pembukaan stream + Tidak terenkripsi + OTR + OpenPGP + OMEMO + Hapus akun + Sementara dimatikan + Publikasikan avatar + Publikasikan kunci OpenPGP + Hapus kunci publik OpenPGP + Yakin ingin menghapus kunci publik OpenPGP Anda dari pengumuman kehadiran?\nKontak Anda tidak lagi dapat mengirimi Anda pesan terenkripsi OpenPGP. + Kunci publik OpenPGP diumumkan + Aktifkan Akun + Apakah Anda yakin.? + Menghapus akun akan menghilangkan semua riwayat percakapan + Rekam suara + alamat XMPP + Blok alamat XMPP + username@example.com + Password + Alamat XMPP salah + Memori habis. Gambar terlalu besar + Anda ingin menambahkan %s ke daftar kontak? + Info Server + XEP-0313: MAM + XEP-0280: Message Carbons + XEP-0352: Client State Indication + XEP-0191: Blocking Command + XEP-0237: Roster Versioning + XEP-0198: Stream Management + XEP-0215: Penemuan layanan eksternal + XEP-0163: PEP (Avatars / OMEMO) + XEP-0363: HTTP File Upload + XEP-0357: Push + tersedia + tidak tersedia + Pemberitahuan kunci publik tidak ditemukan + terakhir terlihat sekarang + Muncul satu menit yang lalu + terlihat %d menit lalu + Muncul satu jam yang lalu + terlihat %d jam lalu + Muncul satu hari yang lalu + terlihat %d hari lalu + Pesan terenkripsi. Harap instal OpenKeychain untuk mendekripsi. + Pesan terenkripsi OpenPGP ditemukan + ID Kunci OpenPGP + Sidik jari OMEMO + v\\Sidik jari OMEMO + Perangkat lainnya + Percayai Sidik Jari OMEMO + Mengambil kunci… + Selesai + Deskripsi + Bookmark + Cari + Masukkan kontak + Hapus kontak + Lihat detil kontak + Blokir kontak + Lepas blokir kontak + Buat + Pilih + Kontak ini sudah ada + Gabung + channel@conference.example/panggilan + channel@conference.example + Simpan sebagai bookmark + Hapus bookmark + Hapus percakapan grup + Hapus channel + Apakah Anda yakin ingin menghancurkan percakapan grup ini?\n\nPeringatan: percakapan grup di server akan dihapus seluruhnya. + Anda yakin ingin menghancurkan channel publik ini?\n\nPeringatan: Channel ini akan dihapus seluruhnya dari server. + Tidak dapat menghapus percakapan grup + Tidak dapat menghapus channel + Edit subyek percakapan grup + Topik + Bergabung ke percakapan grup... + Tinggalkan + Kontak ditambahkan ke daftar anda + Tambah kembali + %s telah membaca hingga disini + %s telah dibaca sampai posisi ini + Publikasi + Klik avatar untuk memilih gambar dari galeri + Mempublikasi... + Server tidak mengijinkan publikasi Anda + Tidak dapat menyimpan Avatar ke memori + (Tekan yang lama untuk mengembalikan semula) + berbisik + kepada %s + Kirim pesan pribadi ke %s + Hubungkan + Akun ini sudah ada + Selanjutnya + Lewati + Nonaktifkan notifikasi + Aktifkan + Percakapan grup memerlukan kata sandi + Masukan password + Request sekarang + Abaikan + Keamanan + Ijinkan mengedit pesan + Pengaturan lanjutan + Harap berhati-hati dengan ini + Tentang %s + Waktu sunyi + Waktu mulai + Waktu selesai + Aktifkan waktu sunyi + Pemberitahuan akan disunyukan ketika jam sunyi. + Lainnya + Anda terhalang dari percakapan grup ini + menggunakan akun %s + Anda tidak terhubung. Coba lagi nanti + Cek %s ukuran + Opsi pesan + Kutipan + Tempel sebagai kutipan + Salin URL asli + Kirim lagi + URL Berkas + Salin URL ke papan klip + Salin alamat XMPP ke papan klip + Salin pesan kesalahan ke papan klip + alamat web + Pindai kode 2D + Tampilkan kode 2D + Tampilkan daftar blokir + Detil akun + Konfirmasi + Coba lagi + Cegah sistem operasi mematikan koneksi + Buat backup + File backup akan disimpan di %s + Membuat file backup + File backup sudah dibuat + File backup sudah disimpan di %s + Pilih berkas + Menerima %1$s (%2$d%% terselesaikan) + Mengunduh %s + Hapus %s + berkas + Buka %s + mengirim (%1$d%% terselesaikan) + %s ditawarkan untuk mengunduh + batalkan pengiriman + Transmisi file dibatalkan + File dihapus + Tampilan read-only tag di bawah kontak + Aktifkan notifikasi + Avatar akun + Bersihkan perangkat + Mengambil data dari server + Tidak ada data lagi di server + Merubah... + Password diganti! + Tidak dapat mengubah password + Ubah password + Password sekarang + Password baru + Aktifkan semua akun + Menonaktifkan semua account + Lakukan aksi dengan + Tidak ada afiliasi + Offline + Orang buangan + Member + Mode lanjut + Memberikan hak istimewa admin + Mencabut hak istimewa admin + Tidak bisa mengubah afiliasi %s + Tendang sekarang + Tidak dapat merumah role %s + Anda tidak berpartisipasi + Tidak pernah + Sampai pemberitahuan selanjutnya + Masukan + Enter untuk mengirim + Tampilkan masukan kunci + Mengubah kunci emoji untuk memasukan kunci + audio + video + Gambar + Berkas PDF + Apl Android + Kontak + Avatar telah diterbitkan! + Mengirim %s + Menawarkan %s + Sembunyikan Offline + %s sedang mengetik… + %s telah berhenti mengetik + Notifikasi ketik pesan + Kirim lokasi + Tampilkan lokasi + Lokasi + Percakapan tertutup + Jangan percaya sistem CA + Semua sertifikat harus disetujui secara manual + Hapus sertifikat + Hapus sertifikat yang disahkan secara manual + Tidak ada sertifikat yang disahkan secara manual + Hapus sertifikat + Hapus seleksi + Batal + + %d sertifikat dihapus + + Aksi Cepat + Tak satupun + Maling sering digunakan + Pilih aksi cepat + Kirim pesan pribadi + Username + Username + Username ini tidak valid + Unduhan gagal: Server tidak ditemukan + Unduh gagal: Berkas tidak ditemukan + Unduhan gagal: Tidak dapat terhubung ke host + Tor network tidak tersedia + Rusak + xmpp.example.com + Koneksi + Hubungkan via Tor + Hostname + Port + + %d pesan + + Sinkronkan dengan kontak + Tampilkan notifikasi untuk semua pesan + Selalu + Pengoptimalan baterai diaktifkan + Non-aktifkan + Area yang dipilih terlalu besar + (Tidak ada akun aktif) + Bagian ini wajib diisi + Perbaiki pesan + Kirim perbaikan pesan + Anda telah menonaktifkan akun ini + Bagikan URI dengan... + Buat Akun + Pilih username anda + Status + Tersedia + Online + Pergi + Tidak Tersedia + Sibuk + Password aman berhasil dibuat + Registrasi gagal: Coba lagi nanti + Registrasi gagal: kata sandi terlalu lemah + Undang user + Membuat percakapan grup... + Undang lagi + Non-aktifkan + Pendek + Sedang + Panjang + Privasi + Tema + Otomatis + Latar Hijau + Gunakan latar hijau untuk pesan masuk + Perangkat ini tidak dipergunakan lagi + Komputer + Ponsel + Tablet + Konsol + Ijinkan + Hapus identitas OMEMO + Hapus kunci terpilih + Tampilkan pesan kesalahan + + %d detik + + + %d menit + + + %d jam + + + %d hari + + + %d minggu + + + %d bulan + + Kontak diblokir + sebagian + Pesan disalin ke clipboard + Kecil + Sedang + Besar + Bagikan Lokasi + Bagikan lokasi + Tampilkan lokasi + Bagikan + Silahkan menunggu… + Cari pesan + Tampilkan percakapan + Kopi alamat website + Kopi alamat XMPP + Pencarian langsung + Nama kontak + Panggilan + Nama + Nama percakapan grup + Pesan + Telepon + Pesan + Telepon masuk + Telepon keluar + Kirim ulang SMS + Kirim ulang SMS (%s) + Silahkan menunggu (%s) + kembali + Ya + Tolak permintaan + Masukkan alamat XMPP + Buat percakapan grup + Nama Channel + alamat XMPP + Buat channel publik... + Sibuk + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml new file mode 100644 index 000000000..4a69b6129 --- /dev/null +++ b/app/src/main/res/values-it/strings.xml @@ -0,0 +1,1024 @@ + + + Impostazioni + Nuova conversazione + Gestisci profili + Gestisci profilo + Chiudi conversazione + Dettagli del contatto + Dettagli chat di gruppo + Dettagli canale + Aggiungi profilo + Modifica il nome + Aggiungi alla rubrica + Cancella dalla lista + Blocca contatto + Sblocca contatto + Blocca dominio + Sblocca dominio + Blocca partecipante + Sblocca partecipante + Gestisci profili + Impostazioni + Condividi con Conversation + Inizia una conversazione + Scegli un contatto + Scegli i contatti + Condividi via account + Lista nera + adesso + 1 min fa + %d min fa + + %d conversazione non letta + %d conversazioni non lette + %d conversazioni non lette + + invio… + Decifrazione messaggio. Attendere prego… + Messaggio cifrato con OpenPGP + Nome utente già in uso + Nickname non valido + Amministratore + Proprietario + Moderatore + Partecipante + Visitatore + Vuoi rimuovere %s dalla lista dei contatti? Le conversazioni con questo contatto non verranno rimosse. + Vorresti impedire a %s di inviarti messaggi? + Vorresti permettere a %s di inviarti messaggi? + Bloccare tutti i contatti da %s? + Sbloccare tutti i contatti da %s? + Contatto bloccato + Bloccato + Vuoi rimuovere %s dai segnalibri? Le conversazioni con questo segnalibro non verranno rimosse. + Registra un nuovo profilo sul server + Cambia la password sul server + Condividi con… + Inizia conversazione + Invita contatto + Invita + Contatti + Contatto + Annulla + Imposta + Aggiungi + Modifica + Elimina + Blocca + Sblocca + Salva + OK + Errore 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 al profilo + Impossibile connettersi a più profili + Tocca per gestire i tuoi profili + Allega file + Aggiungere questo contatto alla lista dei contatti? + Aggiungi contatto + Invio fallito + Preparazione per l\'invio dell\'immagine + Preparazione per l\'invio delle immagini + Condivisione file. Attendere prego… + Svuota la cronologia + Svuota la cronologia della conversazione + Vuoi eliminare tutti i messaggi in questa conversazione?\n\nAttenzione: ciò non influenzerà i messaggi salvati su altri dispositivi o server. + Elimina file + Sei sicuro di voler eliminare questo file?\n\nAttenzione: non verranno eliminate copie di questo file memorizzate in altri dispositivi o server. + Chiudi questa conversazione successivamente + Scegli un dispositivo + Invia messaggio non cifrato + Invia messaggio + Invia messaggio a %s + Invia messaggio cifrato OMEMO + Invia messaggio cifrato v\\OMEMO + Messaggio OpenPGP + Nuovo nome utente in uso + Invia non cifrato + Decifrazione fallita. Forse non disponi della chiave privata corretta. + OpenKeychain + OpenKeychain per cifrare e decifrare i messaggi e gestire le tue chiavi pubbliche.

È pubblicato secondo i termini della GPLv3+ ed è disponibile su F-Droid e Google Play.

(Riavvia %1$s in seguito.)]]>
+ Riavvia + Installa + Per favore installa OpenKeychain + offrendo… + in attesa… + Nessuna chiave OpenPGP trovata + Impossibile decifrare il messaggio perchè il contatto non sta annunciando la sua chiave pubblica.\n\nChiedi al contatto di configurare OpenPGP. + Nessuna chiave OpenPGP trovata + Impossibile cifrare il messaggio perchè i contatti non stanno annunciando la sua chiave pubblica.\n\nChiedi loro di configurare OpenPGP. + Generale + Accetta i file + Accetta automaticamente i file più piccoli di… + Allegati + Notifiche + Vibra + Vibra quando arriva un nuovo messaggio + Notifica LED + Luce di notifica lampeggiante quando arriva un nuovo messaggio + Suoneria + Suono di notifica + Suono di notifica per i nuovi messaggi + Suoneria per chiamate in arrivo + Periodo di grazia + Il periodo di tempo in cui le notifiche vengono silenziate dopo aver rilevato attività su uno dei tuoi altri dispositivi. + Avanzate + Non inviare mai segnalazioni di errore + Se scegli di inviare una segnalazione dell’errore aiuterai lo sviluppo + 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 profilo + Invia aggiornamenti della presenza + Ricevi aggiornamenti della presenza + Chiedi aggiornamenti della presenza + Scegli un’immagine + Scatta una foto + Concedi aggiornamenti della presenza preventivamente + Il file selezionato non è un’immagine + Impossibile convertire l\'immagine + 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 + In linea + In connessione\u2026 + Offline + Non autorizzato + Server non trovato + Connettività assente + Registrazione fallita + Nome utente già in uso + Registrazione completata + Registrazione non supportata dal server + Token di registrazione non valido + Negoziazione TLS fallita + Dominio non verificabile + Violazione della policy + Server non compatibile + Client non compatibile + Errore di stream + Errore apertura flusso + Non cifrato + OTR + OpenPGP + OMEMO + 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 profilo + Sei sicuro? + L\'eliminazione del tuo profilo cancellerà tutta la cronologia dielle conversazioni + Registra la voce + Indirizzo XMPP + Blocca indirizzo XMPP + utente@esempio.com + Password + Questo non è un indirizzo XMPP valido + Memoria esaurita. Immagine troppo grande + Vuoi aggiungere %s alla tua rubrica? + Info server + XEP-0313: MAM + XEP-0280: Message Carbons + XEP-0352: Client State Indication + XEP-0191: Blocking Command + XEP-0237: Roster Versioning + XEP-0198: Stream Management + XEP-0215: Scoperta di servizi esterni + XEP-0163: PEP (Avatars / OMEMO) + XEP-0363: HTTP File Upload + XEP-0357: Push + disponibile + non disponibile + Annuncio chiave pubblica non effettuato + visto adesso + visto un minuto fa + visto %d minuti fa + visto un\'ora fa + visto %d ore fa + visto un giorno fa + visto %d giorni fa + Messaggio cifrato. Installa OpenKeychain per decifrarlo. + Nuovi messaggi cifrati con OpenPGP trovati + ID chiave OpenPGP + Impronta OMEMO + v\\Impronta OMEMO + Impronta OMEMO (origine del messaggio) + v\\Impronta OMEMO (origine del messaggio) + Altri dispositivi + Fidati delle impronte OMEMO + Ricezione chiavi… + Fatto + Decripta + Segnalibri + Cerca + Inserisci contatto + Elimina contatto + Mostra dettagli contatto + Blocca contatto + Sblocca contatto + Crea + Seleziona + Il contatto esiste già + Entra + canale@conferenza.esempio.com/nick + canale@conferenza.esempio.com + Salva come segnalibro + Elimina segnalibro + Distruggi chat di gruppo + Distruggi canale + Sei sicuro di voler distruggere questa chat di gruppo?\n\nAttenzione: la chat di gruppo verrà completamente rimossa dal server. + Sei sicuro di voler distruggere questo canale pubblico?\n\nAttenzione: il canale verrà completamente rimosso sul server. + Distruzione della chat di gruppo fallita + Distruzione canale fallita + Modifica titolo chat di gruppo + Argomento + Ingresso nella chat di gruppo… + Abbandona + Il contatto ti ha aggiunto alla sua lista contatti + Aggiungi anche tu + %s ha letto fino a questo punto + %s ha letto fino a questo punto + %1$s + altri %2$d hanno letto fino a questo punto + Tutti hanno letto fino a questo punto + Pubblica + Tocca l\'avatar per scegliere un\'immagine dalla galleria + Pubblicazione… + Il server ha rifiutato la tua pubblicazione + Impossibile convertire la tua immagine + Impossibile salvare l’avatar sulla memoria interna + (O premi a lungo per ripristinare le impostazioni di default) + Il tuo server non supporta la pubblicazione di avatar + sussurrato + a %s + Invia messaggio privato a %s + Connetti + Questo profilo esiste già + Successivo + Sessione stabilita + Salta + Disattiva le notifiche + Attiva + La chat di gruppo richiede una password + Inserisci la password + Prima chiedi gli aggiornamenti della presenza dal tuo contatto. +\n +\nCiò verrà usato per determinare quale app sta usando il tuo contatto. + Rechiedi adesso + Ignora + Attenzione: inviarlo senza aggiornamenti della presenza reciproci può causare problemi inaspettati.\n\nVai nei dettagli del contatto per verificare le tue sottoscrizioni alla presenza. + Sicurezza + Permetti correzione del messaggio + Consenti ai tuoi contatti di modificare retroattivamente i loro messaggi + Impostazioni per esperti + Fai attenzione con queste impostazioni + Informazioni su %s + Ore di quiete + Orario inizio + Orario fine + Attiva ore di quiete + Le notifiche verranno silenziate durante le ore di quiete + Altro + Sincronizza i segnalibri + Imposta il flag \"auto-entrata\" quando entri o esci da un MUC e reagisci alle modifiche fatte dagli altri client. + Impronta OMEMO copiata negli appunti + Sei stato bandito da questa chat di gruppo + Questa chat di gruppo è solo per membri + Risorse limitate + Sei stato buttato fuori da questa chat di gruppo + La chat di gruppo è stata chiusa + Non sei più in questa chat di gruppo + Hai lasciato questa chat di gruppo per motivi tecnici + usando il profilo %s + ospitato su %s + Controllo %s su host HTTP + Non sei connesso. Riprova più tardi + Controlla dimensione %s + Controlla dimensione %1$s su %2$s + Opzioni del messaggio + Cita + Incolla come citazione + Copia URL originale + Invia di nuovo + URL del file + URL copiato negli appunti + Indirizzo XMPP copiato negli appunti + Messaggio di errore copiato negli appunti + indirizzo web + Scansiona codice a barre 2D + Mostra codice a barre 2D + Mostra la lista nera + Dettagli del profilo + Conferma + Prova di nuovo + Servizio in primo piano + Evita che il sistema operativo chiuda la connessione + Crea un backup + I file di backup verranno salvati in %s + 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 il profilo. + Scegli un file + Ricezione di %1$s file (%2$d%% completato) + Scarica %s + Elimina %s + file + Apri %s + invio (%1$d%% completato) + Preparazione per condividere il file + %s offerto da scaricare + Annulla trasmissione + impossibile condividere il file + trasmissione file annullata + File eliminato + Nessuna app trovata per aprire il file + Nessuna app trovata per aprire il link + Nessuna app trovata per vedere il contatto + Etichette dinamiche + Mostra etichette in sola lettura sotto i contatti + Attiva le notifiche + Nessun server per chat di gruppo trovato + Impossibile creare la chat di gruppo + Avatar del profilo + Copia impronta OMEMO negli appunti + Rigenera chiave OMEMO + Elimina dispositivi + Sei sicuro di voler rimuovere tutti gli altri dispositivi dall\'annuncio OMEMO? La prossima volta che si connetteranno si riannunceranno, ma potrebbero non ricevere i messaggi inviati nel frattempo. + Non ci sono chiavi utilizzabili per questo contatto.\nRicezione di nuove chiavi dal server non riuscita. Forse qualcosa non va con il server del tuo contatto? + Non ci sono chiavi utilizzabili per questo contatto.\nAssicurati di avere reciprocamente la sottoscrizione sulla presenza. + Qualcosa è andato storto + Caricamento della cronologia dal server + Fine cronologia sul server + Caricamento… + Password cambiata! + Impossibile cambiare la password + Cambia password + Password attuale + Nuova password + La password non può essere vuota + Attiva tutti i profili + Disattiva tutti i profili + Esegui azione con + Nessuna affiliazione + Offline + Emarginato + Membro + Modalità avanzata + Concedi appartenenza + Revoca appartenenza + Concedi i privilegi di amministratore + Revoca i privilegi di amministratore + Rendi proprietario + Revoca privilegi di proprietario + Rimuovi dalla chat di gruppo + Rimuovi dal canale + Impossibile cambiare l’affiliazione di %s + Bandisci dalla chat di gruppo + Bandisci dal canale + Stai tentando di rimuovere %s da un canale pubblico. L\'unico modo per farlo è di bandire quell\'utente per sempre. + Bandisci + Impossibile cambiare ruolo di %s + Configurazione chat di gruppo privata + Configurazione canale pubblico + Privato, solo membri + Rendi gli indirizzi XMPP visibili a chiunque + Rendi il canale moderato + Non stai partecipando + Opzioni della chat di gruppo modificate! + Impossibile modificare le opzioni della chat di gruppo + Mai + Fino a nuovo avviso + Ritarda + Rispondi + Segna come già letto + Input + 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 + file multimediale + Documento PDF + Applicazione Android + Contatto + Il tuo avatar è stato pubblicato! + Invio %s + Offrendo %s + Nascondi i contatti offline + %s sta digitando… + %s ha smesso di digitare + %s stanno scrivendo… + %s hanno smesso di scrivere + Notifiche di composizione + Fai sapere ai tuoi contatti quando stai scrivendo loro un messaggio + Invia la posizione + Mostra la posizione + Nessuna app trovata per mostrare la posizione + Posizione + Conversazione interrotta + Chat di gruppo privata abbandonata + Canale pubblico abbandonato + Non ti fidare delle CA di sistema + Tutti i certificati devono essere accettati manualmente + Elimina i certificati + Cancella manualmente i certificati già accettati + Non sono presenti certificati accettati manualmente + Elimina i certificati + Cancella la selezione + Annulla + + Cancellato il %d certificato + Cancellati %d certificati + Cancellati %d certificati + + Sostituisci il tasto \"Invio\" con un\'azione rapida + Azione rapida + Nessuno + Usati recentemente + Scegli azione rapida + Cerca contatti + Cerca segnalibri + Invia messaggio privato + %1$s ha abbandonato la chat di gruppo + Utente + Utente + Questo non è un nome utente valido + Scaricamento fallito: server non trovato + Scaricamento fallito: file non trovato + Scaricamento fallito: impossibile connettersi all\'host + Scaricamento fallito: scrittura del file impossibile + Scaricamento fallito: file non valido + Rete Tor non disponibile + Bind fallito + Il server non è responsabile per questo dominio + Rotto + Disponibilità + \"Non disponibile\" a dispositivo bloccato + Imposta come non disponibile quando il dispositivo è bloccato + \"Occupato\" in modalità silenziosa + Imposta come occupato quando il dispositivo è in modalità silenziosa + 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 profilo + xmpp.esempio.it + Accedi con certificato + Impossibile analizzare il certificato + Preferenze di archiviazione + Preferenze di archiviazione lato server + Ricezione preferenze di archiviazione. Attendere prego… + Impossibile recuperare le preferenze di archiviazione + CAPTCHA necessario + Inserisci il testo dell\'immagine soprastante + Catena di certificati non fidata + L\'indirizzo XMPP non corrisponde al certificato + Rinnova certificato + Errore ricezione chiave OMEMO! + Chiave OMEMO verificata con certificato! + Il tuo dispositivo non supporta la selezione di certificati utente! + Connessione + Connettiti via Tor + Indirizza tutte le connessioni attraverso la rete Tor. Richiede Orbot + Nome host + Porta + Indirizzo server o .onion + Questo non è un numero di porta valido + Questo non è un nome host valido + %1$d su %2$d profili connessi + + %d messaggio + %d messaggi + %d messaggi + + Carica altri messaggi + File condiviso con %s + Immagine condivisa con %s + Immagini condivise con %s + Testo condiviso con %s + Dai a %1$s l\'accesso all\'archiviazione esterna + Dai a %1$s l\'accesso alla fotocamera + Sincronizza con i contatti + %1$s vuole l\'autorizzazione ad accedere alla tua rubrica per confrontarla con la lista di contatti in XMPP.\nCiò mostrerà i nomi ed avatar dei contatti.\n\n%1$s leggerà solamente la rubrica e la confronterà localmente senza inviare nulla al tuo server. +
Non salveremo una copia di quei numeri di telefono.\n\nPer maggiori informazioni leggi la nostra politica sulla privacy.

Ti verrà ora chiesta l\'autorizzazione di accedere ai tuoi contatti.]]>
+ Notifica per tutti i messaggi + Notifica solo quando menzionato + Notifiche disattivate + Notifiche in pausa + 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 + Ottimizzazioni batteria attivate + Il tuo dispositivo sta facendo delle ingenti ottimizzazioni della batteria per %1$s che potrebbero portare ritardi alle notifiche o anche perdita di messaggi.\nSi consiglia di disattivarle. + 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 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 profilo + Errore di sicurezza: accesso file non valido! + Nessuna app trovata per condividere l\'URI + Condividi 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 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 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 + In linea + Assente + Non disponibile + Occupato + È stata generata una password sicura + Il tuo dispositivo non supporta l\'esclusione per l\'ottimizzazione della batteria + Registrazione fallita: riprova più tardi + Registrazione fallita: password troppo debole + Scegli i partecipanti + Creazione chat di gruppo… + Invita di nuovo + Disattiva + Breve + Medio + Lungo + Trasmetti l\'utilizzo + Fa sapere ai tuoi contatti quando usi Conversations + Privacy + Tema + Seleziona il colore + Automatico + Chiaro + Scuro + Sfondo verde + Usa uno sfondo verde per i messaggi ricevuti + Impossibile connettersi a OpenKeychain + Questo dispositivo non è più in uso + Computer + Cellulare + Tablet + Browser web + Console + Necessario pagamento + Concedi l\'autorizzazione ad usare internet + Io + Il contatto chiede la sottoscrizione della presenza + Consenti + Nessuna autorizzazione per accedere a %s + Server remoto non trovato + Scadenza server remoto + Impossibile aggiornare il profilo + Segnala questo indirizzo XMPP per spam. + 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. + Mostra messaggio di errore + Messaggio di errore + Risparmio dati attivato + Il tuo sistema operativo sta limitando l\'accesso internet a %1$s quando è in secondo piano. Per ricevere le notifiche di nuovi messaggi dovresti consentire l\'accesso senza limiti a %1$s quando il \"Risparmio dati\" è attivo.\n%1$s cercherà comunque di risparmiare dati quando possibile. + Il tuo dispositivo non supporta la disattivazione del Risparmio dati per %1$s. + Impossibile creare il file temporaneo + Questo dispositivo è stato verificato + Copia impronta + Hai verificato tutte le chiavi OMEMO in tuo possesso + Il codice a barre non contiene impronte per questa conversazione. + Impronte verificate + Usa la fotocamera per scansionare il codice a barre di un contatto + Attendi la ricezione delle chiavi + Condividi come codice a barre + Condividi come URI XMPP + Condividi come link HTTP + Fiducia cieca prima della verifica + Fidati di nuovi dispositivi da contatti non verificati, ma chiedi una conferma manuale per nuovi dispositivi da contatti verificati. + Chiavi OMEMO accettate ciecamente, perciò potrebbero essere di qualcun altro o qualcuno potrebbe essersi intromesso. + Non fidato + Codice a barre 2D non valido + Svuota la cartella della cache (usata dall\'app fotocamera) + Svuota cache + Svuota archivio privato + Svuota l\'archivio privato nella quale sono memorizzati i file (possono essere riscaricati dal server) + Ho seguito questo link da una fonte fidata + Stai per verificare le chiavi OMEMO di %1$s cliccando un link. Questo metodo è sicuro solo se hai seguito il link da una fonte fidata dove solo %2$s può averlo pubblicato. + Stai per verificare le chiavi OMEMO del tuo stesso account. Questo metodo è sicuro solo se hai seguito il link da una fonte fidata dove solo tu puoi averlo pubblicato. + Continua + Verifica chiavi OMEMO + Mostra inattivi + Nascondi inattivi + Diffida il dispositvo + Sei sicuro di volere rimuovere la verifica di questo dispositivo?\nIl dispositivo e i messaggi provenienti da esso verranno segnati come \"Non fidato\". + + %d secondo + %d secondi + %d secondi + + + %d minuto + %d minuti + %d minuti + + + %d ora + %d ore + %d ore + + + %d giorno + %d giorni + %d giorni + + + %d settimana + %d settimane + %d settimane + + + %d mese + %d mesi + %d mesi + + 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. + Compressione video in corso + Chiuse le relative conversazioni. + Contatto bloccato. + Notifiche da sconosciuti + Notifica messaggi e chiamate ricevuti da sconosciuti. + Ricevuto messaggio da uno sconosciuto + Blocca sconosciuto + Blocca intero dominio + in linea adesso + Ritenta decifrazione + Sessione fallita + Meccanismo SASL degradato + Il server richiede la registrazione sul sito + Apri sito + Nessuna app trovata per aprire il sito + Notifiche avvisi + Mostra notifiche su avvisi + Oggi + Ieri + Convalida hostname con DNSSEC + I certificati dei server che contengono l\'hostname convalidato sono considerati verificati + Il certificato non contiene un indirizzo XMPP + parziale + Registra un video + Copia negli appunti + Messaggio copiato negli appunti + Messaggio + I messaggi privati sono disattivati + App protette + Per ricevere notifiche anche quando lo schermo è spento, devi aggiungere Conversations all\'elenco delle app protette. + Accetti il certificato sconosciuto? + Il certificato del server non è firmato da una Certificate Authority nota. + Accetti il nome del server non corrispondente? + Il server non ha potuto autenticarsi come \"%s\". Il certificato è valido solo per: + Vuoi connetterti comunque? + Dettagli certificato: + Una volta sola + Lo scanner di codici QR ha bisogno di accedere alla fotocamera + Scorri in fondo + Scorri in basso dopo l\'invio di un messaggio + Modifica messaggio di stato + Modifica il messaggio di stato + Disattiva la cifratura + %1$s non riesce a inviare messaggi cifrati a %2$s. Potrebbe essere dovuto al tuo contatto che usa un server obsoleto o un client che non supporta OMEMO. + Impossibile ricevere l\'elenco dispositivi + Impossibile ricevere le chiavi di cifratura + Suggerimento: in alcuni casi può essere risolto aggiungendo a vicenda la vostra lista di contatti. + Sei sicuro di disattivare la cifratura OMEMO per questa conversazione?\nCiò permetterà all\'amministratore del server di leggere i tuoi messaggi, ma potrebbe essere il solo modo di comunicare con persone che usano client obsoleti. + Disattiva adesso + Bozza: + Cifratura OMEMO + 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 dei caratteri + La dimensione dei caratteri usata all\'interno dell\'app. + On in modo predefinito + Off in modo predefinito + Piccolo + Medio + Grande + Il messaggio non è stato criptato per questo dispositivo. + Decifrazione del messaggio OMEMO fallita. + annulla + La condivisione della posizione è disattivata + Fissa la posizione + Sblocca la posizione + Copia la posizione + Condividi la posizione + Direzioni + Condividi la posizione + Mostra la posizione + Condividi + Impossibile avviare la registrazione + Attendere prego… + Dai a %1$s l\'accesso al microfono + Cerca messaggi + GIF + Vedi conversazione + Plugin Condivisione Posizione + Usa il plugin Condividi Posizione al posto della mappa integrata + Copia indirizzo web + Copia indirizzo XMPP + Condivisione file in HTTP per S3 + Ricerca diretta + Nella schermata \'Inizia conversazione\' apri la tastiera e posiziona il cursore nella casella di ricerca + Avatar chat di gruppo + L\'host non supporta gli avatar per chat di gruppo + Solo il proprietario può cambiare l\'avatar della chat di gruppo + Nome contatto + Soprannome + Nome + Il nome è facoltativo + Nome chat di gruppo + Questa chat di gruppo è stata distrutta + Impossibile salvare la registrazione + Servizio in primo piano + 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 profilo. + Messaggi + Chiamate + Messaggi + Chiamate in arrivo + Chiamate in uscita + Chiamate perse + Messaggi silenziosi + Questo gruppo di notifiche è usato per mostrare notifiche che non devono riprodurre alcun suono. Ad esempio mentre si è attivi su un altro dispositivo (Periodo di grazia). + Recapiti falliti + Impostazioni di notifica dei messaggi + Impostazioni di notifica delle chiamate in arrivo + Importanza, suono, vibrazione + Compressione video + Vedi i media + Partecipanti + Browser multimediale + File omesso per violazione di sicurezza. + Qualità dei video + Una qualità inferiore comporta file più piccoli + Media (360p) + Alta (720p) + annullato + Hai già un messaggio in bozze. + Funzionalità non implementata + Codice nazionale non valido + Scegli una nazione + numero di telefono + Verifica il tuo numero di telefono + Quicksy invierà un SMS (possono essere applicati costi dal gestore) per verificare il tuo numero. Inserisci il tuo codice nazionale e numero di telefono: +
%s

È corretto o vuoi modificare il numero?]]>
+ %s non è un numero di telefono valido. + Inserisci il tuo numero di telefono. + Cerca nazioni + Verifica %s + %s.]]> + Ti abbiamo inviato un altro SMS con un codice di 6 cifre. + Inserisci il pin di 6 cifre qua sotto. + Reinvia SMS + Reinvia SMS (%s) + Attendere prego (%s) + indietro + Possibile pin incollato automaticamente dagli appunti. + Inserisci il pin di 6 cifre. + Sei sicuro di voler annullare la procedura di registrazione? + + No + Verifica… + Richiesta SMS… + Il pin che hai inserito è sbagliato. + Il pin che ti abbiamo inviato è scaduto. + Errore di rete sconosciuto. + Risposta dal server sconosciuta. + Impossibile connettersi al server. + Impossibile stabilire una connessione sicura. + Impossibile trovare il server. + Qualcosa è andato storto elaborando la tua richiesta. + Input utente non valido + Temporaneamente non disponibile. Riprova più tardi. + Nessuna connessione di rete. + Riprova in %s + Hai limiti di utilizzo + Troppi tentativi + Stai usando una versione obsoleta di questa app. + Aggiorna + Questo numero di telefono è attualmente connesso con un altro dispositivo. + Inserisci il tuo nome per far sapere chi sei alle persone che non ti hanno nelle loro rubriche. + Il tuo nome + Inserisci il tuo nome + Usa il pulsante modifica per impostare il tuo nome. + Rifiuta la richiesta + Installa Orbot + Avvia Orbot + Nessuna app market installata. + Questo canale renderà pubblico il tuo indirizzo XMPP + e-book + Originale (non compresso) + Apri con… + Immagine profilo di Conversations + Scegli un profilo + Ripristina backup + Ripristina + 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? + Backup e ripristino + Inserisci l\'indirizzo XMPP + Crea chat di gruppo + Entra in un canale pubblico + Crea chat di gruppo privata + Crea canale pubblico + Nome del canale + Indirizzo XMPP + Fornire un nome per il canale + Fornire un indirizzo XMPP + Questo è un indirizzo XMPP. Fornisci un nome. + Creazione canale pubblico… + Questo canale esiste già + Sei entrato in un canale esistente + Impossibile salvare la configurazione del canale + Permetti a chiunque di modificare l\'argomento + Permetti a chiunque di invitare altri + Chiunque può modificare l\'argomento. + I proprietari possono modificare l\'argomento. + Gli admin possono modificare l\'argomento. + I proprietari possono invitare altri. + Chiunque può invitare altri. + Gli indirizzi XMPP sono visibili agli admin. + Gli indirizzi XMPP sono visibili a chiunque. + Questo canale pubblico non ha partecipanti. Invita i tuoi contatti o usa il pulsante condividi per distribuirne l\'indirizzo XMPP. + Questa chat di gruppo privata non ha partecipanti. + Gestisci i privilegi + Cerca partecipanti + File troppo grande + Allega + 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 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 + Condividi file di backup + Backup di Conversations + Evento + Apri backup + Il file selezionato non è un file di backup di Conversations + 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. + + jabber.network + Server locale + La maggior parte degli utenti dovrebbe scegliere ‘jabber.network’ per migliori suggerimenti dall\'intero ecosistema XMPP pubblico. + Metodo di scoperta dei canali + Backup + Al riguardo + Devi attivare un profilo + Chiama + Chiamata in arrivo + Chiamata video in arrivo + Passare a una videochiamata? + Aggiungere altre tracce? + Connessione + Connesso + Riconnessione + Accettazione chiamata + Chiusura chiamata + Rispondi + Rifiuta + Individuazione dispositivi + Sta squillando + Occupato + Impossibile connettere la chiamata + Connessione persa + Chiamata ritirata + Errore dell\'app + Problema di verifica + Riaggancia + Chiamata in corso + Chiamata video in corso + Riconnessione chiamata + Riconnessione chiamata video + Disattiva Tor per le chiamate + Chiamata in arrivo + Chiamata in arrivo · %s + Chiamata persa · %s + Chiamata in uscita + Chiamata in uscita · %s + Chiamata persa + + %1$d chiamata persa da %2$s + %1$d chiamate perse da %2$s + %1$d chiamate perse da %2$s + + + %d chiamata persa + %d chiamate perse + %d chiamate perse + + + %1$d chiamate perse da %2$d contatto + %1$d chiamate perse da %2$d contatti + %1$d chiamate perse da %2$d contatti + + Chiamata vocale + Chiamata video + Aiuto + Passa alla conversazione + Il tuo microfono non è disponibile + Puoi fare solo una chiamata alla volta. + Torna alla chiamata in corso + Impossibile cambiare fotocamera + Fissa in alto + Rimuovi dall\'alto + Traccia GPX + Impossibile correggere il messaggio + Tutte le conversazioni + Questa conversazione + Il tuo avatar + Avatar di %s + Crittografato con OMEMO + Crittografato con OpenPGP + Non crittografato + Esci + Registra in segreteria + Riproduci audio + Pausa + Aggiungi un contatto, crea o visita una chat di gruppo, o scopri canali + + Vedi %1$d partecipante + Vedi %1$d partecipanti + Vedi %1$d partecipanti + + + Un messaggio non è stato recapitato + Alcuni messaggi non sono stati recapitati + Alcuni messaggi non sono stati recapitati + + Recapiti falliti + Altre opzioni + Nessuna applicazione trovata + Invita su Conversations + Impossibile analizzare l\'invito + Il server non supporta la generazione di inviti + Nessun profilo attivo supporta questa funzione + Il backup è iniziato. Riceverai una notifica una volta completato. + Impossibile attivare il video. + Documento di testo + Le registrazioni di profili non sono supportate + Nessun indirizzo XMPP trovato + Errore di autenticazione temporaneo + Elimina avatar + Le chiamate sono disattivate quando si usa Tor + Passa al video + Rifiuta richiesta di passare al video + Distributore di UnifiedPush + Profilo XMPP + Il profilo attraverso cui verranno ricevuti i messaggi push. + Server push + Un server scelto dall\'utente per inoltrare i messaggi push via XMPP al tuo dispositivo. + Nessuno (disattivato) +
\ No newline at end of file diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml new file mode 100644 index 000000000..8488cce22 --- /dev/null +++ b/app/src/main/res/values-iw/strings.xml @@ -0,0 +1,285 @@ + + + הגדרות + שיחה חדשה + נהל חשבונות + פרטי איש קשר + הוסף חשבון + ערוך שם + מחק מרשימת אנשי הקשר + חסום איש קשר + בטל חסימת איש קשר + חסום דומיין + בטל חסימת דומיין + נהל חשבונות + הגדרות + שתף בעזרת Conversations + התחל דיון + רשימת חסימה + ממש עכשיו + לפני דקה + לפני %d דקות + שולח... + כעת מפענח צופן הודעה. אנא המתן… + הודעה מוצפנת OpenPGP + שם כינוי כבר בשימוש + מנהל + בעלים + אחראי (Moderator) + משתתף + מבקר + האם ברצונך לחסום קבלת הודעות מאת %s? + האם ברצונך לבטל את החסימה ולאפשר קבלת הודעות מאת %s? + לחסום את כל האנשים מתוך %s? + לבטל את חסימת כל האנשים מתוך %s? + איש קשר נחסם + צור חשבון חדש בשרת + שינוי סיסמה בשרת + שתף באמצעות... + אנשי קשר + איש קשר + ביטול + הגדר + הוסף + ערוך + מחק + חסום + בטל חסימה + שמור + אישור + שלח עכשיו + לעולם אל תשאל שוב + צרף קובץ + הוסף איש קשר + מסירה נכשלה + נקה היסטוריה + נקה היסטוריית שיחה + שלח הודעה בלתי מוצפנת + של הודעה בהצפנת OMEMO + שלח הודעה בהצפנת OpenPGP + שלח ללא הצפנה + פענוח נכשל. אולי אין לך את המפתח הפרטי המתאים. + OpenKeychain + התחל מחדש + התקן + אנא התקן OpenKeychain + מציע… + ממתין… + לא נמצא מפתח OpenPGP + לא נמצאו מפתחות OpenPGP + כללי + קבל קבצים + קבל אוטומטית קבצים שגודלם קטן מ… + הרטט + לעולם אל תשלח דיווחי קריסה + אשר הודעות + קבל + אירעה שגיאה + החשבון שלך + שלח עדכוני נוכחות + קבל עדכוני נוכחות + בקש עדכוני נוכחות + בחר תמונה + צלם תמונה + הענק מראש בקשת הרשמה + הקובץ שבחרת אינו תמונה + קובץ לא נמצא + שגיאת I/O כללית. אולי אזל לך נפח אחסון? + לא ידוע + מנוטרל זמנית + מקוון + מתחבר\u2026 + לא מקוון + לא מורשה + שרת לא נמצא + אין חיבוריות + הרשמה נכשלה + שם משתמש כבר בשימוש + הרשמה הושלמה + שרת לא מתאים + לא מוצפן + OTR + OpenPGP + OMEMO + מחק + נטרל זמנית + פרסם תמונת פרופיל + פרסם מפתח ציבורי של OpenPGP + הפעל חשבון + האם אתה בטוח? + הקלט קול + username@example.com + סיסמה + פרטי השרת + XEP-0313: MAM - היסטוריית שרת + XEP-0280: Message Carbons + XEP-0352: Client State Indication + XEP-0191: Blocking Command - חסימת אנשי קשר + XEP-0237: Roster Versioning + XEP-0198: Stream Management + XEP-0163: PEP (Avatars / OMEMO) - תמונת פרופיל והצפנת OMEMO + XEP-0363: HTTP File Upload - שליחת קבצים דרך HTTP + זמין + לא זמין + הכרזות מפתח פומבי חסרות + נראה לאחרונה ממש עכשיו + נראה לאחרונה לפני %d דקות + נראה לאחרונה לפני %d שעות + נראה לאחרונה לפני %d ימים + טביעת אצבע של OMEMO + מכשירים אחרים + סמוך על טביעות אצבע OMEMO + בוצע + פענח + חפש + צפה בפרטי איש קשר + חסום איש קשר + בטל חסימת איש קשר + צור + איש קשר כבר קיים + הצטרף + שמור בתור סימנייה + מחק סימנייה + עזוב + איש קשר הוסיף אותך אל רשימת קשר + הוסף בחזרה + %s קרא עד לנקודה זו + פרסם + מעלה… + השרת דחה את ההעלאה + שגיאה בעת שמירת תמונה לזיכרון + (או לחיצה ארוכה כדי להחזיר לברירת מחדל) + בפרטי + בפרטי אל %s + שלח הודעה פרטית אל %s + התחבר + חשבון זה כבר קיים + הבא + דלג + השבת התראות + הפעל + הכנס סיסמא + בקש/י כעת + התעלם + נא להיזהר! + שעות שקטות + זמן התחלה + זמן סיום + הפעל \"שעות שקטות\" + ההתראות יושבתו במהלך שעות שקטות + אחר + משתמש בחשבון: %s + אינך מחובר. נסה שוב אחר כך + בדוק גודל %s + הגדרות הודעה + העתק קישור + שלח שוב + קישור קובץ + הראה רשימת חסומים + פרטי חשבון + אמת + נסה שוב + מונע ממערכת ההפעלה לנתק את החיבור לשרת + בחר קובץ + מקבל %1$s ( הושלם %2$d%% ) + הורד %s + קובץ + פתח %s + שולח ( %1$d%% הושלם ) + הקובץ %s הוצע לאיש הקשר + בטל שליחה + הראה תגי read-only מתחת לאנשי הקשר + אפשר התראות + תמונת פרופיל + העתק טביעת אצבע OMEMO + צור מפתח OMEMO חדש + נקה מכשירים + הורדת היסטוריה מהשרת + אין עוד היסטוריה בשרת + מעדכן... + הסיסמה שונתה! + שינוי הסיסמה נכשל + שינוי סיסמה + סיסמה נוכחית + סיסמה חדשה + הפעל את כל החשבונות + נטרל את כל החשבונות + בצע פעולה באמצעות + אין שיוך + לא מקוון + חבר בקבוצה + מצב מתקדם + הענק הרשאות מנהל + שלול הרשאות מנהל + לא ניתן לשנות את השיוך של %s + חסום עכשיו + לא ניתן לשנות את התפקיד של %s + פרטי, חברים בלבד + אינך משתתף + לעולם לא + עד אחרית הימים + לחצן Enter שולח את ההודעה + הראה את לחצן ה Enter + שנה את לחצן האימוג\'י ללחצן Enter + קול + סרטון + תמונה + מסמך PDF + אפליקציית אנדרויד + איש קשר + תמונת הפרופיל פורסמה! + שולח %s + מציע %s + הסתר בלתי מקוונים + %s מקליד/ה כעת… + %s הפסיק/ה להקליד + התראות הקלדה + שלח מיקום + הראה מיקום + מיקום + השיחה נסגרה + אל תסמוך על ה- CAs של המערכת + כל החתימות הדיגטליות יצטרכו לעבור אימות ידני + מחק חתימות דיגטליות + מחק חתימות דיגטליות שאומתו באופן ידני + אין חתימות דיגטליות שאושרו ידנית + מחק חתימות דיגטליות + מחק פריטים שנבחרו + ביטול + + %d חתימה נמחקה + %d חתימות נמחקו + %d חתימות נמחקו + %d חתימות נמחקו + + פעולה מהירה + כלום + לפי השימוש האחרון + בחר פעולה מהירה + שלח הודעה פרטית + שם משתמש + שם משתמש + שם משתמש זה אינו חוקי + ההורדה נכשלה: שרת לא נמצא + ההורדה נכשלה: הקובץ לא נמצא + ההורדה נכשלה: נכשל ביצוע חיבור לשרת + לא עובד + חידוש תעודה + שגיאה בתפיסת OMEMO! + התחבר דרך Tor + שם מארח + פורט + זהו אינו מספר פורט תקין + זהו אינו שם מארח תקין + %1$d מתוך %2$d חשבונות מחוברים + + הודעה %d + %d הודעות + %d הודעות + %d הודעות + + סנכרן עם אנשי קשר + מקוון + ההודעה הועתקה + הראה מיקום + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml new file mode 100644 index 000000000..54035ec75 --- /dev/null +++ b/app/src/main/res/values-ja/strings.xml @@ -0,0 +1,980 @@ + + + 設定 + 新しい会話 + アカウントを管理 + アカウントを管理 + 会話を閉じる + 連絡先の詳細 + グループチャットの詳細 + 談話室の詳細 + アカウントを追加 + 名前を編集 + アドレス帳に追加 + 名簿から削除 + 連絡先をブロック + 連絡先のブロックを解除 + ドメインをブロック + ドメインのブロックを解除 + 参加者をブロック + 参加者のブロックを解除 + アカウントを管理 + 設定 + 会話で共有 + 会話を開始 + 連絡先を選択 + 連絡先を選択 + アカウントで共有 + ブロック一覧 + ちょうど今 + 1分前 + %d分前 + + %d件の未読の会話 + + 送信中… + メッセージを復号しています。しばらくお待ちください… + OpenPGP 暗号化メッセージ + ニックネームは既に使用されています + このニックネームは使えません + 管理者 + 所有者 + 調停者 + 参加者 + 訪問者 + 連絡先名簿から %s を削除しますか? この連絡先との会話は削除されません。 + %s からあなたに送信されるメッセージをブロックしますか? + %s のブロックを解除し、あなたにメッセージを送信できるようにしますか? + %s からの連絡をすべてブロックしますか? + %s からすべての連絡先のブロックを解除しますか? + 連絡先をブロックしました + ブロックしました + %s のブックマークを削除しますか? このブックマークとの会話は削除されません。 + サーバーに新規アカウントを登録 + サーバーのパスワードを変更 + …で共有 + 会話を始める + 連絡先を招待 + 招待 + 連絡先 + 連絡先 + 中止 + 設定 + 追加 + 編集 + 削除 + ブロック + ブロックを解除 + 保存 + OK + %1$s がクラッシュしました + あなたの XMPP アカウントを使用してスタックトレースの送信をすることで、 %1$s の継続的な開発を支援します。 + 今すぐ送信 + 今後は表示しない + アカウントに接続できません + 複数のアカウントに接続できません + タップしてアカウントを管理 + ファイルを添付 + 連絡先が連絡先名簿にありません。名簿に追加しますか? + 連絡先を追加 + 配信に失敗しました + 送信用画像の準備中 + 送信用画像の準備中 + ファイル共有中。しばらくお待ちください… + 履歴を消去 + 会話履歴を消去 + この会話のすべてのメッセージを削除してもよろしいですか?\n\n警告: 他のデバイスやサーバーに保存されているメッセージのコピーには影響しません。 + ファイルを削除 + このファイルを削除してもよろしいですか?\n\n警告: これは、他のデバイスやサーバーに保存されているファイルのコピーは削除しません。 + この後、この会話を閉じる + デバイスを選択 + 暗号化されていないメッセージを送信 + メッセージを送信 + メッセージを %s に送信 + OMEMO 暗号化メッセージを送信 + v\\OMEMO 暗号化メッセージを送信 + OpenPGP 暗号化メッセージを送信 + ニックネームが変更されました + 暗号化せずに送信 + 復号に失敗しました。適切な秘密鍵を持っていないのかもしれません。 + OpenKeychain + OpenKeychain を利用して、メッセージの暗号化および復号、そしてあなたの公開鍵を管理します。

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

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

今、連絡先へのアクセス権限を付与するよう求められます。]]>
+ すべてのメッセージで通知 + メンションされたときにのみ通知 + 通知は無効 + 通知は一時停止 + 画像の圧縮 + ヒント: この設定に関係なく、個々の画像を非圧縮で送信する場合は、‘画像を選択’ではなく‘ファイルを選択’を使用してください。 + 常に + 大きい画像のみ + 電池最適化が有効 + お使いのデバイスは、%1$s で通知の遅延やメッセージの損失につながる可能性のある、重い電池の最適化を使用しています。\nそれを無効化することをお勧めします。 + お使いのデバイスは、%1$s で通知の遅延やメッセージの損失につながる可能性のある、重い電池の最適化を使用しています。\n\n今、それらを無効化するように求められます。 + 無効 + 選択した範囲が大きすぎます + (アクティベートしたアカウントはありません) + このフィールドは必須項目です + メッセージを修正 + 修正したメッセージを送信 + あなたは信頼を確認するために、この人の指紋を安全に検証しました。“完了”を選択すると、 %s がこのグループチャットの一員であることを確認したことになります。 + このアカウントを無効化しました + セキュリティエラー: 不正なファイルアクセス! + URI を共有するアプリが見つかりません + …で URI を共有 +
電話番号を入力して登録すると、アドレス帳に登録されている電話番号をもとに、Quicksyが自動的に連絡先を提案します。

登録すると、我々のプライバシーポリシーに同意することになります。]]>
+ 同意して続行 + conversations.im 上にアカウントを作成する設定の指南です。¹\nconversations.im をプロバイダーとして選択した場合、あなたの完全な XMPP アドレスを他のプロバイダーのユーザーに示すことで、その人と連絡をとることができます。 + あなたの完全なXMPPアドレスは: %s + アカウントを作成 + 自分のプロバイダーを使用 + ユーザー名を選択 + 在席状況を手動で管理 + ステータスメッセージの編集時に、在席状況を設定します。 + ステータスメッセージ + いつでもチャットできます + オンライン + 離席 + 不在 + 取込中 + 安全なパスワードが生成されました + お使いのデバイスは電池最適化の停止をサポートしていません + 登録に失敗しました: 後でもう一度お試しください + 登録に失敗しました: パスワードが弱すぎます + 参加者を選択 + グループチャットを作成しています… + もう一度招待 + 無効 + + + + ブロードキャストを使用 + Conversations を使用するときに、連絡先に知らせましょう + プライバシー + テーマ + カラーパレットの選択 + 自動 + + + 緑の背景 + 受信したメッセージに緑の背景を使用します + OpenKeychain に接続できません + このデバイスは、現在使用されていません + コンピューター + 携帯電話 + タブレット + ウェブブラウザ + コンソール + 支払が必要です + インターネット使用権限の付与 + 自分 + 連絡先が出席情報サブスクリプションを求めています + 許可 + %s にアクセスする権限がありません + リモートサーバーが見つかりません + リモートサーバーがタイムアウト + アカウントを更新できません + この XMPP アドレスをスパムとして報告する。 + OMEMO ID を削除 + OMEMO 鍵を再生成します。すべての連絡先を再度確認する必要があります。使用するのは最後の手段のみとしてください。 + 選択した鍵を削除 + アバターを公開するには接続する必要があります。 + エラーメッセージを表示 + エラーメッセージ + データセーバーを有効化しました + お使いのオペレーティングシステムは、%1$s がバックグラウンドのときにインターネットにアクセスすることを制限しています。新着メッセージの通知を受信するには、“データセーバー”がオンならば、%1$s に無制限のアクセスを許可する必要があります。\n%1$s は可能なときにデータを保存するための努力をします。 + お使いのデバイスは、%1$s のデータセーバーを無効にできません。 + 一時ファイルを作成できません + このデバイスは検証済です + フィンガープリントをコピー + 所有するすべての OMEMO 鍵を検証完了 + バーコードに、この会話のフィンガープリントが含まれていません。 + フィンガープリントを検証しました + カメラを使用して連絡先のバーコードをスキャンします + 鍵が取得されるのをお待ちください + バーコードで共有 + XMPP URI で共有 + HTTP リンクで共有 + 認証前で鍵を使用 + 認証されていない連絡先からの新規デバイスを信頼するが、認証されている連絡先からの新規デバイスについては手動での確認を求める。 + 認証せずOMEMO 鍵を信用しています。このままでは盗聴される危険性があります。 + 信頼されていない + 不正な二次元バーコード + キャッシュフォルダを消去します (カメラアプリで使用) + キャッシュを消去 + プライベートストレージを消去 + ファイルが保存されているプライベートストレージを消去します (サーバーから再ダウンロードできます) + 信頼できるソースからこのリンクをたどりました + リンクをクリックした後、%1$s の OMEMO 鍵を検証しようとしています。 これは、%2$s がこのリンクを公開した、信頼できるソースからこのリンクをたどった場合にのみ安全です。 + あなたが所有するアカウントの OMEMO 鍵を検証しようとしています。 これは、あなたがこのリンクを公開した、信頼できるソースからこのリンクをたどった場合にのみ安全です。 + 続行 + OMEMO 鍵を検証 + 非アクティブを表示 + 非アクティブを非表示 + 信頼できないデバイス + このデバイスの検証を削除してよろしいですか?\nこのデバイスとそのデバイスからのメッセージは、“信頼できない”とマークされます。 + + %d秒 + + + %d分 + + + %d時間 + + + %d日 + + + %d週 + + + %dか月 + + 自動でメッセージを削除 + 設定された期間よりも古いメッセージを、このデバイスから自動的に削除します。 + メッセージの暗号化中 + ローカル保存期間のためにメッセージを取得しません。 + ビデオを圧縮中 + 対応する会話が閉じられました。 + 連絡先をブロックしました + 見知らぬ人からの通知 + 見知らぬ人から受信したメッセージと通話を通知します。 + 見知らぬ人からメッセージを受信しました + 見知らぬ人をブロック + ドメイン全体をブロック + 今すぐオンライン + 復号を再試行 + セッション失敗 + ダウングレードされた SASL メカニズム + サーバーはウェブサイトでの登録が必要です + ウェブサイトを開く + ウェブサイトを開くアプリが見つかりません + Heads-up 通知 + Heads-up 通知を表示 + 今日 + 昨日 + DNSSEC でホスト名の妥当性を確認 + 検証されたホスト名を含むサーバー証明書は検証済みと見なされます + 証明書は XMPP アドレスを含みません + 一時的 + ビデオを録画 + クリップボードにコピー + メッセージをクリップボードにコピーしました + メッセージ + 非公開メッセージを無効化しました + 保護されたアプリ + 画面がオフになっているときでも通知を受信し続けるには、保護されたアプリの一覧に Conversations を追加する必要があります。 + 未知の証明書を受け入れますか? + サーバー証明書が既知の認証局によって署名されていません。 + 不一致のサーバー名を受け入れますか? + サーバーは\"%s\"として認証できませんでした。証明書は次の場合にのみ有効です: + それでも接続を希望しますか? + 証明書の詳細: + 一度だけ + QR コードスキャナーはカメラにアクセスが必要です + 一番下へスクロール + メッセージ送信後に下へスクロール + ステータスメッセージを編集 + ステータスメッセージを編集 + 暗号化が無効 + %1$s は %2$s に暗号化メッセージを送れません。連絡先が利用しているサーバーが古すぎるか、クライアントが OMEMO を扱えません。 + デバイスの一覧を取得できません + 暗号化の鍵を取得できません + ヒント: お互いが連絡先名簿に加えれば解決するでしょう。 + この会話で OMEMO の暗号化を無効化してもよろしいですか?\nこれにより、サーバー管理者がメッセージを読むことが可能になりますが、時代遅れのクライアントを使っている人と連絡をとるには、この方法しかないかもしれません。 + 今すぐ無効化 + 下書き: + OMEMO 暗号化 + OMEMO は1対1の会話と非公開グループチャットで常に使用されます。 + デフォルトで新しい会話で OMEMO を使用します。 + 新しい会話をするためには、OMEMOを明示的にオンにする必要があります。 + ショートカットを作成 + フォントの大きさ + このアプリで使用される相対的なフォントの大きさ + デフォルトでオン + デフォルトでオフ + + + + このデバイス向けにメッセージは暗号化されませんでした。 + OMEMO メッセージの復号に失敗しました。 + 元に戻す + 位置の共有が無効 + 位置を固定 + 位置を固定しない + 位置をコピー + 位置を共有 + 位置を共有 + 位置を表示 + 共有 + 録音を開始できません + しばらくお待ちください… + %1$s にマイクへのアクセス権を付与 + メッセージを検索 + GIF + 会話を表示 + 位置共有プラグイン + 位置共有プラグインの代わりに、組み込みの地図を使う + ウェブアドレスをコピー + XMPP アドレスをコピー + S3 の HTTP ファイル共有 + 直接検索 + ‘会話を開始’画面でキーボードを開き、検索フィールドにカーソルを置きます + グループチャットのアバター + ホストはグループチャットのアバターをサポートしていません + 所有者だけが、グループチャットのアバターを変更可能です + 連絡先の名前 + ニックネーム + 名前 + 名前の記入は任意です + グループチャット名 + このグループチャットは破棄されました + 録音を保存できません + フォアグラウンドサービス + この通知カテゴリーは %1$s が実行していることを表示する、永続的な通知を表示するために使用されます。 + ステータス情報 + 接続の問題 + この通知カテゴリーは、アカウントへの接続に問題があった場合に、通知を表示するために使用されます。 + メッセージ + 通話 + メッセージ + 着信通話 + 継続中の通話 + 不在着信 + サイレントメッセージ + この通知グループは、音を鳴らしてはいけない通知を表示するために使用します。例えば、他のデバイスでアクティブになっているときなどです (猶予期間)。 + 配信に失敗 + メッセージ通知設定 + 着信通話の通知設定 + 重要性、音、振動 + ビデオの圧縮 + メディアを表示 + 参加者 + メディアブラウザー + セキュリティ違反のため、ファイルが省略されています。 + ビデオの質 + 質が低い程、ファイルは小さくなります + 中 (360p) + 高 (720p) + 中止しました + あなたは既にメッセージを作成中です。 + 実装されてない機能 + 不正な国コード + 国を選択 + 電話番号 + 電話番号を検証 + Quicksy から電話番号を確認するための SMS メッセージ(キャリア料金がかかる場合があります)が送信されます。国番号と電話番号を入力してください: +
%s

これでよろしいでしょうか、それとも番号を編集しますか?]]>
+ %s は有効な電話番号ではありません。 + 電話番号を入力してください。 + 国を検索 + %s を検証 + %sにSMSを送りました。]]> + 6桁のコードを含む別のSMSを送信しました。 + 以下に6桁の pin を入力してください。 + SMS 再送信 + SMS 再送信(%s) + しばらくお待ちください(%s) + 戻る + クリップボードから可能な pin を自動的に貼り付ける。 + 6桁の pin を入力してください。 + 登録手続きを中止してもよろしいのですか? + はい + いいえ + 検証しています… + SMSを要求しています… + 入力された pin が正しくありません。 + 送信した pin の有効期限が切れています。 + 未知のネットワークエラー。 + サーバーからの不明な応答。 + サーバーに接続できません。 + 安全な接続を確立できませんでした。 + サーバーが見つかりません。 + 要求の処理中に、何か問題が発生しました。 + 無効なユーザーの入力 + 一時的に入手不可能です。後でもう一度お試しください。 + ネットワーク接続なし。 + %s でもう一度お試しください。 + 上限に到達しました + 試行が多すぎます + あなたはこのアプリの古いバージョンを使用しています。 + アップデート + この電話番号は現在、他のデバイスでログインしています。 + アドレス帳に登録されていない人にあなたのことを知ってもらうために、名前を入力してください。 + あなたの名前 + あなたの名前を入力 + 編集ボタンを使って名前を設定します。 + 要求を拒否 + Orbot をインストール + Orbot を開始 + マーケットアプリがインストールされていません。 + この談話室では、あなたの XMPP アドレスを公開します + 電子書籍 + そのまま (非圧縮) + …で開く + Conversations プロフィール画像 + アカウントを選択 + バックアップを復元 + 復元 + バックアップを復元するアカウント %s のパスワードを入力してください。 + インストールの複製(同時実行)を作成する際に、バックアップの復元機能を使用しないでください。バックアップの復元は、移行時や元のデバイスを紛失した場合にのみ使用してください。 + バックアップを復元できません。 + バックアップを復号できません。パスワードは正しいですか? + バックアップ&復元 + XMPP アドレスを入力 + グループチャットを作成 + 公開談話室に参加 + 非公開グループチャットを作成 + 公開談話室を作成 + 談話室名 + XMPP アドレス + 談話室の名前をご記入ください + XMPP アドレスをご記入ください + これは XMPP アドレスです。名前をご記入ください。 + 公開談話室を作成中… + この談話室は既に存在します + 存在している談話室に参加しています + 談話室の環境設定を保存できません + 誰にでもトピックの編集を許可 + 誰にでも他の人の招待を許可 + 誰でもトピックを編集できます。 + 所有者はトピックを編集できます。 + 管理者はトピックを編集できます。 + 所有者は他の人を招待できます。 + 誰でも他の人を招待できます。 + XMPP アドレスは管理者が見れます。 + XMPP アドレスは誰でも見れます。 + この公開談話室には参加者がいません。連絡先を招待したり、共有ボタンを使用して XMPP アドレスを配布できます。 + この非公開グループチャットには参加者がいません。 + 権限を管理 + 参加者を検索 + ファイルが大きすぎます + 添付 + 談話室を発見 + 談話室を検索 + プライバシー侵害の可能性あり! + search.jabber.networkを利用します。

この機能を使うと、あなたののIPアドレスや検索キーワードがそのサービスに送信されます。詳しくは、プライバシーポリシーをご覧ください。]]>
+ 既にアカウントを持っています + 既存アカウントを追加 + 新規アカウントを登録 + これはドメインアドレスのようです + とにかく追加 + これは談話室アドレスのようです + バックアップファイルを共有 + Conversations のバックアップ + 出来事 + バックアップを開く + 選択したファイルは、 Conversations のバックアップファイルではありません + このアカウントは既に設定されています + このアカウントのパスワートを入力してください + この操作を実行できません + 公開談話室に参加… + 共有アプリがこのファイルへのアクセス権限を付与していませんでした。 + + jabber.network + ローカルサーバー + ほとんどのユーザーは、公開されている XMPP エコシステム全体からより良い提案を得るために、‘jabber.network’を選択するはずです。 + 談話室の発見方法 + バックアップ + アカウントを有効化してください + 通話をする + 着信通話 + 着信映像通話 + ビデオ通話に切り替えますか? + 接続中 + 接続しました + 再接続中 + 通話受入 + 通話終了 + 応答 + 拒否 + デバイス発見 + 鳴動 + 取込中 + 通話に接続できません + 接続切断 + 撤回された通話 + アプリの失敗 + 検証に問題 + 電話を切る + 継続中の通話 + 継続中の映像通話 + 通話再接続中 + ビデオ通話再接続中 + 通話するのに Tor を無効化 + 着信通話 + 着信通話・%s + 不在着信通話・%s + 発信通話 + 発信通話・%s + 不在着信通話 + + %2$sから%1$d件の不在着信 + + + 不在着信%d件 + + + %2$d人から%1$d件の不在着信 + + 音声通話 + 映像通話 + ヘルプ + 会話に切り替え + マイクが利用できません + 1度に1回線の通話のみ。 + 継続中の通話に戻る + カメラを切り替えできません + 最上に留める + 最上から留めるのをやめる + GPX 追跡 + メッセージを修正できません + すべての会話 + この会話 + あなたのアバター + %s のアバター + OMEMO で暗号化 + OpenPGP で暗号化 + 非暗号化 + 終了 + 音声メールを録音 + 音声再生 + 音声一時中断 + 連絡先を追加、作成またはグループチャットに参加、または談話室を発見する + + %1$d人の参加者を表示 + + + 一部のメッセージを配信できませんでした + + 配信に失敗 + 更なるオプション + アプリケーションが見つかりません + 会話に招待 + 招待を解析できません + サーバーは招待の作成をサポートしていません + この機能をサポートするアクティブなアカウントがありません + バックアップを開始しました。 バックアップが完了すると通知が届きます。 + 映像を有効化できません。 + プレーンテキスト文書 + アカウント登録はサポートされていません + XMPPアドレスがみつかりません + 一時的な認証失敗 + アバターを削除 + Tor使用中のため通話できません + ビデオ通話切替 +
\ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml new file mode 100644 index 000000000..f6589435a --- /dev/null +++ b/app/src/main/res/values-ko/strings.xml @@ -0,0 +1,402 @@ + + + 설정 + 새 대화 + 계정 + 연락처 정보 + 계정 추가 + 이름 편집 + 주소록에 추가 + 명단에서 삭제 + 연락처 + 연락처 차단 해제 + 도메인 차단 + 도메인 차단 해제 + 계정 관리 + 설정 + 대화 공유 + 대화 시작 + 목록 차단 + 방금 + 1분 전 + %d 분 전 + 보내는중... + 메세지 복호화중입니다. 기다리세요... + OpenPGP로 암호화된 메세지 + 사용중인 별명입니다 + 관리자 + 소유자 + 중재자 + 참가자 + 방문자 + %s 이(가) 당신에게 메세지를 보내지 못하도록 차단할까요? + %s 로부터 메세지를 받을 수 있도록 차단을 해제할까요? + %s 의 모든 연락처를 차단할까요? + %s 의 모든 연락처를 차단 해제할까요? + 연락처 차단됨 + 서버에서 새 계정을 등록 + 서버에서 비밀번호 변경 + 공유 + 연락처 + 연락처 + 취소 + 설정 + 추가 + 편집 + 삭제 + 차단 + 차단 해제 + 저장 + 확인 + 지금 보내기 + 더이상 묻지 않기 + 파일 첨부 + 연락처 추가 + 전송 실패 + 파일을 공유중입니다. 기다리세요... + 기록 삭제 + 대화 기록 삭제 + 기기 선택 + 암호화하지 않은 메세지 전송 + %s 에게 매세지 보내기 + OMEMO로 암호화된 메세지 + v\\OMEMO로 암호화된 메세지 전송 + OpenPGP 암호화된 메세지 전송 + 암호화하지 않고 전송 + 복호화 실패. 올바른 개인 키를 가지고 있지 않은 것 같습니다. + OpenKeychain + 재시작 + 설치 + OpenKeychain을 설치하세요 + 제공중... + 대기중... + OpenPGP 키가 발견되지 않음 + OpenPGP 키가 발견되지 않음 + 일반 + 파일 수락 + 이 크기보다 작은 파일을 자동으로 수락 + 첨부파일 + 알림 + 진동 + 새 메세지 도착시 진동 + LED 알림 + 새 메세지 도착시 LED 깜빡이기 + 알림음 + 유예기간 + 고급 + 충돌 보고서 보내지 않음 + 메세지 확인 + 당신이 메시지를 수신하고 읽을 때 친구에게 그 사실을 알려줍니다 + UI + 수락 + 오류가 발생했습니다 + 당신의 계정 + 프레즌스 업데이트 보내기 + 프레즌스 업데이트 받기 + 프레즌스 업데이트 요청 + 사진 선택 + 사진 찍기 + 구독 요청을 선제적으로 허가 + 선택한 파일은 이미지가 아닙니다 + 파일을 찾을 수 없음 + 일반 입출력 오류. 저장소 공간이 부족한 것 같습니다. + 알 수 없음 + 임시로 해제 + 접속중 + 접속중\u2026 + 오프라인 + 승인되지 않음 + 서버를 찾을 수 없음 + 접속할 수 없음 + 등록 실패 + 사용자 이름이 이미 사용중입니다 + 등록 성공 + 정책 위반 + 호환되지 않는 서버 + 스트림 오류 + 암호화되지 않음 + OTR + OpenPGP + OMEMO + 계정 삭제 + 임시로 해제 + 아바타 공개 + OpenPGP 공개 키 공개 + 계정 사용 + 확실합니까? + 녹음 + username@example.com + 암호 + 주소록에 %s를 추가하시겠습니까? + 서버 정보 + XEP-0313: MAM + XEP-0280: Message Carbons + XEP-0352: Client State Indication + XEP-0191: Blocking Command + XEP-0237: Roster Versioning + XEP-0198: Stream Management + XEP-0163: PEP (Avatars / OMEMO) + XEP-0363: HTTP 파일 업로드 + XEP-0357: Push + 가능 + 불가 + 공개 키 선언 누락 + 방금 전까지 접속했었음 + %d 분 전까지 접속했었음 + %d 시간 전까지 접속했었음 + %d 일 전까지 접속했었음 + OpenPGP 키ID + OMEMO 핑거프린트 + v\\OMEMO 핑거프린트 + 다른 기기들 + OMEMO 핑거프린트 신뢰 + 키 가져오는 중... + 완료 + 복호화 + 검색 + 연락처 입력 + 연락처 정보 보기 + 연락처 차단 + 연락처 차단 해제 + 만들기 + 선택 + 이미 존재하는 연락처입니다 + 참석 + 즐겨찾기로 저장 + 즐겨찾기 삭제 + 퇴장 + 연락처가 당신을 연락처 목록에 추가했습니다 + Add back + %s 가 여기까지 읽었습니다 + 공개 + 공개중... + 서버가 당신의 발표를 거부했습니다 + 아바타를 저장할 수 없습니다 + (혹은 기본값을 되돌리기 위해 길게 누름) + 속삭임 + %s 에게 + %s 에게 개인 매세지 보내기 + 접속 + 계정이 이미 존재합니다 + 다음 + 건너뛰기 + 알림 해제 + 사용 + 암호 입력 + 지금 요청 + 무시 + 보안 + 메세지 정정 허가 + 친구들이 메세지를 소급 수정할 수 있도록 허가 + 전문가 설정 + 설정시 주의하시기 바랍니다 + 무음 시간대 + 시작 시간 + 마감 시간 + 무음 시간대 사용 + 무음 시간대에는 알림이 해제됩니다 + 기타 + using account %s + HTTP 호스트에서 %s 확인 중 + 접속중이 아닙니다. 다시 시도하세요. + %s 크기 확인 + %2$s의 %1$s 크기 확인 + 메세지 설정 + 인용 + 원본 URL 복사 + 다시 보내기 + 파일 URL + 2D 바코드를 스캔하세요 + 2D 바코드를 보여주세요 + 차단 목록 보기 + 계정 정보 + 확인 + 다시 시도하세요 + 운영체제가 접속을 해제하지 못하도록 예방합니다 + 파일 선택 + 수신중 %1$s (%2$d%% 완료) + %s 다운로드 + %s 삭제 + 파일 + %s 열기 + 전송중 (%1$d%% 완료) + %s 다운로드 제공됨 + 전송 취소 + 연락처 밑에 읽기 전용 태그 표시 + 알림 사용 + 계정 아바타 + OMEMO 핑거프린트를 클립보드에 복사 + OMEMO 키 다시 생성 + 기기 제거 + 서버로부터 기록 가져오는중 + 서버에 더이상 기록이 없습니다 + 업데이트중... + 암호 변경됨 + 암호를 변경할 수 없습니다 + 암호 변경 + 현재 암호 + 새 암호 + 모든 계정 사용 + 모든 계정 해제 + 다음으로 동작을 수행 + 관련 없음 + 오프라인 + 추방됨 + 멤버 + 고급 모드 + 관리자 특권 허가 + 관리자 특권 철회 + %s 의 관련 여부를 변경할 수 없습니다 + 지금 금지 + %s 의 역할을 변경할 수 없습니다 + 멤버 전용 (사설) + 당신은 참여하고 있지 않습니다 + 안함 + 나중에 알릴때까지 + 입력 + 엔터 키로 전송 + 엔터 키 표시 + 이모티콘 키를 엔터 키로 바꿉니다 + 오디오 + 비디오 + 이미지 + PDF 문서 + 안드로이드 앱 + 연락처 + 아바타가 공개되었습니다 + %s 전송중 + %s 제공중 + 오프라인 숨기기 + %s 이(가) 입력중입니다... + %s 이(가) 입력을 중단했습니다 + %s 이(가) 입력중입니다... + %s 이(가) 입력을 중단했습니다 + 입력 알림 + 메세지를 쓸 때 친구들이 알게 합니다 + 위치 전송 + 위치 표시 + 위치 + 대화 끝남 + 시스템 CA를 신뢰하지 않음 + 모든 인증서는 수동으로 승인되어야 함 + 인증서 삭제 + 수동으로 승인된 인증서 삭제 + 수동으로 승인된 인증서 없음 + 인증서 삭제 + 선택 삭제 + 취소 + + %d 인증서 삭제됨 + + 빠른 동작 + 없음 + 최근 사용된 항목 + 빠른 동작 선택 + 개인 메세지 전송 + 사용자 이름 + 사용자 이름 + 이것은 올바른 사용자 이름이 아닙니다 + 다운로드 실패: 서버가 발견되지 않음 + 다운로드 실패: 파일이 발견되지 않음 + 다운도륻 실패: 호스트에 접속할 수 없음 + 다운로드 실패: 파일을 쓸 수 없습니다 + Tor 네트워크 사용할 수 없음 + 바인드 실패 + 손상됨 + 진동을 자동으로 처리 + 확장 연결 설정 + 계정을 설정할 때 호스트 이름과 포트 설정을 표시합니다 + xmpp.example.com + 보관 설정 + 서버 사이드의 보관 설정 + 보관 설정을 얻고 있습니다. 잠시 기다려주십시오 ... + 위의 이미지에서 텍스트를 입력하십시오 + 인증서 갱신 + OMEMO key를 가져오는 도중 오류가 발생했습니다 + OMEMO 키와 인증서 검증됨 + 기기가 선택된 클라이언트 인증서를 지원하지 않습니다 + 연결 + Tor를 통해 접속 + 모든 연결을 Tor 네트워크를 통하도록 유도함. Orbot이 필요합니다 + 호스트 이름 + 포트 + 올바른 포트 번호가 아닙니다 + 올바른 호스트 이름이 아닙니다 + %2$d 중 %1$d 계정이 연결되었습니다 + + %d 메세지 + + 메세지 더 불러오기 + 연락처와 동기화 + 모든 메세지를 알림 + 알림 해제됨 + 알림 일시중지됨 + 항상 + 배터리 최적화 사용됨 + 해제 + 선택된 영역이 너무 큽니다 + (활성화 된 계정이 없습니다) + 반드시 작성해야 합니다 + 메세지 정정 + 정정한 메세지 전송 + 이 계정을 비활성화했습니다 + URI를 공유할 대상... + 계정 생성 + 다른 서버 이용 + 유저네임을 고르세요 + 상태 메세지 + 대화 가능 + 접속중 + 자리 비움 + 사용할 수 없음 + 바쁨 + 안전한 비밀번호가 생성되었습니다 + 장치가 배터리 최적화 정지를 지원하지 않습니다 + 등록에 실패했습니다. 나중에 다시 시도하십시오 + 등록에 실패했습니다 : 암호가 너무 약합니다 + 참가자를 선택 + 다시 초대 + 해제 + 짧음 + 중간 + + 프라이버시 + 테마 + 색상 팔레트를 선택 + 초록색 배경 + 받은 메시지에 녹색 배경을 사용합니다 + 이 장치는 현재 사용되고 있지 않습니다 + 컴퓨터 + 휴대폰 + 태블릿 + 웹 브라우저 + 콘솔 + 지불 필요 + + 연락처가 참가 구독을 문의합니다 + 허가 + %s에 접근할 권한이 없음 + 원격 서버 찾을 수 없음 + OMEMO ID를 삭제 + 아바타를 게시하려면 연결된 상태여야 합니다. + 에러 메세지 보이기 + 에러 메세지 + 데이터 서버 활성화됨 + 해당 장치가 인증되었습니다 + 핑거프린트 복사 + 인증된 핑거프린트 + 바코드로 공유 + XMPP URI로 공유 + HTTP 링크로 공유 + 신뢰되지 않음 + 잘못된 2D 바코드 + 파일이 저장되어있는 개인 스토리지를 정리합니다 (서버에서 다시 다운로드 할 수 있습니다) + 신뢰할 수 있는 소스에서 해당 링크를 따라갑니다 + 링크를 클릭 한 후 %1$s의 OMEMO 키를 확인하려고 합니다. 이것은 %2$s이 링크를 공개한 신뢰할 수 있는 소스에서 따라가고 있을 때만 안전합니다. + OMEMO 키를 검증 + 설정된 기간보다 오래된 메시지를 장치에서 자동으로 삭제합니다. + 메세지가 클립보드에 복사되었습니다 + 중간 + 위치 표시 + 바쁨 + diff --git a/app/src/main/res/values-land/bools.xml b/app/src/main/res/values-land/bools.xml new file mode 100644 index 000000000..3bd58474a --- /dev/null +++ b/app/src/main/res/values-land/bools.xml @@ -0,0 +1,4 @@ + + + false + diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml new file mode 100644 index 000000000..66ed64d3d --- /dev/null +++ b/app/src/main/res/values-land/dimens.xml @@ -0,0 +1,5 @@ + + 8dp + 12dp + 272dp + diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml new file mode 100644 index 000000000..7adca9d89 --- /dev/null +++ b/app/src/main/res/values-ml/strings.xml @@ -0,0 +1,334 @@ + + + ക്രമീകരണങ്ങൾ + പുതിയ സംഭാഷണം + അക്കൗണ്ടുകൾ നിയന്ത്രിക്കൂ + അക്കൗണ്ട് നിയന്ത്രിക്കൂ + സംഭാഷണം അടയ്ക്കൂ + കോൺ‌ടാക്റ്റ് വിശദാംശങ്ങൾ + ഗ്രൂപ്പ് ചാറ്റ് വിശദാംശങ്ങൾ + ചാനൽ വിവരങ്ങൾ + അക്കൗണ്ട് ചേർക്കൂ + പെര് തിരുത്തുക + കോൺ‌ടാക്റ്റ് തടയുക + കോൺ‌ടാക്റ്റ് തടഞ്ഞത് മാറ്റുക + മേഖല തടയുക + പങ്കാളിയെ തടയുക + ക്രമീകരണങ്ങൾ + സംഭാഷണം ആരംഭിക്കൂ + കോൺ‌ടാക്റ്റ് തിരഞ്ഞെടുക്കുക + കോൺ‌ടാക്റ്റുകൾ തിരഞ്ഞെടുക്കുക + അക്കൗണ്ട് വഴി പങ്കിടുക + തടഞ്ഞവയുടെ പട്ടിക + ഇപ്പോൾ + 1 മിനിറ്റ് മുമ്പ് + %d മിനിറ്റ് മുമ്പ് + + %d വായിക്കാത്ത സംഭാഷണം + + + %d വായിക്കാത്ത സംഭാഷണങ്ങൾ + + + അയയ്ക്കുന്നു... + OpenPGP സുരക്ഷിതമാക്കിയ സന്ദേശം + വിളിപ്പേര് ഇതിനകം ഉപയോഗത്തിലാണ് + അസാധുവായ വിളിപ്പേര് + അഡ്മിൻ + ഉടമ + മോഡറേറ്റർ + പങ്കെടുക്കുന്നയാൾ + സന്ദർശകൻ + %s-ൽ നിന്ന് എല്ലാ കോൺ‌ടാക്റ്റുകളും തടയണോ? + കോൺ‌ടാക്റ്റ് തടഞ്ഞു + തടഞ്ഞു + സെർവറിൽ രഹസ്യവാക്ക് മാറ്റുക + ഇതുമായി പങ്കിടുക… + സംഭാഷണം ആരംഭിക്കുക + കോൺ‌ടാക്റ്റിനെ ക്ഷണിക്കുക + ക്ഷണിക്കൂ + കോൺ‌ടാക്റ്റുകൾ + കോൺ‌ടാക്റ്റ് + റദ്ദാക്കൂ + സജ്ജമാക്കൂ + ചേർക്കൂ + തിരുത്തുക + ഇല്ലാതാക്കൂ + തടയുക + തടഞ്ഞത് മാറ്റുക + സംരക്ഷിക്കൂ + ശരി + %1$s തകർന്നു + ഇപ്പോൾ അയയ്‌ക്കൂ + ഒരിക്കലും ചോദിക്കരുത് + ഫയൽ ഉൾപ്പെടുത്തുക + കോൺടാക്റ്റ് ചേർക്കൂ + ചിത്രങ്ങൾ അയയ്ക്കാൻ തയ്യാറാകുന്നു + ഫയലുകൾ പങ്കിടുന്നു. കാത്തിരിക്കൂ… + ചരിത്രം മായ്ക്കൂ + സംഭാഷണ ചരിത്രം മായ്ക്കൂ + ഫയൽ ഇല്ലാതാക്കൂ + ഉപകരണം തിരഞ്ഞെടുക്കൂ + സുരക്ഷിതമല്ലാത്ത സന്ദേശം അയയ്കൂ + സന്ദേശം അയയ്ക്കൂ + %s-ന് (ക്ക്) സന്ദേശം അയയ്ക്കൂ + OMEMO സുരക്ഷിതമാക്കിയ സന്ദേശം അയയ്ക്കൂ + v\\OMEMO സുരക്ഷിതമാക്കിയ സന്ദേശം അയയ്ക്കൂ + OpenPGP സുരക്ഷിതമാക്കിയ സന്ദേശം അയയ്ക്കൂ + പുതിയ വിളിപ്പേര് ഉപയോഗത്തിലുള്ളതാണ് + OpenKeychain + പുനരാരംഭിക്കൂ + സ്ഥാപിക്കൂ + OpenKeychain ഇൻസ്റ്റാൾ ചെയ്യുക + കാത്തിരിക്കുന്നു... + OpenPGP കീ ഒന്നും കണ്ടെത്തിയില്ല + പൊതുവായവ + ഫയലുകൾ സ്വീകരിക്കൂ + അറ്റാച്ചുമെന്റുകൾ + അറിയിപ്പ് + വൈബ്രേറ്റ് ചെയ്യൂ + LED അറിയിപ്പ് + റിംഗ്‌ടോൺ + അറിയിപ്പ് ശബ്‌ദം + ഇൻകമിംഗ് കോളുകൾക്കുള്ള റിംഗ്‌ടോൺ + വിപുലമായ + ഒരിക്കലും ക്രാഷ് റിപ്പോർട്ടുകൾ അയയ്‌ക്കരുത് + സന്ദേശങ്ങൾ ഉറപ്പാക്കൂ + UI + സ്വീകരിക്കുക + ഒരു പിശക് സംഭവിച്ചു + നിങ്ങളുടെ അക്കൗണ്ട് + സാന്നിധ്യ അപ്‌ഡേറ്റുകൾ അയയ്‌ക്കുക + ചിത്രം തിരഞ്ഞെടുക്കൂ + ഫോട്ടോ എടുക്കൂ + നിങ്ങൾ തിരഞ്ഞെടുത്ത ഫയൽ ഒരു ചിത്രം അല്ല + ഫയൽ കണ്ടില്ല + അജ്ഞാതം + ഓൺലൈൻ + ഓഫ്‌ലൈൻ + സെർവർ കണ്ടെത്തിയില്ല + കണക്റ്റിവിറ്റി ഇല്ല + രജിസ്ട്രേഷൻ പരാജയപ്പെട്ടു + ഉപയോക്തൃനാമം ഇതിനകം നിലവിലുണ്ട് + രജിസ്ട്രേഷൻ പൂർത്തിയായി + നയ ലംഘനം + സുരക്ഷിതമല്ലാത്ത + OTR + OpenPGP + OMEMO + അക്കൗണ്ട് ഇല്ലാതാക്കൂ + അവതാർ പ്രസിദ്ധീകരിക്കൂ + OpenPGP പബ്ലിക് കീ പ്രസിദ്ധീകരിക്കുക + OpenPGP പബ്ലിക് കീ നീക്കം ചെയ്യുക + നിങ്ങള്ക്ക് ഉറപ്പാണോ? + ശബ്‌ദം റെക്കോർഡുചെയ്യൂ + XMPP വിലാസം + XMPP വിലാസം തടയുക + username@example.com + രഹസ്യവാക്ക് + ഈ XMPP വിലാസ‍ം അസാധുവാണ് + മെമ്മറി തീർന്നു. ചിത്രം വളരെ വലുതാണ് + സെർവർ വിവരം + XEP-0313: MAM + ലഭ്യമാണ് + ലഭ്യമല്ല + അവസാനം കണ്ടത് ഇപ്പോൾ + അവസാനം കണ്ടത് ഒരു മിനിറ്റ് മുമ്പ് + അവസാനം കണ്ടത് %d മിനിറ്റ് മുമ്പ് + അവസാനം കണ്ടത് ഒരു മണിക്കൂർ മുമ്പ് + അവസാനം കണ്ടത് %d മണിക്കൂർ മുമ്പ് + അവസാനം കണ്ടത് ഒരു ദിവസം മുമ്പ് + അവസാനം കണ്ടത് %d ദിവസം മുമ്പ് + OMEMO വിരലടയാളം + v\\OMEMO വിരലടയാളം + മറ്റ് ഉപകരണങ്ങൾ + കീകൾ ലഭ്യമാക്കുന്നു... + ചെയ്തു + ഡീക്രിപ്റ്റ് ചെയ്യുക + അടയാളകുറിപ്പുകൾ + തിരയുക + കോൺ‌ടാക്റ്റ് നൽകുക + കോൺ‌ടാക്റ്റ് ഇല്ലാതാക്കൂ + കോൺ‌ടാക്റ്റ് വിവരങ്ങൾ കാണിക്കൂ + കോൺ‌ടാക്റ്റ് തടയുക + കോൺ‌ടാക്റ്റ് തടഞ്ഞത് മാറ്റുക + സൃഷ്ടിക്കൂ + തിരഞ്ഞെടുക്കൂ + കോൺ‌ടാക്റ്റ് ഇതിനകം നിലവിലുണ്ട് + ചേരുക + അടയാളക്കുറിപ്പായി സംരക്ഷിക്കൂ + ഗ്രൂപ്പ് ചാറ്റ് നശിപ്പിക്കൂ + ചാനൽ നശിപ്പിക്കൂ + വിഷയം + ഗ്രൂപ്പ് ചാറ്റിൽ ചേരുന്നു... + തിരികെ ചേർക്കൂ + %s ഇത് വരെ വായിച്ചിട്ടുണ്ട് + %s ഇത് വരെ വായിച്ചിട്ടുണ്ട് + എല്ലാവരും ഇത് വരെ വായിച്ചിട്ടുണ്ട് + പ്രസിദ്ധീകരിക്കൂ + പ്രസിദ്ധീകരിക്കുന്നു... + ഈ അക്കൗണ്ട് ഇതിനകം നിലവിലുണ്ട് + അടുത്തത് + സെഷൻ സ്ഥാപിച്ചു + ഒഴിവാക്കൂ + പ്രാപ്തമാക്കൂ + ഗ്രൂപ്പ് ചാറ്റിന് രഹസ്യവാക്ക് ആവശ്യമാണ് + രഹസ്യവാക്ക് നൽകുക + ഇപ്പോൾ അഭ്യർത്ഥിക്കുക + ഒഴിവാക്കൂ + സുരക്ഷ + വിദഗ്ദ്ധ ക്രമീകരണങ്ങൾ + %s-നെ കുറിച്ച് + ആരംഭ സമയം + മറ്റുള്ളവ + ഈ ഗ്രൂപ്പ് ചാറ്റിൽ നിന്ന് നിങ്ങളെ നിരോധിച്ചിരിക്കുന്നു + നിങ്ങളെ ഗ്രൂപ്പ് ചാറ്റിൽ നിന്ന് പുറത്താക്കി + %s അക്കൗണ്ട് ഉപയോഗിക്കുന്നു + %s-ന്റെ വലുപ്പം പരിശോധിക്കൂ + സന്ദേശ ഓപ്ഷനുകൾ + യഥാർത്ഥ URL പകർത്തുക + വീണ്ടും അയയ്ക്കൂ + ഫയൽ URL + ക്ലിപ്പ്ബോർഡിലേക്ക് URL പകർത്തി + ക്ലിപ്പ്ബോർഡിലേക്ക് XMPP വിലസം പകർത്തി + വെബ് വിലാസം + ഉറപ്പാക്കൂ + വീണ്ടും ശ്രമിക്കുക + ബാക്കപ്പ് സൃഷ്ടിക്കൂ + ബാക്കപ്പ് ഫയലുകൾ സൃഷ്ടിക്കുന്നു + ഫയൽ തിരഞ്ഞെടുക്കൂ + %s ഇല്ലാതാക്കൂ + ഫയൽ + %s തുറക്കൂ + ഫയൽ പങ്കിടാൻ തയ്യാറാകുന്നു + ഫയൽ ഇല്ലാതാക്കി + അറിയിപ്പുകൾ പ്രാപ്തമാക്കൂ + അക്കൗണ്ട് അവതാർ + ഉപകരണങ്ങൾ മായ്ക്കൂ + എന്തോ കുഴപ്പം സംഭവിച്ചു + സെർവറിൽ നിന്ന് ചരിത്രം ലഭ്യമാക്കുന്നു + പുതുക്കുന്നു... + രഹസ്യവാക്ക് മാറ്റി + രഹസ്യവാക്ക് മാറ്റുക + നിലവിലെ രഹസ്യവാക്ക് + പുതിയ രഹസ്യവാക്ക് + ഓഫ്‌ലൈൻ + അംഗം + വിപുലമായ മോഡ് + ഗ്രൂപ്പ് ചാറ്റിൽ നിന്ന് മാറ്റുക + ചാനലിൽ നിന്ന് നീക്കം ചെയ്യുക + ഗ്രൂപ്പ് ചാറ്റിൽ നിന്ന് നിരോധിക്കൂ + ചാനലിൽ നിന്ന് നിരോധിക്കൂ + ഇപ്പോൾ നിരോധിക്കൂ + സ്വകാര്യ, അംഗങ്ങൾ മാത്രം + നിങ്ങൾ പങ്കെടുക്കുന്നില്ല + മറുപടി + വായിച്ചതായി കാണിക്കൂ + എന്റെർ കീ അയയ്ക്കും + ശബ്ദം + വീഡിയോ + ചിത്രം + %s അയയ്ക്കുന്നു + %s ടൈപ്പുചെയ്യുന്നു… + %s ടൈപ്പുചെയ്യുന്നു… + ലൊക്കേഷൻ അയയ്‌ക്കുക + ലൊക്കേഷൻ കാണിക്കൂ + ലൊക്കേഷൻ + റദ്ദാക്കൂ + സമീപകാലത്ത് ഉപയോഗിച്ചത് + കോൺ‌ടാക്റ്റുകൾ തിരയുക + സ്വകാര്യ സന്ദേശം അയയ്‌ക്കൂ + ഉപയോക്തൃനാമം + ഉപയോക്തൃനാമം + ഡൗൺലോഡ് പരാജയപ്പെട്ടു: സെർവർ കണ്ടെത്തിയില്ല + ഡൗൺലോഡ് പരാജയപ്പെട്ടു: ഫയൽ കണ്ടെത്തിയില്ല + തകർന്നു + ലഭ്യത + xmpp.example.com + CAPTCHA നിർബന്ധമാണ് + + %d സന്ദേശം + %d സന്ദേശങ്ങൾ + + കൂടുതൽ സന്ദേശങ്ങൾ ലഭ്യമാക്കൂ + എല്ലാ സന്ദേശങ്ങളും അറിയിക്കൂ + എപ്പോഴും + വലിയ ചിത്രങ്ങൾ മാത്രം + സന്ദേശം തിരുത്തുക + സമ്മതിച്ച് തുടരുക + അക്കൗണ്ട് സൃഷ്ടിക്കൂ + എന്റെ സ്വന്തം ദാതാവിനെ ഉപയോഗിക്കുക + നിങ്ങളുടെ ഉപയോക്തൃനാമം തിരഞ്ഞെടുക്കൂ + ഓൺലൈൻ + ലഭ്യമല്ല + തിരക്കിലാണ് + പങ്കെടുക്കുന്നവരെ തിരഞ്ഞെടുക്കുക + ഗ്രൂപ്പ് ചാറ്റ് സൃഷ്ടിക്കുന്നു… + വീണ്ടും ക്ഷണിക്കൂ + സ്വകാര്യത + രൂപഭംഗി + കമ്പ്യൂട്ടർ + മൊബൈൽ ഫോൺ + ഞാൻ + അനുവദിക്കൂ + + %d മാസം + %d മാസം + + മുഴുവൻ മേഖലയും തടയുക + ഇപ്പോൾ സജീവം + വെബ്സൈറ്റ് തുറക്കൂ + ഇന്ന് + ഇന്നലെ + ക്ലിപ്പ്ബോർഡിലേയ്ക്ക് പകർത്തുക + സന്ദേശം + ഒരിക്കൽ + പങ്കിടുക + സന്ദേശങ്ങൾ തിരയുക + GIF + വിളിപ്പേര് + പേര് + ഗ്രൂപ്പ് ചാറ്റിന്റെ പേര് + സന്ദേശങ്ങൾ + കോളുകൾ + സന്ദേശങ്ങൾ + നിശബ്‌ദ സന്ദേശങ്ങൾ + പങ്കെടുക്കുന്നവർ + റദ്ദാക്കി + രാജ്യ കോഡ് തെറ്റാണ് + ഒരു രാജ്യം തിരഞ്ഞെടുക്കൂ + ഫോൺ നമ്പർ + നിങ്ങളുടെ ഫോൺ നമ്പർ ഉറപ്പാക്കൂ + നിങ്ങളുടെ ഫോൺ നമ്പർ നൽകുക. + %s ഉറപ്പാക്കൂ + SMS വീണ്ടും അയയ്ക്കൂ + SMS വീണ്ടും അയയ്ക്കൂ (%s) + അതെ + ഉറപ്പാക്കുന്നു... + SMS അഭ്യർത്ഥിക്കുന്നു… + നിങ്ങളുടെ പേര് + നിങ്ങളുടെ പേര് നൽകുക + അക്കൗണ്ട് തിരഞ്ഞെടുക്കൂ + XMPP വിലാസം നൽകുക + ഗ്രൂപ്പ് ചാറ്റ് സൃഷ്ടിക്കുക + പൊതു ചാനലിൽ ചേരുക + XMPP വിലാസം + ഫയൽ വളരെ വലുതാണ് + ചാനലുകൾ തിരയുക + നിലവിലുള്ള അക്കൗണ്ട് ചേർക്കുക + ഈ പ്രവർത്തനം നടത്താൻ കഴിഞ്ഞില്ല + പൊതു ചാനലിൽ ചേരുക… + jabber.network + കോൾ സ്വീകരിക്കുന്നു + കോൾ അവസാനിപ്പിക്കുന്നു + ഇൻകമിംഗ് കോൾ + സഹായം + നിങ്ങളുടെ മൈക്രോഫോൺ ലഭ്യമല്ല + GPX ട്രാക്ക് + എല്ലാ സംഭാഷണങ്ങളും + ഈ സംഭാഷണം + നിങ്ങളുടെ അവതാർ + കൂടുതൽ ഓപ്ഷനുകൾ + Conversations-ലേക്ക് ക്ഷണിക്കുക + ബാക്കപ്പ് ആരംഭിച്ചു. അത് പൂർത്തിയായിക്കഴിഞ്ഞാൽ നിങ്ങൾക്ക് ഒരു അറിയിപ്പ് ലഭിക്കും. + diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml new file mode 100644 index 000000000..fdbb541d8 --- /dev/null +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -0,0 +1,471 @@ + + + Innstillinger + Ny samtale + Kontobehandling + Kontaktdetaljer + Gruppesludringsdetaljer + Legg til samtale + Rediger navn + Legg til i kontaktliste + Fjern fra kontaktliste + Blokker kontakt + Avblokker kontakt + Blokker domene + Avblokker domene + Kontobehandling + Innstillinger + Del med Conversation + Start samtale + Velg kontakt + Del via konto + Blokkeringsliste + akkurat nå + 1 minutt siden + %d minutter siden + sender... + Dekrypterer melding mens du venter. + OpenPGP-kryptert melding + Kallenavn allerede i bruk + Ugyldig kallenavn + Admin + Eier + Moderator + Deltager + Besøkende + Vil du forhindre %s fra å sende deg meldinger? + Ønsker du å avblokkere %s og tillate dem å sende deg meldinger? + Blokker alle kontakter fra %s? + Avblokker alle kontakter fra %s? + Kontakt blokkert + Registrer ny konto på tjeneren + Endre passord på tjeneren + Del med... + Start samtale + Inviter kontakt + Kontakter + Kontakt + Avbryt + Sett + Legg til + Rediger + Slett + Blokker + Avblokker + Lagre + OK + Send nå + Aldri spør igjen + Legg til fil + Legg til kontakt + forsendelse feilet + Deler filer. Vent… + Tøm historikk + Tøm samtalehistorikk + Velg enhet + Send ukryptert melding + Send melding + Send melding til %s + Send OMEMO-kryptert melding + Send \\OMEMO-kryptert melding + Send OpenPGP-kryptert melding + Send ukryptert + Dekryptering feilet. Kanskje du ikke lenger har den rette private nøkkelen. + OpenKeychain + Omstart + Installer + Installer OpenKeychain + tilbyr... + venter... + Ingen OpenPGP-nøkkel funnet + Ingen OpenPGP-nøkler funnet + Generelt + Godta filer + Automatisk godkjenning av filer mindre enn... + Vedlegg + Merknad + Vibrer + Vibrer når en ny melding ankommer + LED-merknad + Blink merknadslyset når en ny melding ankommer + Ringetone + Fristperiode + Avansert + Aldri send kræsjrapporter + Bekreft meldinger + Lar dine kontakter vite når du har mottatt og lest deres meldinger + Grensesnitt + Godta + En feil har inntruffet + Din konto + Send oppdateringer for tilstedeværelse + Motta oppdateringer for tilstedeværelse + Etterspør oppdateringer for tilstedeværelse + Velg bilde + Ta bilde + Tillat abonnementsforespørsel på forhånd + Filen du valgte er ikke et bilde + Finner ikke filen + Generell I/O-feil. Har du sluppet opp for lagringsplass? + Ukjent + Midlertidig avskrudd + Pålogget + Kobler til\u2026 + Avlogget + Ikke tillatt + Fant ikke tjener + Ingen tilkobling + Registrering feilet + Brukernavn allerede i bruk + Registrering fullført + TLS-forhandling mislyktes + Praksisbrudd + Ukompatibel tjener + Strømmingsfeil + Ukryptert + OTR + OpenPGP + OMEMO + Slett konto + Skru av midlertidig + Publiser avatar + Publiser OpenPGP offentlig nøkkel + Fjern offentlig OpenPGP-nøkkel + Er du sikker på at du vil fjerne din offentlige OpenPGP-nøkkel fra din tilstedeværelseskunngjøring?\nDine kontakter vil ikke lenger kunne sende deg OpenPGP-krypterte meldinger. + Skru på konto + Bekreft. + Ta opp stemme + brukernavn@eksempel.no + Passord + Ønsker du å legge %s til i din kontaktliste? + Tjenerinfo + XEP-0313: MAM + XEP-0280: Meldingskarboner + XEP-0352: Identifisering av klientstatus + XEP-0191: Blokkeringskommando + XEP-0237: Kontaktliste-versjonering + XEP-0198: Behandling av dataflyt + XEP-0163: PEP (Avatarer / OMEMO) + XEP-0363: HTTP-filopplasting + XEP-0357: Push + tilgjengelig + utilgjengelig + Manglende annonsering av offentlig nøkkel + i syne + sist sett for %d minutter siden + sist sett %d timer siden + sist sett for %d dager siden + OpenPGP nøkkel-ID + OMEMO-fingeravtrykk + v\\OMEMO-fingeravtrykk + Andre enheter + Stol på OMEMO-fingeravtrykk + Hener inn nøkler… + Ferdig + Dekrypter + Søk + Angi kontakt + Slett kontakt + Vis kontaktdetaljer + Blokker kontakt + Avblokker kontakt + Lag + Velg + Kontakten finnes allerede + Ta del i + Lagre som bokmerke + Slett bokmerke + Endre gruppesludringsemne + Tar del i gruppesludring… + Forlat + Kontakt la deg til i sin liste + Gjengjeld tjenesten + %s har lest hit + %s har lest hit + Publiser + Publiserer… + Tjeneren avslo din publisering + Kunne ikke lagre avatarbilde til lagringsområde + (Eller trykk lenge for å gå tilbake til forvalg) + hvisket + til %s + Send privat melding til %s + Koble til + Denne kontoen finnes allerede + Neste + Hopp over + Deaktiver varslinger + Skru på + Gruppesludringen krever passord + Skriv inn passord + Send forespørsel nå + Ignorer + Sikkerhet + Tillat meldingskorrigering + La dine kontakter korrigere sine meldinger i ettertid + Ekspertinnstillinger + Vær forsiktig med disse + Stille tidsavgrensning + Oppstart + Avslutning + Aktiver stille tidsavgrensning + Varslinger blir ikke spilt under stilletid + Annet + Du er bannlyst fra denne gruppesludringen + Denne gruppesludringen er kun for medlemmer + Du har blitt kastet ut av denne gruppesludringen + Denne gruppesludringen ble avsluttet + Du er ikke lenger i denne gruppesludringen + bruker konto %s + Sjekker %s på HTTP-tjener + Du er ikke tilkoblet. Prøv igjen senere + Sjekk %s størrelse + Sjekk %1$s størrelse på %2$s + Meldingsvalg + Siter + Kopier orginal nettadresse + Send igjen + Filens nettadresse + Skann 2D-strekkode + Vis 2D-strekkode + Vis blokkeringsliste + Kontodetaljer + Bekreft + Prøv igjen + Forhindrer operativsystemet fra å drepe tilkoblingen din + Velg fil + Mottak av %1$s (%2$d%% fullført) + Last ned %s + Slett %s + fil + Åpne %s + forsendelse av (%1$d%% fullført) + %s tilbudt for nedlasting + Avbryt overføring + Vis \"bare-les\"-merkelapper under kontakter + Aktiver varslinger + Fant ingen gruppesludringstjener + Konto-avatar + Kopier OMEMO-fingeravtrykk til utklippstavle + Regenerer OMEMO-nøkkel + Rens enheter + Henter inn historikk fra tjener + Ikke mer historikk på tjeneren + Oppdaterer… + Passord endret! + Kunne ikke endre passord + Endre passord + Gjeldende passord + Nytt passord + Skru på alle kontoer + Koble fra alle kontoer + Utfør handling med + Ingen tilknytning + Frakoblet + Fredløs + Medlem + Avansert modus + Innlem som administrator + Tilbakekall administratorrettigheter + Fjern fra gruppesludring + Kunne ikke endre tilknytningen til %s + Bannlys fra gruppesludring + Bannlys nå + Kunne ikke endre rollen til %s + Privat, kun for medlemmer + Du deltar ikke + Endret gruppesludringsvalg! + Kunne ikke endre gruppesludringsvalg + Aldri + Til videre beskjed + Slumre + Svar + Merk som lest + Inndata + Enter-forsendelsesknapp + Vis enter-tast + Endre smilefjas-tast til en enter-tast + lyd + film + stillbilde + PDF-dokument + Android-app + Kontakt + Avatar publisert! + Sender %s + Tilbyr %s + Ikke vis frakoblede + %s skriver… + %s har sluttet å skrive + %s skriver… + %s har sluttet å skrive + Varsler for skriving + Lar dine kontakter få nyss om at du skriver til dem + Send plasseringsdata + Vis plasseringsdata + Plasseringsdata + Samtale lukket + Ikke stol på systemets CA-er + Alle sertifikat må godkjennes manuelt + Fjern sertifikater + Slett sertifikater som er godkjent manuelt + Ingen manuelt godkjente sertifikater + Fjern sertifikater + Slett innhold i merket område + Avbryt + + %d sertifikat slettet + %d sertifikater slettet + + Hurtighandling + Ingen + Senest brukt + Velg hurtighendelse + Send privat melding + Brukernavn + Brukernavn + Dette er ikke et gyldig brukernavn + Nedlasting feilet: Fant ikke tjener + Nedlasting feilet: Fant ikke fila + Nedlasting feilet: Kunne ikke koble til tjeneren + Nedlasting mislyktes: Kunne ikke skrive fil + Tor-nettverk utilgjengelig + Klarte ikke å binde + Knekt + Behandle vibrering som stille-modus + Utvidede tilkoblingsinnst. + Vis vertsnavn og portinnstillinger når du setter opp en ny konto + xmpp.eksempel.no + Arkiveringsinnstillinger + Arkiveringsinnstillinger på tjenersiden + Henter inn arkiveringsinnstillinger. Vent… + Skriv inn teksten fra bildet ovenfor + Forny sertifikat + Feil ved innhenting av OMEMO-nøkkel! + Bekreftet OMEMO-nøkkel med sertifikat! + Din enhet støtter ikke valg av klientsertifikat! + Tilkobling + Koble til via Tor + Send alle tilkoblinger i tunnel gjennom Tor-nettverket. Krever Orbot + Tjenernavn + Port + Dette er ikke et gyldig portnummer + Dette er ikke et gyldig tjenernavn + %1$d av %2$d kontoer tilkoblet + + %d melding + %dmeldinger + + Last inn flere meldinger + Synkroniser med kontakter + Varsle ved alle meldinger + Varsle bare når fremhevet + Varslinger deaktivert + Varslinger pauset + Alltid + Batterioptimaliseringer aktivert + Deaktiver + Det valgte området er for stort + (Ingen aktiverte kontoer) + Dette feltet er påkrevd + Korriger melding + Send korrigert melding + Du har skrudd av denne kontoen + Del URI med… + Opprett konto + Bruk min egen tilbyder + Velg ditt brukernavn + Statusmelding + Ledig for sludring + Pålogget + Borte + Ikke tilgjengelig + Opptatt + Et sikkert passord har blitt opprettet + Din enhet støtter ikke å melde seg ut av batterioptimeringsprogrammet + Registrering mislyktes: Prøv igjen senere + Registrering mislyktes: Passordet er for svakt + Velg deltagere + Opprett gruppesludring… + Inviter igjen + Deaktiver + Kort + Middels + Lang + Personvern + Drakt + Velg fargepalett + Grønn bakgrunn + Bruk grønn bakgrunn for mottatte meldinger + Denne enheten er ikke lenger i bruk + Datamaskin + Mobiltelefon + Nettbrett + Nettleser + Konsoll + Betaling kreves + Meg + Kontakt ber om tilstedeværelsesabonnement + Tillat + Ingen tilgang til %s + Fjerntjener ble ikke funnet + Slett OMEMO-identiteter + Slett valgte nøkler + Du må være tilkoblet for å publisere din avatar. + Vis feilmelding + Feilmelding + Datasparing påskrudd + Denne enheten har blitt bekreftet + Kopier fingeravtrykk + Bekreftede fingeravtrykk + Bruk kameraet for å skanne en kontakts strekkode + Vent på innhenting av nøkler + Del som strekkode + Del som XMPP-URI + Del som HTTP-lenke + Blind tillit før bekreftelse + Ikke betrodd + Ugyldig 2D-strekkode + Tøm hurtiglager + Tøm privat lagring + Tøm privat lagring der filene beholdes (De kan lastes ned igjen fra tjeneren) + Jeg fulgte denne lenken fra en tiltrodd kilde + Du er i ferd med å bekrefte at OMEMO-nøklene til %1$s etter å ha trykket på en lenke. Dette er bare sikkert hvis du fulgte denne lenken fra en tiltrodd kilde der bare %2$s kunne ha offentliggjort denne lenken. + Bekreft OMEMO-nøkler + Fjern tiltro til enhet + Automatisk meldingssletting + Slett meldinger fra denne enheten automatisk hvis de er eldre enn oppsatt tidsramme. + Krypterer melding + Henter ikke inn meldinger på grunn av lokal oppbevaringsperiode. + Komprimerer video + Samsvarende samtaler lukket. + Kontakt blokkert. + Varslinger fra fremmede + Mottok melding fra fremmed + Blokker fremmed + Blokker hele domenet + pålogget akkurat nå + Prøv å dekryptere igjen + Økt-feil + Nedgradert SASL-mekanisme + Tjeneren krever registrering på nettsiden + Åpne nettside + Oppsprettsmerknader + I dag + I går + Gyldig vertsnavn med DNSSEC + Tjenersertifikat som inneholder gyldige vertsnavn anses som bekreftet + delvis + Ta opp video + Kopier til utklippstavle + Melding kopiert til utklippstavle + Melding + Private meldinger er skrudd av + Beskyttede programmer + For å motta merknader, selv når skjermen er skrudd av, må du legge til Conversations i listen over beskyttede programmer. + Middels + Vis plasseringsdata + Gruppesludringsnavn + Opprett gruppesludring + Opptatt + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 000000000..2bbe27628 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,31 @@ + + + + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml new file mode 100644 index 000000000..f67b0c76c --- /dev/null +++ b/app/src/main/res/values-nl/strings.xml @@ -0,0 +1,827 @@ + + + Instellingen + Nieuw gesprek + Accounts beheren + Account beheren + Gesprek sluiten + Contactgegevens + Gespreksgegevens + Kanaalinformatie + Account toevoegen + Naam veranderen + Toevoegen aan adresboek + Verwijderen uit lijst + Contact blokkeren + Contact deblokkeren + Domein blokkeren + Domein deblokkeren + Deelnemer blokkeren + Deelnemer deblokkeren + Accounts beheren + Instellingen + Delen in gesprek + Gesprek starten + Contact kiezen + Contacten kiezen + Delen via account + Geblokkeerde contacten + zojuist + 1 min. geleden + %d min. geleden + + %d ongelezen gesprek + + + %d ongelezen gesprekken + + + versturen… + Bericht aan het ontsleutelen. Even geduld… + OpenPGP-versleuteld bericht + Naam is al in gebruik + Ongeldige bijnaam + Beheerder + Eigenaar + Moderator + Deelnemer + Bezoeker + Wil je %s uit je contactenlijst verwijderen? De gesprekken met deze contactpersoon zullen niet worden verwijderd. + Wil je alle berichten van %s blokkeren? + Wil je %s deblokkeren en er weer berichten van kunnen ontvangen? + Alle contacten van %s blokkeren? + Alle contacten van %s deblokkeren? + Contact geblokkeerd + Geblokkeerd + Wil je %s als bladwijzer verwijderen? De gesprekken met deze bladwijzer zullen niet worden verwijderd. + Nieuwe account op server registreren + Wachtwoord op server veranderen + Delen met… + Gesprek beginnen + Contact uitnodigen + Uitnodigen + Contacten + Contact + Annuleren + Instellen + Toevoegen + Bewerken + Verwijderen + Blokkeren + Deblokkeren + Opslaan + Oké + %1$s is gecrasht + Door crashrapportages via uw XMPP account te sturen help je de ontwikkeling van %1$s. + Nu versturen + Niet opnieuw vragen + Verbinding maken met account mislukt + Verbinden met meerdere accounts mislukt + Tik hier op om accounts te beheren + Bestand bijvoegen + Wil je dit ontbrekende contact toevoegen aan je contactenlijst? + Contact toevoegen + afleveren mislukt + Voorbereiden om afbeelding te sturen + Voorbereiden om afbeeldingen te sturen + Bestanden delen. Even geduld… + Geschiedenis wissen + Gespreksgeschiedenis wissen + Bestand verwijderen + Weet je zeker dat je dit bestand wil verwijderen?\n\nWaarschuwing: Dit zal kopieën van dit bericht die opgeslagen zijn op andere apparaten of servers niet verwijderen. + Dit gesprek nadien sluiten + Apparaat kiezen + Verstuur onversleuteld bericht + Verstuur bericht + Bericht sturen naar %s + Verstuur OMEMO-versleuteld bericht + Verstuur v\\OMEMO-versleuteld bericht + Verstuur OpenPGP-versleuteld bericht + Nieuwe bijnaam in gebruik + Verstuur onversleuteld + Ontsleutelen mislukt. Misschien heb je niet de juiste private sleutel. + OpenKeychain + Herstarten + Installeren + Gelieve OpenKeychain te installeren + bezig met aanbieden… + wachten… + Geen OpenPGP-sleutel gevonden + Geen OpenPGP-sleutels gevonden + Algemeen + Aanvaard bestanden + Aanvaard automatisch bestanden kleiner dan… + Bijlagen + Melding + Trillen + Trillen wanneer een nieuw bericht ontvangen wordt + LED-melding + Meldingslicht knipperen wanneer een nieuw bericht ontvangen wordt + Meldingstoon + Uitstelperiode + Geavanceerd + Verstuur nooit crashrapportages + Bevestig berichten + Laat je contacten weten wanneer je hun berichten ontvangen en gelezen hebt + Voorkom schermafdrukken + Gebruikersomgeving + OpenKeychain veroorzaakte een fout. + Slechte sleutel voor versleuteling. + Aanvaarden + Er is een fout opgetreden + Fout + Je account + Verstuur aanwezigheidsupdates + Ontvang aanwezigheidsupdates + Vraag naar aanwezigheidsupdates + Afbeelding kiezen + Foto nemen + Op voorhand toestemming verlenen voor abonneren + Het bestand dat je gekozen hebt is geen afbeelding + Kon de afbeelding niet converteren + Bestand niet gevonden + Algemene I/O-fout. Misschien is er geen opslagruimte meer beschikbaar? + Onbekend + Tijdelijk uitgeschakeld + Online + Verbinden\u2026 + Offline + Niet gemachtigd + Server niet gevonden + Geen verbinding + Registratie mislukt + Gebruikersnaam is al in gebruik + Registratie voltooid + Ongeldig registratietoken + TLS-onderhandeling mislukt + Domein niet verifieerbaar + Beleidsschending + Incompatibele server + Fout bij stream + Fout bij openen van stream + Onversleuteld + OTR + OpenPGP + OMEMO + Account verwijderen + Tijdelijk uitschakelen + Avatar publiceren + OpenPGP-publieke sleutel publiceren + OpenPGP-publieke sleutel verwijderen + Weet je zeker dat je je OpenPGP-publieke sleutel uit je aanwezigheidsaankondiging wil verwijderen?\nJe contacten zullen je geen OpenPGP-versleutelde berichten meer kunnen sturen. + OpenPGP-publieke sleutel gepubliceerd. + Account inschakelen + Weet je het zeker? + Stem opnemen + XMPP-adres + XMPP-adres blokkeren + gebruikersnaam@voorbeeld.nl + Wachtwoord + Dit is geen geldig XMPP-adres + Geen geheugen beschikbaar. Afbeelding is te groot + Wil je %s toevoegen aan je adresboek? + Server-info + XEP-0313: MAM + XEP-0280: Message Carbons + XEP-0352: Client State Indication + XEP-0191: Blocking Command + XEP-0237: Roster Versioning + XEP-0198: Stream Management + XEP-0163: PEP (Avatars / OMEMO) + XEP-0363: HTTP File Upload + XEP-0357: Push + beschikbaar + niet beschikbaar + Ontbrekende publieke sleutel-aankondigingen + zojuist voor het laatst gezien + een minuut geleden voor het laatst gezien + %d minuten geleden voor het laatst gezien + een uur geleden voor het laatst gezien + %d uur geleden voor het laatst gezien + een dag geleden voor het laatst gezien + %d dagen geleden voor het laatst gezien + Nieuw OpenPGP-versleuteld bericht gevonden + OpenPGP-sleutel-ID + OMEMO-vingerafdruk + v\\OMEMO-vingerafdruk + OMEMO-vingerafdruk (herkomst bericht) + v\\OMEMO-vingerafdruk (herkomst bericht) + Andere apparaten + Vertrouw OMEMO-vingerafdrukken + Sleutels ophalen… + Klaar + Ontsleutelen + Bladwijzers + Zoeken + Contact invoeren + Contact verwijderen + Contactgegevens bekijken + Contact blokkeren + Contact deblokkeren + Aanmaken + Selecteren + Het contact bestaat al + Deelnemen + kanaal@groepsgesprek.voorbeeld.nl/naam + kanaal@groepsgesprek.voorbeeld.nl + Opslaan als bladwijzer + Bladwijzer verwijderen + Groepsgesprek vernietigen + Kanaal vernietigen + Weet je zeker dat je dit groepsgesprek wilt vernietigen?\n\nWaarschuwing: Dit gesprek wordt volledig verwijderd van de server. + Weet je zeker dat je dit openbare kanaal wilt vernietigen?\n\nWaarschuwing: Dit kanaal wordt volledig verwijderd van de server. + Kan groepsgesprek niet vernietigen + Kan kanaal niet vernietigen + Gespreksonderwerp bewerken + Onderwerp + Deelnemen aan groepsgesprek… + Verlaten + Contact heeft je toegevoegd aan zijn/haar contacten + Ook toevoegen + %s heeft tot hier gelezen + %s hebben tot hier gelezen + %1$s en nog %2$d meer hebben tot hier gelezen + Iedereen heeft tot hier gelezen + Publiceer + Tik op avatar om een foto uit de galerij te kiezen + Publiceren… + De server weigerde de publicatie van je afbeelding + Kon de afbeelding niet converteren + Fout bij opslaan van avatar + (Of hou lang ingedrukt om de oorspronkelijke terug te zetten) + Je server ondersteunt de publicatie van avatars niet + gefluisterd + naar %s + Privébericht sturen naar %s + Verbind + Deze account bestaat al + Volgende + Sessie is tot stand gekomen + Overslaan + Meldingen uitschakelen + Inschakelen + Wachtwoord vereist voor toegang tot groepsgesprek + Wachtwoord invoeren + Nu aanvragen + Negeren + Beveiliging + Berichtcorrectie toestaan + Sta je contacten toe hun berichten na het versturen te verbeteren + Instellingen voor experts + Wees voorzichtig met deze instellingen + Over %s + Stille uren + Begintijd + Eindtijd + Stille uren inschakelen + Tijdens stille uren worden meldingen onderdrukt + Andere + OMEMO-vingerafdruk gekopieerd naar klembord + Je bent verbannen uit dit groepsgesprek + Dit groepsgesprek is enkel voor leden + Bronbeperking + Je bent uit dit groepsgesprek verwijderd + Het groepsgesprek is uitgeschakeld + Je neemt niet langer deel aan dit groepsgesprek + met account %s + %s op HTTP-host nakijken + Je bent niet verbonden. Probeer later opnieuw + Bestandsgrootte van %s controleren + Bestandsgrootte van %1$s op %2$s controleren + Berichtopties + Citeren + Plakken als citaat + Oorspronkelijke URL kopiëren + Opnieuw versturen + Bestands-URL + URL gekopieerd naar klembord + XMPP-adres gekopieerd naar klembord + Foutmelding gekopieerd naar klembord + webadres + 2D-streepjescode scannen + 2D-streepjescode tonen + Geblokkeerde contacten weergeven + Accountgegevens + Bevestigen + Opnieuw proberen + Voorgronddienst + Belet het besturingssysteem je verbinding te onderbreken + Back-up creëren + Back-upbestanden worden opgeslagen in %s + Bezig met creëren van back-upbestanden... + Je back-up is opgeslagen + De back-upbestanden zijn opgeslagen in %s + Bezig met herstellen van back-up... + Je back-up is hersteld + Vergeet niet om de account in te schakelen. + Bestand kiezen + Ontvangen van %1$s (%2$d%% voltooid) + %s downloaden + %s verwijderen + bestand + %s openen + versturen (%1$d%% voltooid) + %s aangeboden om te downloaden + Bestandsoverdracht annuleren + bestandsoverdracht geannuleerd + Geen app om bestand te openen + Dynamische tags + Toon enkel-lezen tags onder contacten + Meldingen inschakelen + Geen groepsgespreksserver gevonden + Account-avatar + OMEMO-vingerafdruk kopiëren naar klembord + OMEMO-sleutel opnieuw aanmaken + Apparaten wissen + Er ging iets mis + Geschiedenis van server halen + Geen verdere geschiedenis op server + Bijwerken… + Wachtwoord gewijzigd! + Kon wachtwoord niet wijzigen + Wachtwoord wijzigen + Huidig wachtwoord + Nieuw wachtwoord + Wachtwoord mag niet leeg zijn + Alle accounts inschakelen + Alle accounts uitschakelen + Actie uitvoeren met + Geen aansluiting + Offline + Verstoteling + Lid + Geavanceerde modus + Lidprivileges verlenen + Lidprivileges intrekken + Beheerdersprivileges verlenen + Beheerdersprivileges ontzeggen + Eigenaarsprivileges verlenen + Eigenaarsprivileges intrekken + Verwijderen uit groepsgesprek + Verwijderen uit kanaal + Kon aansluiting niet wijzigen + Verbannen uit groepsgesprek + Verbannen uit kanaal + Nu verbannen + Kon rol van %s niet wijzigen + Instellingen voor privégroep + Instellingen voor openbaar kanaal + Privé, enkel leden + XMPP-adressen openbaren + Kanaal modereren + Je neemt geen deel + Gespreksopties aangepast! + Kon gespreksopties niet aanpassen + Nooit + Voor onbepaalde duur + Sluimeren + Beantwoorden + Markeren als gelezen + Invoer + Enter is versturen + Toon enter-toets + Verander de emoticon-toets in een enter-toets + audio + video + afbeelding + PDF-document + Android-applicatie + Contact + Avatar is gepubliceerd! + Bezig met versturen van %s + Bezig met aanbieden van %s + Offline contacten verbergen + %s is aan het typen… + %s is gestopt met typen + %s zijn aan het typen… + %s zijn gestopt met typen + Aan-het-typen-meldingen + Laat je contacten weten wanneer je ze een nieuw bericht schrijft + Locatie versturen + Locatie weergeven + Geen app om locatie weer te geven + Locatie + Gesprek gesloten + Privégroep verlaten + Openbaar kanaal verlaten + Systeem-CA\'s niet vertrouwen + Alle certificaten moeten handmatig goedgekeurd worden + Certificaten verwijderen + Handmatig goedgekeurde certificaten verwijderen + Geen handmatig goedgekeurde certificaten + Certificaten verwijderen + Selectie verwijderen + Annuleren + + %d certificaat verwijderd + %d certificaten verwijderd + + Snelle actie + Geen + Recent gebruikt + Snelle actie kiezen + Contacten zoeken + Bladwijzers doorzoeken + Privébericht sturen + Gebruikersnaam + Gebruikersnaam + Dit is geen geldige gebruikersnaam + Downloaden mislukt: server niet gevonden + Downloaden mislukt: bestand niet gevonden + Downloaden mislukt: kon geen verbinding maken met host + Download mislukt: kon bestand niet schrijven + Tor-netwerk niet beschikbaar + Bindingsfout + Gebroken + Aanwezigheid + Trillen behandelen als stille modus + Uitgebreide verbindingsinstellingen + Toon hostnaam- en poortinstellingen bij instellen van een account + xmpp.voorbeeld.be + Archiefvoorkeuren + Voorkeuren voor archief aan serverzijde + Ophalen van archiefvoorkeuren. Even geduld… + Voer de tekst van de afbeelding hierboven in + XMPP-adres komt niet overeen met certificaat + Certificaat vernieuwen + Fout bij ophalen van OMEMO-sleutel! + OMEMO-sleutel geverifieerd met certificaat! + Je apparaat ondersteunt de selectie van cliënt-certificaten niet! + Verbinding + Verbinden via Tor + Tunnel alle verbindingen door het Tor-netwerk. Vereist Orbot + Hostnaam + Poort + Dit is geen geldig poortnummer + Dit is geen geldige hostnaam + %1$d van %2$d accounts verbonden + + %d bericht + %d berichten + + Laad meer berichten + Synchroniseer met contacten +
We bewaren geen kopie van deze telefoonnummers.\n\nVoor meer informatie, bekijk ons privacybeleid.

Je wordt nu gevraagd om toegang te verlenen tot je contactpersonen.]]>
+ Melding bij alle berichten + Melding enkel wanneer aangesproken + Meldingen uitgeschakeld + Meldingen gepauzeerd + Afbeeldingscompressie + Altijd + Enkel grote afbeeldingen + Batterij-optimalisaties ingeschakeld + Uitschakelen + Het gekozen vlak is te groot + (Geen actieve accounts) + Dit veld is vereist + Bericht corrigeren + Gecorrigeerd bericht versturen + Je hebt deze account uitgeschakeld + URI delen met… +
Je meldt je aan met je telefoonnummer en Quicksy zal automatisch—gebaseerd op de telefoonnummers in je adresboek—mogelijke contacten aanbevelen.

Door je aan te melden, stem je in met ons privacybeleid.]]>
+ Je volledige XMPP-adres zal %s zijn + Account aanmaken + Gebruik mijn eigen provider + Kies je gebruikersnaam + Aanwezigheid handmatig beheren + Stel je aanwezigheid in bij het bewerken van je statusbericht. + Statusbericht + Beschikbaar voor gesprek + Online + Afwezig + Niet beschikbaar + Bezig + Een veilig wachtwoord is aangemaakt + Je apparaat ondersteunt het uitschakelen van batterij-optimalisatie niet + Registratie mislukt: probeer later opnieuw + Registratie mislukt: wachtwoord te zwak + Kies deelnemers + Groepsgesprek aanmaken… + Opnieuw uitnodigen + Uitschakelen + Kort + Gemiddeld + Lang + Privacy + Thema + Kies het kleurenpalet + Automatisch + Groene achtergrond + Gebruik groene achtergrond voor ontvangen berichten + Dit apparaat wordt niet meer gebruikt + Computer + Mobiele telefoon + Tablet + Webbrowser + Console + Betaling vereist + Ik + De contactpersoon vraagt om aanwezigheidsupdates + Toestaan + Geen toestemming om toegang te krijgen tot %s + Externe server niet gevonden + Time-out bij externe server + OMEMO-identiteiten verwijderen + Geselecteerde sleutels verwijderen + Je moet verbonden zijn om je avatar te kunnen publiceren. + Toon foutbericht + Foutbericht + Gegevensbesparing ingeschakeld + Dit apparaat is geverifieerd + Vingerafdruk kopiëren + Geverifieerde vingerafdrukken + Gebruik de camera om de streepjescode van een contact te scannen + De sleutels worden opgehaald. Even geduld. + Delen als streepjescode + Delen als XMPP-URI + Delen als HTTP-link + Blindelings vertrouwen vóór verificatie + Onvertrouwd + Ongeldige 2D-streepjescode + Cache wissen + Privéopslag wissen + Privéopslag waar bestanden worden bijgehouden wissen (de bestanden kunnen opnieuw gedownload worden van de server) + Ik heb deze link gevolgd vanuit een betrouwbare bron + Je staat op het punt de OMEMO-sleutels van %1$s te verifiëren door op een link te klikken. Dit is enkel veilig als je de link van een betrouwbare bron hebt gevolgd, waarbij enkel %2$s de link gepubliceerd kan hebben. + OMEMO-sleutels verifiëren + Niet-actieve tonen + Niet-actieve verbergen + Apparaat niet meer vertrouwen + + %d seconde + %d seconden + + + %d minuut + %d minuten + + + %d uur + %d uur + + + %d dag + %d dagen + + + %d week + %d weken + + + %d maand + %d maand + + Automatisch berichten verwijderen + Verwijder automatisch berichten van dit apparaat ouder dan de ingestelde tijdsperiode. + Bericht wordt versleuteld + Berichten worden niet opgehaald wegens lokale bewaarperiode. + Video wordt gecomprimeerd + Bijbehorende gesprekken gesloten. + Contact geblokkeerd. + Meldingen van onbekenden + Bericht ontvangen van onbekende + Vreemde blokkeren + Volledig domein blokkeren + nu online + Ontsleutelen opnieuw proberen + Sessiefout + SASL-mechanisme neergewaardeerd + Server vereist registratie op website + Website openen + Heads-up-meldingen + Vandaag + Gisteren + Valideer hostnaam met DNSSEC + Servercertificaten die de gevalideerde hostnaam bevatten worden beschouwd als geverifieerd + Certificaat bevat geen XMPP-adres + gedeeltelijk + Video opnemen + Kopiëren naar klembord + Bericht gekopieerd naar klembord + Bericht + Privéberichten zijn uitgeschakeld + Beschermde apps + Om meldingen te blijven ontvangen, zelfs wanneer het scherm uit staat, moet je Conversations toevoegen aan de lijst met beschermde apps. + Onbekend certificaat aanvaarden? + Het servercertificaat is niet ondertekend door een gekende certificaatautoriteit. + Verkeerde servernaam aanvaarden? + De server kon niet authenticeren als ‘%s’. Het certificaat is enkel geldig voor: + Wil je toch verbinding maken? + Certificaatgegevens: + Eenmalig + De QR-codescanner heeft toegang nodig tot de camera + Scrollen naar beneden + Scroll naar beneden na versturen van bericht + Statusbericht bewerken + Statusbericht bewerken + Versleuteling uitschakelen + Tip: in sommige gevallen kan dit opgelost worden door elkaar toe te voegen aan je contacten. + Weet je zeker dat je OMEMO-versleuteling voor dit gesprek wil uitschakelen?\nDit zal je serverbeheerder de mogelijkheid geven je berichten in te kijken, maar is mogelijk de enige manier om te communiceren met anderen die gebruik maken van verouderde cliënten. + Nu uitschakelen + Ontwerp: + OMEMO-versleuteling + OMEMO zal altijd gebruikt worden voor één-op-één- privégroepsgesprekken. + OMEMO zal standaard gebruikt worden voor nieuwe gesprekken. + OMEMO zal uitdrukkelijk ingeschakeld moeten worden voor nieuwe gesprekken. + Snelkoppeling aanmaken + Lettergrootte + De relatieve lettergrootte in de app. + Standaard aan + Standaard uit + Klein + Gemiddeld + Groot + Bericht is niet versleuteld voor dit apparaat. + Kan OMEMO-bericht niet ontsleutelen. + ongedaan maken + Delen van locatie is uitgeschakeld + Positie vergrendelen + Positie ontgrendelen + Locatie kopiëren + Locatie delen + Routebeschrijving + Locatie delen + Locatie weergeven + Delen + Even geduld… + Berichten zoeken + GIF + Gesprek bekijken + Plug-in voor delen van locatie + Gebruik de ‘Plug-in voor delen van locatie’ in plaats van de ingebouwde kaart + Webadres kopiëren + XMPP-adres kopiëren + Bestanden delen via HTTP voor S3 + Rechtstreeks zoeken + Open het toetsenbord op het scherm ‘Gesprek starten’ en plaats de cursor in het zoekveld + Gespreksafbeelding + Host ondersteunt geen gespreksafbeeldingen + Enkel de eigenaar kan de gespreksafbeelding wijzigen + Contactnaam + Bijnaam + Naam + Naam is niet vereist + Gespreksnaam + Dit groepsgesprek is verwijderd + Voorgronddienst + Statusinformatie + Verbindingsproblemen + Deze meldingscategorie wordt gebruikt om een melding weer te geven ingeval er een probleem is bij het verbinden met een account. + Berichten + Berichten + Stille berichten + Deze meldingscategorie wordt gebruikt om meldingen weer te geven die geen geluid mogen maken. Bijvoorbeeld, indien actief op een ander apparaat (uitstelperiode). + Belang, geluid, trillen + Videocompressie + Media bekijken + Deelnemers + Mediabrowser + Bestand weggelaten wegens beveiligingsovertreding. + Videokwaliteit + Een lagere kwaliteit zorgt voor kleinere bestanden + Gemiddeld (360p) + Hoog (720p) + geannuleerd + Je bent al een bericht aan het opstellen. + Functie nog niet geïmplementeerd + Ongeldige landcode + Kies een land + telefoonnummer + Verifieer je telefoonnummer + Quicksy zal een sms sturen om je telefoonnummer te bevestigen (providerkosten mogelijk van toepassing). Voer je landcode en telefoonnummer in: +
%s

Is dit oké, of wil je het nummer bewerken?]]>
+ %s is geen geldig telefoonnummer. + Voer je telefoonnummer in. + Landen doorzoeken + %s verifiëren + %s.]]> + We hebben je nóg een sms gestuurd met 6-cijferige code. + Voer de 6-cijferige code hieronder in. + Sms opnieuw versturen + Sms opnieuw versturen (%s) + Even geduld (%s) + terug + Mogelijke pincode is automatisch van het klembord geplakt. + Voer je 6-cijferige code in. + Weet je zeker dat je de registratieprocedure wilt stopzetten? + Ja + Nee + Bezig met verifiëren… + Bezig met aanvragen van sms… + De ingevoerde code is onjuist. + De toegestuurde code is verlopen. + Onbekende netwerkfout. + Onbekend serverantwoord. + Er is iets misgegaan tijdens het verwerken van je verzoek. + Ongeldige gebruikersinvoer + Tijdelijk niet beschikbaar; probeer het later opnieuw. + Geen netwerkverbinding. + Probeer het opnieuw over %s + Je bent beperkt + Te veel pogingen + Je gebruikt een verouderde versie van deze app. + Bijwerken + Dit telefoonnummer is al in gebruik op een ander apparaat. + Voer je naam in om mensen buiten je adresboek te laten weten wie je bent. + Je naam + Voer je naam in + Gebruik de knop ‘Bewerken’ om je naam in te stellen. + Verzoek afwijzen + Orbot installeren + Orbot starten + Geen app-winkel geïnstalleerd. + Dit kanaal openbaart je XMPP-adres + e-boek + Origineel (zonder compressie) + Openen met… + Conversations-profielafbeelding + Kies een account + Back-up herstellen + Herstellen + Voer het wachtwoord in voor %s om de back-up te herstellen. + Gebruik de back-upfunctie niet als je een installatie wilt klonen (gelijktijdig draaien). Back-ups zijn alleen bedoeld voor migraties of als je het oorspronkelijke apparaat bent kwijtgeraakt. + Back-up en herstel + Voer XMPP-adres in + Groepsgesprek creëren + Deelnemen aan openbaar kanaal + Privégroep creëren + Openbaar kanaal creëren + Kanaalnaam + XMPP-adres + Voer een naam in voor het kanaal + Voer een XMPP-adres in + Dit is een XMPP-adres. Voer een naam in. + Bezig met creëren van openbaar kanaal... + Dit kanaal bestaat al + Je hebt deelgenomen aan een bestaand kanaal + Iedereen mag het onderwerp aanpassen + Iedereen mag anderen uitnodigen + Iedereen mag het onderwerp aanpassen. + Eigenaars mogen het onderwerp aanpassen. + Beheerders mogen het onderwerp aanpassen. + Eigenaren mogen anderen uitnodigen. + Iedereen mag anderen uitnodigen. + XMPP-adressen zijn zichtbaar voor beheerders. + XMPP-adressen zijn openbaar. + Dit openbare kanaal heeft geen deelnemers. Nodig je contactpersonen uit of gebruik de knop ‘Delen’ om het XMPP-adres te delen. + Deze privégroep heeft geen deelnemers. + Privileges beheren + Deelnemers zoeken + Bestand te groot + Bijvoegen + Kanalen ontdekken + Kanalen doorzoeken + Mogelijke privacyschending! + search.jabber.network.

Door deze functie te gebruiken, zullen je IP-adres en zoekopdrachten naar die dienst verstuurd worden. Bekijk hun privacybeleid voor meer informatie.]]>
+ Ik heb al een account + Bestaande account toevoegen + Nieuwe account registreren + Dit lijkt op een domeinadres + Tóch toevoegen + Dit lijkt op een kanaaladres + Back-upbestanden delen + Back-up van Conversations + Gebeurtenis + Back-up openen + Het geselecteerde bestand is geen Conversations-back-upbestand + Deze account is al ingesteld + Voer het wachtwoord voor deze account in + Deelnemen aan openbaar kanaal… + Lokale server + Over + Bezig + Videogesprek + Je microfoon is niet beschikbaar + Je kunt slechts één gesprek tegelijk voeren. + Terug naar lopend gesprek + Kon camera niet wisselen + Bovenaan vastzetten + Bovenaan losmaken + Kon bericht niet corrigeren + Alle gesprekken + Dit gesprek + Je avatar + Avatar voor %s + Versleuteld met OMEMO + Onversleuteld + Speel audio + Pauzeer audio + + Bekijk %1$d deelnemer + Bekijk %1$d deelnemers + + + Een bericht kon niet worden afgeleverd + Sommige berichten konden niet worden afgeleverd + + Mislukte afleveringen + Meer opties + Geen applicatie gevonden + Nodig uit bij Conversations + Kan uitnodiging niet verwerken + Geen actieve accounts ondersteunen deze functie + De backup is gestart. Je krijgt een bericht als het voltooid is. + Kan video niet schakelen. + Onversleuteld document + Accountregistraties zijn niet ondersteund +
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml new file mode 100644 index 000000000..e7d739a5f --- /dev/null +++ b/app/src/main/res/values-pl/strings.xml @@ -0,0 +1,1043 @@ + + + Ustawienia + Nowa konwersacja + Zarządzaj kontami + Zarządzaj kontem + Zamknij rozmowę + Szczegóły kontaktu + Szczegóły konferencji + Szczegóły kanału + Dodaj konto + Edytuj nazwę + Dodaj do kontaktów + Usuń z rostera + Zablokuj kontakt + Odblokuj kontakt + Zablokuj domenę + Odblokuj domenę + Zablokuj użytkownika + Odblokuj użytkownika + Zarządzaj kontami + Ustawienia + Udostępnij w konwersacji + Rozpocznij konwersację + Wybierz kontakt + Wybierz kontakty + Udostępnij za pomocą + Czarna lista + przed chwilą + minutę temu + %d minut temu + + %d nieprzeczytana konwersacja + %d nieprzeczytane konwersacje + %d nieprzeczytanych konwersacji + %d nieprzeczytanych konwersacji + + wysyłanie… + Odszyfrowywanie wiadomości. To zajmie tylko chwilę… + Wiadomość zaszyfrowana OpenPGP + Nazwa jest już w użyciu + NIeprawidłowy pseudonim + Admin + Właściciel + Moderator + Uczestnik + Gość + Czy chcesz usunąć %s ze swojej listy kontaktów? Rozmowy z tym kontaktem nie zostaną usunięte. + Czy na pewno chcesz zablokować wiadomości od użytkownika %s? + Czy na pewno chcesz odblokować wiadomości przychodzące od użytkownika %s? + Zablokować wszystkie kontakty z %s? + Odblokować wszystkie kontakty z %s? + Kontakt zablokowany + Zablokowane + Czy chcesz usunąć zakładkę %s? Rozmowy z tą zakładką nie zostaną usunięte. + Zarejestruj nowe konto na serwerze + Zmień hasło na serwerze + Udostępnij… + Rozpocznij rozmowę + Zaproś kontakt + Zaproś + Kontakty + Kontakt + Anuluj + Ustaw + Dodaj + Edytuj + Usuń + Zablokuj + Odblokuj + Zapisz + Ok + %1$s uległo awarii + Używając swojego konta XMPP do wysyłania śladów stosu pomagasz w rozwoju %1$s. + Wyślij teraz + Nie pytaj ponownie + Nie można połączyć z kontem + Nie można połączyć z wieloma kontami + Dotknij aby zarządzać swoimi kontami + Załącz plik + Dodać ten brakujący kontakt do twojej listy kontaktów? + Dodaj kontakt + wysyłanie nie powiodło się + Przygotowanie do wysłania obrazka + Przygotowanie do wysłania obrazków + Udostępnianie plików. Proszę czekać… + Wyczyść historię + Wyczyść historię konwersacji + Czy chcesz usunąć wszystkie wiadomości w tej rozmowie?\n\nOstrzeżenie: To nie ma wpływu na wiadomości składowane na innych urządzeniach lub serwerach. + Usuń plik + Czy na pewno usunąć ten plik\? +\n +\nUwaga: Działanie nie wpływa na kopie pliku przechowywane na innych urządzeniach lub serwerach. + Zamknij konwersację po zakończeniu + Wybierz urządzenie + Wyślij wiadomość bez szyfrowania + Wyślij wiadomość + Wyślij wiadomość do %s + Wyślij wiadomość zaszyfrowaną OMEMO + Wyślij wiadomość zaszyfrowaną v\\OMEMO + Wyślij zaszyfrowaną wiadomość (OpenPGP) + Nowy pseudonim jest już użyciu + Wyślij bez szyfrowania + Nie można odszyfrować. Sprawdź poprawność klucza prywatnego. + OpenKeychain + OpenKeychain aby szyfrować i odszyfrowywać wiadomości i zarządzać twoimi kluczami publicznymi.

OpenKeychain jest na licencji GPLv3+ i jest dostępny przez F-Droid lub Google Play.

(Proszę zrestartować %1$s po zainstalowaniu.)]]>
+ Zrestartuj + Zainstaluj + Proszę zainstalować OpenKeychain + oferowanie… + oczekiwanie… + Nie znaleziono klucza OpenPGP + Nie można zaszyfrować twojej wiadomości bo ten kontakt nie ogłasza swojego publicznego klucza.\n\nPoproś kontakt aby ustawił OpenPGP. + Nie znaleziono kluczy OpenPGP + Nie można zaszyfrować twojej wiadomości bo twoje kontakty nie ogłaszają swoich kluczy publicznych.\n\nPoproś aby ustawili OpenPGP. + Główne + Akceptuj pliki + Automatycznie akceptuj pliki mniejsze niż… + Załączniki + Powiadomienie + Wibracje + Wibruj gdy nadejdzie wiadomość + Powiadomienie diodą LED + Migaj lampką powiadamiającą gdy nadejdzie wiadomość + Dzwonek + Dźwięk powiadomień + Dźwięk powiadomień dla nowych wiadomości + Dzwonek dla przychodzących rozmów + Czas bez powiadomień + Długość czasu kiedy powiadomienia są uśpione po wykryciu aktywności na jednym z twoich innych urządzeń. + Zaawansowane + Nie wysyłaj raportów awarii + 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. + Akceptuj + Wystąpił błąd + Błąd + Twoje konto + Wysyłaj powiadomienia obecności + Otrzymuj powiadomienia obecności + Poproś o powiadomienia obecności + Wybierz obraz + Zrób zdjęcie + Automatyczne powiadomienia obecności + Wybrany plik nie jest obrazem + Błąd konwersji obrazu + Nie odnaleziono pliku + Ogólny błąd wejścia/wyjścia. Być może skończyło się miejsce w pamięci\? + 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 + Łączenie… + Rozłączono + Błąd uwierzytelnienia + Nie odnaleziono serwera + Brak połączenia + Błąd rejestracji + Nazwa jest już w użyciu + Zarejestrowano pomyślnie + 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 + Niekompatybilny klient + Błąd strumienia + Błąd otwierania strumienia + Bez szyfrowania + OTR + OpenPGP + OMEMO + Usuń konto + Wyłącz tymczasowo + Publikuj awatar + Udostępnij klucz publiczny OpenPGP + Usuń klucz publiczny OpenPGP + Czy na pewno chcesz usunąć klucz publiczny OpenPGP ze swojej propagacji obecności?\nTwoje kontakty nie będą już mogły wysyłać Ci wiadomości zaszyfrowanych OpenPGP. + Klucz publiczny OpenPGP został opublikowany. + Włącz konto + Czy na pewno? + Usunięcie konta usuwa całą historię rozmów + Nagraj głos + Adres XMPP + Zablokuj adres XMPP + username@example.com + Hasło + To nie jest poprawny adres XMPP + Brak pamięci. Obraz jest za duży + Czy chcesz dodać %s do listy kontaktów? + Informacje o serwerze + XEP-0313: MAM + XEP-0280: Kopie wiadomości + XEP-0352: Wskaźnik stanu klienta + XEP-0191: Polecenia Blokujące + XEP-0237: Roster Versioning + XEP-0198: Zarządzanie Strumieniem + XEP-0215: Wykrywanie Zewnętrznych Usług + XEP-0163: PEP (Awatary / OMEMO) + XEP-0363: Przesyłanie plików przez HTTP + XEP-0357: Push + dostępny + niedostępny + Brak informacji o kluczu publicznym + widziany chwilę temu + widziany minutę temu + widziany %d minut(y) temu + widziany godzinę temu + widziany %d godzin(y) temu + widziany wczoraj + widziany %d dni temu + Wiadomość zaszyfrowana. Zainstaluj OpenKeychain aby odszyfrować. + Znaleziono nowe wiadomości zaszyfrowane przez OpenPGP + ID klucza OpenPGP + Odcisk OMEMO + Odcisk v\\OMEMO + Odcisk OMEMO (pochodzenie wiadomości) + v\\Odcisk OMEMO (pochodzenie wiadomości) + Pozostałe urządzenia + Zaufane odciski OMEMO + Pobieranie kluczy… + Ukończono + Odszyfruj + Zakładki + Szukaj + Wpisz kontakt + Usuń kontakt + Szczegóły kontaktu + Zablokuj kontakt + Odblokuj kontakt + Utwórz + Wybierz + Kontakt już istnieje + Dołącz + kanał@konferencje.example.com/nick + kanał@konferencja.example.com + Dodaj jako zakładkę + Usuń zakładkę + Usuń konferencję + Usuń kanał + Czy jesteś pewien, że chcesz usunąć tą konferencję?\n\nOstrzeżenie: Ta konferencja zostanie całkowicie usunięta na serwerze. + Czy na pewno chcesz usunąć ten kanał publiczny?\n\nOstrzeżenie: Ten kanał zostanie całkowicie usunięty z serwera. + Usuwanie konferencji nieudane + Nie można usunąć kanału + Edytuj tytuł konferencji + Temat + Dołączanie do konferencji… + Opuść pokój + Kontakt dodał ciebie do swojej listy kontaktów + Również dodaj + %s przeczytał do tego miejsca + %s przeczytali do tego miejsca + %1$s i %2$d osób przeczytało do tego miejsca + Wszyscy przeczytali do tego miejsca + Publikuj + Dotknij awatar, żeby wybrać obraz z galerii + Publikowanie… + Serwer odrzucił żądanie publikacji + Nie można skonwertować obrazu + Nie udało się zapisać obrazu w pamięci urządzenia + (lub długo przytrzymaj, aby ustawić domyślny) + Twój serwer nie udostępnia możliwości publikacji awatarów + szepcze + do %s + Wyślij prywatną wiadomość do %s + Połącz + Konto już istnieje + Dalej + Połączono z serwerem + Pomiń + Wyłącz powiadomienia + Włącz + Konferencja wymaga hasła + Wprowadź hasło + Poproś kontakt o udostępnianie powiadomień o obecności. +\n +\nPozwoli to na ustalenie klienta, z którego korzysta rozmówca. + Zażądaj teraz + Ignoruj + Uwaga: Wysyłanie bez obustronnych powiadomień o obecności może powodować nieoczekiwane problemy.\n\nSprawdź subskrypcję powiadomień w szczegółach kontaktu. + Bezpieczeństwo + Pozwól na poprawianie wiadomości + Pozwól swoim kontaktom poprawiać wiadomości + Ustawienia zaawansowane + Modyfikuj ustawienia ostrożnie + O %s + Godziny ciszy + Początek + Koniec + Włącz godziny ciszy + Powiadomienia będą wyciszone w wybranym przedziale czasu + Inne + Synchronizuj zakładki + Ustaw flagę automatycznego dołączania przy wchodzeniu lub opuszczaniu pokoju i reaguj na zmiany innych klientów. + Odcisk klucza OMEMO został skopiowany do schowka + Zbanowany + Konferencja tylko dla użytkowników + Ograniczenie zasobu + Wykopany + Konferencja została zamknięta + Nie uczestniczysz już w tej konferencji + Opuszczono rozmowę grupową z powodu usterki technicznej + używając konta %s + udostępnione na %s + Sprawdzanie %s na hoście HTTP + Brak połączenia. Spróbuj ponownie później + Sprawdź rozmiar %s + Sprawdź rozmiar %1$s na %2$s + Opcje wiadomości + Cytat + Wklej jako cytat + Skopiuj oryginalny URL + Wyślij ponownie + URL pliku + Skopiowano URL do schowka + Skopiowano adres XMPP do schowka + Skopiowano komunikat błędu do schowka + adres URL + Zeskanuj kod + Pokaż kod QR + Wyświetl listę banów + Szczegóły konta + Potwierdź + Spróbuj ponownie + Usługa na pierwszym planie + Uniemożliwia systemowi przerwanie połączenia + Utwórz kopię zapasową + Kopia zapasowa będzie zapisana w %s + Tworzenie kopii zapasowej + Kopia zapasowa została utworzona + Kopia zapasowa zapisana w %s + Przywracanie kopii zapasowej + Kopia zapasowa została przywrócona + Nie zapomnij o włączeniu tego konta. + Wybierz plik + Odbieranie %1$s (ukończono %2$d%%) + Pobierz %s + Usuń %s + plik + Otwórz %s + Wysyłanie (ukończono %1$d%%) + Przygotowanie do udostępnienia obrazka + Zaproponowano pobranie pliku %s + Anuluj przesyłanie + Nie udało się udostępnić pliku + transmisja pliku anulowana + Plik usunięty + Nie odnaleziono aplikacji do otwarcia pliku + Nie odnaleziono aplikacji do otwarcia łącza + Nie odnaleziono aplikacji do wyświetlenia kontaktu + Dynamiczne tagi + Wyświetlaj etykiety pod kontaktami + Włącz powiadomienia + Nie znaleziono serwera konferencji + Nie udało się utworzyć rozmowy grupowej + Awatar konta + Skopiuj odcisk klucza OMEMO do schowka + Wygeneruj ponownie klucz OMEMO + Wyczyść urządzenia + Czy na pewno chcesz usunąć wszystkie inne urządzenia z ogłoszenia OMEMO? Następnym razem gdy połączą się Twoje urządzenia, ogłoszą się one ponownie, ale mogą nie otrzymać wiadomości wysłanych w międzyczasie. + Nie ma dostępnych kluczy dla tego kontaktu.\nPobieranie nowych kluczy z serwera nie powiodło się. Być może jest coś nie tak z serwerem którego używa kontakt? + Brak dostępnych kluczy dla tego kontaktu.\nUpewnij się, że wzajemnie powiadamiacie się o obecności. + Coś poszło źle + Pobieranie historii z serwera + Koniec historii na serwerze + Aktualizowanie… + Hasło zostało zmienione! + Nie udało się zmienić hasła + Zmień hasło + Obecne hasło + Nowe hasło + Hasło nie może być puste + Aktywuj wszystkie konta + Wyłącz wszystkie konta + Użyj + Brak stanowiska + Offline + Wykluczony + Członek + Tryb zaawansowany + Przyznaj uprawnienia członkostwa + Usuń uprawnienia członkostwa + Przyznaj uprawnienia administratora + Odbierz uprawnienia administratora + Przyznaj uprawnienia właściciela + Usuń uprawnienia właściciela + Usuń z konferencji + Usuń z kanału + Nie udało się zmienić stanowiska dla %s + Zbanuj + Zbanuj na kanale + Chcesz usunąć %s z publicznego kanału. Jedynym sposobem aby to zrobić jest zbanowanie tego użytkownika na zawsze. + Zbanuj teraz + Nie udało się zmienić funkcji %s + Konfiguracja prywatnej rozmowy grupowej + Konfiguracja publicznego kanału + Prywatne, tylko dla członków + Spraw aby adres XMPP był widoczny dla wszystkich + Włącz moderację na kanale + Nie bierzesz udziału + Ustawienia konferencji zostały zmodyfikowane! + Nie można zmodyfikować ustawień konferencji + Nigdy + Ręcznie + Odłóż + Odpowiedz + Już przeczytane + Ustawienia wprowadzania + Enter wysyła + Użyj klawisza Enter aby wysłać wiadomość. Możesz zawsze użyć Ctrl+Enter do wysyłania wiadomości, nawet jeśli ta opcja jest wyłączona. + Pokaż klawisz Enter + Zamień klawisz emotikon na klawisz Enter + plik audio + plik wideo + obraz + grafika wektorowa + plik multimediów + Dokument PDF + Aplikacja Androida + Kontakt + Avatar został pomyślnie opublikowany! + Wysyłanie %s + Oferowanie %s + Ukryj niedostępnych + %s pisze… + %s już nie pisze + %s piszą… + %s przestali pisać + Powiadomienia pisania + Powiadamiaj rozmówcę, kiedy rozpoczynasz nową wiadomość + Wyślij lokalizację + Pokaż lokalizację + Nie odnaleziono aplikacji do wyświetlenia lokalizacji + Lokalizacja + Zamknięto konwersację + Opuszczono prywatną rozmowę grupową + Opuszczono publiczny kanał + Nie ufaj certyfikatom systemowym + Wymagaj ręcznego potwierdzania certyfikatów + Usuń certyfikaty + Wybierz zaufane certyfikaty do usunięcia + Brak ręcznie zaufanych certyfikatów + Usuń certyfikaty + Usuń zaznaczone + Anuluj + + Usunięto %d certyfikat + Usunięto %d certyfikaty + Usunięto %d certyfikatów + Usunięto %d certyfikatów + + Zastąp przycisk wysyłania szybką akcją + Szybka akcja + Brak + Ostatnio używana + Wybierz szybką akcję + Przeszukuj kontakty + Przeszukaj zakładki + Wyślij wiadomość prywatną + %1$s opuścił konferencję + Nazwa użytkownika + Nazwa użytkownika + Błędna nazwa użytkownika + Pobieranie nieudane: Nie odnaleziono serwera + Pobieranie nieudane: Nie odnaleziono pliku + Pobieranie nieudane: Nie można połączyć z hostem + Pobieranie niepowiodło się: brak możliwości zapisu pliku + Pobieranie nieudane: Nieprawidłowy plik + Sieć TOR jest niedostepna + Błąd połączenia (zasób) + Serwer nie odpowiada domenie + Zepsute + Dostępność + Niedostępny kiedy urządzenie jest zablokowane + Pokaż jako Niedostępny kiedy urządzenie jest zablokowane + Zajęty w trybie cichym + Pokaż jako Zajęty jeśli urządzenie jest w trybie cichym + Traktuj tryb wibracji jak tryb cichy + Pokaż jako Zajęty kiedy urządzenie jest w trybie wibracji + Rozszerzone ustawienia połączenia + Pokaż nazwę hosta i ustawienia portu przy dodawaniu konta + xmpp.example.com + Zaloguj przy użyciu certyfikatu + Nie mogę odczytać certyfikatu + Preferencje archiwizacji + Preferencje archiwizacji po stronie serwera + Pobieranie preferencji archiwizacji. Proszę czekać… + Nie można pobrać preferencji archiwizacji + CAPTCHA wymagana + Wprowadź tekst z powyższego obrazka + Łańcuch certyfikatów nie jest zaufany + Adres XMPP nie pasuje do certyfikatu + Odnów certyfikat + Błąd pobierania klucza OMEMO! + Zweryfikowano klucz OMEMO z certyfikatem! + Twoje urządzenie nie wspiera wyboru certyfikatów klienckich! + Połączenie + Połącz przez sieć TOR + Tuneluj wszystkie połączenia przez sieć TOR. Wymaga zainstalowania aplikacji \"Orbot\" + Nazwa hosta + Port + Adres serwera lub adres \".onion\" + To nie jest prawidłowy numer portu + To nie jest prawidłowa nazwa hosta + %1$d z %2$d kont połączonych + + %d wiadomość + %d wiadomości + %d wiadomości + %d wiadomości + + Załaduj wiecej wiadomości + Plik udostępniony %s + Obraz udostępniony %s + Obrazy udostępnione %s + Tekst udostępniony %s + Pozwól %1$s na dostęp do zewnętrznego magazynu + Pozwól %1$s na dostępu do aparatu + Synchronizuj z kontaktami + %1$s potrzebuje dostępu do twojej książki adresowej aby dopasować ją z twoją listą kontaktów XMPP.\nDzięki temu wyświetlone zostaną pełne nazwy i awatary kontaktów.\n\n%1$s użyje książki adresowej wyłącznie do lokalnego dopasowania bez wysyłania czegokolwiek na serwer. +
Nie przechowujemy kopii tych numerów.\n\nAby uzyskać więcej informacji przeczytaj naszą politykę prywatności.

Zostaniesz poproszony o pozwolenie na dostęp do twoich kontaktów.]]>
+ Powiadom o wszystkich wiadomościach + Powiadamiaj tylko w przypadku wzmianki o mnie + Powiadomienia wyłączone + Powiadomienia wstrzymane + Kompresja obrazów + Podpowiedź: Użyj \'Wybierz plik\' zamiast \'Wybierz obraz\' aby wysłać poszczególne obrazki bez kompresji bez względu na to ustawienie. + Zawsze + Tylko duże obrazki + Optymalizacje zużycia baterii włączone + Twoje urządzenie ma włączone agresywne oszczędzanie baterii przez co %1$s może odbierać wiadomości z opóźnieniem.\nZalecamy wyłączenie tych optymalizacji. + Twoje urządzenie stosuje agresywne oszczędzanie baterii, przez co %1$s może odbierać wiadomości z opóźnieniem lub je tracić. +\n +\nZostaniesz poproszony o jego wyłączenie. + Wyłącz + Zaznaczony obszar jest zbyt duży + (Brak aktywynych kont) + To pole jest wymagane + Popraw wiadomość + Wyślij poprawioną wiadomość + Już zaufałeś temu kontaktowi. Wybierając \'zrobione\' potwierdzasz, że %s jest członkiem tej rozmowy grupowej. + Wyłączyłeś to konto + Błąd bezpieczeństwa: nieprawidłowy dostęp do pliku! + Nie odnaleziono aplikacji do udostępnienia URI + Udostępnij URI za pomocą… +
Zapisujesz się przy użyciu numeru telefonu i Quicksy automatycznie - na podstawie numerów telefonów w książce adresowej - zasugeruje potencjalne kontakty dla ciebie.

Zapisując się zgadzasz się na naszą politykę prywatności.]]>
+ Zgoda i kontynuuj + Poprowadzimy cię przez proces tworzenia konta na conversations.im. +\nKiedy wybierzesz conversations.im jako dostawcę będziesz mógł komunikować się z innymi osobami jeśli podasz im swój pełen adres XMPP. + Twój pełen adres XMPP to: %s + Utwórz konto + Użyj innego serwera + Wybierz nazwę użytkownika + Zarządzaj dostępnością ręcznie + Ustaw dostępność w oknie edytowania wiadomości statusu. + Status + Chętny do rozmowy + Dostępny + Zaraz wracam + Niedostępny + Zajęty + Zostało wygenerowane bezpieczne hasło + Twoje urządzenie nie pozwala na wyłączenie optymalizacji baterii + Rejestracja nie powiodła się: spróbuj później + Rejestracja nie powiodła się: hasło zbyt słabe + Wybierz członków + Tworzenie konferencji… + Zaproś ponownie + Wyłącz + Krótki + Średni + Długi + Ogłaszaj użycie + Powiadamiaj kontakty o tym, że używasz Conversations + Prywatność + Skórka + Wybierz paletę kolorów + Automatycznie + Jasny + Ciemny + Zielone tło + Używaj zielonego tła dla otrzymanych wiadomości + Nie można połączyć się z OpenKeychain + Urządzenie to nie jest już używane + Komputer + Komórka + Tablet + Przeglądarka + Konsola + Płatność wymagana + Udziel pozwolenia na dostęp do Internetu + Ja + Kontakt prosi o udostępnienie statusu + Pozwól + Brak pozwolenia na dostęp do %s + Nie znaleziono serwera + Brak odpowiedzi od zdalnego serwera + Nie można zaktualizować konta + Zgłoś spam z tego adresu XMPP. + Usuń tożsamości OMEMO + Wygeneruj jeszcze raz klucze OMEMO. Wszystkie twoje kontakty będą musiały zweryfikować cię ponownie. Użyj tego tylko w ostateczności. + Usuń zaznaczone klucze + Musisz być połączony/na, aby opublikować swój awatar. + Pokaż komunikaty błędów + Komunikat o błędzie + Oszczędzanie danych jest włączone + Twój system operacyjny ogranicza %1$s dostęp do internetu w tle. Aby otrzymywać powiadomienia o nowych wiadomościach należy pozwolić %1$s na nieograniczone dostęp kiedy opcja Oszczędzania Danych jest włączona.\n%1$s będzie oszczędzać transfer danych kiedy to możliwe. + Twoje urządzenie nie wspiera wyłączenia Oszczędzania danych dla %1$s. + Niemożna utworzyć pliku tymczasowego + To urządzenie zostało zweryfikowane + Skopiuj odcisk + Zweryfikowałeś wszystkie klucze OMEMO które posiadasz + Kod kreskowy nie zawiera odcisków dla tej rozmowy. + Zaufane odciski + Użyj aparatu, aby zeskanować kod kreskowy kontaktu + Proszę czekać na ściągnięcie kluczy + Udostępnij przez kod QR + Udostępnij przez URI XMPP + Udostępnij przez link HTTP + Ślepo Ufaj Przed Weryfikacją + Automatycznie ufaj wszystkim nowym urządzeniom kontaktów, którzy nie zostali zweryfikowani wcześniej i poproś o ręczne potwierdzenie za każdym razem, kiedy zweryfikowany kontakt dodaje nowe urządzenie. + Ślepo zaufane klucze OMEMO, to jest mogą należeć do kogoś innego lub ktoś może się podszywać. + Niezaufane + Nieprawidłowy kod kreskowy 2D + Wyczyść cache (używane przez aparat) + Wyczyść cache + Wyczyść prywatny magazyn + Wyczyść prywatny magazyn gdzie trzymane są pliki (mogą zostać pobrane ponownie z serwera) + Trafiłem na ten link w zaufanym źródle + Zaraz zweryfikujesz klucz OMEMO %1$s klikając w link. Jest to bezpieczne jedynie, kiedy link pochodzi z zaufanego źródła gdzie tylko %2$s mógł go opublikować. + Weryfikujesz właśnie klucze OMEMO własnego konta. To jest bezpieczne tylko jeśli kliknąłeś łącze w miejscu w którym jedynie ty mogłeś je zamieścić. + Kontynuuj + Zweryfikuj klucze OMEMO + Pokaż nieaktywne + Ukryj nieaktywne + Przestań ufać urządzeniu + Czy jesteś pewien, że chcesz cofnąć weryfikację tego urządzenia?\nUrządzenie to, i wiadomości z niego przychodzące będą oznaczane jako niezaufane. + + %d sekunda + %d sekundy + %d sekund + %d sekund + + + %d minuta + %d minuty + %d minut + %d minut + + + %d godzina + %d godziny + %d godzin + %d godzin + + + %d dzień + %d dni + %d dni + %d dni + + + %d tydzień + %d tygodnie + %d tygodni + %d tygodni + + + %d miesiąc + %d miesiące + %d miesięcy + %d miesięcy + + Automatyczne usuwanie wiadomości + Automatycznie usuwaj z tego urządzenia wiadomości starsze niż skonfigurowany okres czasu. + Szyfrowanie wiadomości + Nie pobieram wiadomości przez lokalny okres retencji. + Kompresuję film + Odpowiadające rozmowy zostały zamknięte. + Kontakt zablokowany. + Powiadomienia od nieznajomych + Powiadamiaj przy wiadomościach i połączeniach od nieznajomych. + Odebrano wiadomość od nieznajomego + Zablokuj nieznajomego + Zablokuj całą domenę + online w tej chwili + Ponownie spróbuj odszyfrować + Błąd sesji + Starszy mechanizm SASL + Serwer wymaga rejestracji na stronie + Otwórz stronę + Nie znaleziono aplikacji do otwarcia strony + Powiadomienia heads-up + Pokazuj powiadomienia Heads-up + Dzisiaj + Wczoraj + Potwierdź nazwę hosta za pomocą DNSSEC + Certyfikaty serwera posiadające prawidłową nazwę hosta są uznawane za zweryfikowane + Certyfikat nie zawiera żadnych adresów XMPP + częściowo + Nagraj film + Skopiuj do schowka + Wiadomość skopiowana do schowka + Wiadomość + Prywatne wiadomości są wyłączone + Aplikacje chronione + Aby otrzymywać powiadomienia nawet kiedy ekran jest wyłączony musisz dodać Conversations do listy chronionych aplikacji. + Zaakceptować nieznany certyfikat? + Certyfikat serwera nie jest podpisany przez znany Urząd Certyfikacji. + Czy zaakceptować niepasującą nazwę serwera? + Nie można potwierdzić serwera jako \"%s\". Certyfikat jest ważny tylko dla: + Czy chcesz kontynuować połączenie? + Szczegóły certyfikatu: + Tylko raz + Skaner kodów QR potrzebuje dostępu do aparatu + Przesuń na dół + Przesuń na dół po wysłaniu wiadomości + Edytuj komunikat statusu + Edytuj komunikat statusu + Wyłącz szyfrowanie + %1$s nie mogło wysłać zaszyfrowanej wiadomości do %2$s. Możliwe, że kontakt używa starego serwera lub klienta który nie wspiera OMEMO. + Nie powiodło się pobranie listy urządzeń + Nie powiodło się pobranie kluczy szyfrowania + Podpowiedź: W niektórych przypadkach może pomóc wzajemne dodanie się do listy kontaktów. + Czy na pewno chcesz wyłączyć szyfrowanie OMEMO dla tej konwersacji\? +\nAdministrator twojego serwera będzie mógł czytać twoje wiadomości, ale może to być jedyny sposób aby komunikować się z ludźmi korzystającymi z przestarzałych klientów. + Wyłącz teraz + Szkic: + Szyfrowanie OMEMO + OMEMO będzie zawsze używane w rozmowach 1:1 oraz prywatnych rozmowach grupowych. + OMEMO będzie używane domyślnie dla nowych rozmów. + OMEMO będzie musiało być włączone ręcznie dla nowych rozmów. + Utwórz Skrót + Rozmiar Czcionki + Relatywny rozmiar czcionki używany wewnątrz aplikacji. + Włączone domyślnie + Wyłączone domyślnie + Mała + Średnia + Duża + Wiadomość nie była zaszyfrowana dla tego urządzenia. + Błąd odszyfrowywania wiadomości OMEMO. + cofnij + Udostępnianie lokalizacji jest wyłączone + Zablokuj pozycję + Odblokuj pozycję + Skopiuj lokalizację + Udostępnij lokalizację + Kierunki + Udostępnij lokalizację + Pokaż lokalizację + Udostępnij + Nie można rozpocząć nagrywania + Proszę czekać… + Pozwól %1$s na dostęp do mikrofonu + Wyszukaj wiadomości + GIF + Pokaż konwersację + Wtyczka Udostępniania Lokalizacji + Użyj Wtyczki Udostępniania Lokalizacji zamiast wbudowanej mapy + Skopiuj URL + Skopiuj adres XMPP + Udostępnianie plików przez HTTP S3 + Wyszukiwanie bezpośrednie + Na ekranie \'Rozpocznij konwersację\' otwórz klawiaturę i umieść kursor w polu wyszukiwania + Awatar konwersacji + Serwer nie wspiera awatarów konwersacji + Tylko właściciel może zmienić awatar konwersacji + Nazwa kontaktu + Pseudonim + Nazwa + Nie trzeba podawać nazwy + Nazwa konferencji + Ta konferencja została usunięta + Nie można rozpocząć nagrywania + Usługa na pierwszym planie + Ta kategoria powiadomień jest używana aby wyświetlać stałe powiadomienie oznaczające, że %1$s działa. + Wiadomość Statusu + Problemy z połączeniem + Ta kategoria powiadomień jest używana aby wyświetlać powiadomienia oznaczające, że Conversations ma problemy z połączeniem. + Wiadomości + Połączenia + Wiadomości + Połączenia przychodzące + Połączenia wychodzące + Nieodebrane rozmowy + Ciche wiadomości + Ta kategoria powiadomień jest używana aby wyświetlać powiadomienia które nie powodują żadnych dźwięków. Na przykład w ciągu aktywności na innym urządzeniu (okres karencji). + Nie dostarczone wiadomości + Ustawienia powiadomień wiadomości + Ustawienia powiadomień dla przychodzących połączeń + Ważność, Dźwięk, Wibracja + Kompresja wideo + Pokaż media + Uczestnicy + Przeglądarka mediów + Plik pominięty w związku z naruszeniem bezpieczeństwa. + Jakość wideo + Niższa jakość gwarantuje mniejszy rozmiar + Średnia (360p) + Wysoka (720p) + anulowane + Już tworzysz nową wiadomość. + Funkcja niezaimplementowana + Nieprawidłowy kod kraju + Wybierz kraj + numer telefonu + Zweryfikuj swój numer telefonu + Quicksy wyśle wiadomość SMS (operator może naliczyć opłatę) aby zweryfikować numer telefonu. Wpisz kod kraju i numer telefonu: +
%s

Czy wszystko się zgadza czy też chciałbyś zmienić numer?]]>
+ %s nie jest prawidłowym numerem telefonu. + Proszę wpisać swój numer telefonu. + Przeszukaj kraje + Zweryfikuj %s + %s.]]> + Wysłaliśmy kolejny SMS z 6 cyfrowym kodem. + Proszę wpisać 6-cyfrowy PIN poniżej. + Wyślij SMSa ponownie + Wyślij SMSa ponownie (%s) + Proszę czekać (%s) + wstecz + Automatycznie wklejono prawdopodobny PIN ze schowka. + Proszę wpisać 6-cyfrowy PIN. + Czy na pewno chcesz przerwać procedurę rejestracji? + Tak + Nie + Weryfikowanie… + Prośba o SMS… + PIN który wpisałeś jest nieprawidłowy. + PIN który wysłaliśmy stracił ważność. + Nieznany błąd sieci. + Nieznana odpowiedź serwera. + Nie można połączyć się z serwerem. + Nie można uzyskać bezpiecznego połączenia. + Nie można połączyć się z serwerem. + Wystąpił błąd przy przetwarzaniu twojego żądania. + Nieprawidłowa wartość użytkownika + Tymczasowo niedostępne. Spróbuj później. + Brak połączenia z siecią. + Spróbuj ponownie po %s + Wykorzystałeś limit zapytań + Za dużo prób + Używasz przestarzałej wersji aplikacji. + Aktualizuj + Twój numer telefonu jest aktualnie zalogowany na innym urządzeniu. + Proszę wpisać swoją nazwę aby ludzie którzy mają ciebie w kontaktach wiedzieli kim jesteś. + Twoja nazwa + Wpisz swoją nazwę + Użyj przycisku edycji aby ustawić swoją nazwę. + Odrzuć żądanie + Zainstaluj Orbot + Uruchom Orbot + Aplikacja marketu niezainstalowana. + Ten kanał sprawi, że twój adres XMPP będzie publiczny + e-book + Oryginalne (nieskompresowane) + Otwórz za pomocą… + Obrazek profilowy Conversations + Wybierz konto + Przywróć kopię zapasową + Przywróć + Wpisz swoje hasło do konta %s aby przywrócić kopię zapasową. + Nie używaj kopii zapasowej aby klonować (uruchamiać równolegle) instalację. Przywracanie kopii jest przeznaczone tylko do migracji albo kiedy urządzenie zostało zgubione. + Nie można przywrócić kopii zapasowej. + Nie można odszyfrować kopii zapasowej. Czy hasło jest poprawne? + Kopia i Przywracanie + Wpisz adres XMPP + Nowa rozmowa grupowa + Dołącz do kanału publicznego + Nowa prywatna rozmowa grupowa + Nowy kanał publiczny + Nazwa kanału + Adres XMPP + Podaj nazwę kanału + Podaj adres XMPP + To jest adres XMPP. Podaj nazwę. + Tworzenie kanału publicznego… + Ten kanał już istnieje + Dołączono do istniejącego kanału + Nie można ustawić konfiguracji kanału + Pozwól wszystkim na zmianę tematu + Pozwól wszystkim na zapraszanie innych + Każdy może zmieniać temat. + Właściciele mogą zmieniać temat. + Administratorzy mogą zmieniać temat. + Właściciele mogą zapraszać innych. + Każdy może zapraszać innych. + Adresy XMPP widoczne dla administratorów. + Adresy XMPP widoczne dla wszystkich. + Ten publiczny kanał nie ma uczestników. Zaproś swoje kontakty lub użyj udostępniania aby opublikować adres XMPP. + Ta prywatna rozmowa grupowa nie ma uczestników. + Zarządzaj uprawnieniami + Wyszukaj uczestników + Plik jest zbyt duży + Załącz + Odkryj kanały + Wyszukaj kanał + Możliwe naruszenie prywatności! + search.jabber.network.

Używając tej funkcji twój adres IP oraz kryteria wyszukiwania zostaną wysłane do tej usługi. Sprawdź Politykę Prywatności aby uzyskać więcej informacji.]]>
+ Już mam konto + Dodaj istniejące konto + Zarejestruj nowe konto + To wygląda jak nazwa domeny + Dodaj i tak + To wygląda jak adres kanału + Udostępnij pliki kopii zapasowych + Kopia zapasowa Conversations + Zdarzenie + Otwórz kopię zapasową + Plik, który otworzyłeś, nie jest plikiem kopii zapasowej Conversations + To konto zostało już ustawione + Proszę podać hasło dla tego konta + Nie można wykonać tej akcji + Dołącz do publicznego kanału… + Aplikacja udostępniająca nie udzieliła pozwolenia na dostęp do tego pliku. + + jabber.network + Serwer lokalny + Większość użytkowników powinna wybrać \'jabber.network\' dla lepszych sugestii z całego ekosystemu XMPP. + Metoda odkrywania kanałów + Kopia zapasowa + O aplikacji + Proszę włączyć konto + Zadzwoń + Połączenie przychodzące + Wideorozmowa przychodząca + Przełączyć na rozmowę wideo? + Włączyć dodatkowe ścieżki? + Łączenie + Połączony + Ponowne łączenie + Akceptowanie połączenia + Kończenie połączenia + Połącz + Odrzuć + Wyszukiwanie urządzeń + Dzwonienie + Zajęty + Nie można wykonać połączenia + Utracono połączenie + Anulowane połączenie + Błąd aplikacji + Problem z weryfikacją + Rozłącz + Połączenie wychodzące + Wideorozmowa wychodząca + Ponowne łączenie rozmowy + Ponowne łączenie rozmowy wideo + Wyłącz Tor aby dzwonić + Połączenie przychodzące + Połączenie przychodzące · %s + Nieodebrane połączenie · %s + Połączenie wychodzące + Połączenie wychodzące · %s + Nieodebrane połączenie + + %1$d nieodebrana rozmowa od %2$s + %1$d nieodebrane rozmowy od %2$s + %1$d nieodebranych rozmów od %2$s + %1$d nieodebranych rozmów od %2$s + + + %d nieodebrana rozmowa + %d nieodebrane rozmowy + %d nieodebranych rozmów + %d nieodebranych rozmów + + + %1$d nieodebrana rozmowa od %2$d kontaktu + %1$d nieodebrane rozmowy od %2$d kontaktu + %1$d nieodebranych rozmów od %2$d kontaktów + %1$d nieodebranych rozmów od %2$d kontaktów + + Połączenie audio + Połączenie wideo + Pomoc + Przełącz do rozmowy + Twój mikrofon jest niedostępny + Możesz mieć tylko jedno połączenie na raz. + Powróć do trwającego połączenia + Nie można zmienić aparatu + Przypnij + Odepnij + Ścieżka GPX + Nie można poprawić wiadomości + Wszystkie rozmowy + Ta rozmowa + Twój awatar + Awatar dla %s + Zaszyfrowane OMEMO + Zaszyfrowane OpenPGP + Niezaszyfrowane + Wyjście + Zapisz pocztę głosową + Odtwórz audio + Spauzuj audio + Dodaj kontakt, stwórz lub dołącz do rozmowy grupowej lub odkryj kanały + + Pokaż %1$d uczestnika + Pokaż %1$d uczestników + Pokaż %1$d uczestników + Pokaż %1$d uczestników + + + Wiadomość nie mogła zostać dostarczona + Niektóre wiadomości nie mogły być dostarczone + Niektóre wiadomości nie mogły być dostarczone + Niektóre wiadomości nie mogły być dostarczone + + Nie dostarczone wiadomości + Więcej ustawień + Nie znaleziono żadnej aplikacji + Zaproś do Conversations + Nie można przetworzyć zaproszenia + 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 + Rejestracja kont nie jest wspierana + Nie znaleziono adresu XMPP + Tymczasowy błąd uwierzytelniania + Usuń awatar + Dzwonienie jest wyłączone podczas używania Tora + Przełącz na wideo + Odrzuć prośbę przełączenia na wideo + Dystrybutor UnifiedPush + Konto XMPP + Konto, poprzez które będą odbierane powiadomienia push. + Serwer push + Dowolnie wybrany serwer push do przekazywania wiadomości push przez XMPP na Twoje urządzenie. + Brak (nieaktywne) +
\ No newline at end of file diff --git a/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml similarity index 100% rename from src/main/res/values-pt-rBR/strings.xml rename to app/src/main/res/values-pt-rBR/strings.xml diff --git a/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt/strings.xml similarity index 100% rename from src/main/res/values-pt-rPT/strings.xml rename to app/src/main/res/values-pt/strings.xml diff --git a/src/main/res/values-ro-rRO/strings.xml b/app/src/main/res/values-ro-rRO/strings.xml similarity index 100% rename from src/main/res/values-ro-rRO/strings.xml rename to app/src/main/res/values-ro-rRO/strings.xml diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml new file mode 100644 index 000000000..ddc23d005 --- /dev/null +++ b/app/src/main/res/values-ru/strings.xml @@ -0,0 +1,994 @@ + + + Настройки + Новая беседа + Управление аккаунтами + Управление аккаунтом + Закрыть беседу + Сведения о контакте + Подробности конференции + Сведения о канале + Добавить аккаунт + Редактировать контакт + Добавить в адресную книгу + Удалить из списка + Заблокировать контакт + Разблокировать контакт + Заблокировать домен + Разблокировать домен + Заблокировать участника + Разблокировать участника + Управление аккаунтами + Настройки + Поделиться + Начать беседу + Выберите контакт + Выберите контакты + Поделиться через аккаунт + Чёрный список + только что + 1 минуту назад + %d мин. назад + + %d непрочитанная беседа + + + %d непрочитанные беседы + + + %d непрочитанных бесед + + + %d непрочитанных бесед + + + отправка… + Расшифровка сообщения. Подождите… + OpenPGP зашифр. сообщение + Имя уже используется + Некорректный никнейм + Администратор + Владелец + Модератор + Участник + Посетитель + Вы хотите удалить %s из своего списка контактов? Беседы, связанные с этим контактом, будут сохранены. + Вы хотите заблокировать дальнейшие сообщения от %s? + Вы хотите разблокировать пользователя %s? + Заблокировать всех пользователей домена %s? + Разблокировать всех пользователей домена %s? + Контакт заблокирован + Заблокирован + Вы хотите удалить %s из избранного? Беседы, связанные с данной закладкой, будут сохранены. + Создать новый аккаунт на сервере + Изменить пароль на сервере + Поделиться с + Начать беседу + Пригласить контакт + Пригласить + Контакты + Контакт + Отмена + Установить + Добавить + Редактировать + Удалить + Заблокировать + Разблокировать + Сохранить + ОК + В %1$s произошёл сбой + Отправляя отчёты об ошибках, вы помогаете совершенствованию %1$s. + Отправить сейчас + Больше не спрашивать + Не удалось подключиться к учетной записи + Не удалось подключиться к учетным записям + Нажмите, чтобы настроить учетные записи + Прикрепить файл + Контакт не находится в вашем списке контактов. Хотите добавить его? + Добавить контакт + доставка не удалась + Подготовка к передаче изображения + Подготовка к передаче изображений + Обмен файлами. Пожалуйста, подождите… + Очистить историю + Очистить историю + Вы хотите удалить все сообщения в этой беседе?\n\nВнимание: Данная операция не повлияет на сообщения, хранящиеся на других устройствах или серверах. + Удалить файл + Вы уверены, что хотите удалить этот файл?\n\nПредупреждение: Данная операция не удалит копии этого файла, хранящиеся на других устройствах или серверах. + Закрыть эту беседу + Выберите устройство + Нешифрованное сообщение + Сообщение + Сообщение для %s + OMEMO-зашифр. сообщение + v\\OMEMO-зашифр. сообщение + OpenPGP зашифр. сообщение + Имя уже используется + Отправить в незашифрованном виде + Расшифровка не удалась. Вероятно, что у вас нет надлежащего ключа. + Установите OpenKeychain + OpenKeychain для шифрования и дешифрования сообщений и управления открытыми ключами.

OpenKeychain распространяется под лицензией GPLv3+ и доступна для загрузки через F-Droid или Google Play.

(Потребуется перезапуск %1$s после установки.)]]>
+ Перезапуск + Установка + Пожалуйста, установите OpenKeychain + предложение… + ожидание… + Нет OpenPGP ключа + Conversations не может зашифровать сообщение, потому что ваш собеседник не анонсирует свой открытый ключ.\n\nПожалуйста, попросите вашего собеседника настроить OpenPGP. + Нет OpenPGP ключей + Conversations не может зашифровать сообщение, потому что ваши собеседники не анонсируют свои открытые ключи.\n\nПожалуйста, попросите ваших собеседников настроить OpenPGP. + Общие + Принимать файлы + Автоматический приём файлов… + Вложения + Уведомление + Вибрация + Вибрировать, когда приходят новые сообщения + Светодиодное уведомление + Мерцание индикатора при получении нового сообщения + Мелодия звонка + Звук уведомления + Звук уведомления о новых сообщениях + Мелодия входящего звонка + Грейс-период + Время, на которое уведомления будут отключены, когда вы пользуетесь аккаунтом на другом устройстве. + Дополнительно + Не отправлять отчёты об ошибках + Отправляя отчеты об ошибках, вы помогаете разработке этого приложения + Отчёты о получении + Позволяет вашим контактам видеть, когда вы получили и прочитали их сообщения + Запретить скриншоты + Прятать содержимое приложения при переключении приложений и запретить скриншоты + Интерфейс + OpenKeychain вызвал ошибку. + Неподходящий ключ для шифрования. + Принять + Произошла ошибка + Ошибка + Ваш аккаунт + Отправлять присутствие + Получать присутствие + Запрашивать присутствие + Выбрать изображение + Сделать снимок + Удовлетворять запросы на подписки + Выбранный файл не является изображением + Не удалось конвертировать изображение + Файл не найден + Общая ошибка ввода/вывода. Возможно, на устройстве недостаточно свободного места? + У приложения, которым вы выбрали это изображение, недостаточно прав, чтобы прочитать этот файл.\n\nПожалуйста, используйте другой файловый менеджер, чтобы выбрать это изображение. + Приложение, которое вы использовали для публикации этого файла, не предоставило достаточно разрешений. + Неизвестен + Временно отключён + В сети + Соединение\u2026 + Не в сети + Неавторизован + Сервер не найден + Нет подключения к сети + Регистрация не удалась + Имя пользователя уже используется + Регистрация завершена + Сервер не поддерживает возможность регистрации + Неправильный токен регистрации + Не удалось согласовать TLS + Домен не поддается проверке + Нарушение правил + Несовместимый сервер + Ошибка потока + Ошибка открытия потока + Без шифра + OTR + OpenPGP + OMEMO + Удалить аккаунт + Временно отключить + Разместить аватар + Анонсировать OpenPGP ключ + Удалить открытый ключ OpenPGP + Вы действительно хотите удалить ваш OpenPGP публичный ключ из опубликованных?\nВаши собеседники не смогут больше отправлять вам зашифрованные OpenPGP сообщения. + Публичный ключ OpenPGP опубликован. + Включить аккаунт + Вы уверены? + Удаление аккаунта также удалит всю историю вашей переписки + Запись голоса + XMPP-адрес + Заблокировать XMPP-адрес + username@example.com + Пароль + Недопустимый XMPP-адрес + Нехватка памяти. Изображение слишком большое + Вы хотите добавить %s в вашу адресную книгу? + Информация о сервере + XEP-0313: Архив сообщений + XEP-0280: Дублиров. сообщений + XEP-0352: Состояние клиента + XEP-0191: Команда блокирования + XEP-0237: Версии списков + XEP-0198: Управление потоками + XEP-0215: Обнаружение внешних служб + XEP-0163: PEP (Аватары / OMEMO) + XEP-0363: Загрузка по HTTP + XEP-0357: Push-уведомления + доступно + недоступно + Отсутствие анонсирования открытых ключей + Присутствие: только что + Присутствие: одну минуту назад + Присутствие: %d мин. назад + Присутствие: один час назад + Присутствие: %d час. назад + Присутствие: один день назад + Присутствие: %d дн. назад + Зашифрованное сообщение. Пожалуйста, установите OpenKeychain для расшифровки. + Найдены новые OpenPGP зашифрованые сообщения + ID OpenPGP ключа + OMEMO-отпечаток + v\\OMEMO-отпечаток + OMEMO-отпечаток (выбранного сообщения) + v\\OMEMO-отпечаток (выбранного сообщения) + Другие устройства + Доверенные отпечатки OMEMO + Получение ключей… + Готово + Расшифровать + Закладки + Поиск + Добавить контакт + Удалить контакт + Посмотреть данные контакта + Заблокировать контакт + Разблокировать контакт + Создать + Выбрать + Контакт уже существует + Присоединиться + канал@конференция.пример.com/никнейм + канал@конференция.пример.com + Сохранить закладку + Удалить закладку + Уничтожить конференцию + Уничтожить канал + Вы уверены, что хотите распустить эту конференцию?\n\nПредупреждение:Конференция будет полностью удалена с сервера. + Вы уверены, что хотите закрыть этот публичный канал?\n\nПредупреждение: Канал будет полностью удален с сервера. + Не удалось уничтожить конференцию + Не удалось уничтожить канал + Редактировать тему конференции + Тема + Вход в конференцию… + Покинуть + Собеседник добавил вас в список контактов + Добавить в ответ + %s прочит. сообщ. до этого момента + %s прочитали сообщения до этого момента + %1$s + ещё %2$d прочитали до этого места + Все прочитали сообщения до этого момента + Опубликовать + Нажмите на аватар, чтобы выбрать новую фотографию из галереи + Установка… + Сервер отклонил размещение аватара + Не удалось преобразовать вашу фотографию + Не удалось сохранить аватар + (Или долгое прикосновение, чтобы вернуть значения по умолчанию) + Ваш сервер не поддерживает публикацию аватаров + шёпот + отправить %s + Приватное сообщение %s + Подключиться + Аккаунт уже существует + Далее + Сеанс установлен + Пропустить + Отключить уведомления + Включить + Конференция требует авторизации + Введите пароль + Пожалуйста, сначала запросите обновления присутствия у вашего собеседника.\n\nЭта информация будет использоваться для определения того, каким клиентом пользуется ваш собеседник. + Запросить сейчас + Игнорировать + Внимание: Если обновления присутствия не включены на обеих сторонах, это может привести к возникновению неожиданных проблем.\n\nПросмотрите сведения о контакте для проверки настроек обновлений присутствия. + Безопасность + Разрешить исправление сообщений + Позволить контактам редактировать сообщения + Расширенные настройки + Пожалуйста, будьте осторожны с данными настройками + О %s + Тихие часы + Начало + Окончание + Включить режим «тихих часов» + Уведомления будут отключены во время «тихих часов» + Другие + OMEMO-отпечаток скопирован в буфер обмена + Вы заблокированы в этой конференции + Эта конференция — только для участников + Ресурсное ограничение + Вас выгнали из этой конференции + Конференция была остановлена + Вы больше не состоите в этой конференции + используется аккаунт %s + размещено на %s + Проверка %s на сервере HTTP + Вы неподключены. Попробуйте позже + Проверить размер (%s) + Проверить размер на %2$s (%1$s) + Опции сообщения + Цитировать + Вставить как цитату + Копировать адрес ссылки + Отправить ещё раз + URL файла + Ссылка скопирована в буфер обмена + XMPP-адрес скопирован в буфер обмена + Сообщение об ошибке скопировано в буфер обмена + веб-адрес + Сканировать 2D штрихкод + Показать 2D штрихкод + Показать чёрный список + Сведения об учётной записи + Подтвердить + Повторить + Процесс переднего плана + Не позволяет операционной системе закрыть ваше соединение + Создать резервную копию + Файлы резервной копии будут сохранены в %s + Создание резервной копии + Ваша резервная копия была создана + Файлы резервной копии сохранены в %s + Восстановление из резервной копии + Восстановление из резервной копии выполнено + Не забудьте включить аккаунт + Выбрать файл + %1$s загружается (%2$d%% выполнено) + Загрузить %s + Удалить %s + файл + Открыть %s + отправка (%1$d%% выполнено) + Файл готовится для передачи + %s предлагается скачать + Отменить передачу + передача файла не удалась + передача файла отменена + Файл был удалён + Не найдено приложения для открытия файла + Не найдено приложения, способного открыть эту ссылку + Не найдено приложения для просмотра контакта + Динамические тэги + Отображать теги только для чтения под контактами + Включить уведомления + Сервер конференции не найден + Не удалось создать конференцию + Аватар аккаунта + Скопировать OMEMO-отпечаток в буфер обмена + Создать ключ OMEMO заново + Очистить устройства + Вы уверены, что хотите очистить все остальные устройства из анонса ключей OMEMO? При соединении устройств в следующий раз новые ключи анонсируются автоматически, но устройства могут не получить сообщения, посланные до этого. + Для этого контакта нет доступных ключей.\nНе удалось получить новые ключи от сервера. Возможно, что-то не так с сервером вашего собеседника. + Нет доступных ключей для данного контакта.\nУбедитесь, что у вас обоих есть подписка на присутствие. + Что-то пошло не так + Получение истории с сервера + На сервере больше нет истории + Обновление… + Пароль изменён! + Не удалось изменить пароль + Изменить пароль + Текущий пароль + Новый пароль + Пароль не может быть пустым + Включить все аккаунты + Отключить все аккаунты + Взаимодействовать с + Посетитель + Не в сети + Заблокирован + Участник + Расширенный режим + Предоставить права участника + Снять права участника + Назначить администратором + Снять административные права + Назначить администратором + Снять права администратора + Убрать из конференции + Исключить + Не удалось изменить принадлежность %s + Заблокировать в конференции + Заблокировать + Вы пытаетесь исключить %s из публичного канала. Единственный способ это сделать — навсегда заблокировать этого пользователя. + Заблокировать + Не удалось сменить роль %s + Настройки приватной конференции + Настройки публичного канала + Приватная + Сделать XMPP адрес видимым для всех + Сделать канал модерируемым + Вы не участвуете + Настройки конференции изменены! + Не удалось изменить настройки конференции + Никогда + Пока не включу + Повтор + Ответить + Прочитано + Ввод + Отправка по \"Enter\" + Отправлять сообщения клавишей Enter. Даже если эта опция отключена, сообщение можно отправить, нажав Ctrl+Enter. + Показывать клавишу ввода + Поменять кнопку смайликов на кнопку ввода + аудио + видео + изображение + векторная графика + PDF-документ + Приложение Android + Контакт + Аватар загружен! + Отправляется %s + Предложен %s + Скрыть пользователей вне сети + %s печатает… + %s прекратил набор + %s печатают… + %s перестали печатать + Оповещения о наборе + Позволяет вашим контактам видеть, когда вы пишете им новое сообщение + Отправить местоположение + Показать местоположение + Не найдено приложения для отображения местоположения + Местоположение + Беседа окончена + Покинул приватную конференцию + Покинул публичный канал + Не доверять системным УЦ + Все сертификаты должны быть подтверждены вручную + Удалить сертификаты + Удалить сертификаты, подтверждённые вручную + Не найдено сертификатов, подтверждённых вручную + Удалить сертификаты + Удалить отмеченные + Отмена + + Удалён %d сертификат + Удалено %d сертификата + Удалено %d сертификатов + Удалено %d сертификатов + + Заменить кнопку \"Отправить\" кнопкой быстрого действия + Быстрое действие + Нет + Последнее выбранное + Выбрать быстрое действие + Поиск контактов + Поиск закладок + Отправить личное сообщение + %1$s покинул конференцию + Имя пользователя + Имя пользователя + Недопустимое имя пользователя + Загрузка не удалась: сервер не найден + Загрузка не удалась: файл не найден + Загрузка не удалась: не удалось подключиться к серверу + Загрузка не удалась: ошибка записи файла + Сеть Tor недоступна + Ошибка связывания + Сервер не ответственен за этот домен + Повреждено + Доступность + \"Отошёл\" когда экран заблокирован + Устанавливает статус \"Отошёл\", когда устройство заблокировано + \"Не беспокоить\" в беззвучном режиме + Устанавливает статус \"Не беспокоить\", когда устройство в беззвучном режиме + Не доступен в режиме вибрации + Устанавливает статус \"Не беспокоить\", когда устройство в режиме вибрации + Расширенные настройки подключения + Показывать имя сервера и порт в настройках аккаунтов + xmpp.example.com + Авторизироваться с помощью сертификата + Не удалось прочитать сертификат + Настройки архивирования + Настройки архивирования на сервере + Получение настроек архивирования. Пожалуйста, подождите… + Не удалось получить настройки архивирования + Необходима проверка CAPTCHA + Введите текст с изображения + Ненадежная цепь сертификатов + XMPP-адрес не соответствует сертификату + Обновить сертификат + Ошибка при получении OMEMO-ключа! + Ключ OMEMO проверен с сертификатом! + Ваше устройство не поддерживает выбор клиентских сертификатов! + Подключение + Соединение через Tor + Направить все соединения через сеть Tor. Требуется Orbot + Имя сервера + Порт + Сервер- или .onion-адрес + Это недопустимый номер порта + Это недопустимое имя сервера + %1$d из %2$d аккаунтов соединены + + %d сообщение + %d сообщения + %d сообщений + %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 является участником конференции. + Вы отключили этот аккаунт + Ошибка безопасности: недействительный доступ к файлу + Не найдено приложения для передачи URI + Отправить URI… +
После авторизации по номеру телефона Quicksy автоматически, основываясь на вашей адресной книге, предложит добавить возможные контакты.

Регистрируясь, вы соглашаетесь с нашей политикой конфиденциальности.]]>
+ Согласиться и продолжить + Мы поможем Вам создать аккаунт на conversations.im¹.\nВыбрав conversations.im в качестве провайдера, вы сможете общаться с пользователями других провайдеров, сообщив им свой полный XMPP-адрес. + Ваш полный XMPP-адрес будет: %s + Создать аккаунт + Использовать свой провайдер + Выберите имя пользователя + Управлять доступностью вручную + Устанавливать свою доступность при редактировании статусного сообщения + Статусное собщение + Свободен для общения + В сети + Отошёл + Недоступен + Занят + Пароль был сгенерирован + Ваше устройство не поддерживает отключение оптимизации энергопотребления + Регистрация не удалась: повторите попытку позднее + Регистрация не удалась: слишком слабый пароль + Выбрать участников + Создание конференции… + Пригласить ещё раз + Выключен + Короткий + Средний + Длинный + Оповещать других об использовании + Позволяет вашим контактам видеть, когда вы используете Conversations + Приватность + Тема + Выбрать цветовую палитру + Автоматически + Светлая + Темная + Зелёный фон + Использовать зелёный фон для полученных сообщений + Не удалось подключиться к OpenKeyChain + Данное устройство больше не используется + Компьютер + Телефон + Планшет + Веб-браузер + Консоль + Требуется оплата + Предоставить доступ к Интернету + Я + Контакт запрашивает подписку + Разрешить + Нет доступа к %s + Удалённый сервер не найден + Время ожидания удаленного сервера истекло + Не удалось обновить учетную запись + Отправить жалобу на спам от этого XMPP-адреса. + Удалить OMEMO-ключи + Создать заново OMEMO-ключи. Вашим контактам потребуется повторно подтвердить ваши ключи. Используйте только в крайнем случае. + Удалить отмеченные + Вы должны подключиться для публикации аватара. + Показать текст ошибки + Текст ошибки + Режим экономии трафика включен + Ваша операционная система не позволяет %1$s получать доступ в Интернет в фоновом режиме. Для получения уведомлений вы должны дать %1$s неограниченный доступ в режиме экономии трафика.\n%1$s постарается экономить трафик по возможности. + Ваше устройство не поддерживает отключение режима экономии трафика для %1$s. + Не удалось создать временный файл + Это устройство было подтверждено + Копировать отпечаток + Все имеющиеся у вас OMEMO-ключи были подтверждены + Штрих-код не содержит цифрового отпечатка для этой беседы. + Подтверждённые отпечатки + Используйте камеру для сканирования штрихкода контакта + Подождите получения ключей + Отправить штрихкод + Отправить XMPP URI + Отправить HTTP ссылку + Слепое доверие перед подтверждением + Автоматически доверять всем новым устройствам контактов, которые не были подтверждены ранее, но запрашивать ручное подтверждение каждый раз, когда подтвержденный контакт добавляет новое устройство. + Принятие OMEMO-ключей вслепую. Это означает, что собеседник может оказаться недоверенным лицом. + Недоверенный + Некорректный 2D штрихкод + Очистить кэш (используется камерой) + Очистить кэш + Очистить приватное хранилище. + Очистить закрытое хранилище, где хранятся файлы (Файлы можно заново скачать с сервера) + Открывать ссылки из надёжного источника + Вы подтверждаете OMEMO-ключи %1$s после нажатия на ссылку. Это безопасно только если вы перешли по ссылке из доверенного источника, где только %2$s мог разместить эту ссылку. + Проверить OMEMO-ключи + Показывать неактивные + Скрыть неактивные + Прекратить доверять устройству + Вы действительно хотите удалить устройство из доверенных?\Устройство и сообщения, полученные с этого устройства, будут помечаться как недоверенные. + + %d секунда + %d секунды + %d секунд + %d секунд + + + %d минута + %d минуты + %d минут + %d минут + + + %d час + %d часа + %d часов + %d часов + + + %d день + %d дня + %d дней + %d дней + + + %d неделя + %d недели + %d недель + %d недель + + + %d месяц + %d месяца + %d месяцев + %d месяцев + + Автоматическое удаление сообщений + Автоматически удалять сообщения с этого устройства по прошествии заданного времени. + Зашифровать сообщение + Не загружаем сообщения, в соответствии с локальным сроком хранения. + Сжимание видео + Соответствующие беседы закрыты. + Контакт заблокирован + Уведомления от неизвестных контактов + Уведомлять о сообщениях и звонках от незнакомых контактов. + Получено сообщение от неизвестного контакта + Заблокировать неизвестный контакт + Заблокировать весь домен + сейчас онлайн + Повторить расшифровку + Сбой сеанса + Устарелый механизм SASL + Сервер требует регистрации на сайте + Открыть сайт + Не найдено приложения, способного открыть этот веб-сайт + Экранные уведомления + Показывать экранные уведомления + Сегодня + Вчера + Проверить имя сервера с помощью DNSSEC + Серверные сертификаты, содержащие проверенное имя хоста, считаются проверенными + Сертификат не содержит XMPP-адрес + частичный + Записать видео + Скопировать в буфер обмена + Сообщение скопировано в буфер обмена + Сообщение + Личные сообщения выключены + Защищенные приложения + Чтобы продолжать получать уведомления, даже если экран выключен, вам необходимо добавить Conversations в список защищенных приложений. + Принять Неизвестный Сертификат? + Этот сертификат сервера не подписан ни одним из известных центров сертификации. + Принять несовпадающее имя сервера? + Серверу не удалось аутентифицироваться в качестве \"%s\". Сертификат подходит только для: + Вы все равно хотите подключиться? + Детали сертификата: + Один раз + Сканеру QR-кода необходим доступ к камере + Прокручивать вниз + Прокручивать вниз после отправки сообщения + Редактировать статусное сообщение + Редактировать статусное сообщение + Отключить шифрование + %1$s не удалось отправить зашифрованные сообщения для %2$s. Причиной этому может быть использование получателем устаревшего сервера или клиента, которые не поддерживают OMEMO. + Не удалось получить список устройств + Не удалось получить ключи шифрования + Подсказка: в некоторых случаях это может исправлено добавлением друг друга в список контактов. + Вы уверены, что хотите выключить OMEMO-шифрование для этой беседы?\nЭто позволит администратору сервера читать ваши сообщения, но также это может быть единственным способом связи с людьми, использующими устаревшие клиенты. + Отключить сейчас + Черновик: + OMEMO-шифрование + OMEMO будет всегда использоваться для одиночных бесед и закрытых конференций. + OMEMO будет использоваться по умолчанию для новых бесед. + OMEMO нужно будет явно включать для новых бесед. + Создать ярлык + Размер шрифта + Относительный размер шрифта, используемый в приложении. + Включено по умолчанию + Выключено по умолчанию + Маленький + Средний + Большой + Сообщение не зашифровано для этого устройства. + Не удалось расшифровать OMEMO-сообщение. + отменить + Обмен информацией о местонахождении отключен + Закрепить позицию + Открепить позицию + Копировать местоположение + Поделиться местоположением + Направления + Поделиться местоположением + Показать местоположение + Поделиться + Не удалось начать запись + Пожалуйста, подождите… + Предоставить %1$s разрешение на использование микрофона + Поиск сообщений + GIF + Посмотреть беседу + Расширение для обмена информацией о местонахождении + Используйте расширение для обмена информацией о местонахождении вместо встроенной карты + Копировать веб-адрес + Копировать XMPP-адрес + Файлообмен по HTTP для S3 + Быстрый поиск + На экране \"Начать беседу\" открывать клавиатуру и ставить курсор в поле поиска + Аватар конференции + Сервер не поддерживает наличие аватар у конференций + Только владелец может менять аватар конференции + Имя контакта + Никнейм + Название + Предоставление имени необязательно + Название конференции + Эта конференция была уничтожена + Не удалось сохранить запись + Процесс переднего плана + Эта категория уведомлений используется для отображения постоянного уведомления о том, что %1$s запущен. + Информация о статусе + Проблемы с подключением + Эта категория уведомлений используется для отображения оповещений, в случае если есть проблема с соединением. + Сообщения + Вызовы + Сообщения + Входящие вызовы + Активные вызовы + Тихие сообщения + Эта группа уведомлений используется для отображения беззвучных оповещений. Например, при активности на другом устройстве (Грейс-период). + Недоставленные сообщения + Настройки уведомлений о сообщениях + Настройки уведомлений о входящих вызовах + Приоритет, звук, вибрация + Сжатие видео + Просмотр медиа + Участники + Просмотр медиафайлов + Файл не прикреплен из соображений безопасности. + Качество видео + Чем ниже качество, тем меньше объем файлов + Среднее (360p) + Высокое (720р) + отменено + Вы уже пишите черновик сообщения. + Функция не реализована + Неверный код страны + Выберите страну + номер телефона + Проверьте ваш номер телефона + Quicksy отправит SMS (оператором может взиматься абонентская плата) для проверки вашего номера телефона. Введите код страны и номер телефона: +
%s

Продолжить или вы желаете изменить номер?]]>
+ %s не является корректным номером телефона. + Пожалуйста, введите ваш номер телефона. + Поиск стран + Подтвердите %s + %s.]]> + Мы отправили вам еще одну SMS с кодом из 6 цифр. + Пожалуйста, введите код из 6 цифр ниже. + Отправьте заново SMS + Отправьте заново SMS (%s) + Пожалуйста, подождите (%s) + назад + Автоматически вставлен возможный код из буфера обмена. + Пожалуйста, введите ваш код из 6 цифр. + Вы уверены, что хотите прервать процедуру регистрации? + Да + Нет + Подтверждение… + Запрос SMS… + Введенный вами код некорректен. + Отправленный вам код просрочен. + Неизвестная ошибка сети. + Неизвестный ответ от сервера. + Не удалось подключиться к серверу. + Не удалось установить безопасное соединение. + Не удалось найти сервер. + Что-то пошло не так с обработкой вашего запроса. + Некорректный ввод + Временно недоступно. Попробуйте снова позже. + Нет подключения к сети. + Пожалуйста, попробуйте еще раз через %s + У вас есть ограничение скорости + Слишком много попыток + Вы используете устаревшую версию приложения + Обновить + Этот номер телефона в данный момент авторизирован на другом устройстве. + Пожалуйста, введите ваше имя, чтобы другие люди, у которых нет вас в списке контактов, знали кто вы. + Ваше имя + Введите ваше имя + Используйте кнопку редактирования, чтобы задать ваше имя. + Отклонить запрос + Установите Orbot + Запустите Orbot + Не установлен магазин приложений. + Этот канал сделает ваш XMPP-адрес публичным + Электронная книга + Оригинал (без сжатия) + Открыть с помощью… + Картинка профиля Conversations + Выбрать аккаунт + Восстановить из резервной копии + Восстановить + Введите пароль учетной записи %s для восстановления резервной копии. + Не используйте восстановление резервной копии для дублирования установленного приложения (одновременного исполнения). Восстановление резервной копии нужно лишь для того, чтобы перенести данные на другое устройство или на случай потери своего устройства. + Не удалось восстановить резервную копию. + Не удалось расшифровать резервную копию. Вы ввели верный пароль? + Резервное копирование и восстановление + Введите XMPP-адрес + Создать конференцию + Присоединиться к каналу + Создать закрытую конференцию + Создать публичный канал + Название канала + XMPP-адрес + Пожалуйста, предоставьте имя для канала + Пожалуйста, предоставьте XMPP-адрес + Это XMPP-адрес. Пожалуйста, предоставьте имя. + Создание публичного канала… + Этот канал уже существует + Вы присоединились к существующему каналу + Не удалось сохранить настройки канала + Разрешить всем редактировать тему. + Разрешить всем приглашать других + Кто угодно может редактировать тему. + Владельцы могут редактировать тему. + Администраторы могут редактировать тему. + Владельцы могут приглашать других. + Кто угодно может приглашать других. + XMPP-адреса видимы для администраторов. + XMPP-адреса видимы для всех. + У этого публичного канала нет участников. Пригласите ваших знакомых или нажмите на кнопку \"Поделиться\", чтобы отправить XMPP-адрес. + У этой приватной конференции нет участников. + Управление правами + Поиск участников + Объем файла слишком велик + Прикрепить + Найти каналы + Поиск каналов + Возможно нарушение конфиденциальности! + search.jabber.network.

Использование этого сервиса требует передачи вашего IP-адреса и поисковых запросов. Для дополнительной информации смотрите Политику конфиденциальности.]]>
+ У меня уже есть аккаунт + Добавить существующий аккаунт + Зарегистрировать новую учетную запись + Это похоже на имя домена + Добавить все равно + Это похоже на адрес канала + Поделиться резервными копиями + Резервная копия Conversations + Событие + Открыть резервную копию + Выбранный вами файл не является файлом резервной копии Conversations + Эта учетная запись уже настроена + Пожалуйста, введите пароль этой учетной записи + Не удалось совершить это действие + Присоединиться к публичному каналу… + Приложение для обмена не предоставило право доступа к этому файлу. + + jabber.network + Локальный сервер + Большиству пользователей следует выбрать ‘jabber.network’ для получения наиболее подходящих предложений от всей публичной экосистемы XMPP. + Способ поиска каналов + Резервное копирование + О приложении + Пожалуйста, активируйте учетную запись + Позвонить + Входящий вызов + Входящий видеовызов + Соединение + Соединение установлено + Принятие вызова + Завершение вызова + Ответить + Отклонить + Поиск устройств + Вызов + Занято + Не удалось установить соединение + Соединение потеряно + Вызов отменён + Ошибка приложения + Завершить + Активный вызов + Активный видеовызов + Отключите Tor для совершения звонков + Входящий вызов + Входящий вызов · %s + Пропущенный вызов · %s + Исходящий вызов + Исходящий вызов · %s + Пропущен вызов + Аудиовызов + Видеовызов + Помощь + Переключиться на беседу + Микрофон недоступен + Нельзя одновременно совершать больше одного вызова. + Вернуться к текущему вызову + Не удалось переключить камеру + Прикрепить + Открепить + GPX-трек + Не удалось исправить сообщение + Все беседы + Эта беседа + Ваш аватар + Аватар для %s + Зашифровано с помощью OMEMO + Зашифровано с помощью OpenPGP + Не зашифровано + Выйти + Записать голосовое сообщение + Воспроизвести аудио + Остановить воспроизведение + Добавить контакт, создать или присоединиться к конференции, или найти каналы + + Просмотр %1$d участника + Просмотр %1$d участников + Просмотр %1$d участников + Просмотр %1$d участников + + + Не удалось доставить сообщение + Не удалось доставить сообщения + Не удалось доставить сообщения + Не удалось доставить некоторые сообщения + + Недоставленные сообщения + Ещё + Не найдено приложения + Пригласить в Conversations + Невозможно разобрать приглашение + Сервер не поддерживает создание приглашений + Ни один активный аккаунт не поддерживает эту функцию + Резервное копирование было начато. Вы получите уведомление, как только оно будет завершено. + Невозможно включить видео. + Текстовые данные +
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml new file mode 100644 index 000000000..e63f51f24 --- /dev/null +++ b/app/src/main/res/values-sk/strings.xml @@ -0,0 +1,528 @@ + + + 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ť + Nastaviť + Pridať + Upraviť + Vymazať + Zablokovať + 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 + Požiadať o zmeny stavu + Vybrať obrázok + 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 + Pripájam\u2026 + Offline + Neschválený + Server sa nenašiel + Žiadne pripojenie + 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 + OpenPGP + OMEMO + Vymazať účet + 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 + XEP-0352: Client State Indication + 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ší + 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 + 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... + Heslo zmenené! + Nepodarilo sa zmeniť heslo + Zmeniť heslo + Aktuálne heslo + Nové heslo + Povoliť všetky účty + Vypnúť všetky účty + Uskutočniť akciu s + Nepridružený + Offline + Vylúčený + Člen + Rozšírený režim + Povoliť administrátorské výsady + Odobrať administrátorské výsady + Nepodarilo sa zmeniť pripojenie užívateľa %s + Vylúčiť + Nepodarilo sa zmeniť úlohu %s + Nezúčastňujete sa + Nikdy + Až do odvolania + Enter odosiela + Zobraziť klávesu enter + Zmeniť klávesu s emotikonmi na klávesu enter + audio + video + obrázok + dokument PDF + Android App + Kontakt + Avatar sa publikoval! + 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 + Konverzácia zatvorená + Nedôverovať systému CAs + Všetky certifikáty musia byť ručne schválené + Odstrániť certifikáty + Vymazať ručne schválené certifikáty + Žiadne ručne schválené certifikáty + Odstrániť certifikáty + Vymazať výber + Zrušiť + + %d certifikátu vymazaných + %d certifikátu vymazaných + %d certifikátov vymazaných + %d certifikátov vymazaných + + Rýchla akcia + Žiadny + Naposledy použitý + Vybrať rýchlu voľbu + Poslať súkromnú správu + 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/app/src/main/res/values-sq-rAL/strings.xml b/app/src/main/res/values-sq-rAL/strings.xml new file mode 100644 index 000000000..9ffcd8727 --- /dev/null +++ b/app/src/main/res/values-sq-rAL/strings.xml @@ -0,0 +1,1006 @@ + + + Bisedë e re + Administroni llogari + Administroni llogari + Mbylle bisedën + Hollësi kontakti + Hollësi fjalosjeje grupi + Hollësi kanali + Shtoni llogari + Përpunoni emër + Shtoje te libër adresash + Bllokojeni kontaktin + Zhbllokoje kontaktin + Blloko përkatësin + Zhbllokoje përkatësinë + Blloko pjesëmarrësin + Zhbllokoje pjesëmarrësin + Administroni Llogari + Formësime + Jepe përmes Conversation + Nisni Bisedë + Zgjidhni Kontakt + Zgjidhni Kontakte + Jepe përmes llogarie + mu tani + 1 minutë më parë + %d minuta më parë + po dërgohet… + Po shfshehtëzohet mesazhi. Ju lutem prisni… + Mesazhi i fshehtëzuar me OpenPGP + Nofka është tashmë e regjistruar + Nofkë e pavlefshme + Përgjegjës + I zoti + Moderator + Pjesëmarrës + Vizitor + Të bllokohen krejt kontaktet nga %s\? + Të zhbllokohen krejt kontaktet prej %s\? + Kontakti u bllokua + Bllokuar + Regjistroni llogari të re në shërbyes + Ndryshoni fjalëkalim te shërbyesi + Ndajeni me… + Nisni bisedë + Ftoni kontakt + Ftoni + Kontakte + Kontakt + Anuloje + Shtoni + Përpunojeni + Fshije + Bllokoje + Zhbllokoje + Ruaje + OK + %1$s u vithis + Dërgoje tani + Mos ripyet më kurrë + S’u lidh dot te llogaria + S’u lidh dot te llogari të shumta + Prekeni, që të administroni llogaritë tuaja + Bashkëngjitni kartelë + Të shtohet te lista juaj e kontakteve ky kontakt që mungon\? + Shtoni kontakt + dërgimi dështoi + Po bëhet gati për dërgim figure + Po bëhet gati për dërgim figurash + Po jepen kartela. Ju lutemi, prisni… + Spastro historikun + Spastoni Historik Bisedash + Fshije kartelën + Mbylle këtë bisedë më pas + Zgjidhni pajisje + Dërgoni mesazh të pafshehtëzuar + Dërgoni mesazh + Dërgoje mesazhin për %s + Dërgoni një mesazh të fshehtëzuar me OMEMO + Dërgoni një mesazh të fshehtëzuar me v\\OMEMO + Dërgoni një mesazh të fshehtëzuar me OpenPGP + Nofkë e re në përdorim + Dërgoje të pafshehtëzuar + Instaloje + Ju lutemi, instaloni OpenKeychain + po ofrohet… + po pritet… + S’u gjet Kyç OpenPGP + S’u gjetën Ke OpenPGP + Të Përgjithshme + Prano kartela + Bashkëngjitje + Njoftim + Dridhu + Dridhu, kur mbërrin mesazh i ri + Njoftim LED + Zile + Tingull njoftimi + Tingull njoftimi për mesazhe të rinj + Zile për thirrje ardhëse + Të mëtejshme + Mos dërgo kurrë njoftime vithisjesh + Ripohoni Mesazhe + Pengo Foto Ekrani + UI + OpenKeychain prodhoi një gabim. + Kyç i pavlefshëm për fshehtëzim. + Pranoje + Ndodhi një gabim + Gabim + Llogaria juaj + Dërgo përditësime pranie + Merr përditësime pranie + Pyet për përditësime pranie + Zgjidhni foto + Bëni një foto + Kartela që përzgjodhët s’është figurë + S’u gjet kartelë + E panjohur + Përkohësisht i çaktivizuar + I lidhur + Po lidhet… + I shkëputur + E paautorizuar + S’u gjet shërbyes + Pa lidhje + Regjistrimi dështoi + Emër përdoruesi tashmë në përdorim + Regjistrimi u plotësua + Regjistrim i pambuluar nga shërbyesi + Token-i i pavlefshëm regjistrimi + Tratativa TLS dështoi + Përkatësi: e paverifikueshme + Dhunim rregullash + Shërbyes i papërputhshëm + Klient jo i përputhshëm + Gabim rrjedhe + Gabim në hapjen e rrjedhës + Të pafshehtëzuara + OTR + OpenPGP + OMEMO + Fshije llogarinë + Çaktivizoje përkohësisht + Publikoje avatarin + Publikoni kyçin publik OpenPGP + Hiq kyç publik OpenPGP + Kyçi publik OpenPGP u bë publik. + Aktivizoje llogarinë + Jeni i sigurt\? + Incizoni zë + Adresë XMPP + Bllokoj adresë XMPP + username@example.com + Fjalëkalim + Kjo s’është adresë XMPP e vlefshme + Mbaroi kujtesa. Figurë shumë e madhe + Doni të shtohet %s te libri juaj i adresave\? + Hollësi shërbyesi + XEP-0313: MAM + XEP-0198: Administrim Rrjedhe + XEP-0163: PEP (Avatarë / OMEMO) + XEP-0363: Ngarkim Kartelash HTTP + jo i passhëm + parë së fundi mu tani + parë së fundi një minutë më parë + parë së fundi %d minuta më parë + parë së fundi një orë më parë + parë së fundi %d orë më parë + parë së fundi një ditë më parë + parë së fundi %d ditë më parë + U gjetën mesazhe të rinj të fshehtëzuar me OpenPGP + ID Kyçi OpenPGP + Shenja gishtash OMEMO + Shenja gishtash v\\OMEMO + Pajisje të tjera + Beso Shenja Gishtash OMEMO + Po sillen kyçe… + U bë + Shfshehtëzoje + Faqerojtës + Kërko + Jepni Kontakt + Fshije kontaktin + Shihni hollësi kontakti + Bllokojeni kontaktin + Zhbllokoje kontaktin + Krijoje + Përzgjidhni + Kontakti ekziston tashmë + Hyni + kanal@konferencë.shembull.com/nofkë + Ruaje si faqerojtës + Fshije faqerojtësin + Asgjëso fjalosje grupi + Asgjësoje kanalin + S’u asgjësua dot fjalosje në grup + S’u asgjësua dot kanali + Përpunoni subjekt fjalosjeje në grup + Po hyhet në fjalosje grupi… + Dil + Kontakti u shtua te listë kontaktesh + Rishtoje + %s ka lexuar deri në këtë pikë + %s ka lexuar deri në këtë pikë + Po publikohet… + Shërbyesi hodhi poshtë urdhrin publikimin tuaj + S’u shndërrua dot fotoja juaj + S’u ruajt dot avatari në disk + Shërbyesi juaj nuk mbulon publikim avatarësh + pëshpëriti + për %s + Dërgo mesazh privat te %s + Lidhe + Ka tashmë një llogari të tillë + Pasuesi + U vendos sesion + Anashkaloje + Çaktivizo njoftimet + Aktivizoje + Fjalosja në grupi lyp fjalëkalim + Jepni fjalëkalim + Kërkoje tani + Shpërfille + Siguri + Lejo ndreqje mesazhi + Rregullime ekspertësh + Ju lutemi, hapni sytë me këto + Mbi %s + Orë të Qeta + Kohë fillimi + Kohë përfundimi + Aktivizoni orë të qeta + Tjetër + Njëkohëso faqerojtës + U kopjuan në të papastër shenja gishtash OMEMO + Jeni dëbuar nga kjo fjalosje në grup + Kjo fjalosje në grup është vetëm për anëtarë + Kufizim burimesh + Jepni përzënë nga kjo fjalosje në grup + Fjalosje në grup qe mbyllur + S’jeni më te kjo fjalosje grupi + duke përdorur llogari %s + strehuar në %s + Po kontrollohet %s te strehë HTTP + S’jeni i lidhur. Riprovoni më vonë + Kontrolloni madhësi %s + Kontrolloni madhësinë e %1$s në %2$s + Mundësi mesazhi + Ngjite si citim + Kopjoji URL-në origjinale + Ridërgoje + URL kartele + URL-ja u kopjua në të papastër + Adresa XMPP u kopjua në clipboard + Mesazhi i gabimit u kopjua në të papastër + adresë web + Skano Kod me vija 2D + Shfaq Kod me vija 2D + Shfaqe listë bllokimesh + Hollësi llogarie + Ripohojeni + Riprovoni + Shërbim në prapaskenë + Krijo kopjeruajtje + Kartelat kopjeruajtje do të depozitohen në %s + Po krijohen kartela kopjeruajtje + Kopjeruajtja juaj u krijua + Po rikthehet kopjeruajtje + Kopjeruajtja juaj u rikthye + Mos harroni të aktivizoni llogarinë. + Zgjidhni kartelë + Po merret %1$s (plotësuar %2$d%%) + Shkarkoni %s + Fshije %s + kartelë + Hap %s + po dërgohet (plotësuar %1$d%%) + Po bëhet gati për dhënie kartele + %s ofruar për shkarkim + s’u nda dot kartelë me të tjerë + shpërngulje kartelash e anuluar + Kartela u fshi + S’u gjet aplikacion për hapje të kartelës + S’u gjet aplikacion për hapje të lidhjes + S’u gjet aplikacion për të parë kontakte + Etiketa Dinamike + Aktivizo njoftimet + S’u gjet shërbyes fjalosjeje grupi + S’u krijua dot fjalosje grupi + Avatar llogarie + Kopjoje shenja gishtash OMEMO në të papastër + Riprodho kyç OMEMO + Spastro pajisje + Diç shkoi ters + Po sillet historik prej shërbyesi + S’ka historik tjetër në shërbyes + Po përditësohet… + Fjalëkalimi u ndryshua! + Fjalëkalimi s’u ndryshua dot + Ndryshoni fjalëkalimin + Fjalëkalimi i tanishëm + Fjalëkalim i ri + Fjalëkalimi s’mund të jetë i zbrazët + Aktivizo krejt llogaritë + Çaktivizo krejt llogaritë + Kryeje veprimin me + Pa përshoqërim + I shkëputur + Anëtar + Mënyrë e thelluar + Akordojini privilegje anëtari + Shfuqizoni privilegje anëtari + Akordoni privilegje përgjegjësi + Shfuqizoni privilegje përgjegjësi + Akordojini privilegje të zoti + Shfuqizoni privilegje të zoti + Hiqe prej fjalosjeje grupi + Hiqe prej kanali + S’u ndryshua dot përshoqërim i %s + Dëboje nga fjalosje në grup + Dëboje nga kanali + Dëboje tani + S’u ndryshua dot roli i %s + Formësim fjalosje private në grup + Formësim kanali publik + Private, vetëm për anëtarë + S’po merrni pjesë + U ndryshuan mundësi fjalosjeje në grup! + Kurrë + Deri sa të jepet njoftim tjetër + Përgjigjuni + Vëri shenjë si të lexuar + Tasti Enter bën dërgimin + Shfaq tastin Enter + Ndryshoje tastin e emotikoneve si tast Enter + audio + video + figurë + grafik vektorial + kartelë multimedia + Dokument PDF + Aplikacion + Kontakt + Avatari u bë publik! + Po ofrohet %s + %s po shkruan… + %s ka reshtur së shkruari + %s po shkruajnë… + %s ka reshtur së shkruari + Njoftime shtypjesh + Dërgo vendndodhjen + Shfaq vendndodhje + S’u gjet aplikacion për shfaqje të vendndodhjes + Vendndodhje + Biseda u mbyll + Braktisi fjalosje private në grup + Braktisi kanal majtas + Krejt dëshmitë duhet të miratohen dorazi + Hiqni dëshmi + Fshi dëshmi të miratuara dorazi + S’u dhanë dëshmi dorazi + Hiqi dëshmitë + Fshije përzgjedhjen + Anuloje + Veprim i Shpejtë + Asnjë + Më të përdorur së fundi + Zgjidhni veprim të shpejtë + Kërko te kontaktet + Kërko te faqerojtës + Dërgo mesazh privat + %1$s e braktisi fjalosjen në grup + Emër përdoruesi + Emër përdoruesi + Ky s’është emër i vlefshëm përdoruesi + Shkarkimi dështoi: S’u gjet shërbye + Shkarkimi dështoi: S’u gjet kartelë + Shkarkimi dështoi: S’u lidh dot te strehë + Shkarkimi dështoi: S’u arrit të shkruhej te kartelë + Shkarkimi dështoi: Kartelë e pavlefshme + Rrjet Tor jo në punë + Dështim lidhjeje + Shërbyesi s’është përgjegjës për këtë përkatësi + I dëmtuar + “I larguar”, kur pajisja është e kyçur + Shfaqmë si “I lraguar”, kur pajisja është e kyçur + Shfaqmë si “I zënë”, kur pajisja është në dridhje + Rregullime të zgjeruara lidhjeje + Hyni me dëshmi + S’u analizua dot dëshmi + Parapëlqime arkivimi + Parapëlqime arkivimi më anë të shërbyesit + S’u prunë dot parapëlqime arkivimi + Zgjidhja e CAPTCHA-s është e domosdoshme + Jepni tekstin prej figurës më sipër + Varg jo i besuar dëshmish + Adresa XMPP s’përputhet me dëshminë + Rinovoni dëshminë + Gabim në sjellje kyçi OMEMO! + U verifikua kyç OMEMO me dëshmi! + Lidhje + Lidhu përmes Tor-i + Strehëemër + Portë + Ky s’është numër i vlefshëm porte + Ky s’është një strehëemër i vlefshëm + Njëkohësoje me kontaktet + Njofto për krejt mesazhet + Njoftomë vetëm kur përmendem + Njoftime të çaktivizuara + Njoftime të ndalura + Ngjeshje Figure + Përherë + Vetëm figura të mëdha + Me optimizime baterie të aktivizuara + Çaktivizoje + Zona e përzgjedhur është shumë e madhe + (Pa llogari të aktivizuara) + Kjo fushë është e domosdoshme + Ndreqe mesazhin + Dërgo mesazhin e saktësuar + E keni çaktivizuar këtë llogari + Gabim sigurie: Hyrje e pavlefshme te kartelë! + S’u gjet aplikacion për të dhënë URI + Jepjani URI-n… + Pajtohuni dhe vazhdoni + Adresa juaj e plotë XMPP do të jetë: %s + Krijo Llogari + Përdorni shërbimin tuaj + Zgjidhni emrin tuaj si përdorues + Administrojeni dorazi praninë + Mesazh gjendjeje + I lirë për Fjalosje + Në linjë + I larguar + I zënë + U prodhua një fjalëkalim i siguruar + Regjistrimi dështoi: Riprovoni më vonë + Regjistrimi dështoi: Fjalëkalim shumë i dobët + Zgjidhni pjesëmarrës + Po krijohet fjalosje në Grup… + Riftojeni + Çaktivizoje + E shkurtër + Mesatare + E gjatë + Përdorim transmetimi + Privatësi + Temë + Përzgjidhni paletën e ngjyrave + Automatike + E çelët + E errët + Sfond i gjelbër + S’u lidh dot te OpenKeychain + Kjo pajisje s’është më në përdorim + Kompjuter + Telefon celular + Tablet + Shfletues + Konsolë + Lypset pagesë + Akordo leje për përdorim të Internetit + Unë + Kontakti kërkon pajtim pranie + Lejoje + S’ka leje për përdorim të %s + S’u gjet shërbyes i largët + Mbarim kohe shërbyesi të largët + S’u përditësua dot llogaria + Raportojeni këtë adresa për mesazhe të padëshiruar. + Fshini identitete OMEMO + Fshi kyçet e përzgjedhur + Kopjo shenja gishtash + Kodi me vija nuk përmban shenja gishtash për këtë bisedë. + Shenja gishtash të verifikuar + Përdorni kamerën e telefonit tuaj që të skanoni një kod me vija + Ju lutemi, pritni të sillen kyçet + Ndajeni si Kod me vija + Jepe si URI XMPP + Ndajeni me të tjerë si lidhje HTTP + Besim i Verbër Para Verifikimi + Jo i besuar + Kod 2D me vija i pavlefshëm + Spastro fshehtinën + Spastro depozitë private + Vazhdoni + Verifikoni kyçe OMEMO + Shfaq joaktive + Mos e beso pajisjen + + %d minutë + %d minuta + + + %d orë + %d orë + + + %d ditë + %d ditës + + + %d javë + %d javë + + Fshirje e automatizuar mesazhesh + Po fshehtëzohet meszhi + Po ngjishet video + Biseda përkatëse u mbyll. + Kontakti u bllokua. + Njoftime prej të panjohurish + U mor mesazh prej të panjohuri + Blloko të huaj + Bllokoje krejt përkatësinë + në linjë mu tani + Riprovo shfshehtëzimin + Dështim sesioni + Shërbyesi kërkon doemos regjistrim përmes një sajti + Hap sajtin + S’u gjetën aplikacione për hapjen e sajtit + Sot + Dje + Vlerësoj strehemër me DNSSEC + të pjesshme + Regjistroni video + Kopjoje në të papastër + Mesazhi u kopjua në të papastër + Mesazh + Mesazhet private janë të çaktivizuara + Aplikacione të Mbrojtur + Të pranohet Dëshmi e Panjohur\? + Dëshmia e shërbyesit s’është nënshkruar prej një Autoriteti të njohur Dëshmish. + Të Pranohet Emër Shërbyesi i Ngatërruar\? + Doni të lidheni, sido qoftë\? + Hollësi dëshmie: + Një herë + Që të skanojë kodin QR, skanerit i duhet leje përdorimi të kamerës + Rrëshqitni drejt fundit + Përpunoni Mesazh gjendjeje + Përpunoni mesazh gjendjeje + Çaktivizo fshehtëzimin + S’u soll dot listë pajisjesh + S’u sollën dot kyçe fshehtëzimi + Çaktivizoje tani + Skicë: + Fshehtëzim OMEMO + Krijoni Shkurtore + Madhësi Shkronjash + On, si parazgjedhje + Off, si parazgjedhje + Mesatare + E madhe + Mesazhi s’qe fshehtëzuar për këtë pajisje. + S’’u arrit të shfshehtëzohet mesazh fshehtëzuar me OMEMO. + zhbëje + Tregimi i vendndodhjes është i çaktivizuar + Ndreqe pozicionin + Shfiksoje pozicionin + Kopjo Vendndodhjen + Jepe Vendndodhjen + Drejtime + Jepe vendndodhjen + Shfaq vendndodhje + Ndajeni me të tjerë + S’u fillua dot regjistrim + Ju lutemi, prisni… + Akordoni hyrje %1$s për mikrofonin + Kërko te mesazhet + GIF + Shihni bisedë + Shtojcë Tregimi Vendndodhjeje + Kopjo adresë web + Kopjo adresë XMPP + Dhënie Kartelash HTTP për S3 + Kërkim i Drejtpërdrejtë + Avatar fjalosjeje në grup + Emër kontakti + Nofkë + Emër + Jepni një emër, nëse doni + Emër fjalosjeje në grup + Kjo fjalosje në grup është asgjësuar + S’u ruajt dot regjistrim + Shërbim në pjesën e dukshme + Hollësi Gjendjeje + Probleme Lidhjeje + Mesazhe + Thirrje + Mesazhe + Thirrje ardhëse + Thirrje në kryerje e sipër + Thirrje të humbura + Mesazhe heshtazi + Dërgime të dështuar + Rregullime njoftimesh mesazhesh + Rregullime njoftimesh për thirrje ardhëse + Rëndësi, Tingull, Dridhje + Ngjeshje videoje + Shihni media + Pjesëmarrës + Shfletues mediash + Cilësi Video + Mesatare (360p) + E lartë (720p) + anuluar + Po skiconi tashmë një mesazh. + Veçori e pasendërtuar + Kod i pavlefshëm vendi + Zgjidhni vend + numër telefoni + Verifikoni numrin tuaj të telefonit + %s s’është numër telefoni i vlefshëm. + Ju lutemi, jepni numrin e telefonit tuaj. + Kërko te vendet + Verifikoni %s + Ridërgo SMS + Ridërgo SMS (%s) + Ju lutemi, pritni (%s) + mbrapsht + Jeni i sigurt se doni të ndëpritet procedura e regjistrimit\? + Po + Po verifikohet… + PIN-i që dhatë është i pasaktë. + Gabim i panjohur rrjeti. + Përgjigje e panjohur nga shërbyesi. + S’u lidh dot te shërbyesi. + S’u vendos dot një lidhje të sigurt. + S’u gjet dot shërbyes. + Diç shkoi ters gjatë përpunimit të kërkesës tuaj. + Dhënie e pavlefshme nga përdoruesi + Përkohësisht i pakapshëm. Riprovoni më vonë. + S’ka lidhje rrjeti. + Ju lutemi, riprovoni pas %s + Shumë përpjekje + Përditësoje + Emri juaj + Jepni emrin tuaj + Hidhe poshtë kërkesën + Instaloni Orbot-in + Nisni Orbot-in + e-libër + Origjinalja (e pangjeshur) + Hape me… + Foto profili Conversations + Zgjidhni llogari + Riktheje kopjeruajtjen + Riktheje + S’u rikthye dot kopjeruajtje. + S’u shfehtëzua dot kopjeruajtje. A është i saktë fjalëkalimi\? + Kopjeruani & Riktheni + Jepni adresë XMPP + Krijoni fjalosje grupi + Hyni në kanal publik + Krijoni fjalosje private në grup + Krijoni kanal publik + Emër kanali + Adresë XMPP + Ju lutemi, jepni një emër për kanalin + Ju lutemi, jepni një adresë XMPP + Kjo është një adresë XMPP. Ju lutemi, jepni një emër. + Po krijohet kanal publik… + Ky kanal ekziston tashmë + Hytë në një kanal ekzistues + S’u ruajt dot formësim kanali + Lejo këdo të përpunojë temën + Lejo këdo të ftojë të tjerë + Cilido mund të përpunojë temën. + Të zotët mund të përpunoni subjektin. + Përgjegjësit mund të përpunojnë temën. + Të zotët mund të ftojnë të tjerë. + Cilido mund të ftojë të tjerë. + Adresat XMPP janë të dukshme për përgjegjës. + Adresat XMPP janë të dukshme për këdo. + Kjo fjalosje private në grup s’ka anëtarë. + Administroni privilegje + Kërko te pjesmarrës + Kartelë shumë e madhe + Bashkëngjite + Zbuloni kanale + Kërko te kanale + Cenim potencial privatësie! + Kam tashmë një llogari + Shtoni llogari ekzistuese + Regjistroni llogari të re + Kjo duket si adresë përkatësie + Shtoje sido qoftë + Ndani me të tjerë kartela kopjeruajtjesh + Kopjeruajtje bisede + Akt + Hap kopjeruajtje + Kartela që përzgjodhët s’është kartelë kopjeruajtjeje Conversations + Kjo llogari është ujdisur tashmë + Ju lutemi, jepni fjalëkalimin për këtë llogari + S’u krye dot ky veprim + Fjalosje në Grup & Kanale + Shërbyes vendor + Metodë zbulimi kanalesh + Kopjeruajtje + Mbi + Ju lutemi, aktivizoni një llogari + Bëni thirrje + Thirrje ardhëse + Thirrje video ardhëse + Të kalohet në thirrje video\? + Po lidhet + I lidhur + Po rilidhet + Po pranohet thirrja + Po përfundohet thirrja + Përgjigje + Hidhe tej + Po pikasen pajisje + Po i bihet ziles + I zënë + S’u bë dot lidhje e thirrjes + Humbi lidhja + Thirrje e tërhequr + Dështim aplikacioni + Problem verifikimi + Mbylle + Thirrje në kryerje e sipër + Thirrje video në kryerje e sipër + Po rilidhet thirrja + Po rilidhet thirrje video + Që të bëni thirrje, çaktivizoni Tor-in + Thirrje ardhëse + Thirrje ardhëse · %s + Thirrje e humbur · %s + Thirrje për + Thirrje për · %s + Thirrje e humbur + + %1$d thirrje e humbur, prej %2$s + %1$d thirrje të humbura, prej %2$s + + + %d thirrje e humbur + %d thirrje të humbura + + Mikrofoni juaj s’është i përdorshëm + Mund të kryeni vetëm një thirrje në herë. + S’u ndërrua dot kahu i kamerës + Fiksoje në krye + Shfiksoje prej kreut + Gjurmë GPX + S’u ndreq dot mesazhi + Krejt bisedat + Këtë bisedë + Avatari juaj + Avatar për %s + Fshehtëzuar me OMEMO + Fshehtëzuar me OpenPGP + Jo e fshehtëzuar + Dalje + Incizo postë zanore + Luaje audion + Ndale videon + + Shihni %1$d Pjesëmarrës + Shihni %1$d Pjesëmarrës + + + S’u dha dot një mesazh + S’u dhanë dot disa mesazhe + + Dërgime të dështuara + Më tepër mundësi + S’u gjet aplikacion + Ftojeni te Conversations + S’arrihet të analizohet ftesë + Shërbyesi s’mbulon prodhim ftesash + Këtë veçori s’e mbulon ndonjë llogari aktive + S’arrihet të aktivizohet video. + Dokument tekst i thjeshtë + Nuk mbulohen regjistrime llogarish + S’u gjet adresë XMPP + Dështim i përkohshëm mirëfilltësimi + Fshije avatarin + Kalo në thirrje video + Hidh poshtë kalim në thirrje video + Hyni në kanal publik… + + %d bisedë e palexuar + %d biseda të palexuara + + + U fshi %d dëshmi + U fshi %d dëshmi + + + %d muaj + %d muaj + + S’u shndërrua dot kartelë figure + Temë + Rregullime + Rinise + Duke dërguar “stack traces” ndihmoni në zhvillimin + Bëje kanalin të moderuar + U lidhën %1$d nga %2$d e llogarive + Ngarko më tepër mesazhe + Akordoni hyrje %1$s për kamerën + Shfaq mesazh gabimi + Mesazh Gabimi + S’u krijua dot kartelë e përkohshme + Kjo pajisje u verifikua + Që të vazhdoni të merrni njoftime, edhe kur ekrani juaj është i fikur, duhet të shtoni Conversations te lista e aplikacioneve të mbrojtur. + Kjo duket si adresë kanali + Thirrje audio + Thirrje video + Ndihmë + Kalo te bisedë + Rikthehu te thirrja që po bëhej + + %d mesazh + %d mesazhe + + Llogari XMPP + Asnjë (e çaktivizuar) + + %d sekondë + %d sekonda + + Jo + PIN-i që keni dërguar, ka skaduar. + Po përdorni një version të vjetruar të këtij aplikacioni. + Prej këtij numri telefoni është bërë hyrja me një tjetër pajisje. + Ju lutemi, jepni emrin tuaj, për t’u bërë të ditur se cili jeni, personave që s’ju kanë në librat e tyre të adresave. + Që të caktoni emrin tuaj, përdorni butonin e përpunimeve. + Ky kanal do ta bëjë publike adresën tuaj XMPP + Po kërkohet SMS… + Pajisja juaj përdor optimizime shumë të thella baterie për %1$s, çka mund të shpjerë në vonesa njoftimesh, ose madje edhe humbje mesazhesh. +\n +\nTani do t’ju kërkohet t’i çaktivizoni ato. + I keni vlerësuar në mënyrë të parrezik shenjat e gishtave të këtij personi, për të ripohuar besimin. Duke përzgjedhur “U bë”, thjesht po ripohoni se %s është pjesë e kësaj fjalosjeje në grup. + Quicksy është një pjellë e klientit popullor XMPP, Conversations, me zbulim të automatizuar kontaktesh.<br><br>Regjistroheni me numrin tuaj të telefonit dhe Quicksy do të sugjerojë në mënyrë të automatizuar—bazuar në numrat e telefonave në librin tuaj të adresave—kontakte të mundshëm për ju.<br><br>Duke u regjistruar, pajtoheni me <a href=https://quicksy.im/#privacy>rregullat tona të privatësisë</a>. + Ju ndan një hap nga verifikimi i kyçeve OMEMO të llogarisë tuaj. Kjo është e sigurt vetëm nëse e ndoqët këtë lidhje prej një burimi të besuar, ku vetëm ju do të mund ta kishit publikuar këtë lidhje. + Jeni i sigurt se doni të hiqet verifikimi i kësaj pajisjeje\? +\nKësaj pajisjeje dhe mesazheve prej saj do t’u vihet shenjë si “Jo i besuar”. + Fshi vetvetiu nga kjo pajisje mesazhe që janë më të vjetër se intervali kohor i formësuar. + S’po sillen mesazhe, për shkak të një periudhe lokale mbajtjeje. + Fshije prej liste + Do të donit të hiqet %s prej listës tuaj të kontakteve\? Bisedat me këtë kontakt s’do të hiqen. + Do të donit t’i bllokohet %s dërgimi i mesazheve për ju\? + Do të donit të zhbllokohet %s dhe të lejohet t’ju dërgojë mesazhe\? + Do të donit të hiqet %s si faqerojtës\? Bisedat me këtë faqerojtës s’do të hiqen. + Përdorimi i llogarisë tuaj XMPP për të dërguar “stack traces” ndihmon zhvillimin e pandërprerë të %1$s. + Doni të fshihen krejt mesazhet te kjo bisedë\? +\n +\nKujdes: Kjo s’do të ndikojë mesazhet e depozituar në pajisje apo shërbyes të tjerë. + Jeni i sigurt se doni të fshihet kjo kartelë\? +\n +\nKujdes: Kjo s’do të fshijë kopje të kësaj kartele që janë depozituar në pajisje apo shërbyes të tjerë. + Shfshehtëzimi dështoi. Ndoshta s’keni kyçin e duhur privat. + OpenKeychain + %1$s përdor <b>OpenKeychain</b> që të fshehtëzojë dhe shfshehtëzojë mesazhe dhe të administrojë kyçet tuaj publikë.<br><br>Licensohet sipas kushteve të GPLv3+ dhe mund të merret në F-Droid dhe Google Play.<br><br><small>(Ju lutemi, riniseni %1$s më pas.)</small> + Mesazhi juaj s’u fshehtëzua dot, ngaqë kontakti juaj s’po deklaron kyçin e vet publik. +\n +\nJu lutemi, kërkojini kontaktit tuaj të ujdisë OpenPGP-në. + Mesazhi juaj s’u fshehtëzua dot, ngaqë kontaktet tuaj s’po deklarojnë kyçet e tyre publikë. +\n +\nJu lutemi, kërkojuni të ujdissin OpenPGP-në. + Pranoni vetvetiu kartela më të vogla se… + Xixëllo dritëz njoftimesh, kur mbërrin një mesazh i ri + Kohëzgjatje heshtimi njoftimesh, pas pikasjeje veprimtarie në një nga pajisjet tuaja të tjera. + Bëjuni të ditur kontakteve tuaja kur keni marrë dhe lexuar mesazhet e tyre + Fshih lëndë aplikacioni te këmbyesi i aplikacioneve dhe blloko fotografim ekrani + Gabim i përgjithshëm I/O. Ndoshta ju është mbaruar hapësirë depozitimi\? + Aplikacioni që përdorët për të përzgjedhur këtë figurë nuk dha leje të mjaftueshme për leximin e kartelës. +\n +\nPërdorni një tjetër përgjegjës kartelash për të zgjedhur një figurë. + Aplikacioni që përdorët për të dhënë këtë kartelë, nuk jep leje të mjaftueshme. + Jeni i sigurt se doni të hiqet kyçi juaj publik OpenPGP nga njoftimi juaj për prani\? +\nKontaktet tuaj s’do të jenë më në gjendje t’ju dërgojnë mesazhe të fshehtëzuar me OpenPGP. + Fshirja e llogarisë tuaj fshin krejt historikun e bisedave tuaja + Mesazh i fshehtëzuar. Që ta shfshehtëzoni, ju lutemi, instaloni OpenKeychain. + Shenja gishtash OMEMO (origjinë mesazhi) + Shenja gishtash v\\OMEMO (origjinë mesazhi) + kanal@konferencë.example.com + Jeni i sigurt se doni të asgjësohet kjo fjalosje në grup\? +\n +\nKujdes: Fjalosja në grup do të hiqet plotësisht te shërbyesi. + Jeni i sigurt se doni të asgjësohet ky kanal publik\? +\n +\nKujdes: Kanali do të hiqet plotësisht te shërbyesi. + %1$s +%2$d të tjerë kanë lexuar deri në këtë pikë + Gjithkush ka lexuar deri në këtë pikë + Prekni avatarin që të përzgjidhni një foto nga galeri + (Ose shtypeni gjatë, për të ri kthyer parazgjedhjet) + Ju lutemi, së pari kërkoni përditësime pranie nga kontakti juaj. +\n +\nKjo do të përdoret për të përcaktuar cilin aplikacion fjalosjeje po përdor kontakti juaj. + Lejojuni kontakteve tuaja të përpunojnë mesazhet e tyre edhe më pas + Gjatë orëve të qetësisë, njoftimet do të heshtohen + Kalo në “autojoin”, kur hyhet ose dilet nga një MUC dhe reago te ndryshime të bëra nga klientë të tjerë. + Kujdes: Dërgimi i kësaj, pa përditësime të dyanshme pranie, mund të shkaktojë probleme të papritura. +\n +\nKaloni te “Hollësi kontakti”, që të verifikoni pajtime tuajat pranie. + E braktisët këtë fjalosje grupi për arsye teknike + I pengon sistemit operativ të asgjësojë lidhjen tuaj + Kartelat e kopjeruajtjes janë depozituar në %s + Anuloje transmetimin + Shfaq etiketa vetëm-lexim nën kontakte + Jeni i sigurt se doni të spastrohen krejt pajisjet e tjera nga njoftimi OMEMO\? Herës tjetër që pajisjet tuaja lidhen, do të rinjoftojnë veten, por ndërkohë mund të mos marrin mesazhet e dërguar. + S’ka kyçe të përdorshëm për këtë kontakt. +\nS’u sollën dot kyçe të rinj nga shërbyesi. Ndoshta ka diçka gabim me shërbyesin tuaj të kontakteve\? + S’ka kyçe të përdorshëm për këtë kontakt. +\nSigurohuni se keni që të dy pajtim pranie. + Po rrekeni të hiqni %s nga një kanal publik. Rruga e vetme për ta bërë këtë është ta dëboni përgjithmonë atë përdorues. + Bëji adresat XMPP të dukshme për këdo + S’u ndryshuan dot mundësi fjalosjeje në grup + Përdorni tastin Enter për të dërguar mesazhin. Mundeni përherë të përdorni Ctrl+Enter për të dërguar mesazhin, edhe nëse kjo mundësi është e çaktivizuar. + Po dërgohet %s + Fshih të shkëputurat + Bëjuni të ditur kontakteve tuaj, kur shkruani mesazhe për ta + Mos beso DA sistemi + Zëvendëso butonin “Dërgoje” me veprim të shpejtë + “I zënë” nën mënyrën e heshtur + Shfaqe si “I zënë”, kur pajisja gjendet nën mënyrën e heshtur + Trajtoje dridhjen si mënyrë heshturazi + Kur ujdiset një llogari, shfaq rregullime strehëemri dhe porte + xmpp.example.com + Po sillen parapëlqime arkivimi. Ju lutemi, pritni… + Pajisja juaj nuk mbulon përzgjedhjen e dëshmive të klientëve! + Kaloji krejt lidhjet përmes rrjetit Tor. Lyp Orbot + Kartela iu dha %s + Figura iu dha %s + Figurat iu dhanë %s + Teksti iu dha %s + Akordoji %1$s hyrje te depozitë e jashtme + %1$s dëshiron leje të përdorë librin tuaj të adresave, për përkim me listën tuaj të kontakteve XMPP. +\nKjo do të sjellë shfaqjen e emrave të plotë dhe avatarëve të kontakteve tuaj. +\n +\n%1$s vetëm sa do të lexojë librin tuaj të adresave dhe bëjë lokalisht përkimin, pa ngarkuar gjë në shërbyesin tuaj. + Që të bëjë sugjerime rreth kontaktesh të mundshëm që përdorin tashëm Quicksy-n, i duhet hyrje në numrat e telefonave të kontakteve.<br><br>S’do të depozitojmë kopje të këtyre numrave të telefonave. +\n +\nPër më tepër hollësi, lexoni <a href=https://quicksy.im/#privacy>rregullat tona të privatësisë</a>.<br><br>Tani do t’ju kërkohet të akordoni leje hyrjeje te kontaktet tuaja. + Ndihmëz: Përdorni “Zgjidhni kartelë”, në vend se “Zgjidhni foto”, për të dërguar figura të pangjeshura, pavarësisht nga ky rregullim. + Pajisja juaj përdor optimizime shumë të thella baterie për %1$s, çka mund të shpjerë në vonesa njoftimesh, ose madje edhe humbje mesazhesh. +\nRekomandohet të çaktivizohen ato. + Pajisja juaj nuk mbulon zgjedhjen e lënies jashtë nga optimizim baterie + Bëjuni të ditur kontakteve tuaj, kur përdorni Conversations + Për mesazhe të marrë përdor sfond të gjelbër + Riprodhoni kyçet tuaj OMEMO. Krejt kontakteve tuaj do t’ju duhet t’ju verifikojnë sërish. Këtë përdoreni si zgjidhjen e fundit. + Që të bëni publik avatarin tuaj, duhet të jeni i lidhur. + Kursyesi i të dhënave u aktivizua + Sistemi juaj operativ po e kufizon hyrjen e %1$s në Internet, kur gjendet në sfond. Që të merrni njoftime për mesazhe të rinj, duhet t’i lejoni %1$s hyrje të pakufizuar , kur “Ruajtësi i të dhënave” është aktiv. +\n%1$s do të bëjë prapë një përpjekje të kursejë të dhëna, kur është e mundur. + Pajisja juaj nuk mbulon çaktivizim Kursyesi të dhënash për %1$s. + Verifikuat krejt kyçet OMEMO që zotëroni + Beso pajisje të reja prej kontaktesh të paverifikuar, por kërko ripohim dorazi për pajisje të reja për kontakte të verifikuarbut prompt manual confirmation of new devices for verified contacts. + Spastro dosje fshehtinë (përdorur nga aplikacioni kamerë) + Spastro depozitë private ku mbahen kartelat (Ato mund të rishkarkohen prej shërbyesit) + E ndoqa këtë lidhje prej një burimi të besuar + Ju ndan një hap nga verifikimi i kyçeve OMEMO të %1$s, pas klikimit të një lidhjeje. Kjo është e sigurt vetëm nëse e ndoqët këtë lidhje prej një burimi të besuar, ku vetëm %2$s do të mund ta kishte publikuar këtë lidhje. + Fshih jo aktivet + Njofto për mesazhe dhe thirrje të mara prej të huajish. + Dëshmi shërbyesi që përmbajnë strehëemrin e vlerësuar, konsiderohen të verifikuara + Dëshmia s’përmban adresë XMPP + Shërbyesi s’bëri dot mirëfilltësimin si “%s”. Dëshmia është e vlefshme vetëm për: + Pas dërgimit të një mesazhi, rrëshqit poshtë + %1$s s’është në gjendje të fshehtëzojë mesazhe te %2$s. Kjo mund të vijë për shkak se kontakti juaj përdor një shërbyes, ose klient të vjetruar, që s’mund të përdorë OMEMO. + Ndihmëz: Në disa raste, kjo mund të ndreqet duke shtuar listat e kontakteve të njëri-tjetrit. + Jeni i sigurt se doni të çaktivizohet fshehtëzimi OMEMO për këtë bisedë\? +\nKjo do t’i lejojë përgjegjësit të shërbyesit tuaj të lexojë mesazhet tuaj, por mund të jetë e vetmja rrugë për të komunikuar me persona që përdorin klientë të vjetruar. + OMEMO do të përdoret përherë për fjalosje tek-për-tek dhe në grup. + OMEMO do të përdoret për biseda të reja, si parazgjedhje. + OMEMO do të duhet të aktivizohet shprehimisht për biseda të reja. + Madhësia relative e shkronjave të përdorura brenda aplikacionit. + Te skena “Nisni Bisedë” hapni tastierën dhe vendoseni kursorin te fusha e kërkimeve + Streha nuk mbulon avatarë fjalosjeje në grup + Vetëm i zoti mund të ndryshyjë avatarin e një fjalosje në grup + Përdor Shtojcën Për Tregim Vendndodhjeje, në vend se hartën e brendshme + Kjo kategori njoftimesh përdoret për të shfaqur një njoftim të përhershëm që tregon se %1$s është në funksionim. + Kjo kategori njoftimesh përdoret për të shfaqur njoftim në rast se ka problem me lidhjen me një llogari. + Ky grup njoftimesh përdoret për të shfaqur njoftime që s’duhet të shkaktojnë ndonjë tingull. Për shembull, kur është aktiv në një tjetër pajisje (Grace Period). + Cilësi më e ulët do të thotë kartela më të vogla + Quicksy do të dërgojë një mesazh SMS (mund të aplikohen tarifa shërbimi) për të verifikuar numrin tuaj të telefonit. Jepni kodin e vendit tuaj dhe numrin e telefonit: + Ju kemi dërguar një tjetër SMS me një kod prej 6 shifrash. + Ju lutemi, jepni më poshtë PIN-in tuaj prej 6 shifrash. + Ju lutemi, jepni PIN-in tuaj prej 6 shifrash. + Kartelë e shpërfillur, për shkak cenimi sigurie. + Do të verifikojmë numrin e telefonit

%s

Dakord, apo do të donit të përpunonit numrin\?
+ Ju kemi dërguar një SMS te %s. + Që të rikthehet kopjeruajtja, jepni fjalëkalimin tuaj për llogarinë %s. + Mos përdorni veçorinë e rikthimit të një kopjeruajtje në një përpjekje për të klonuar (xhiruar në të njëjtën kohë) një instalim. Rikthimi i një kopjeruajtje është menduar vetëm për migrime, ose në rast se humbët pajisjen origjinale. + Ky kanal publik s’ka pjesëmarrës. Ftoni kontaktet tuaj, ose përdorni butonin e ndarjes me të tjerët për të dhënë adresën XMPP të tij. + Zbulimi i kanaleve përdor një shërbim prej pale të tretë, të quajtur <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Përdorimi i kësaj veçorie do t’i transmetojë atij shërbimi adresën tuaj IP dhe termat tuaj të kërkimeve. Për më tepër hollësi, shihni <a href=https://search.jabber.network/privacy>Rregulla Privatësie</a> prej tyre. + Aplikacioni dhënës nuk akordoi leje për hyrje në këtë kartelë. + Shumica e përdoruesve duhet të zgjedhin ‘jabber.network’ për sugjerime më të mira nga krejt ekosistemi publik XMPP. + Shtoni kontakt, krijoni ose hyni në një fjalosje në grup, ose zbuloni kanale + Kopjeruajtja u nis. Do të merrni një njoftim, sapo të jetë plotësuar. + Thirrjet janë të çaktivizuara, kur përdoret Tor-i + Llogaria përmes së cilës do të merren mesazhet push. + Shërbyes Push + Një shërbyes push i zgjedhur nga përdoruesi, përmes të cilit të kalohen te pajisja juaj mesazhet push përmes XMPP-je. + Ka të hartuar një udhërrëfyes mbi krijim llogarish te conversations.im. +\nKu zgjidhet conversations.im si shërbim, do të jeni në gjendje të komunikoni me përdorues prej shërbimesh të tjera duke u dhënë atyre adresën tuaj të plotë XMPP. + + %1$d thirrje të humbur prej %2$d kontakti + %1$d thirrje të humbur prej %2$d kontaktesh + +
\ No newline at end of file diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml new file mode 100644 index 000000000..df624e9d8 --- /dev/null +++ b/app/src/main/res/values-sr/strings.xml @@ -0,0 +1,690 @@ + + + Поставке + Нова преписка + Управљај налозима + Управљај налогом + Затвори преписку + Детаљи контакта + Детаљи групног ћаскања + Детаљи канала + Додај налог + Уреди име + Додај у именик + Обриши са списка контаката + Блокирај контакт + Одблокирај контакт + Блокирај домен + Одблокирај домен + Блокирај учесника + Одблокирај учесника + Управљање налозима + Поставке + Подели у преписци + Почни преписку + Изабери контакт + Изабери контакте + Подели преко налога + Списак блокираних + управо сад + пре минут + пре %d минута + + %d непрочитана порука + + + %d непрочитане поруке + + + %d непрочитаних порука + + + шаљем… + Дешифрујем поруку, сачекајте… + ОпенПГП шифрована порука + Надимак је већ у употреби + Неисправан надимак + Администратор + Власник + Модератор + Учесник + Посетилац + Да ли желите да уклоните %s са вашег списка контаката? Преписке са овим контактом неће бити уклоњене. + Желите ли да блокирате поруке од %s? + Желите ли да одблокирате %s и допустите им да вам шаљу поруке? + Блокирати све контакте од %s? + Одблокирати све контакте од %s? + Контакт блокиран + Блокиран + Да ли желите да уклоните %s са обележивача? Преписке са овим контактом неће бити уклоњене. + Региструј нови налог на серверу + Промени лозинку на серверу + Подели помоћу… + Почни преписку + Позови контакт + Позови + Контакти + Контакт + Одустани + Постави + Додај + Уреди + Обриши + Блокирај + Одблокирај + Сачувај + У реду + %1$s је принудно заустављен + Слањем извештаја рада путем вашег ИксМПП налога помажете даљем развоју %1$s. + Пошаљи одмах + Не питај више + Не могу да се повежем са налогом + Не могу да се повежем са више налога + Тапните овде да бисте управљали вашим налозима + Приложи фајл + Контакт није на вашем списку контаката. Желите ли да га додате? + Додај контакт + испорука није успела + Припремам слику за пренос + Припремам слике за пренос + Делим фајлове, сачекајте… + Очисти историјат + Брисање историјата преписки + Желите ли да обришете све поруке из ове преписке?\n\nУпозорење: Ово неће утицати на поруке складиштене на осталим уређајима или серверима. + Обриши датотеку + Желите ли да обришете ову датотеку?\n\nУпозорење: Ово неће утицати на копије ове датотеке складиштене на осталим уређајима или серверима. + Затвори ову преписку након тога + Изаберите уређај + Пошаљи нешифровану поруку + Пошаљи поруку + Пошаљи поруку за %s + Пошаљи ОМЕМО шифровану поруку + Пошаљи v\\ОМЕМО шифровану поруку + Пошаљи ОпенПГП шифровану поруку + Нови надимак је у употреби + Пошаљи нешифровано + Шифровање није успело. Можда немате одговарајући лични кључ. + Отворени кључарник + Поново покрени + Инсталирај + Инсталирајте Отворени кључарник + нудим… + чекам… + Нема ОпенПГП кључа + Није могуће шифровати вашу поруку јер контакт није објавио свој јавни кључ.\n\nЗамолите контакт да подеси ОпенПГП. + Нема ОпенПГП кључева + Није могуће шифровати вашу поруку јер контакти нису објавили своје јавне кључеве.\n\nЗамолите контакте да подесе ОпенПГП. + Опште + Прихватај фајлове + Аутоматски прихватај фајлове мање од… + Прилози + Обавештење + Вибрирај + Вибрирање кад стигне нова порука + ЛЕД светло + Трептање ЛЕД светла кад стигне нова порука + Звук + Звук обавештења + Звук обавештења нових порука + Мелодија долазног позива + Период одгоде + Напредно + Никад не шаљи извештаје о паду + Слањем извештаја рада помажете развоју апликације. + Потврди поруке + Обзнаните контактима када примите и прочитате њихове поруке + Сучеље + Отворени кључарник је направио грешку. + Лош кључ за шифровање. + Прихвати + Десила се грешка + Грешка + Ваш налог + Шаљи ажурирања присутности + Примај ажурирања присутности + Питај за ажурирања присутности + Изабери слику + Фотографиши + Унапред дозволи захтев за претплатом + Изабрани фајл није слика + Не могу преобратити датотеку фотографије + Фајл није нађен + Општа У/И грешка. Можда вам је нестало простора у складишту? + Апликација из које делите ову слику не даје дозволу довољну да се датотека учита.\n\nПоделите слику другим претраживачем датотека. + Апликација из које делите овај садржај не даје довољну дозволу. + Непознато + Привремено искључен + На вези + Повезивање\u2026 + Ван везе + Неовлашћен + Сервер није нађен + Нема везе + Регистрација није успела + Корисничко име је већ у употреби + Регистрација завршена + Овај сервер не подржава регистрацију + Неисправан регистрациони токен + ТЛС преговарање није успело + Непроверљив домен + Нарушавање полисе + Некомпатибилан сервер + Грешка тока + Грешка при отварању тока + Нешифровано + ОТР + ОпенПГП + ОМЕМО + Обриши налог + Привремено искључи + Објави аватар + Објави ОпенПГП јавни кључ + Уклони ОпенПГП кључ + Желите ли заиста да уклоните ваш ОпенПГП кључ из ваше објаве присутности?\nВаши контакти више неће моћи да вам шаљу ОпенПГП шифроване поруке. + ОпенПГП кључ је објављен. + Укључи налог + Да ли сте сигурни? + Брисањем налога бришете и целу историју ваших разговора. + Сними глас + ИксМПП адреса + Блокирај ИксМПП адресу + korisnickoime@primer.com + Лозинка + Ово је неисправна ИксМПП адреса + Недовољно меморије. Фотографија је превелика + Желите ли да додате %s у ваш именик? + Подаци о серверу + XEP-0313: МАМ + XEP-0280: копије порука + XEP-0352: индикација стања клијента + XEP-0191: наредба блокирања + XEP-0237: верзионисање ростера + XEP-0198: менаџмент тока + XEP-0215: Проналажење спољњих сервиса + XEP-0163: PEP (аватари/ОМЕМО) + XEP-0363: ХТТП отпремање фајлова + XEP-0357: „push“ + доступан + недоступан + Недостају објаве јавног кључа + виђен/а мало пре + виђен/а пре минут + виђен/а пре %d минута + виђен/а пре сат времена + виђен/а пре %d сати + виђен/а јуче + виђен/а пре %d дана + Шифрована порука. Инсталирајте Отворени кључарник да је дешифрујете. + Пронаћене су нове ОпенПГП шифроване поруке + ИД ОпенПГП кључа + ОМЕМО отисак + v\\ОМЕМО отисак + ОМЕМО отисак (порекло поруке) + v\\ОМЕМО отисак (порекло поруке) + Остали уређаји + Поуздај се у ОМЕМО отиске + Добављам кључеве… + Готово + Дешифруј + Обележивачи + Тражи + Унеси контакт + Обриши контакт + Прикажи детаље контакта + Блокирај контакт + Одблокирај контакт + Направи + Изабери + Контакт већ постоји + Придружи се + channel@conference.example.com/nick + channel@conference.example.com + Сачувај као обележивач + Обриши обележивач + Уклони групно ћаскање + Уклони канал + Да ли сигурно жеите да уклоните ово групно ћаскање?\n\nУпозорење: Групно ћаскање ће бити потпуно обрисано са сервера. + Да ли сигурно жеите да уклоните овај јавни канал?\n\nУпозорење: Канал ће бити потпуно обрисан са сервера. + Не могу уклонити групно ћаскање + Не могу уклонити канал + Уреди предмет групног ћаскања + Тема + Улазим у групно ћаскање… + Напусти + Контакт вас је додао на списак контаката + Додај га + %s је прочитао довде + %s је прочитао/ла довде + %1$s + %2$d других су прочитали довде + Сви су прочитали довде + Објави + Тапните аватар да изаберете слику из галерије + Објављујем… + Сервер је одбио вашу објаву + Не могу преобратити вашу фотографију + Не могох да сачувам аватар на диск + (или притисните дуго да вратите подразумевани) + Ваш сервер не подржава објаву аватара + шапну + за %s + Пошаљи личну поруку за %s + Повежи + Овај налог већ постоји + Следеће + Сесија успостављена + Прескочи + Искључи обавештења + Укључи + Групно ћаскање захтева лозинку + Унесите лозинку + Најпре захтевајте ажурирање присутности од вашег контакта.\n\nОво ће омогућити да се одреди који клијент ваш контакт користи. + Захтевај одмах + Занемари + Упозорење: Слањем овога без обостраног ажурирања присутности може изазвати неочекиване проблеме.\n\nИдите у „Детаљи контакта” да потврдите вашу претплату за присутност. + Безбедност + Дозволи исправљање порука + Дозвољава вашим контактима да ретроактивно уређују њихове поруке + Поставке за стручњаке + Будите пажљиви са овим + О %s + Тихи сати + Време почетка + Време завршетка + Укључи тихе сате + Обавештења ће бити ућуткана за време тихих сати + Остало + ОМЕМО отисак копиран на клипборд + Забрањен вам је приступ овом групном ћаскању + Ово групно ћаскање је само за чланове + Ограничење ресурса + Шутнути сте из овог групног ћаскања + Групно ћаскање је угашено + Више нисте у овом групном ћаскању + преко налога %s + код домаћина %s + Проверавам %s на ХТТП домаћину + Нисте повезани. Покушајте поново касније + Провери величину %s + Провери величину %1$s na %2$s + Опције поруке + Цитирај + Налепи као навод + Копирај изворни УРЛ + Пошаљи поново + УРЛ фајла + УРЛ је копиран на клипборд + ИксМПП адреса копирана на клипборд + Порука грешке копирана на клипборд + веб адреса + Очитај 2Д бар-кôд + Прикажи 2Д бар-кôд + Прикажи списак блокираних + Детаљи налога + Потврди + Покушај поново + Сервис у првом плану + Спречава оперативни систем да прекине вашу везу + Направите резерву + Резерва ће бити складиштена у %s + Правим резерву + Ваша резерва је направљена + Резерве су складиштене у %s + Учитавам резерву + Ваша резерва је учитана + Не заборавите да омогућите налог + Изабери фајл + Примам %1$s (%2$d%% завршено) + Преузми %s + Обриши %s + фајл + Отвори %s + шаљем (%1$d%% завршено) + Припремам датотеку за пренос + %s понуђен за преузимање + Прекини пренос + не могу поделити датотеку + пренос датотеке је прекинут + Датотека је обрисана + Нема апликације за отварање датотеке + Нема апликације за отварање везе + Нема апликације за приказ контакта + Динамичке ознаке + Приказ ознака испод контаката + Укључи обавештења + Сервер групног ћаскања није нађен + Не могу направити групно ћаскање + Аватар налога + Копирај ОМЕМО отисак на клипборд + Поново генериши ОМЕМО кључ + Очисти уређаје + Нема употребљивих кључева за овај контакт.\nПроверите да ли сте одобрили узајамно ажурирање присутности. + Нешто је пошло по злу + Добављам историјат са сервера + Нема више историјата на серверу + Ажурирам… + Лозинка промењена! + Не могох да променим лозинку + Промени лозинку + Текућа лозинка + Нова лозинка + Лозинка не може бити празна + Укључи све налоге + Искључи све налоге + Изврши радњу са + Без припадности + Ван везе + Изгнаник + Члан + Напредни режим + Одобри админ. привилегије + Укини админ. привилегије + Одобри админ. привилегије + Одобри админ. привилегије + Одобри власничке привилегије + Укини власничке привилегије + Уклони из групног ћаскања + Уклони из канала + Не могох да изменим припадност за %s + Забрани приступ групном ћаскању + Забрани приступ каналу + Покушавате да уклоните %s из јавног канала. Ово се за стално постиже једино забраном приступа кориснику. + Забрани одмах + Не могох да изменим улогу за %s + Поставке приватног групног ћаскања + Поставке јавног канала + Лична, само чланови + Начините ИксМПП адресу видљиву свима + Начините канал модерисаним + Не учествујете + Опције групног ћаскања измењене! + Не могу да изменим опције групног ћаскања + никад + до даљњег + Одложи + Одговори + Означи прочитаним + Унос + Ентер шаље + Прикажи Ентер тастер + Промени тастер за емотиконе у ентер тастер + звук + видео + слика + ПДФ документ + Апликација за Андроид + Контакт + Аватар је објављен! + Шаљем %s + Нудим %s + Сакриј неповезане + %s куца… + %s престаде да куца + %s куцају… + %s престаше да куцају + Обавештења о куцању + Обзнаните контактима када им куцате поруке + Пошаљи локацију + Прикажи локацију + Нема апликације за приказ локације + Локација + Преписка затворена + Напустили сте групно ћаскање + Напустили сте јавни канал + Не поуздај се у системска сертификациона тела + Сви сертификати морају ручно да се одобре + Уклони сертификате + Обриши ручно одобрене сертификате + Нема ручно одобрених сертификата + Уклањање сертификата + Обриши изабрано + Одустани + + %d сертификат обрисан + %d сертификата обрисана + %d сертификата обрисано + + Замени дугме за слање брзом радњом + Брза радња + Ниједна + Недавно коришћена + Изаберите брзу радњу + Тражи контакте + Претрага обележивача + Пошаљи личну поруку + %1$s је напустио/ла групно ћаскање + Корисничко име + Корисничко име + Ово није исправно корисничко име + Преузимање није успело: сервер није нађен + Преузимање није успело: фајл није нађен + Преузимање није успело: не могох да се повежем са домаћином + Преузимање није успело: не могох да упишем фајл + Тор мрежа недоступна + Неуспех свезивања + Сервер није одговоран за овај домен + Оштећен + Доступност + Увек када је уређај закљчан + Прикажи ме одсутним када је уређај закљчан + Заузет у нечујном режиму + Прикажи ме заузетум у нечујном режиму + Вибрација је нечујни режим + Прикажи ме заузетум у режиму вибрације + Проширене поставке повезивања + Приказ домаћина и порта у поставкама налога + xmpp.primer.com + Пријавите се сертификатом + Не могу прочитати сертификат + Поставке архивисања + Серверске поставке архивисања + Добављам поставке архивисања, сачекајте… + Не могу да добавим поставке архивисања + КЕПЧА је обавезна + Унесите текст са слике изнад + Неповерљив ланац сертификата + ИксМПП адреса се не слаже са сертификатом + Обнови сертификат + Грешка добављања ОМЕМО кључа! + Оверен ОМЕМО кључ помоћу сертификата! + Ваш уређај не подржава избор сертификата клијента! + Повезивање + Повежи се преко Тора + Тунеловање свих веза кроз Тор мрежу. Захтева Орбот + Име домаћина + Порт + Серверска или .onion адреса + Ово није исправан број порта + Ово није исправно име домаћина + %1$d од %2$d налога повезано + + %d порука + %d поруке + %d порука + + Учитај још порука + Датотека подељена са %s + Слика подељена са %s + Слике подељене са %s + Текст подељен са %s + Дозволите да %1$s приступи спољној меморији + Дозволите да %1$s приступи камери + Синхронизуј са контактима + Обавештења за све поруке + Обавести само када ме помињу + Обавештења искључена + Обавештења паузирана + Компресија слике + увек + Само велике слике + Оптимизација батерије је укључена + Искључи + Назначена површина је превелика + (Нема активираних налога) + Ово поље је захтевано + Исправи поруку + Пошаљи исправљену поруку + Искључили сте овај налог + Безбедносна грешка: неисправан приступ датотеци! + Нема апликације за дељење ресурса + Подели везу помоћу… + Сложи се и настави + Ваша цела ИксМПП адреса ће бити: %s + Направи налог + Користићу сопствени провајдер + Одредите ваше корисничко име + Ручно мењај доступност + Поставите присутност при измени ваше поруке стања. + Порука стања + Слободан за ћаскање + На вези + Одсутан + Недоступан + Заузет + Безбедна лозинка је направљена + Ваш уређај не подржава искључивање оптимизације батерије + Регистрација није успела: покушајте поново касније + Регистрација није успела: лозинка преслаба + Додај учеснике + Правим групно ћаскање… + Пошаљи поново + Искључи + Кратак + Средњи + Дуг + Објави употребу + Обзнаните контакте кад користите Конверзацију + Приватност + Тема + Избор палете боја + Аутопатски + Светла + Тамна + Зелена позадина + Зелена позадина за примљене поруке + Не могу да се повежем са Отвореним кључарником + Овај уређај више није у употреби + Рачунар + Мобилни телефон + Таблет + Веб прегледач + Конзола + Захтевано је плаћање + Дозволите приступ интернету + Ја + Контакт пита за претплату на ажурирање присутности + Дозволи + Нема дозвола за приступ %s + Удаљени сервер није нађен + Удаљени сервер се не одазива + Не могу да ажурирам налог + Пријавите ову ИксМПП адресу због нежељених порука. + Обриши ОМЕМО идентитете + Обриши изабране кључеве + Морате бити повезани да бисте објавили ваш аватар. + Прикажи поруку грешке + Порука грешке + Чувар протока укључен + Ваш уређај не подржава искључење „Уштеде података” за %1$s. + Не могу да направим привремену датотеку + Овај уређај је оверен. + Копирај отисак + Оверени отисци + Користи камеру за очитавање контактова бар-кôда + Сачекајте на добављање кључева + Подели као бар-кôд + Подели као ИксМПП УРИ + Подели као ХТТП везу + Слепо веруј и пре провере + Непоуздан + Неисправан 2Д бар-кôд + Очисти кеш + Очисти лично складиште + Овери ОМЕМО кључеве + Прикажи неактивне + Сакриј неактивне + + %d секунда + %d секунде + %d скунди + + + %d минута + %d минуте + %d минута + + + %d сат + %d сата + %d сати + + + %d дан + %d дана + %d дана + + + %d седмица + %d седмице + %d седмица + + + %d месец + %d месеца + %d месеци + + Аутоматско брисање порука + Аутоматско брисање порука са овог уређаја које су старије од постављеног времена. + Шифрујем поруку + Компресујем видео + Одговарајуће преписке затворене. + Контакт блокиран. + Обавештења од непознатих + Примљена порука од незнанца + Блокирај странца + Блокирај читав домен + на вези сада + Покушај дешифровање поново + Неуспех сесије + Сервер захтева регистрацију на сајту + Отвори сајт + Данас + Јуче + делимично + Сними видео + Копирај на клипборд + Порука копирана на клипборд + Порука + Нацрт: + ОМЕМО ће увек бити у употреби за један-на-један и приватна групна ћаскања. + Направи пречицу + Величина фонта + Подразумевано укључено + Подразумевано искључено + Средњи + врати + Дељење локације је искључено + Копирај локацију + Подели локацију + Упутства + Подели локацију + Прикажи локацију + Дели + Сачекајте… + Тражи поруке + Погледај преписку + Копирај веб адресу + Аватар групног ћаскања + Домаћин не подржава аватар групног ћаскања + Само власник може да промени аватар групног ћаскања + Име контакта + Надимак + Име + Име је опционо + Назив групног ћаскања + Ово групно ћаскање је обрисано + Поруке + Поруке + Тихе поруке + Видео компресија + Заузет + diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml new file mode 100644 index 000000000..b606cdb84 --- /dev/null +++ b/app/src/main/res/values-sv/strings.xml @@ -0,0 +1,913 @@ + + + Inställningar + Ny konversation + Kontoinställningar + Hantera konto + Stäng konversation + Kontaktdetaljer + Gruppchattdetaljer + Kanaldetaljer + Lägg till konto + Ändra namn + Lägg till i kontakter + Ta bort kontakt + Blockera kontakt + Avblockera kontakt + Blockera domän + Avblockera domän + Blockera deltagare + Avblockera deltagare + Hantera konton + Inställningar + Dela med konversation + Starta konversation + Välj kontakt + Välj kontakter + Dela via konto + Blockeringslista + just nu + 1 min sedan + %d min sedan + + %d oläst konversation + %d olästa konversationer + + skickar… + Avkrypterar meddelande. Vänta… + OpenPGP-krypterat meddelande + Nick används redan + Ogiltigt smeknamn + Admin + Ägare + Moderator + Deltagare + Besökare + Vill du ta bort %s från din kontaktlista? Konversationer med denna kontakt kommer inte tas bort. + Vill du blockera %s från att skicka dig meddelanden? + Vill du avblockera %s och tillåta denne att skicka dig meddelanden? + Blockera alla kontakter från %s? + Avblockera alla kontakter från %s? + Kontakt blockerad + Blockerad + Vill du ta bort %s som ett bokmärke? Konversationer med detta bokmärke kommer inte tas bort. + Registrera nytt konto på servern + Byt lösenord på server + Dela med… + Börja konversation + Bjud in kontakt + Bjud in + Kontakter + Kontakt + Avbryt + Sätt + Lägg till + Ändra + Ta bort + Blockera + 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 + Förbereder att skicka bilder + 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 + Skicka meddelande + Skicka meddelande till %s + Skicka OMEMO-krypterat meddelande + Skicka v\\OMEMO-krypterat meddelande + Skicka OpenPGP-krypterat meddelande + Nytt smeknamn används + 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… + Bifogningar + Notifiering + Vibrera + Vibrera när meddelande tagits emot + 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 + Fel + Ditt konto + Skicka tillgänglighetsuppdatering + Ta emot tillgänglighetsuppdateringar + Be om tillgänglighetsuppdateringar + Välj bild + 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 + Ansluter\u2026 + Offline + Otillåten + Server ej funnen + Ingen anslutning + 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 + OMEMO + Ta bort konto + Inaktivera tillfälligt + Publicera avatarbild + 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 + XEP-0280: Message Carbons + XEP-0352: Client State Indication + XEP-0191: Blocking Command + XEP-0237: Roster Versioning + XEP-0198: Stream Management + XEP-0215: External Service Discovery + XEP-0163: PEP (Avatarbilder / OMEMO) + XEP-0363: Ladda upp via HTTP + XEP-0357: Push + tillgänglig + 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... + Klar + Avkryptera + Bokmärken + Sök + Fyll i kontakt + Ta bort kontakt + Se kontaktdetaljer + Blockera kontakt + Avblockera kontakt + Skapa + 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 + Expertinställningar + Var försiktig med dem + Om %s + Tysta timmar + Starttid + Sluttid + Aktivera tysta timmar + Notifieringar kommer vara tysta under tysta timmar + Annat + 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 + Kontrollera filstorlek för %1$s på %2$s + Meddelandealternativ + Citera + Klistra in som citat + Kopiera orginal-URL + Skicka igen + Fil-URL + Kopierade URL till urklipp + Kopierade XMPP-adress till urklipp + Kopierade felmeddelande till urklipp + webbadress + Scanna 2D-streckkod + Visa 2D-streckkod + Visa blockeringslista + 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 + Skapar filer för säkerhetskopia + Din säkerhetskopia har skapats + Säkerhetskopians filer har lagrats i %s + Återställer säkerhetskopia + Din säkerhetskopia har återställts + Glöm inte att aktivera kontot. + Välj fil + Tar emot %1$s (%2$d%% klart) + Ladda ner %s + Ta bort %s + 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 + Uppdaterar… + Lösenord bytt! + Kunde inte byta lösenord + 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 + Ingen anknytning + Offline + 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 + Avatarbild har publicerats! + Skickar %s + Erbjuder %s + Dölj ej anslutna + %s skriver... + %s har slutat skriva + %s skriver... + %s har slutat skriva + Skriv-notifieringar + 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 + Ta bort manuellt accepterade certifikat + Inga manuellt accepterade certifikat + Ta bort certifikat + Ta bort val + Avbryt + + %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 + Nerladdning gick fel: Server hittades inte + Nerladdning gick fel: Filen hittades inte + Nerladdningen gick fel: Kunder inte ansluta till server + Nerladdning gick fel: Kunde inte skriva fil + Nedladdning misslyckades: Ogiltig 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! + Din enhet stödjer inte val av klientcertifikat! + Anslutning + Ansluten via Tor + 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 + + %d meddelande + %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... +
Du registrerar dig med ditt telefonnummer och Quicksy kommer automatiskt – baserat på telefonnumren i din adressbok – att föreslå möjliga kontakter till dig.

Genom att registrera dig godkänner du vår integritetspolicy.]]>
+ Acceptera och gå vidare + En guide har skapats för kontoskapande på conversations.im.¹\nNär du väljer conversations.im som leverantör kommer du att kunna kommunicera med användare av andra leverantörer genom att ge dem din fullständiga XMPP-adress. + Din fullständiga XMPP-adress kommer att vara: %s + Skapa konto + Använd min egen leverantör + Välj användarnamn + Hantera tillgänglighet manuellt + Ställ in din tillgänglighet när du redigerar ditt statusmeddelande. + Statusmeddelande + Tillgänglig + Online + Borta + Ej tillgänglig + Upptagen + Ett säkert lösenord har genererats + Din enhet stödjer inte deaktivering av batterioptimeringar + Registreringfel: Försök igen senare + Registreringsfel: Lösenordet är för svagt + Välj deltagare + Skapar gruppchatt... + Bjud in igen + Deaktivera + Kort + Medium + Lång + Gör användandet offentligt + Låter dina kontakter veta när du använder Conversations + Privatliv + 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 + Surfplatta + 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 + Ditt operativsystem begränsar åtkomsten till Internet i bakgrunden för %1$s. För att få aviseringar om nya meddelanden bör du tillåta obegränsad åtkomst för %1$s, när databesparing är på.\n %1$s kommer fortfarande att anstränga sig för att spara data när det är möjligt. + Din enhet stöder inte inaktivering av databesparing för %1$s. + 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 + Vänta medans nycklar hämtas + Dela som streckkod + 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) + Jag följde denna länk från en trovärdig källa + Du håller på att verifiera OMEMO-nyckeln för %1$s efter att du följt en länk. Detta är endast säkert om du följde länken från en trovärdig källa där endast %2$s kan ha publiserat denna länk. + Du är på väg att verifiera OMEMO-nycklarna för ditt eget konto. Detta är bara säkert om du följde den här länken från en pålitlig källa där bara du kunde ha publicerat den här länken. + Fortsätt + Verifiera OMEMO-nycklar + Visa inaktiva + Dölj inaktiva + Lita ej på enhet + Är du säker på att du vill ta bort verifieringen av den här enheten?\nDen här enheten och meddelanden från den kommer att markeras som \"Ej betrodd\". + + %d sekund + %d sekunder + + + %d minut + %d minuter + + + %d timme + %d timmar + + + %d dag + %d dagar + + + %d vecka + %d veckor + + + %d månad + %d månader + + Automatisk borttagning av meddelanden + Ta automatiskt bort meddelanden från denna enhet som är äldre än den konfigurerade tidsramen. + Krypterar meddelande + Hämtar inte meddelanden på grund av inställningen för borttagning av gamla meddelanden. + Komprimerar video + Korresponderande konversationer är stängda. + Kontakt blockerad. + Notifieringar från främlingar + Meddela för meddelanden och samtal från främlingar. + Mottagna meddelanden från främlingar + Blockera främling + Blockera hel domän + online just nu + Försök dekryptera igen + Sessionsfel + Nedgraderad SASL-mekanism + Servern kräver registrering via webbplatsen + Ö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 + Servercertifikat som innehåller det validerade värdnamnet anses vara verifierade + Certifikatet innehåller ej en XMPP-adress + delvis + Spela in video + Kopiera till urklipp + Meddelande kopierat till urklipp + Meddelande + Privata meddelanden är inaktiverade + Skyddade applikationer + För att fortsätta ta emot aviseringar, även när skärmen är avstängd, måste du lägga till Conversations i listan över skyddade applikationer. + Godkänn okänt certifikat? + Servercertifikatet är inte signerat av en känd certifikatutfärdare. + Acceptera servernamn som inte matchar? + Servern kunde inte autentisera som \"%s\". Certifikatet är endast giltigt för: + Vill du ansluta ändå? + Certifikatdetaljer: + En gång + QR-läsaren behöver åtkomst till kameran + Bläddra till botten + Bläddra ner efter att du har skickat ett meddelande + Redigera Statusmeddelande + Redigera statusmeddelande + Inaktivera kryptering + %1$s kan inte skicka krypterade meddelanden till %2$s. Detta kan bero på att din kontakt använder en föråldrad server eller klient som inte kan hantera OMEMO. + Det gick inte att hämta enhetslistan + Det gick inte att hämta krypteringsnycklar + Tips: I vissa fall kan detta åtgärdas genom att lägga till varandra i era respektive kontaktlistor. + Är du säker på att du vill inaktivera OMEMO-kryptering för den här konversationen?\nDetta gör att din serveradministratör kan läsa dina meddelanden, men det kan också vara det enda sättet att kommunicera med människor som använder äldre klienter. + Inaktivera nu + Utkast: + OMEMO-kryptering + OMEMO kommer alltid att användas för privata konversationer och privata gruppchattar. + OMEMO kommer att användas som standard för nya konversationer. + OMEMO måste manuellt aktiveras för varje ny konversation. + Skapa genväg + Textstorlek + Den relativa teckenstorleken som används i appen. + På som standard + Av som standard + Liten + Mellan + Stor + Meddelandet är inte krypterat för den här enheten. + Misslyckades med att dekryptera OMEMO-meddelandet. + ångra + Platsdelning är inaktiverat + Lås position + Lås upp position + Kopiera plats + Dela plats + Hänvisningar + Dela plats + Visa plats + Dela + Det gick inte att starta inspelningen + Var god dröj... + Ge %1$s tillgång till mikrofonen + Söka i meddelanden + GIF + Visa konversation + Dela plats-tillägget + Kopiera webbadress + Kopiera XMPP-adress + HTTP-fildelning för S3 + Direktsök + Gruppkonversationens visningsbild + Värden stöder inte visningsbilder för gruppkonversationer + Endast ägaren kan ändra visningsbilden för gruppkonversationen + Kontaktnamn + Smeknamn + Namn + Att ange ett namn är valfritt + Gruppchattens namn + Kunde inte att spara inspelningen + Förgrundsservice + Statusinformation + Anslutningsproblem + Meddelanden + Samtal + Meddelanden + Inkommande samtal + Pågående samtal + Tysta meddelanden + Misslyckade leveranser + Videokompression + Visa media + Deltagare + Mediautforskare + Videokvalitet + Mellan (360p) + Hög (720p) + avbruten + Du håller redan på att skriva ett meddelande. + Välj ett land + telefonnummer + Bekräfta ditt telefonnummer + tillbaka + Ja + Nej + Bekräftar... + Okänt nätverksfel. + För många försök + Du använder en föråldrad version av denna app. + Uppdatera + Ditt namn + Skriv in ditt namn + Avslå begäran + Installera Orbot + Starta Orbot + e-bok + Öppna med... + Konversationens profilbild + 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. + Säkerhetskopia & Återställ + Ange XMPP-adress + Skapa gruppchatt + Anslut till publik gruppkonversation + Skapa sluten gruppchatt + Skapa publik gruppkonversation + Kanalnamn + XMPP-adress + Vänligen ange ett namn på kanalen + Ange en XMPP-adress + Detta är en XMPP-adress. Ange ett namn. + Skapar publik gruppkonversation... + Denna kanal finns redan + Du har gått med i en befintlig kanal + Det gick inte att spara kanalkonfigurationen + Tillåt vem som helst att ändra ämnet + Tillåt vem som helst att bjuda in andra + Vem som helst kan ändra ämnet. + Ägaren kan ändra ämnet. + Administratörer kan ändra ämnet. + Ägare kan bjuda in andra. + Vem som helst kan bjuda in andra. + XMPP-adresser är synliga för administratörer. + XMPP-adresser är synliga för alla. + Den här publika gruppkonversationen har inga deltagare. Bjud in dina kontakter eller använd \'dela-knappen\' för att dela XMPP-adressen. + Denna slutna gruppchatt har inga deltagare. + Hantera rättigheter + Sök efter deltagare + För stor fil + Bifoga + Upptäck kanaler + Sök efter gruppkonversationer + Möjlig integritetskränkning! + Jag har redan ett konto + Lägg till befintligt konto + Skapa nytt konto + Detta verkar vara ett domännamn + Lägg till ändå + Detta ser ut som en kanaladress + Dela säkerhetskopior + Säkerhetskopior för Conversations + Händelse + Öppna säkerhetskopia + Filen du valde är inte en säkerhetskopia till Conversations + Det här kontot har redan konfigurerats + Var god ange lösenordet för det här kontot + Det gick inte att utföra den här åtgärden + Anslut till publik gruppkonversation... + Delnings-appen gav inte behörighet till att komma åt den här filen. + + jabber.network + Lokal server + De flesta användare bör välja \"jabber.network\" för bättre förslag från hela det offentliga XMPP-ekosystemet. + Metod för kanalupptäckt + Säkerhetskopiering + Om + Aktivera ett konto + Ring + Inkommande samtal + Inkommande videosamtal + Ansluter + Ansluten + Återansluter + Accepterar samtal + Avslutar samtal + Svara + Avvisa + Upptäcker enheter + Ringer + Upptagen + Kunde inte koppla samtal + Anslutning bröts + Återkallat samtal + Appmisslyckande + Verifikationsproblem + Lägg på + Pågående samtal + Pågående videosamtal + Återansluter samtalet + Återansluter videosamtalet + Inaktivera Tor för att ringa samtal + Inkommande samtal + Inkommande samtal · %s + Missat samtal · %s + Utgående samtal + Pågående samtal · %s + Missat samtal + Röstsamtal + Videosamtal + Hjälp + Växla till konversation + Din mikrofon är inte tillgänglig + Du kan bara ha ett samtal åt gången. + Återgå till pågående samtal + Kunde inte växla kamera + Fäst flik till toppen + Ta bort flik från toppen + GPX-spår + Kunde inte korrigera meddelandet + Alla konversationer + Den här konversationen + Din visningsbild + Visningsbild för %s + Krypterad med OMEMO + Krypterad med OpenPGP + Inte krypterad + Avsluta + Spela in ett röstmeddelande + Spela upp ljud + Pausa ljud + Lägg till kontakt, skapa eller gå med i gruppchatt eller upptäck kanaler + + Visa %1$d deltagare + Visa %1$d deltagare + + Misslyckade leveranser + Fler alternativ + Ingen applikation hittades + Bjud in till Conversations + Ingen XMPP-adress hittades +
\ No newline at end of file diff --git a/src/main/res/values-land/bools.xml b/app/src/main/res/values-sw600dp/device.xml similarity index 53% rename from src/main/res/values-land/bools.xml rename to app/src/main/res/values-sw600dp/device.xml index 1aa953fcc..76b119505 100644 --- a/src/main/res/values-land/bools.xml +++ b/app/src/main/res/values-sw600dp/device.xml @@ -1,4 +1,4 @@ - false + true diff --git a/app/src/main/res/values-szl/strings.xml b/app/src/main/res/values-szl/strings.xml new file mode 100644 index 000000000..b6004932c --- /dev/null +++ b/app/src/main/res/values-szl/strings.xml @@ -0,0 +1,1023 @@ + + + Sztelōnki + Nowo godka + Sztelōnki kōnt + Sztelōnki kōnta + Zawrzij godka + Informacyje kōntaktu + Informacyje kōnferyncyje + Informacyje kanału + Przidej kōnto + Edytuj miano + Przidej do kōntaktōw + Skasuj z rostera + Zablokuj kōntakt + Ôdblokuj kōntakt + Ôdblokuj dōmyna + Ôdblokuj dōmyna + Zablokuj kōntakt + Ôdblokuj kōntakt + Sztelōnki kōnt + Sztelōnki + Udostympnij we godce + Zacznij godka + Ôbier kōntakt + Ôbier kōntakty + Udostympnij bez + Czorno lista + przed chwilōm + minuta tymu + %d minut tymu + + %d niyprzeczytano kōnwersacyjo + + + %d niyprzeczytane kōnwersacyje + + + %d niyprzeczytanych kōnwersacyji + + + wysyłanie… + Ôdszyfrowowanie wiadōmości. To weźnie ino chwila… + Wiadōmość zaszyfrowano OpenPGP + Miano je już zajynte + Niynoleżny pseudōnim + Admin + Posiedziciel + Moderatōr + Uczestnik + Gość + Chcesz wymazać kōntakt %s ze listy\? Godki ze tym kōntaktym niy bydōm wymazane. + Na zicher chcesz zablokować wiadōmości ôd kōntaktu %s\? + Na zicher chcesz ôdblokować wiadōmości ôd kōntaktu %s\? + Zablokować wszyjske kōntakty ze %s\? + Ôdblokować wszyjske kōntakty ze %s\? + Kōntakt zablokowany + Zablokowane + Chcesz wymazać zokłodka %s\? Godki z niōm niy bydōm wymazane. + Zaregistruj nowe kōnto na serwerze + Umiyń hasło na serwerze + Udostympnij… + Zacznij godka + Zaproś kōntakt + Zaproś + Kōntakty + Kōntakt + Pociep + Nasztaluj + Przidej + Edytuj + Wymaż + Zablokuj + Ôdblokuj + Spamiyntej + OK + We %1$s doszło do awaryje + Jak używosz swojigo kōnta XMPP do wysyłanio sztrekōw sztapla, to pōmogosz przi budowaniu %1$s. + Wyślij teroz + Niy pytej zaś + Niy idzie połōnczyć z kōntym + Niy idzie połōnczyć z mockōm kōnt + Tyknij, coby sztelować swoje kōnta + Przidej zbiōr + Przidać tyn kōntakt do twojij listy kōntaktōw\? + Przidej kōntakt + wysyłanie sie niy podarziło + Rychtowanie do wysłanio ôbrozka + Rychtowanie do wysłanio ôbrozkōw + Udostympnianie zbiorōw. Czekej… + Wymaż historyjo + Wymaż historyjo kōnwersacyje + Chcesz wymazać wszyjske wiadōmości we tyj godce\? +\n +\nPozōr: To niy wpływo na wiadōmości trzimane na inkszych maszinach abo serwerach. + Wymaż zbiōr + Na zicher wymazać tyn zbiōr\? +\n +\nPozōr: To niy wpływo na kopije zbioru trzimane na inkszych maszinach abo serwerach. + Zawrzij potym tyż kōnwersacyjo + Ôbier maszina + Wyślij wiadōmość bez szyfrowanio + Wyślij wiadōmość + Wyślij wiadōmość do kōntaktu %s + Wyślij wiadōmość zaszyfrowano OMEMO + Wyślij wiadōmość zaszyfrowano v\\OMEMO + Wyślij zaszyfrowano wiadōmość (OpenPGP) + Przemianek je umiyniōny + Wyślij bez szyfrowanio + Niy idzie ôdszyfrować. Możno niy mosz noleznego prywatnego klucza. + OpenKeychain + %1$s używo <b>OpenKeychain</b>, żeby szyfrować i ôdszyfrowować wiadōmości i zarzōndzać twojimi publicznymi kluczami.<br><br>OpenKeychain je na licyncyji GPLv3+ i je dostympny we F-Droid abo Google Play.<br><br><small>(Puść %1$s na nowo po zainstalowaniu.)</small> + Zresztartuj + Zainstaluj + Zainstaluj OpenKeychain + ôferowanie… + czekanie… + Niy szło znojś klucza OpenPGP + Niy idzie zaszyfrować twojij wiadōmości, bo tyn kōntakt niy ôgłoszo swojigo publicznego klucza. +\n +\nPoproś kōntakt, żeby nasztalowoł OpenPGP. + Niy szło znojś kluczy OpenPGP + Niy idzie zaszyfrować twojij wiadōmości bo twoje kōntakty niy ôgłoszajōm swojich kluczy publicznych. +\n +\nPoproś, żeby nasztalowały OpenPGP. + Bazowe + Akceptuj zbiory + Autōmatycznie akceptuj zbiory myńsze niż… + Przidowki + Powiadōmiynie + Wibracyje + Wibruj, jak przidzie wiadōmość + Powiadōmiynie diodōm LED + Migej diodōm, jak przidzie wiadōmość + Zwōnek + Klang powiadōmiyń + Klang powiadōmiyń ô nowych wiadōmościach + Zwōnek przi przichodzōncych połōnczyniach + Czas bez powiadōmiyń + Dugość czasu, kej powiadōmiynia sōm wyciszōne po wykryciu aktywności na jednyj z twojich inkszych maszin. + Rozszyrzōne + Niy wysyłej reportōw awaryje + Jak wysyłosz sztreki sztapla, to pōmogosz przi budowaniu + Potwierdzynia wiadōmości + Przizwōl na wysyłanie do ôsōb ze listy kōntaktōw informacyje ô twojim dostaniu i przeczytaniu wiadōmości ôd nich + Blokuj zopisy ekranu + Skryj zawartość aplikacyje we szaltrze aplikacyji i zablokuj zopisy ekranu + UI + OpenKeychain zgłosiyło błōnd. + Zły klucz szyfrowanio. + Akceptować + Doszło do błyndu + Błōnd + Twoje kōnto + Wysyłej powiadōmiynia ôbecności + Dostowej powiadōmiynia ôbecności + Poproś ô powiadōmiynia ôbecności + Ôbier ôbroz + Zrōb fotografijo + Autōmatyczno zwolo na subskrypcyjo + Ôbrany zbiōr to niy ôbroz + Błōnd kōnwersyje ôbrazu + Niy szło znojś zbioru + Ôgōlny feler wchodu/wychodu. Możno skōńczōł sie plac na dane\? + Aplikacyjo użyto do ôbioru ôbrazu niy przizwolyła na ôdczyt zbioru. +\n +\nÔbier ôbroz przi użyciu inkszego mynedżera zbiorōw. + Aplikacyjo użyto do udostympniynio tego zbioru niy dała stykajōncych uprawniyń. + Niyznōmy + Tymczasowo zastawiōne + Połōnczōne + Łōnczynie… + Rozłōnczōne + Błōnd autoryzacyje + Niy szło znojś serwera + Brak połōnczynio + Błōnd registracyje + Miano zajynte + Zaregistrowano sprownie + Tyn serwer niy spiyro registracyje + Niynoleżny tokyn registracyje + Niy podarziła sie negocjacyjo TLS + Dōmyna niyweryfikowalno + Naruszynie prawideł + Serwer niykōmpatybilny + Błōnd potoku + Błōnd ôtwiyranio potoku + Bez szyfrowanio + OTR + OpenPGP + OMEMO + Wymaż kōnto + Zastow tymczasowo + Publikuj awatar + Udostympnij klucz publiczny OpenPGP + Wymaż klucz publiczny OpenPGP + Na zicher chcesz wymazać klucz publiczny OpenPGP ze swojij propagacyje ôbecności\? +\nTwoje kōntakty niy bydōm już mogły wysyłać ci wiadōmości zaszyfrowanych OpenPGP. + Klucz publiczny OpenPGP ôstoł ôpublikowany. + Włōncz kōnto + Na zicher\? + Wymazanie kōnta wymazuje cołko historyjo godek + Nagrej głos + Adresa XMPP + Zablokuj adresa XMPP + username@example.com + Hasło + To niyma noleżno adresa XMPP + Brak pamiyńci. Ôbroz je za srogi + Chcesz przidać %s do listy kōntaktōw\? + Informacyje ô serwerze + XEP-0313: MAM + XEP-0280: Kopije wiadōmości + XEP-0352: Skaźnik Sztandu Klijynta + XEP-0191: Rozkoz blokowanio + XEP-0237: Wersyjowanie listy + XEP-0198: Sztelōnki potoku + XEP-0215: Wykrywanie Zewnyntrznych Usug + XEP-0163: PEP (Awatary / OMEMO) + XEP-0363: Przesyłanie zbiorōw bez HTTP + XEP-0357: Push + dostympny + niydostympny + Brak informacyje ô kluczu publicznym + prawie widziany + widziany przed minutōm + widziany prze %d minutami + widziany przed godzinōm + widziany przed %d godzinami + widziany wczorej + widziany przed %d dniami + Wiadōmość zaszyfrowano. Zainstaluj OpenKeychain, coby ôdszyfrować. + Znojdziōne nowe wiadōmości zaszyfrowane we OpenPGP + ID klucza OpenPGP + Ôdcisk OMEMO + Ôdcisk v\\OMEMO + Ôdcisk OMEMO tyj wiadōmości + Ôdcisk v\\OMEMO tyj wiadōmości + Insze masziny + Zadufane ôdciski OMEMO + Pobiyranie kluczy… + Skōńczōno + Ôdszyfruj + Zokłodki + Szukej + Wpisz kōntakt + Wymaż kōntakt + Informacyje ô kōntakcie + Zablokuj kōntakt + Ôdblokuj kōntakt + Stwōrz + Ôbier + Kōntakt już istniyje + Prziwstōń + kanal@konferyncyje.prziklod.com/przemianek + kanal@konferyncyje.prziklod.com + Przidej za zokłodka + Wymaż zokłodka + Wymaż kōnferyncyjo + Wymaż kanał + Je żeś zicher, iże chcesz wymazać ta kōnferyncyjo\? +\n +\nPozōr: Ta kōnferyncyjo bydzie doimyntnie wymazano na serwerze. + Na zicher chcesz wymazać tyn kanał publiczny\? +\n +\nPozōr: Tyn kanał bydzie doimyntnie wymazany ze serwera. + Niy szło wymazać kōnferyncyje + Niy szło wymazać kanału + Edytuj tytuł kōnferyncyje + Tymat + Przistympowanie do kōnferyncyje… + Ôpuść izba + Kōntakt przidoł cie do listy kōntaktōw + Tyż przidej + Kōntakt %s przeczytoł dotōnd + Kōntakty %s przeczytały dotōnd + Kōntakty %1$s i %2$d inszych przeczytało dotōnd + Wszyjscy przeczytali dotōnd + Publikuj + Tyknij awatar, coby ôbrać ôbroz z galeryje + Publikowanie… + Serwer ôdkozoł publikacyje + Niy szło skōnwertować ôbrazu + Niy szło spamiyntać ôbrazu we pamiyńci masziny + (abo dugo przitrzim, coby nasztalować wychodny) + Twōj serwer niy umożliwo publikacyje awatarōw + szepce + do %s + Wyślij prywatno wiadōmość do %s + Połōncz + Kōnto już istniyje + Dalij + Połōnczōno ze serwerym + Przeskocz + Zastow powiadōmiynia + Włōncz + Kōnferyncyjo wymogo hasła + Wkludź hasło + Poproś kōntakt ô udostympniynie powiadōmiyń ô ôbecności. +\n +\nTo bydzie używane do skazowanio, jakigo programu używo twōj kōntakt. + Poproś teroz + Ignoruj + Pozōr: Wysyłanie bez powiadōmiyń ô ôbecności ze ôbōch strōn może prziniyś niyôczekowane problymy. +\n +\nIdź do „Informacyji ô kōntakcie” i wejzdrzij do subskrypcyji ôbecności. + Bezpieczyństwo + Przizwōl na poprowianie wiadōmości + Przizwōl swojim kōntaktōm poprowiać wiadōmości + Sztelōnki eksperta + Modyfikuj je pozornie + Ô %s + Godziny cisze + Poczōntek + Kōniec + Włōncz godziny cisze + Powiadōmiynia bydōm wyciszōne we ôbranych godzinach + Inksze + Ôdcisk klucza OMEMO bōł skopiowany do skrytki + Ôd tyj grupy mosz wykluczynie + Kōnferyncyjo ino dlo czōnkōw + Ukrōcynie zasobu + Ze tyj kōnferyncyje cie wyciepli + Kōnferyncyjo ôstała zawarto + Już żeś niy je we tyj kōnferyncyji + ze użyciym kōnta %s + trzimane na %s + Wybadowanie %s na hoście HTTP + Brak połōnczynio. Sprōbuj zaś niyskorzij + Wybadej srogość %s + Wybadej srogość %1$s na %2$s + Ôpcyje wiadōmości + Cytata + Wraź za cytata + Skopiyruj ôryginalny URL + Wyślij zaś + URL zbioru + Skopiowano URL do skrytki + Skopiowano adresa XMPP do skrytki + Skopiowano kōmunikat błyndu do skrytki + adresa necowo + Zeskanuj kod + Pokoż kod QR + Pokoż wykoz wykluczyń + Informacyje kōnta + Potwiyrdź + Sprōbuj zaś + Usuga na piyrszym planie + Niy zwolo systymowi na przerwanie połōnczynio + Stwōrz kopijo ibryczno + Kopijo ibryczno bydzie spamiyntano we %s + Tworzynie kopije ibrycznyj + Kopijo ibryczno stworzōno + Kopijo ibryczno spamiyntano we %s + Prziwrocanie ze kopije ibrycznyj + Prziwrocanie ze kopije ibrycznyj gotowe + Niy zapōmnij ô aktywacyji tego kōnta. + Ôbier zbiōr + Ôdbiyranie %1$s (skōńczōne %2$d%%) + Pobier %s + Wymaż %s + zbiōr + Ôtwōrz %s + Wysyłanie (skōńczōne %1$d%%) + Rychtowanie do udostympniynio ôbrozka + Zapropōnowano pobranie zbioru %s + Pociep przesyłanie + niy szło udostympnić zbioru + transmisyjo zbioru pociepniynto + Zbiōr wymazany + Niy szło znojś aplikacyje do ôtwarcia zbioru + Niy szło znojś aplikacyje do ôtwarcia linka + Niy szło znojś aplikacyje do pokozanio kōntaktu + Dynamiczne tagi + Pokazuj etykety pod kōntaktami + Włōncz powiadōmiynia + Niy szło znojś serwera kōnferyncyje + Niy szło stworzić kōnferyncyje + Awatar kōnta + Skopiuj ôdcisk klucza OMEMO do skrytki + Wygyneruj zaś klucz OMEMO + Wymaż masziny + Na zicher chcesz wymazać wszyjske inksze masziny z ôgłoszynio OMEMO\? Jak twoje masziny sie zaś połōnczōm, to sie zaś ôgłoszōm, ale mogōm niy dostać wiadōmości wysłanych bez tyn czas. + Niy ma dostympnych kluczy dlo tego kōntaktu. +\nNiy szło pobrać nowych kluczy ze serwera. Możno je coś niy tak ze serwerym ôd twojigo kōntaktu\? + Brak dostympnych kluczy dlo tego kōntaktu. +\nDej pozōr, czy wzajymnie powiadōmiocie sie ô ôbecności. + Coś poszło źle + Pobiyranie historyje z serwera + Kōniec historyje na serwerze + Aktualizowanie… + Hasło było zmiyniōne! + Niy szło zmiynić hasła + Zmiyń hasło + Teroźne hasło + Nowe hasło + Hasło niy może być prōzne + Aktywuj wszyjske kōnta + Zastow wszyjske kōnta + Użyj + Brak stanowiska + Offline + Wykluczōny + Czōnek + Tryb rozszyrżōny + Prziznej uprawniynia czōnkostwa + Wymaż uprawniynia czōnkostwa + Prziznej uprawniynia administratora + Odbierz uprawniynia administratora + Prziznej uprawniynia posiedziciela + Wymaż uprawniynia posiedziciela + Wyciep z kōnferyncyje + Wyciep z kanału + Niy szło umiynić stanowiska ôd %s + Wyklucz + Wyklucz na kanale + Chcesz wyciepnōńć %s z publicznego kanału. Jedyny spusōb na to, to je wykluczyć ta ôsoba na dycki. + Wyklucz teroz + Niy szło zmiynić funkcyje %s + Kōnfiguracyjo prywatnyj kōnferyncyje + Kōnfiguracyjo publicznego kanału + Prywatne, ino dlo czōnkōw + Niych adresa XMPP bydzie widzialno dlo wszyjskich + Niych kanał bydzie moderowany + Niy bieresz udziału + Sztalōnki kōnferyncyje były zmiyniōne! + Niy idzie zmiynić sztelōnkōw kōnferyncyje + Nigdy + Ryncznie + Ôdłōż + Ôdpowiydz + Ôznocz za przeczytane + Sztelōnki wkludzanio + Enter wysyło + Używej knefla Enter do wysyłanio wiadōmości. Dycki możesz używać Ctrl+Enter, żeby wysłać wiadōmość, nawet jak ta ôpcyjo je zastawiōno. + Pokoż knefel Enter + Umiyń knefel emotikōnōw na Enter + zbiōr audio + zbiōr wideo + ôbroz + wektorowo grafika + Dokumynt PDF + Aplikacyjo Androida + Kōntakt + Avatar bōł sprownie ôpublikowany! + Wysyłanie %s + Propōnowanie %s + Skryj niydostympnych + %s pisze… + %s niy pisze + %s piszōm… + %s niy piszōm + Powiadōmiynia pisanio + Dowej znać kōntaktōm, jak dō nich piszesz + Wyślij lokalizacyjo + Pokoż lokalizacyjo + Niy szło znojś aplikacyje do pokazowanio lokalizacyje + Lokalizacyjo + Kōnwersacyjo zawarto + Ôpuś prywatno kōnferyncyjo + Ôpuś publiczny kanał + Niy dufej certyfikatōm systymowym + Wymogej ryncznego potwiyrdzanio certyfikatōw + Wymaż certyfikaty + Ôbier zadufane certyfikaty do wymazanio + Brak ryncznie zadufanych certyfikatōw + Wymaż certyfikaty + Wymaż zaznaczōne + Pociep + + Wymazany %d certyfikat + Wymazane %d certyfikaty + Wymazane %d certyfikatōw + + Zastōmp knefel wysyłanio gibkōm akcyjōm + Gibko akcyjo + Brak + Ôstatnio używano + Ôbier gibko akcyjo + Przeszukej kōntakty + Przeszukej zokłodki + Wyślij wiadōmość prywatno + %1$s już niy je we kōnferyncyji + Miano ôd używocza + Miano ôd używocza + Niynolezne miano ôd używocza + Pobiyranie niyudane: Niy szło znojś serwera + Pobiyranie niyudane: Niy szło znojś zbioru + Pobiyranie niyudane: Niy szło połōnczyć z hostym + Pobiyranie niyudane: Niy szło spamiyntać zbioru + Pobiyranie niypodarzōne: Niynoleżny zbiōr + Nec TOR je niydostympny + Błōnd połōnczynio + Serwer niy ôdpado dōmynie + Zepsute + Dostympność + Status „W ôddali”, kej ekran je zastawiōny + Ôznaczo twōj zasōb za „W ôddali”, kej ekran je zastawiōny + „Niy szterować” we trybie cichym + Ôznaczo twōj zasōb za „Niy szterować”, kej maszina je w trybie cichym + Traktuj tryb wibracyje jak tryb cichy + Ôznaczo twōj zasōb za „Niy szterować”, kej maszina je w trybie wibracyje + Rozszyrzōne sztelōnki połōnczynio + Pokoż miano ôd hosta i sztelōnki portu przi przidowaniu kōnta + xmpp.prziklod.com + Wloguj certyfikatym + Niy szło ôdczytać certyfikatu + Preferyncyje archiwizacyje + Preferyncyje archiwizacyje po strōnie serwera + Pobiyranie preferyncyji archiwizacyje. Czekej… + Niy idzie pobrać preferyncyji archiwizacyje + Wymogano CAPTCHA + Wkludź tekst z ôbrozka wyżyj + Lyńcuch certyfikatōw niyma zadufany + Adresa XMPP niy pasuje do certyfikatu + Ôdnōw certyfikat + Błōnd pobiyranio klucza OMEMO! + Klucz OMEMO zweryfikowany certyfikatym! + Twoja maszina niy spiyro ôbiyranio certyfikatōw klijynckich! + Połōnczynie + Połōncz bez nec TOR + Tuneluj wszyjske połōnczynia bez nec TOR. Wymogo aplikacyje Orbot + Miano hosta + Port + Adresa serwera abo .onion + To niyma noleżny numer portu + To niyma noleżne miano hosta + %1$d z %2$d kōnt połōnczōnych + + %d wiadōmość + %d wiadōmości + %d wiadōmości + + Zaladuj wiyncyj wiadōmości + Zbiōr dzielōny ze %s + Ôbroz dzielōny ze %s + Ôbrazy dzielōne ze %s + Tekst dzielōny ze %s + Przizwōl %1$s na dostymp do zewnyntrznego składu + Przizwōl %1$s na dostymp do aparatu + Synchrōnizuj z kōntaktami + %1$s potrzebuje twojij zwōle na dopasowanie twojich kōntaktōw XMPP z wykazym kōntaktōw w telefōnie. +\nTo pokoże jejich połne miana i awatary. +\n +\n%1$s ino przeczyto twoje kōntakty i dopasuje je lokalnie, bez wysyłanio na twōj serwer. + Quicksy potrzebuje dostympu do numerōw telefōnōw twojich kōntaktōw, coby zasugerować ci kōntakty, co już używajōm Quicksy. <br><br>Niy trzimiymy kopiji tych numerōw. +\n +\nAby dostać wiyncyj informacyje przeczytej naszo polityka prywatności</a>. <br><br>Teroz pojawi sie prośba ô zwōlo na dostymp do twojich kōntaktōw. + Powiadōm ô wszyskich wiadōmościach + Powiadōmiej ino w przipodku spōmniynio ô mie + Powiadōmiynia zastawiōne + Powiadōmiynia strzimane + Kōmpresyjo ôbrazōw + Podpowiydź: Użyj “Wybier zbiōr” zamiast “Wybier ôbroz”, coby wysłać pojedyncze ôbrozki bez kōmpresyje bez zglyndu na tyn sztalōnek. + Dycki + Ino sroge ôbrozki + Ôptymalizacyje używanio bateryje włōnczōne + Twoja maszina mo włōnczōne agresywne szporowanie bateryje, bez co %1$s może ôdbiyrać wiadōmości z ôpōźniyniym. +\nZastawiynie tych ôptymalizacyji je rekōmyndowane. + Twoja maszina mo włōnczōne agresywne szporowanie bateryje, bez co %1$s może ôdbiyrać wiadōmości z ôpōźniyniym. +\n +\nTeroz pojawi sie prośba ô jejich zastawiynie. + Zastow + Zaznaczōne przestrzyństwo je za sroge + (Brak aktywynych kōnt) + To pole je wymogane + Poprow wiadōmość + Wyślij poprawiōno wiadōmość + Tyn kōntakt już bōł ôd ciebie zweryfikowany. Bez wybranie “Gotowe” ino potwiyrdzosz, że %s noleży do tyj grupowyj godki. + To kōnto było ôd ciebie zastawiōne + Feler bezpieczyństwa: niynoleżny dostymp do zbioru! + Niy szło znojś aplikacyje do udostympniynio URI + Udostympnij URI ze pōmocōm… + Quicksy to modyfikacyjo popularnego klijynta XMPP Conversations, z autōmatycznym wykrywaniym kōntaktōw.<br><br>Zapisujesz sie przi użyciu numeru telefōnu i Quicksy autōmatycznie — podle numerōw telefōnōw we adresowyj ksiōnżce — zasugeruje potyncjalne kōntakty dlo ciebie.<br><br>Bez zapisanie sie zgodzosz sie na naszo <a href=https://quicksy.im/#privacy>polityka prywatności</a>. + Zgodzōm sie, kōntynuuj + Pokludzymy cie bez proces tworzynio kōnta na conversations.im.¹ +\nKej ôbieresz conversations.im za liferanta, to poradzisz kōmunikować sie ze używoczami inkszych liferantōw, jeźli podosz im swoja połno adresa XMPP. + Twoja połno adresa XMPP to: %s + Stwōrz kōnto + Użyj inkszego serwera + Ôbier miano ôd używocza + Regyruj dostympnościōm ryncznie + Sztaluj dostympność we ôknie edytowanio wiadōmości statusu. + Status + Pogodo + Je + Fōrt + Niy ma + Zajynte + Bezpieczne hasło je wygynerowane + Twoja maszina niy przizwolo na zastawiynie ôptymalizacyje bateryje + Registracyjo niy podarziła sie: sprōbuj niyskorzij + Registracyjo niy podarziła sie: hasło za słabe + Ôbier czōnkōw + Tworzynie kōnferyncyje… + Zaproś zaś + Zastow + Krōtki + Postrzedni + Dugi + Roznajmuj użycie + Informuje twoje kōntakty ô tym, kedy używosz Conversations + Prywatność + Tymat + Wybier paleta farbōw + Autōmatycznie + Jasny + Ciymny + Zielōny zadek + Używej zielōnego zadku dlo dostanych wiadōmości + Niy idzie sie połōnczyć z OpenKeychain + Ta maszina juz niy je używano + Kōmputer + Mobilniok + Tablet + Przeglōndarka necu + Kōnsola + Wymogany płat + Dej zwōlo na dostymp do Internetu + Jo + Kōntakt prosi ô udostympniynie statusu + Przizwōl + Brak zwōle na dostymp do %s + Niy szło znojś serwera + Brak ôdpowiedzi ôd zdalnego serwera + Niy szło zaktualizować kōnta + Zgłoś ta adresa XMPP za spamowanie. + Wymaż tożsamości OMEMO + Wygyneruj jeszcze roz klucze OMEMO. Wszyske twoje kōntakty bydōm musiały cie zaś zweryfikować. Użyj tego ino w ôstateczności. + Wymaż zaznaczōne klucze + Potrzebne połōnczynie, coby ôpublikować twōj awatar. + Pokoż kōmunikaty felerōw + Kōmunikat ô felerze + Szporowanie danych je włōnczōne + Twōj systym ôperacyjny blokuje dostymp do internetu %1$s, jak ôn funguje we zadku. Coby dostować powiadōmiynia ô nowych wiadōmościach, trzeba dać %1$s niyôgraniczōny dostymp do internetu, kedy szporowanie danych je włōnczōne. +\n%1$s bydzie durch ôgraniczoł transfer danych, kedy ino to je możliwe. + Twoja maszina niy spiyro zastawianio szporowanio danych dlo %1$s. + Niy szło stworzić tymczasowego zbioru + Maszina je zweryfikowano + Skopiyruj ôdcisk + Wszyske twoje klucze OMEMO sōm zweryfikowane + Kod kryskowy niy zawiyro ôdciskōw dlo tyj godki. + Zaufane ôdciski + Użyj fotoaparatu, coby zeskanować kod kryskowy kōntaktu + Czekej na ściōngniyńcie kluczy + Udostympnij bez kod QR + Udostympnij bez URI XMPP + Udostympnij bez link HTTP + Ôd Razu Ufej Przed Weryfikacyjōm + Autōmatycznie ufej wszyskim nowym maszinōm ôd niyzweryfikowanych kōntaktōw, ale proś ô rynczne potwierdzynie nowych maszin ôd zweryfikowanych kōntaktōw. + Ôd razu zaufane klucze OMEMO, to znaczy mogōm noleżeć do kogoś inkszego abo ftoś może sie podepnōńć. + Niezaufane + Niynoleżny kod kryskowy 2D + Wypucuj cache (używane ôd fotoaparatu) + Wysnoż cache + Wysnoż prywatny skłod + Wysnoż prywatny skłod, kaj sōm trzimane zbiory (mogōm być pobrane zaś z serwera) + Trefiōłch tyn link we zaufanym zdrzōdle + Zaroz zweryfikujesz klucz OMEMO %1$s bez klikniyńcie w link. To je bezpiecznie ino, kej link je ze zaufanego zdrzōdła, kaj ino %2$s mōg go ôpublikować. + Zaroz zweryfikujesz klucze OMEMO swojego kōnta. To je bezpieczne ino jeźli ôtwiyrosz tyn link ze zaufanego zdrzōdła, kaj ino ty możesz ôpublikować tyn link. + Dalij + Zweryfikuj klucze OMEMO + Pokoż niyaktywne + Skryj niyaktywne + Przestōń ufać maszinie + Je żeś zicher, iże chcesz cofnōńć weryfikacyjo tyj masziny\? +\nTa maszina, i wiadōmości, co bydōm z nij przichodzić, bydōm ôznaczane za niyzaufane. + + %d sekunda + %d sekundy + %d sekund + + + %d minuta + %d minuty + %d minut + + + %d godzina + %d godziny + %d godzin + + + %d dziyń + %d dni + %d dni + + + %d tydziyń + %d tydnie + %d tydni + + + %d miesiōnc + %d miesiōnce + %d miesiyncy + + Autōmatyczne wymazowanie wiadōmości + Autōmatycznie wymazuj z tyj masziny wiadōmości starsze aniżeli skōnfigurowany ôkres czasu. + Szyfrowanie wiadōmości + Bez pobiyranio wiadōmości bez lokalny ôkres retyncyje. + Kōmpresowanie filmu + Powiōnzane godki zawarte. + Kōntakt zablokowany. + Powiadōmiynia ôd cudzych ludzi + Powiadōmiej przi wiadōmościach i połōnczyniach ôd cudzych ludzi. + Prziszła widōmość ôd kogoś cudzego + Zablokuj cudzo ôsoba + Zablokuj cołko dōmyna + teroz online + Sprōbuj zaś ôdszyfrować + Feler sesyje + Starszy mechanizm SASL + Serwer wymogo registracyje na strōnie + Ôtwōrz strōna + Niy szło znojś aplikacyje do ôtwarciŏ strōny + Powiadōmiynia heads-up + Pokazuj powiadōmiynia Heads-up + Dzisiej + Wczorej + Potwiyrdź miano ôd hosta ze pōmocōm DNSSEC + Certyfikaty serwera, co posiadajōm noleżne miano ôd hosta, sōm uznowane za zweryfikowane + Certyfikat niy zawiyro adresy XMPP + czyńściowo + Nagrej film + Skopiyruj do skrytki + Wiadōmość skopiyrowano do skrytki + Wiadōmość + Prywatne wiadōmości sōm zastawiōne + Aplikacyje chrōniōne + Coby dostować wiadōmości, kedy ekran je zastawiōny, musisz przidać Conversations do listy chrōniōnych aplikacyji. + Zaakceptować niyznōmy certyfikat\? + Certyfikat ôd serwera niy ma podpisany ôd znōmego Amtu Certyfikacyje. + Zaakceptować niypasujōnce miano ôd serwera\? + Niy idzie potwiyrdzić serwera za “%s. Certyfikat je ważny ino dlo: + Chcesz kōntynuować połōnczynie\? + Informacyje certyfikatu: + Roz + Skaner kodōw QR potrzebuje dostympu do aparatu + Przewiń na spodek + Przewiń na spodek po wysłaniu wiadōmości + Edytuj kōmunikat statusu + Edytuj kōmunikat statusu + Zastow szyfrowanie + %1$s niy mogło wysłać zaszyfrowanyj wiadōmości do %2$s. Możliwe, iże kōntakt używo starego serwera abo klijynta, co niy spiyro OMEMO. + Niy podarziło sie pobranie listy maszin + Niy podarziło sie pobranie kluczy szyfrowanio + Podpowiydź: W niykerych przipodkach może pōmōc wzajymne przidanie sie do listy kōntaktōw. + Na zicher chcesz zastawić szyfrowanie OMEMO dlo tyj kōnwersacyje\? +\nAdministratōr twojigo serwera bydzie mōg czytać twoje wiadōmości, ale może to być jedyny spōsōb, coby kōmunikować sie z ludźmi, co używajōm starych klijyntōw. + Zastow teroz + Cychōnek: + Szyfrowanie OMEMO + OMEMO bydzie dycki używane w godkach 1:1 i prywatnych grupowych godkach. + OMEMO bydzie używane wychodnie przi nowych godkach. + OMEMO bydzie musiało być włōnczōne ryncznie przi nowych godkach. + Stwōrz Skrōt + Srogość czciōnki + Relatywno srogość czciōnki używanyj we aplikacyji. + Włōnczōne wychodnie + Zastawiōne wychodnie + Mało + Strzednio + Srogo + Wiadōmość niy była zaszyfrowano dlo tyj masziny. + Niy szło ôdszyfrować wiadōmości OMEMO. + cofnij + Udostympnianie lokalizacyje je zastawiōne + Zablokuj pozycyjo + Ôdblokuj pozycyjo + Skopiyruj lokalizacyjo + Udostympnij lokalizacyjo + Kerōnki + Udostympniej lokalizacyjo + Pokazuj lokalizacyjo + Udostympnij + Niy idzie zaczōńć nagrowanio + Czekej… + Przizwōl %1$s na dostymp do mikrofōnu + Szukej we widōmościach + GIF + Pokoż kōnwersacyjo + Przidowek Udostympnianio Lokalizacyje + Użyj Przidowka Udostympnianio Lokalizacyje zamiast wbudowanyj karty + Skopiyruj URL + Skopiyruj adresa XMPP + Udostympnianie zbiorōw bez HTTP S3 + Bezpostrzednie wyszukowanie + Na ekranie “Zacznij kōnwersacyjo” ôtwōrz tastatura i wraź kursōr w polu wyszukowanio + Awatar kōnwersacyje + Serwer niy spiyro awatarōw kōnwersacyje + Ino posiedziciel może zmiynić awatar kōnwersacyje + Miano ôd kōntaktu + Pseudōnim + Miano + Niy trza podować miana + Miano ôd kōnferyncyje + Ta kōnferyncyjo ôstała wymazano + Niy idzie zaczōńć nagrowanio + Usuga na piyrszym planie + Ta kategoryjo powiadōmiyń je używano, coby pokazować stałe powiadōmiynie, co ôznaczo, iże %1$s funguje. + Wiadōmość Statusu + Problymy z połōnczyniym + Ta kategoryjo powiadōmiyń je używano, coby pokazować stałe powiadōmiynie, co ôznaczo, iże Conversations mo problymy z połōnczyniym. + Wiadōmości + Połōnczynia + Wiadōmości + Połōnczynia, co przichodzōm + Połōnczynia, co wychodzōm + Ciche wiadōmości + Ta kategoryjo powiadōmiyń je używano coby pokazować powiadōmiynia, co niy powodujōm żodnych klangōw. Bez tyn przikłod w czasie aktywności na inkszyj maszinie (ôkres karyncyje). + Niypodarzōne wysyłki + Sztalōnki powiadōmiyń wiadōmości + Sztalōnki powiadōmiyń dlo połōnczyń, co przichodzōm + Ważność, Klang, Wibracyjo + Kōmpresyjo wideo + Pokoż media + Uczestnicy + Przeglōndarka mediōw + Zbiōr pōminiynty skirz naruszynio bezpiyczyństwa. + Jakość wideo + Niższo jakość gwarantuje myńszo srogość + Postrzednio (360p) + Wysoko (720p) + pociepniynte + Już tworzisz nowo wiadōmość. + Funkcyjo niyzaimplymyntowano + Niynoleżny kod kraju + Ôbier kroj + numer telefōnu + Zweryfikuj swōj numer telefōnu + Quicksy wyśle SMS (mogōm być naliczane płaty) coby zweryfikować numer telefōnu. Wpisz kod ôd kraju i numer telefōnu: + Zweryfikujymy numer telefōnu

%s

Zgodzo sie wszysko, abo chcesz zmiynić numer\?
+ %s to niy ma noleżny numer telefōnu. + Wkludź swōj numer telefōnu. + Przeszukej kraje + Zweryfikuj %s + Wysłali my SMS do %s. + Wysłali my dalszy SMS z 6 cyfrowym kodym. + Wkludź 6-cyfrowy kod PIN niżyj. + Wyślij SMS zaś + Wyślij SMS zaś (%s) + Czekej (%s) + nazod + Autōmatycznie bōł wrażōny prowdopodobny PIN ze skrytki. + Wkludź 6-cyfrowy PIN. + Na zicher chcesz przerwać procedura registracyje\? + Ja + Niy + Weryfikowanie… + Żōndanie SMS… + Wkludzōny PIN je niynoleżny. + Wysłany ôd nos PIN straciōł ważność. + Niyznōmy feler necu. + Niyznōmo ôdpowiydź serwera. + Niy idzie sie połōnczyć z serwerym. + Niy idzie dostać bezpiecznego połōnczynio. + Niy szło znojś serwera. + Coś poszło źle przi przetworzaniu twojigo żōndanio. + Niynoleżny wert używocza + Tymczasowo niydostympne. Sprōbuj niyskorzij. + Brak połōnczynio z necym. + Sprōbuj zaś za %s + Limit pytań spotrzebowany + Za moc prōb + Używosz zastarzałyj wersyje aplikacyje. + Aktualizuj + Twōj numer telefōnu je aktualnie wlogowany na inkszyj maszinie. + Wkludź swoje miano, coby ludzie, co cie majōm we kōntaktach, wiedzieli, fto żeś je. + Twoje miano + Wkludź swoje miano + Użyj knefla edycyje coby nasztalować swoje miano. + Ôdciepżōndanie + Zainstaluj Orbot + Puść Orbot + Żodno marketowo aplikacyjo niy je zainstalowano. + Tyn kanał sprawi, iże twoja adresa XMPP bydzie publiczno + e-book + Ôryginalne (niyskōmpresowane) + Ôtwōrz ze pōmocōm… + Profilowy ôbrozek Conversations + Ôbier kōnto + Prziwrōć kopijo ibryczno + Prziwrōć + Wkludź swoje hasło do kōnta %s coby prziwrōcić kopijo ibryczno. + Niy używej kopije ibrycznyj, coby klōnować (puszczać rōwnolygle) instalacyjo. Prziwrocanie kopije je przeznaczōne ino do migracyje abo kedy maszina była stracōno. + Niy idzie prziwrōcić kopije ibrycznyj. + Niy idzie ôdszyfrować kopije ibrycznyj. Je hasło noleżne\? + Kopijo i Prziwrocanie + Wkludź adresa XMPP + Nowo grupowo godka + Prziwstōń do publicznego kanału + Nowo prywatno grupowo godka + Nowy publiczny kanał + Miano ôd kanału + Adresa XMPP + Podej miano ôd kanału + Podej adresa XMPP + To je adresa XMPP. Podej miano. + Tworzynie kanału publicznego… + Tyn kanał już istniyje + Przistympujesz do istniyjōncego kanału + Niy szło spamiyntać kōnfiguracyje ôd kanału + Przizwōl wszyskim na zmiana tymatu + Przizwōl wszyskim na zaproszanie inkszych + Kożdy może zmiyniać tymat. + Posiedziciele mogōm zmiyniać tymat. + Administratorzi mogōm zmiyniać tymat. + Posiedziciele mogōm zaproszać inkszych. + Kożdy może zaproszać inkszych. + Adresy XMPP widzialne dlo administratorōw. + Adresy XMPP widzialne dlo wszyskich. + Tyn publiczny kanał niy mo uczestnikōw. Zaproś swoje kōntakty abo użyj udostympnianio, coby ôpublikować adresa XMPP. + Ta prywatno grupowo godka niy mo uczestnikōw. + Regyruj uprawniyniami + Wyszukej uczestnikōw + Zbiōr je za srogi + Przidej + Ôdkryj kanały + Wyszukej kanał + Możliwe naruszynie prywatności! + Ôdkrywanie kanałōw używo usugi trzecij fiyrmy <a href=https://search.jabber.network>search.jabber.network</a>. <br><br>Użycie tyj funkcyje prześle dō nij twoja adresa IP jak tyż kryteria wyszukowanio. Wejzdrzij na <a href=https://search.jabber.network/privacy>Polityka Prywatności</a>, coby dostać wiyncyj informacyji. + Już mōm kōnto + Przidej kōnto, co juz istniyje + Zaregistruj nowe kōnto + To wyglōndo jak miano ôd dōmyny + Przidej tak by tak + To wyglōndo jak adresa ôd kanału + Udostympnij zbiory kopiji ibrycznych + Kopijo ibryczno Conversations + Zdarzynie + Ôtwōrz kopijo ibryczno + Wybrany zbiōr to niy ma zbiōr kopije ibrycznyj Conversations + To kōnto już je nasztalowane + Podej hasło dlo tego kōnta + Niy idzie wykōnać tyj akcyje + Przistōmp do publicznego kanału… + Aplikacyjo, co udostympnio, niy dała zwōle na dostymp do tego zbioru. + Grupowe godki i kanały + jabber.network + Serwer lokalny + Wiynkszość używoczōw powinna ôbrać “jabber.network” dlo lepszych dorad ze cołkigo ekosystymu XMPP. + Metoda ôdkrywanio kanałōw + Kopijo ibryczno + Ô aplikacyji + Włōncz kōnto + Zazwōń + Przichodzi połōnczynie + Przichodzi połōnczynie wideo + Łōnczynie + Połōnczōny + Łōnczynie zaś + Akceptowanie połōnczynio + Kōńczynie połōnczynio + Ôdbier + Ôdciep + Wyszukowanie maszin + Zwōniynie + Zajynte + Niy idzie wykōnać połōnczynio + Połōnczynie serwane + Połōnczynie pociepniynte + Feler aplikacyje + Problym weryfikacyje + Rozłōncz + Połōnczynie wychodzi + Połōnczynie wideo wychodzi + Łōnczynie zaś + Łōnczynie zaś + Zastow Tor coby zwōnić + Połōnczynie przichodzōnce + Połōnczynie przichodzōnce · %s + Niyôdebrane · %s + Połōnczynie wychodzōnce + Połōnczynie wychodzōnce · %s + Niyôdebrane połōnczynie + Połōnczynie audio + Połōnczynie wideo + Pōmoc + Przejdź do kōnwersacyje + Twōj mikrofōn je niydostympny + Możesz mieć ino jedno połōnczynie w jednyj chwili. + Wrōć do trwajōncego połōnczynio + Niy idzie zmiynić fotoaparatu + Przidej do ôblubiōnych + Wymaż ze ôblubiōnych + Śledzynie GPX + Niy szło skorygować wiadōmości + Wszyske kōnwersacyje + Ta kōnwersacyjo + Twōj awatar + Awatar ôd %s + Zaszyfrowane bez OMEMO + Zaszyfrowane bez OpenPGP + Niyzaszyfrowane + Zawrzij + Nagrej głosowo wiadōmość + Grej źwiynk + Spauzuj źwiynk + Przidej kōntakt, stwōrz abo przistōmp do grupowyj godki, abo ôdkrywej kanały + + Pokoż %1$d uczestnika + Pokoż %1$d uczestnikōw + Pokoż %1$d uczestnikōw + + + Wiadōmość niy mogła być dolifrowano + Wielaś wiadōmości niy mogło być dolifrowanych + Wielaś wiadōmości niy mogło być dolifrowanych + + Niypodarzōne lifrowania + Wiyncyj ôpcyjōw + Żodno aplikacyjo niyznojdziōno + Zaproś do Conversations + Niy idzie przetworzić zaproszynio + Serwer niy spiyro gynerowanio zaproszyń + Żodne aktywne kōnta niy spiyrajōm tyj funkcyje + Tworzynie kopije ibrycznyj je puszczōne. Dostaniesz powiadōmiynie, jak bydzie gotowo. + Niy idzie włōnczyć wideo. + Dokumynt ze samym tekstym + Registracyjo kōnt niy je spiyrano + Żodno adresa XMPP niyznojdziōno +
diff --git a/src/main/res/values-tr-rTR/strings.xml b/app/src/main/res/values-tr-rTR/strings.xml similarity index 100% rename from src/main/res/values-tr-rTR/strings.xml rename to app/src/main/res/values-tr-rTR/strings.xml diff --git a/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk/strings.xml similarity index 100% rename from src/main/res/values-uk-rUA/strings.xml rename to app/src/main/res/values-uk/strings.xml diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml new file mode 100644 index 000000000..afb4b7296 --- /dev/null +++ b/app/src/main/res/values-vi/strings.xml @@ -0,0 +1,956 @@ + + + Cài đặt + Cuộc hội thoại mới + Quản lý tài khoản + Quản lý tài khoản + Đóng cuộc hội thoại + Thông tin liên hệ + Chi tiết cuộc trò chuyện nhóm + Chi tiết kênh + Thêm tài khoản + Chỉnh sửa tên + Thêm vào danh bạ + Xoá khỏi danh sách bạn bè + Chặn liên hệ + Bỏ chặn liên hệ + Chặn miền + Bỏ chặn miền + Chặn thành viên + Bỏ chặn thành viên + Quản lý tài khoản + Cài đặt + Chia sẻ qua Conversation + Khởi chạy Conversation + Chọn liên hệ + Chọn liên hệ + Chia sẻ qua tài khoản + Danh sách chặn + mới đây + 1 phút trước + %d phút trước + + %d cuộc hội thoại chưa đọc + + + đang gửi... + Đang giải mã tin nhắn. Xin chờ... + Tin nhắn mã hoá bằng OpenPGP + Biệt danh đã được sử dụng + Biệt danh không hợp lệ + Quản trị viên + Chủ nhân + Điều phối viên + Thành viên + Khách + Bạn có muốn xoá %s khỏi danh sách liên hệ của bạn không? Các cuộc hội thoại với liên hệ này sẽ không bị xoá. + Bạn có muốn chặn %s gửi tin nhắn cho bạn? + Bạn có muốn bỏ chặn %s và cho phép họ gửi tin nhắn cho bạn? + Chặn tất cả liên hệ từ %s? + Bỏ chặn tất cả liên hệ từ %s? + Đã chặn liên hệ + Đã chặn + Bạn có muốn xoá dấu trang %s không? Các cuộc hội thoại với dấu trang này sẽ không bị xoá. + Đăng ký tài khoản mới trên máy chủ + Đổi mật k trên máy chủ + Chia sẻ với... + Bắt đầu cuộc hội thoại + Mời liên hệ + Mời + Danh bạ + Liên hệ + Huỷ + Đặt + Thêm + Chỉnh sửa + Xoá + Chặn + Bỏ chặn + Lưu + OK + %1$s đã đột ngột dừng + Việc sử dụng tài khoản XMPP của bạn để gửi báo cáo hoạt động sẽ giúp sự phát triển liên tục của %1$s. + Gửi ngay + Đừng hỏi lại nữa + Không thể kết nối đến tài khoản + Không thể kết nối đến nhiều tài khoản + Nhấn để quản lý các tài khoản của bạn + Đính kèm tập tin + Thêm liên hệ bị thiếu này vào danh sách liên hệ? + Thêm liên hệ + thất bại khi chuyển + Đang chuẩn bị sẵn sàng để gửi hình ảnh + Đang chuẩn bị sẵn sàng để gửi các hình ảnh + Đang chia sẻ các tập tin. Xin chờ... + Xoá lịch sử + Xoá lịch sử hội thoại + Bạn có muốn xoá tất cả tin nhắn trong cuộc hội thoại này không?\n\nCảnh báo: Việc này sẽ không ảnh hưởng đến các tin nhắn được lưu trữ trên các thiết bị hoặc máy chủ khác. + Xoá tệp + Bạn có chắc bạn muốn xoá tệp này không?\n\nCảnh báo: Việc này sẽ không xoá các bản sao được lưu trữ trên các thiết bị hoặc máy chủ khác của tệp này. + Đóng cuộc hội thoại này sau đó + Chọn thiết bị + Gửi tin nhắn không mã hoá + Gửi tin nhắn + Gửi tin nhắn đến %s + Gửi tin nhắn mã hoá OMEMO + Gửi tin nhắn mã hoá v\\OMEMO + Gửi tin nhắn mã hoá OpenPGP + Biệt danh mới đang được sử dụng + Gửi dạng không mã hoá + Giải mã thất bại. Có lẽ bạn không có đúng khoá cá nhân. + OpenKeychain + OpenKeychain để mã hoá và giải mã các tin nhắn và quản lý các mã khoá công khi của bạn.

Nó được cấp phép dưới GPLv3 và có sẵn trên F-Droid và Google Play.

(Vui lòng khởi động lại %1$s sau đó.)]]>
+ Khởi chạy lại + Cài đặt + Xin cài đặt OpenKeychain + đang đề xuất... + đang chờ... + Không tìm thấy khoá OpenPGP + Không thể mã hoá tin nhắn của bạn vì liên hệ của bạn không thông báo mã khoá công khai của họ.\n\nVui lòng yêu cầu liên hệ của bạn thiết lập OpenPGP. + Không tìm thấy các khoá OpenPGP + Không thể mã hoá tin nhắn của bạn vì các liên hệ của bạn không thông báo mã khoá công khai của họ.\n\nVui lòng yêu cầu họ thiết lập OpenPGP. + Tổng quan + Chấp thuận các tập tin + Tự động chấp thuận các tập tin nhỏ hơn... + Tập tin đính kèm + Thông báo + Rung + Rung khi có tin nhắn mới + Thông báo đèn LED + Chớp đèn thông báo khi có tin nhắn mới + Âm báo + Âm thanh thông báo + Âm thanh thông báo cho các tin nhắn mới + Nhạc chuông cho các cuộc gọi đến + Thời gian gia hạn thông báo + Khoảng thời gian mà các thông báo được giữ im lặng sau khi phát hiện hoạt động trên một trong những thiết bị khác. + Nâng cao + Không bao giờ gửi báo cáo dừng chạy + 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. + Chấp thuận + Đã có lỗi xảy ra + Lỗi + Tài khoản của bạn + Gửi cập nhật hiện diện + Nhận cập nhật hiện diện + Hỏi cập nhật hiện diện + Chọn hình + Chụp hình + Ưu tiên trao quyền yêu cầu đăng ký + Tập tin bạn chọn không phải là hình ảnh + Không thể chuyển đổi tệp hình ảnh + Không tìm thấy tập tin + Lỗi I/O tổng quát. Có lẽ đã hết dung lượng lưu trữ? + Ứng dụng mà bạn dùng để chọn hình ảnh này không cung cấp đủ quyền để đọc tệp.\n\nHãy sử dụng trình quản lý tệp khác để chọn hình ảnh + Ứng dụng bạn dùng để chia sẻ tệp này không cung cấp đủ quyền. + Không rõ + Tạm thời tắt + Trực tuyến + Đang kết nối\u2026 + Ngoại tuyến + Chưa xác minh + Không tìm thấy máy chủ + Không có kết nối mạng + Đăng ký thất bại + Tên người dùng đã được sử dụng + Đăng ký hoàn tất + Việc đăng ký không được máy chủ hỗ trợ + Mã đăng ký không hợp lệ + Thương lượng TLS thất bại + Miền không thể xác minh được + Vi phạm chính sách + Máy chủ không tương thích + Lỗi truyền phát + Lỗi khi mở luồng truyền + Không mã hoá + OTR + OpenPGP + OMEMO + Xoá tài khoản + Tạm thời tắt + Đăng ảnh đại diện + Đăng khoá công cộng OpenPGP + Xoá mã khoá OpenPGP công khai + Bạn có chắc bạn muốn xoá mã khoá OpenPGP công khai của bạn khỏi sự thông báo có mặt của bạn không?\nCác liên hệ của bạn sẽ không thể gửi các tin nhắn được mã hoá bằng OpenPGP cho bạn nữa. + Đã xuất bản mã khoá OpenPGP công khai. + Bật tài khoản + Bạn chắc chứ? + Việc xoá tài khoản sẽ xoá toàn bộ lịch sử cuộc hội thoại của bạn + Ghi âm + Địa chỉ XMPP + Chặn địa chỉ XMPP + username@example.com + Mật khẩu + Đây không phải là địa chỉ XMPP hợp lệ + Hết bộ nhớ. Hình ảnh quá lớn + Bạn có muốn thêm %s vào danh bạ? + Thông tin máy chủ + XEP-0313: MAM + XEP-0280: Message Carbons + XEP-0352: Biểu thị trạng thái máy trạm + XEP-0191: Blocking Command + XEP-0237: Phiên bản hoá danh sách bạn bè + XEP-0198: Stream Management + XEP-0215: Khám phá dịch vụ ngoài + XEP-0163: PEP (Avatars / OMEMO) + XEP-0363: HTTP File Upload + XEP-0357: Push + sẵn sàng + không sẵn sàng + Thông báo khoá công cộng bị thất lạc + thấy lần cuối vừa đây + đã xem một phút trước + thấy lần cuối %d phút trước + đã xem một tiếng trước + thấy lần cuối %d tiếng trước + đã xem một ngày trước + thấy lần cuối %d ngày trước + Tin nhắn được mã hoá. Vui lòng cài đặt OpenKeychain để giải mã nó. + Đã tìm thấy các tin nhắn được mã hoá bằng OpenPGP mới + ID khoá OpenPGP + Dấu vân tay OMEMO + Dấu vân tay v\\OMEMO + Mã vân tay OMEMO (nguồn gốc tin nhắn) + v\\Mã vân tay OMEMO (nguồn gốc tin nhắn) + Các thiết bị khác + Tin tưởng các dấu vân tay OMEMO + Đang nhận khoá... + Xong + Giải mã + Dấu trang + Tìm kiếm + Nhập liên hệ + Xoá liên hệ + Xem chi tiết liên hệ + Chặn liên hệ + Bỏ chặn liên hệ + Tạo + Chọn + Đã có liên hệ này rồi + Tham gia + channel@conference.example.com/nick + channel@conference.example.com + Lưu thành đánh dấu + Xoá đánh dấu + Phá huỷ cuộc trò chuyện nhóm + Phá huỷ kênh + Bạn có chắc bạn muốn phá huỷ cuộc trò chuyện nhóm này không?\n\nCảnh báo: Cuộc trò chuyện nhóm này sẽ bị xoá hoàn toàn trên máy chủ. + Bạn có chắc bạn muốn phá huỷ kênh công khai này không?\n\nCảnh báo: Kênh này sẽ bị xoá hoàn toàn trên máy chủ. + Không thể phá huỷ cuộc trò chuyện nhóm + Không thể phá huỷ kênh + Chỉnh sửa chủ đề cuộc trò chuyện nhóm + Chủ đề + Đang tham gia cuộc trò chuyện nhóm... + Rời khỏi + Liên hệ đã thêm bạn vào danh bạ + Thêm họ vào + %s đã đọc đến điểm này + %s đã đọc cho đến lúc này + %1$s +%2$d người khác đã đọc cho đến lúc này + Mọi người đã đọc cho đến lúc này + Đăng + Nhấn ảnh đại diện để chọn ảnh từ thư viện + Đang đăng... + Máy chủ đã từ chối đăng tải của bạn + Không thể chuyển đổi hình ảnh + Không thể lưu ảnh đại diện vào ổ đĩa + (Hoặc nhấn giữ để chuyển về mặc định) + Máy chủ của bạn không hỗ trợ việc công khai ảnh đại diện + đã thì thầm + đến %s + Gửi tin nhắn riêng tư đến %s + Kết nối + Đã có tài khoản này rồi + Tiếp theo + Đã thiết lập phiên làm việc + Bỏ qua + Tắt thông báo + Bật + Cuộc trò chuyện nhóm yêu cầu mật khẩu + Nhập mật khẩu + Vui lòng yêu cầu cập nhật sự có mặt từ liên hệ của bạn trước tiên.\n\nViệc này sẽ được sử dụng để xác định ứng dụng trò chuyện mà liên hệ của bạn đang dùng. + Yêu cầu ngay + Bỏ qua + Cảnh báo: Việc gửi cái này mà không có cập nhật sự có mặt chung có thể sẽ gây ra các vấn đề không mong đợi.\n\nHãy đi đến \"Chi tiếi liên hệ\" để xác minh đăng ký sự có mặt của bạn. + Bảo mật + Cho phép việc sửa tin nhắn + Cho phép các liên hệ của bạn chỉnh sửa cảc tin nhắn của họ + Cài đặt chuyên gia + Xin hãy cẩn trọng với chúng + Giới thiệu về %s + Giờ yên lặng + Thời gian bắt đầu + Thời gian kết thúc + Bật giờ yên lặng + Thông báo sẽ được tắt trong giờ yên lặng + Khác + Đã sao chép mã vân tay OMEMO vào bộ nhớ tạm + Bạn bị cấm khỏi cuộc trò chuyện nhóm này + Cuộc trò chuyện nhóm này chỉ dành cho thành viên + Tài nguyên bị hạn chế + Bạn đã bị đá ra khỏi cuộc trò chuyện nhóm này + Cuộc trò chuyện nhóm bị ngừng hoạt động + Bạn không còn ở trong cuộc trò chuyện nhóm này nữa + đang dùng tài khoản %s + được lưu trữ trên %s + Đang kiểm tra %s trên máy chủ HTTTP + Bạn chưa kết nối mạng. Xin thử lại sau + Kiểm tra kích cỡ %s + Kiểm tra %1$s kích cỡ trên %2$s + Tuỳ chọn tin nhắn + Trích dẫn + Dán làm trích dẫn + Sao chép URL gốc + Gửi lại + URL tập tin + Đã sao chép URL vào bộ nhớ tạm + Đã sao chép địa chỉ XMPP vào bộ nhớ tạm + Đã sao chép thông báo lỗi vào bộ nhớ tạm + địa chỉ web + Quét mã vạch 2D + Hiện mã vạch 2D + Quét danh sách chặn + Chi tiết tài khoản + Xác nhận + Thử lại + Dịch vụ ở trước + Ngăn hệ điều hành ngắt kết nối của bạn + Tạo bản sao lưu + Các tệp sao lưu sẽ được lưu trữ trong %s + Đang tạo các tệp sao lưu + Đã tạo bản sao lưu + Đã lưu trữ các tệp sao lưu trong %s + Đang khôi phục bản sao lưu + Đã khôi phục bản sao lưu + Đừng quên bật tài khoản. + Chọn tập tin + Đang nhận %1$s (đã hoàn tất %2$d%%) + Tải về %s + Xoá %s + tập tin + Mở %s + đang gửi (đã hoàn tất %1$d%%) + Đang chuẩn bị sẵn sàng để chia sẻ tệp + Đã đề xuất tải về %s + Huỷ chuyển tập tin + không thể chia sẻ tệp + đã huỷ truyền tệp + Đã xoá tệp + Không tìm thấy ứng dụng nào để mở tệp + Không tìm thấy ứng dụng nào để mở liên kết + Không tìm thấy ứng dụng nào để xem liên hệ + Thẻ năng động + Hiện nhãn chỉ đọc bên dưới các liên hệ + Bật thông báo + Không tìm thấy máy chủ trò chuyện nhóm nào + Không thể tạo cuộc trò chuyện nhóm + Ảnh đại diện tài khoản + Sao chép dấu vân tay OMEMO vào clipboard + Tạo lại khoá OMEMO + Xoá các thiết bị + Bạn có chắc bạn muốn xoá tất cả thiết bị khác khỏi thông báo OMEMO không? Lần sau khi các thiết bị của bạn kết nối, chúng sẽ tự thông báo lại, nhưng có thể sẽ không nhận các tin nhắn được gửi trong lúc đó. + Không có mã khoá dùng được nào có sẵn cho liên hệ này.\nKhông thể lấy mã khoá mới từ máy chủ. Có lẽ có gì đó sai với máy chủ của liên hệ? + Không có mã khoá dùng được nào có sẵn cho liên hệ này.\nHãy chắc chắn là cả hai có đăng ký sự có mặt. + Có gì đó sai đã xảy ra + Đang nhận lịch sử từ máy chủ + Không còn lịch sử nào trên máy chủ + Đang cập nhật... + Đã đổi mật khẩu! + Không thể đổi mật khẩu + Đổi mật khẩu + Mật khẩu hiện tại + Mật khẩu mới + Mật khẩu không thể trống + Bật toàn bộ tài khoản + Tắt toàn bộ tài khoản + Thực hiện thao tác với + Không có quan hệ gì + Ngoại tuyến + Kẻ bị ruồng bỏ + Thành viên + Chế độ nâng cao + Cấp đặc quyền thành viên + Thu hồi đặc quyền thành viên + Trao quyền quản trị + Huỷ quyền quản trị + Cấp đặc quyền chủ sở hữu + Thu hồi đặc quyền chủ sở hữu + Xoá khỏi cuộc trò chuyện nhóm + Xoá khỏi kênh + Không thể đổi mối quan hệ của %s + Cấm khỏi cuộc trò chuyện nhóm + Cấm khỏi kênh + Bạn đang cố xoá %s khỏi một kênh công khai. Cách duy nhất để làm thế là cấm người dùng đó mãi mãi. + Cấm ngay + Không thể đổi phận sự của %s + Thiết lâp cuộc trò chuyện nhóm riêng tư + Thiết lập kênh công khai + Riêng, chỉ dành cho thành viên + Làm cho các địa chỉ XMPP có thể được bất kỳ ai nhìn thấy + Làm cho kênh được kiểm duyệt + Hiện bạn chưa tham gia + Đã sửa đổi tuỳ chọn cuộc trò chuyện nhóm! + Không thể sửa đổi tuỳ chọn cuộc trò chuyện nhóm + Chưa từng + Cho đến thông báo tiếp theo + Báo lại + Trả lời + Đánh dấu là đã đọc + Đầu vào + Bấm Enter để gửi + Sử dụng phím Enter để gửi tin nhắn. Bạn luôn có thể sử dụng Ctrl+Enter để gửi tin nhắn, kể cả khi tuỳ chọn này bị tắt. + Hiện nút Enter + Đổi nút biểu tượng cảm xúc thành nút Enter + âm thanh + video + hình ảnh + hình ảnh véc tơ + tài liệu PDF + Ứng dụng Android + Liên hệ + Đã đăng tải ảnh đại diện! + Đang gửi %s + Đang đề xuất %s + Ẩn ngoại tuyến + %s đang gõ... + %s đã ngừng gõ + %s đang gõ... + %s đã ngừng gõ + Thông báo đang gõ + Để cho các liên hệ của bạn biết khi bạn đang viết tin nhắn cho họ + Gửi vị trí + Hiện vị trí + Không tìm thấy ứng dụng nào để hiển thị vị trí + Vị trí + Đã đóng cuộc hội thoại + Đã rời khỏi cuộc trò chuyện nhóm riêng tư + Đã rời khỏi kênh công khai + Đừng tin các CA hệ thống + Tất cả chứng nhận phải được phê duyệt thủ công + Xoá các chứng nhận + Xoá thủ công các chứng nhận đã phê duyệt + Không có chứng nhận được phê duyệt thủ công + Xoá các chứng nhận + Xoá lựa chọn + Huỷ + + Đã xoá %d chứng nhận + + Thay thế nút \"Gửi\" bằng hành động nhanh + Thao tác nhanh + Không có + Dùng gần đây nhất + Chọn thao tác nhanh + Tìm kiếm liên hệ + Tìm kiếm dấu trang + Gửi tin nhắn cá nhân + %1$s đã rời khỏi cuộc trò chuyện nhóm + Tên người dùng + Tên người dùng + Đây không phải là tên người dùng hợp lệ + Tải xuống thất bại: Không thấy máy chủ + Tải xuống thất bại: Không thấy tập tin + Tải xuống thất bại: Không thể kết nối đến máy chủ + Tải xuống thất bại: Không thể ghi tệp + Mạng Tor chưa sẵn sàng + Gắn kết thất bại + Máy chủ không chịu trách nhiệm cho miền này + Bị hỏng + Tính khả dụng + Vắng mặt khi thiết bị bị khoá + Hiện là Vắng mặt khi thiết bị bị khoá + Bận ở chế độ im lặng + Hiện là Bận khi thiết bị ở chế độ im lặng + Coi chế độ rung như chế độ im lặng + Hiện là Bận khi thiết bị ở chế độ rung + Cài đặt kết nối mở rộng + Hiện tên máy chủ và cài đặt cổng khi thiết lập tài khoản + xmpp.example.com + Đăng nhập bằng chứng chỉ + Không thể xử lý chứng chỉ + Cài đặt lưu trữ + Cài đặt lưu trữ ở phía máy chủ + Đang lấy cài đặt lưu trữ. Vui lòng đợi... + Không thể lấy cài đặt lưu trữ + Yêu cầu CAPTCHA + Nhập văn bản trong hình ảnh ở trên + Chuỗi chứng chỉ không được tin tưởng + Địa chỉ XMPP không khớp với chứng chỉ + Gia hạn chứng nhận + Lỗi nhập khoá OMEMO! + Khoá OMEMO đã xác minh với chứng nhận! + Thiết bị không hỗ trợ chọn lựa các chứng chỉ của máy trạm! + Kết nối + Kết nối đến Tor + Chuyển toàn bộ kết nối thông qua mạng Tor. Cần có Orbot + Tên máy chủ + Cổng + Địa chỉ máy chủ hoặc .onion + Đây không phải là số cổng hợp lệ + Đây không phải là tên máy chủ hợp lệ + %1$d trên %2$d tài khoản đã kết nối + + %dv tin nhắn + + Tải thêm tin nhắn + Đã chia sẻ tệp với %s + Đã chia sẻ hình ảnh với %s + Đã 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 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ạ.]]>
+ Thông báo tất cả tin nhắn + Chỉ thông báo khi được nhắc đến + Đã tắt thông báo + Đã dừng thông báo + Nén hình ảnh + Gợi ý: Sử dụng \'Chọn tệp\' thay vì \'Chọn ảnh\' để gửi từng hình ảnh không nén riêng biệt mà không tính đến cài đặt này. + Luôn luôn + Chỉ các hình ảnh lớn + Đã bật tối ưu pin + Thiết bị của bạn đang sử dụng tối ưu hoá pin sâu cho %1$s, điều này có thể dẫn đến thông báo bị trì hoãn hay thậm chí là mất tin nhắn.\nChúng tôi khuyên bạn tắt tối ưu hoá pin. + Thiết bị của bạn đang sử dụng tối ưu hoá pin sâu cho %1$s, điều này có thể dẫn đến thông báo bị trì hoãn hay thậm chí là mất tin nhắn.\nBây giờ bạn sẽ được hỏi để tắt tối ưu hoá pin. + Tắt + Khu vực chọn quá lớn + (Không có tài khoản đã kích hoạt) + Trường này là bắt buộc + Sửa tin nhắn + Gửi tin nhắn đã sửa + Bạn đã xác minh mã kiểm tra của người này một cách bảo mật để xác nhận sự tin tưởng. Bằng cách chọn \"Xong\" bạn chỉ đang xác nhận rằng %s ở trong cuộc trò chuyện nhóm này. + Bạn đã tắt tài khoản này + 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/app/src/main/res/values-zh-rCN/strings.xml similarity index 100% rename from src/main/res/values-zh-rCN/strings.xml rename to app/src/main/res/values-zh-rCN/strings.xml diff --git a/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml similarity index 100% rename from src/main/res/values-zh-rTW/strings.xml rename to app/src/main/res/values-zh-rTW/strings.xml diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..fe65a2a6a --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,64 @@ + + + #43A047 + #006E1C + #FFFFFF + #98F994 + #002204 + #52634F + #FFFFFF + #D5E8CF + #111F0F + #38656A + #FFFFFF + #BCEBF0 + #002023 + #BA1A1A + #FFDAD6 + #FFFFFF + #410002 + #FCFDF6 + #1A1C19 + #FCFDF6 + #1A1C19 + #DEE5D8 + #424940 + #72796F + #F0F1EB + #2F312D + #7DDC7A + #000000 + #006E1C + #C2C9BD + #000000 + #7DDC7A + #00390A + #005313 + #98F994 + #BACCB3 + #253423 + #3B4B38 + #D5E8CF + #A0CFD4 + #00363B + #1F4D52 + #BCEBF0 + #FFB4AB + #93000A + #690005 + #FFDAD6 + #1A1C19 + #E2E3DD + #1A1C19 + #E2E3DD + #424940 + #C2C9BD + #8C9388 + #1A1C19 + #E2E3DD + #006E1C + #000000 + #7DDC7A + #424940 + #000000 + diff --git a/app/src/main/res/values/device.xml b/app/src/main/res/values/device.xml new file mode 100644 index 000000000..f99fe3e59 --- /dev/null +++ b/app/src/main/res/values/device.xml @@ -0,0 +1,4 @@ + + + false + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 000000000..7f85bd709 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + 48dp + 336dp + 24dp + diff --git a/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml similarity index 100% rename from src/main/res/values/integers.xml rename to app/src/main/res/values/integers.xml diff --git a/app/src/main/res/values/new_launcher_background.xml b/app/src/main/res/values/new_launcher_background.xml new file mode 100644 index 000000000..42c4dd324 --- /dev/null +++ b/app/src/main/res/values/new_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml similarity index 100% rename from src/main/res/values/strings.xml rename to app/src/main/res/values/strings.xml diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..ddf74586f --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,31 @@ + + + + diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 000000000..fa0f996d2 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 000000000..9ee9997b0 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/quicksy/res/drawable/ic_launcher_foreground.xml b/app/src/quicksy/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..40af0ee17 --- /dev/null +++ b/app/src/quicksy/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/app/src/quicksy/res/mipmap-anydpi-v26/new_launcher.xml b/app/src/quicksy/res/mipmap-anydpi-v26/new_launcher.xml new file mode 100644 index 000000000..d8028846d --- /dev/null +++ b/app/src/quicksy/res/mipmap-anydpi-v26/new_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/quicksy/res/mipmap-anydpi-v26/new_launcher_round.xml b/app/src/quicksy/res/mipmap-anydpi-v26/new_launcher_round.xml new file mode 100644 index 000000000..d8028846d --- /dev/null +++ b/app/src/quicksy/res/mipmap-anydpi-v26/new_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/quicksy/res/mipmap-hdpi/ic_launcher_background.png b/app/src/quicksy/res/mipmap-hdpi/ic_launcher_background.png new file mode 100644 index 000000000..0e5a16390 Binary files /dev/null and b/app/src/quicksy/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/app/src/quicksy/res/mipmap-hdpi/new_launcher.png b/app/src/quicksy/res/mipmap-hdpi/new_launcher.png new file mode 100644 index 000000000..5a399668a Binary files /dev/null and b/app/src/quicksy/res/mipmap-hdpi/new_launcher.png differ diff --git a/app/src/quicksy/res/mipmap-hdpi/new_launcher_round.png b/app/src/quicksy/res/mipmap-hdpi/new_launcher_round.png new file mode 100644 index 000000000..c82cdf735 Binary files /dev/null and b/app/src/quicksy/res/mipmap-hdpi/new_launcher_round.png differ diff --git a/app/src/quicksy/res/mipmap-mdpi/ic_launcher_background.png b/app/src/quicksy/res/mipmap-mdpi/ic_launcher_background.png new file mode 100644 index 000000000..57edd5ca3 Binary files /dev/null and b/app/src/quicksy/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/app/src/quicksy/res/mipmap-mdpi/new_launcher.png b/app/src/quicksy/res/mipmap-mdpi/new_launcher.png new file mode 100644 index 000000000..a2b125abf Binary files /dev/null and b/app/src/quicksy/res/mipmap-mdpi/new_launcher.png differ diff --git a/app/src/quicksy/res/mipmap-mdpi/new_launcher_round.png b/app/src/quicksy/res/mipmap-mdpi/new_launcher_round.png new file mode 100644 index 000000000..d52ec2242 Binary files /dev/null and b/app/src/quicksy/res/mipmap-mdpi/new_launcher_round.png differ diff --git a/app/src/quicksy/res/mipmap-xhdpi/ic_launcher_background.png b/app/src/quicksy/res/mipmap-xhdpi/ic_launcher_background.png new file mode 100644 index 000000000..7e4e772d0 Binary files /dev/null and b/app/src/quicksy/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/app/src/quicksy/res/mipmap-xhdpi/new_launcher.png b/app/src/quicksy/res/mipmap-xhdpi/new_launcher.png new file mode 100644 index 000000000..d6df0ec83 Binary files /dev/null and b/app/src/quicksy/res/mipmap-xhdpi/new_launcher.png differ diff --git a/app/src/quicksy/res/mipmap-xhdpi/new_launcher_round.png b/app/src/quicksy/res/mipmap-xhdpi/new_launcher_round.png new file mode 100644 index 000000000..660ef96ef Binary files /dev/null and b/app/src/quicksy/res/mipmap-xhdpi/new_launcher_round.png differ diff --git a/app/src/quicksy/res/mipmap-xxhdpi/ic_launcher_background.png b/app/src/quicksy/res/mipmap-xxhdpi/ic_launcher_background.png new file mode 100644 index 000000000..3ceecd75a Binary files /dev/null and b/app/src/quicksy/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/app/src/quicksy/res/mipmap-xxhdpi/new_launcher.png b/app/src/quicksy/res/mipmap-xxhdpi/new_launcher.png new file mode 100644 index 000000000..d7787abdd Binary files /dev/null and b/app/src/quicksy/res/mipmap-xxhdpi/new_launcher.png differ diff --git a/app/src/quicksy/res/mipmap-xxhdpi/new_launcher_round.png b/app/src/quicksy/res/mipmap-xxhdpi/new_launcher_round.png new file mode 100644 index 000000000..2b4830476 Binary files /dev/null and b/app/src/quicksy/res/mipmap-xxhdpi/new_launcher_round.png differ diff --git a/app/src/quicksy/res/mipmap-xxxhdpi/ic_launcher_background.png b/app/src/quicksy/res/mipmap-xxxhdpi/ic_launcher_background.png new file mode 100644 index 000000000..b75f8bc45 Binary files /dev/null and b/app/src/quicksy/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/app/src/quicksy/res/mipmap-xxxhdpi/new_launcher.png b/app/src/quicksy/res/mipmap-xxxhdpi/new_launcher.png new file mode 100644 index 000000000..dffe54a80 Binary files /dev/null and b/app/src/quicksy/res/mipmap-xxxhdpi/new_launcher.png differ diff --git a/app/src/quicksy/res/mipmap-xxxhdpi/new_launcher_round.png b/app/src/quicksy/res/mipmap-xxxhdpi/new_launcher_round.png new file mode 100644 index 000000000..f11138637 Binary files /dev/null and b/app/src/quicksy/res/mipmap-xxxhdpi/new_launcher_round.png differ diff --git a/app/src/quicksy/res/values-ar/strings.xml b/app/src/quicksy/res/values-ar/strings.xml new file mode 100644 index 000000000..ddf1d8788 --- /dev/null +++ b/app/src/quicksy/res/values-ar/strings.xml @@ -0,0 +1,12 @@ + + + مدى الوقت الذي يظل فيه Quicksy هادئًا بعد رؤية نشاط على جهاز آخر + عبر إرسال الأخطاء انت تقوم بالمساعدة في تطوير برمجة Quicksy + إجعل كلّ جهات إتصالك تعلم أنك تستعمل كويكسي + للمواصلة في إستقبال التنبيهات، حتى والشاشة مغلقة، يجب عليك أن تضيف Quicksy إلى قائمة التطبيقات المحميّة. + صورة حساب Quicksy + إن كويكسي Quicksy غير متوفر في بلدكم. + لا يمكن التأكد من خادم الهويّة. + خطأ أمني مجهول. + تجاوز الوقت أثناء الإتصال بالخادم. + \ No newline at end of file diff --git a/app/src/quicksy/res/values-bg/strings.xml b/app/src/quicksy/res/values-bg/strings.xml new file mode 100644 index 000000000..c41bf67c9 --- /dev/null +++ b/app/src/quicksy/res/values-bg/strings.xml @@ -0,0 +1,12 @@ + + + Времето, през което Quicksy няма да прави нищо, след като забележи дейност на друго устройство + Изпращайки проследявания на стека, Вие помагате за непрекъснатото развитие на Quicksy + Така всичките Ви контакти ще знаят кога използвате Quicksy + Ако искате да продължите да получавате известия дори когато екранът е заключен, трябва да добавите Quicksy към списъка със защитени приложения. + Профилна снимка за Quicksy + Quicksy не може да се използва във Вашата страна. + Идентичността на сървъра не може да бъде потвърдена. + Неизвестна грешка в сигурността. + Времето за изчакване на сървъра изтече. + diff --git a/app/src/quicksy/res/values-ca/strings.xml b/app/src/quicksy/res/values-ca/strings.xml new file mode 100644 index 000000000..c8b158f26 --- /dev/null +++ b/app/src/quicksy/res/values-ca/strings.xml @@ -0,0 +1,12 @@ + + + El temps que Quicksy roman en silenci després de veure activitat en un altre dispositiu. + En enviar les traces de les piles, vostè està ajudant al desenvolupament continu de Quicksy + Avisi a tots els seus contactes quan utilitzi Quicksy + Per seguir rebent notificacions, fins i tot quan la pantalla està desactivada, és necessari afegir Quicksy a la llista d\'aplicacions protegides. + Imatge de perfil en Quicksy + Quicksy no està disponible al teu país. + No es pot verificar la identitat del servidor. + Error de seguretat desconegut. + Temps d\'espera mentre es connecta al servidor. + diff --git a/app/src/quicksy/res/values-da-rDK/strings.xml b/app/src/quicksy/res/values-da-rDK/strings.xml new file mode 100644 index 000000000..0969f1822 --- /dev/null +++ b/app/src/quicksy/res/values-da-rDK/strings.xml @@ -0,0 +1,12 @@ + + + Hvor lang tid Quicksy er stille efter at have set aktivitet på en anden enhed + Ved at indsende \"stack traces\" hjælper du udviklingen af Quicksy + Lad alle dine kontakter vide når du bruger Quicksy + For at modtage notifikationer, selv når skærmen er slukket, skal du tilføje Quicksy til listen over beskyttede apps. + Quicksy profilbillede + Quicksy er ikke tilgængelig i dit land. + Kan ikke bekræfte server identitet. + Ukendt sikkerhedsfejl. + Timeout under tilslutning til serveren. + diff --git a/app/src/quicksy/res/values-de/strings.xml b/app/src/quicksy/res/values-de/strings.xml new file mode 100644 index 000000000..8dff5deae --- /dev/null +++ b/app/src/quicksy/res/values-de/strings.xml @@ -0,0 +1,12 @@ + + + Zeitspanne, in der Quicksy still bleibt, nachdem es Aktivitäten auf einem anderen Gerät erkannt hat + Mit dem Einsenden von Absturzberichten hilfst du bei der Weiterentwicklung von Quicksy + Informiere deine Kontakte, wann du Quicksy nutzt + Um weiterhin Benachrichtigungen zu erhalten, auch wenn der Bildschirm ausgeschaltet ist, musst du Quicksy zur Liste der geschützten Apps hinzufügen. + Quicksy Profilbild + Quicksy ist in deinem Land nicht verfügbar. + Überprüfung der Serveridentität ist nicht möglich. + Unbekannter Sicherheitsfehler. + Zeitüberschreitung bei der Verbindung zum Server. + \ No newline at end of file diff --git a/app/src/quicksy/res/values-el/strings.xml b/app/src/quicksy/res/values-el/strings.xml new file mode 100644 index 000000000..46c41a729 --- /dev/null +++ b/app/src/quicksy/res/values-el/strings.xml @@ -0,0 +1,12 @@ + + + Ο χρόνος σίγασης ειδοποιήσεων του Quicksy αφότου ανιχνευθεί δραστηριότητα σε μια από τις άλλες συσκευές σας. + Στέλνοντας ίχνη στοίβας προωθείτε την συνεχόμενη ανάπτυξη του Quicksy + Επιτρέψτε στις επαφές σας να γνωρίζουν πότε χρησιμοποιείτε το Quicksy + Για να συνεχίσετε να λαμβάνετε ειδοποιήσεις, ακόμα κι όταν η οθόνη είναι σβηστή, χρειάζεται να προσθέσετε το Quicksy στον κατάλογο με τις προστατευμένες εφαρμογές. + Φωτογραφία προφίλ του Quicksy + Το Quicksy δεν είναι διαθέσιμο στην χώρα σας. + Αδυναμία επαλήθευσης της ταυτότητας του διακομιστή. + Άγνωστο σφάλμα ασφάλειας. + Λήξη χρονικού ορίου κατά τη σύνδεση στον διακομιστή. + diff --git a/app/src/quicksy/res/values-es/strings.xml b/app/src/quicksy/res/values-es/strings.xml new file mode 100644 index 000000000..9b4de637d --- /dev/null +++ b/app/src/quicksy/res/values-es/strings.xml @@ -0,0 +1,12 @@ + + + Cantidad de tiempo que Quicksy permanece en silencio después de detectar actividad en otro dispositivo + Al enviar informes de fallas, ayudará a desarrollar Quicksy aún más + Informar a tus contactos cuando usas Quicksy + Para continuar recibiendo notificaciones incluso cuando la pantalla está apagada, debe agregar Quicksy a la lista de aplicaciones protegidas. + Foto del perfil de Quicksy + Quicksy no está disponible en tu país. + No se ha podido verificar la identidad del servidor. + Error de seguridad desconocido. + Se ha superado el tiempo máximo de espera conectando al servidor. + \ No newline at end of file diff --git a/app/src/quicksy/res/values-fi/strings.xml b/app/src/quicksy/res/values-fi/strings.xml new file mode 100644 index 000000000..9a988a15d --- /dev/null +++ b/app/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/app/src/quicksy/res/values-fr/strings.xml b/app/src/quicksy/res/values-fr/strings.xml new file mode 100644 index 000000000..11792be31 --- /dev/null +++ b/app/src/quicksy/res/values-fr/strings.xml @@ -0,0 +1,12 @@ + + + Durée d’inactivité de Quicksy après avoir repéré un changement sur un autre appareil + En envoyant des traces d’appels, vous aidez le développement de Quicksy + Faites savoir à tous vos contacts quand vous utilisez Quicksy + Pour continuer à recevoir des notifications, même lorsque l’écran est éteint, vous devez ajouter Quicksy à la liste des applications protégées. + Photo de profil Quicksy + Quicksy n’est pas disponible dans votre pays. + Impossible de vérifier l’identité du serveur. + Erreur de sécurité inconnue. + Délai expiré lors de la connexion au serveur. + diff --git a/app/src/quicksy/res/values-gl/strings.xml b/app/src/quicksy/res/values-gl/strings.xml new file mode 100644 index 000000000..0f1343d66 --- /dev/null +++ b/app/src/quicksy/res/values-gl/strings.xml @@ -0,0 +1,12 @@ + + + O período de tempo que Quicksy permanece acalado tras ver actividade noutro dispositivo + Enviando trazas do rexistro estás axudando ao desenvolvemento de Quicksy + Permitir a todos os teus contactos saber cando estás a utilizar Quicksy + Para seguir recibindo notificacións, mesmo coa pantalla apagada, tes que engadir a Quicksy á lista de apps protexidas. + Imaxe de perfil Quicksy + Quicksy non está dispoñible no teu país. + Non se puido verificar a identidade do servidor. + Fallo de seguridade descoñecido. + Caducou a conexión mentras conectaba co servidor. + \ No newline at end of file diff --git a/app/src/quicksy/res/values-hr/strings.xml b/app/src/quicksy/res/values-hr/strings.xml new file mode 100644 index 000000000..80b63a28f --- /dev/null +++ b/app/src/quicksy/res/values-hr/strings.xml @@ -0,0 +1,12 @@ + + + Duljina vremena u kojem Quicksy šuti nakon što vidi aktivnost na drugom uređaju + Slanjem tragova hrpe pomažete tekući razvoj Quicksyja + Obavijestite sve svoje kontakte kada koristite Quicksy + Kako biste nastavili primati obavijesti, čak i kada je ekran isključen, trebate dodati Quicksy na popis zaštićenih aplikacija. + Quicksy profilna slika + Quicksy nije dostupan u vašoj zemlji. + Nije moguće potvrditi identitet poslužitelja. + Nepoznata sigurnosna pogreška. + Istek vremena tijekom povezivanja s poslužiteljem. + diff --git a/app/src/quicksy/res/values-hu/strings.xml b/app/src/quicksy/res/values-hu/strings.xml new file mode 100644 index 000000000..af37d97bf --- /dev/null +++ b/app/src/quicksy/res/values-hu/strings.xml @@ -0,0 +1,12 @@ + + + A Quicksy csendben marad ennyi ideig, miután aktivitást észlelt egy másik eszközön + A veremkiíratások elküldésével segíti a Quicksy alkalmazás folyamatos fejlesztését + Tudassa az összes partnerével, hogy a Quicksy alkalmazást használja + Ha akkor is szeretne értesítéseket kapni, amikor a kijelző ki van kapcsolva, hozzá kell adnia a Quicksy alkalmazást a védett alkalmazások listájához. + Quicksy profilkép + A Quicksy nem érhető el az Ön országában. + Nem sikerült ellenőrizni a kiszolgáló személyazonosságát. + Ismeretlen biztonsági hiba. + Időtúllépés a kiszolgálóhoz való csatlakozáskor. + diff --git a/app/src/quicksy/res/values-id/strings.xml b/app/src/quicksy/res/values-id/strings.xml new file mode 100644 index 000000000..79a2ff4dc --- /dev/null +++ b/app/src/quicksy/res/values-id/strings.xml @@ -0,0 +1,12 @@ + + + Durasi Quicksy tetap diam setelah melihat aktivitas di perangkat lain + Dengan mengirimkan data log, Anda sedang membantu pengembangan Quicksy + Ijinkan kontak anda mengetahui kapan anda menggunakan Quicksy + Untuk tetap menerima notifikasi, bahkan saat layar mati, Anda perlu menambahkan Quicksy ke daftar aplikasi yang dilindungi. + Gambar profil Quicksy + Quicksy tidak tersedia di negara anda. + Gagal memverifikasi identitas server + Error keamanan tidak dikenal. + Waktu habis saat menghubungi server. + diff --git a/app/src/quicksy/res/values-it/strings.xml b/app/src/quicksy/res/values-it/strings.xml new file mode 100644 index 000000000..a6ebedd77 --- /dev/null +++ b/app/src/quicksy/res/values-it/strings.xml @@ -0,0 +1,12 @@ + + + Il periodo di tempo in cui Quicksy resta silenzioso quando vede attività su un altro dispositivo + Se scegli di inviare una segnalazione dell’errore aiuterai lo sviluppo di Quicksy + Fai sapere ai tuoi contatti quando usi Quicksy + Per ricevere notifiche anche quando lo schermo è spento, devi aggiungere Quicksy all\'elenco delle app protette. + Immagine profilo di Quicksy + Quicksy non è disponibile nella tua nazione. + Impossibile verificare l\'identità del server. + Errore di sicurezza sconosciuto. + Tentativo di connessione al server scaduto. + diff --git a/app/src/quicksy/res/values-ja/strings.xml b/app/src/quicksy/res/values-ja/strings.xml new file mode 100644 index 000000000..d846cd3df --- /dev/null +++ b/app/src/quicksy/res/values-ja/strings.xml @@ -0,0 +1,12 @@ + + + 別のデバイスで活動を見た後、Quicksy を静かにする時間の長さ + スタックトレースを送信することで、あなたは Quicksy の継続的な開発を支援しています + Quicksy を使用するときに、すべての連絡先に知らせましょう + 画面がオフになっている場合でも通知を受信し続けるには、保護されたアプリのリストに Quicksy を追加する必要があります。 + Quicksy プロフィール写真 + Quicksy はあなたの国で利用不可です。 + サーバーの同一性を確認できません。 + 未知のセキュリティエラー。 + サーバーへの接続中にタイムアウトが発生しました。 + diff --git a/app/src/quicksy/res/values-nl/strings.xml b/app/src/quicksy/res/values-nl/strings.xml new file mode 100644 index 000000000..eec11a1f9 --- /dev/null +++ b/app/src/quicksy/res/values-nl/strings.xml @@ -0,0 +1,12 @@ + + + Hoe lang Quicksy stil blijft na activiteit op een ander apparaat waar te nemen + Door crashrapportages te versturen help je de ontwikkeling van Quicksy + Laat al je contactpersonen weten wanneer je Quicksy gebruikt + Om meldingen te blijven ontvangen, zelfs wanneer het scherm uit staat, moet je Quicksy toevoegen aan de lijst met beschermde apps. + Quicksy-profielafbeelding + Quicksy is niet beschikbaar in je land. + Kan serveridentiteit niet verifiëren. + Onbekende beveiligingsfout. + Time-out bij verbinden met server. + diff --git a/app/src/quicksy/res/values-pl/strings.xml b/app/src/quicksy/res/values-pl/strings.xml new file mode 100644 index 000000000..d94b0c775 --- /dev/null +++ b/app/src/quicksy/res/values-pl/strings.xml @@ -0,0 +1,12 @@ + + + Czas, przez który Quicksy jest cicho po zobaczeniu aktywności na innym urządzeniu + Wysyłając nam ślady stosu pomagasz w rozwoju Quicksy + Powiadom kontakty o tym że używasz Quicksy + Aby otrzymywać powiadomienia nawet kiedy ekran jest wyłączony musisz dodać Quicksy do listy chronionych aplikacji. + Obrazek profilowy Quicksy + Quicksy nie jest dostępne w Twoim kraju. + Nie udało się sprawdzić tożsamości serwera. + Nieznany błąd bezpieczeństwa. + Błąd czasu oczekiwania na połączenie z serwerem. + \ No newline at end of file diff --git a/app/src/quicksy/res/values-pt-rBR/strings.xml b/app/src/quicksy/res/values-pt-rBR/strings.xml new file mode 100644 index 000000000..3d6b15498 --- /dev/null +++ b/app/src/quicksy/res/values-pt-rBR/strings.xml @@ -0,0 +1,12 @@ + + + Espaço de tempo em que o Quicksy ficará sem notificações, após alguma atividade em outro dispositivo. + Ao enviar os stack traces você está colaborando com o desenvolvimento do Quicksy. + Permite que todos os seus contatos saibam quando você usa o Quicksy. + Para continuar recebendo notificações, mesmo com a tela apagada, você precisa adicionar o Quicksy à lista de apps protegidos. + Imagem de perfil do Quicksy + Quicksy agora está disponível no seu país. + Não foi possível verificar a identidade do servidor. + Erro de segurança desconhecido. + Tempo esgotado ao tentar conectar ao servidor. + diff --git a/app/src/quicksy/res/values-ro-rRO/strings.xml b/app/src/quicksy/res/values-ro-rRO/strings.xml new file mode 100644 index 000000000..7dd79ce4a --- /dev/null +++ b/app/src/quicksy/res/values-ro-rRO/strings.xml @@ -0,0 +1,12 @@ + + + Durata de timp cât Quicksy păstrează liniștea după ce a observat activitate pe un alt dispozitiv + Trimițând datele despre erori ajutați la continuarea dezvoltării aplicației Quicksy + Contactele vă sunt anunțate atunci când folosiți Quicksy + Pentru a continua să primiți notificări, chiar și când ecranul este oprit, trebuie să adăugați Quicksy în lista de aplicații protejate. + Poză profil Quicksy + Quicksy nu este disponibilă în țara dumneavoastră. + Nu s-a putut verifica identitatea serverului. + Eroare de securitate necunoscută. + A expirat timpul de așteptare conexiune server. + diff --git a/app/src/quicksy/res/values-ru/strings.xml b/app/src/quicksy/res/values-ru/strings.xml new file mode 100644 index 000000000..c4915772b --- /dev/null +++ b/app/src/quicksy/res/values-ru/strings.xml @@ -0,0 +1,12 @@ + + + Время, на которое уведомления от Quicksy будут отключены, когда вы пользуетесь аккаунтом на другом устройстве. + Отправляя отчёты об ошибках, вы помогаете в разработке Quicksy + Извещать собеседников, когда вы пользуетесь Quicksy + Чтобы продолжать получать уведомления, даже если экран выключен, вам необходимо добавить Quicksy в список защищенных приложений. + Аватар для Quicksy + Quicksy не доступно в вашем регионе + Не удалось подтвердить сервер. + Неизвестная ошибка безопасности. + Время ожидания подключения к серверу вышло. + diff --git a/app/src/quicksy/res/values-sk/strings.xml b/app/src/quicksy/res/values-sk/strings.xml new file mode 100644 index 000000000..df1b5a1e0 --- /dev/null +++ b/app/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. + diff --git a/app/src/quicksy/res/values-sq/strings.xml b/app/src/quicksy/res/values-sq/strings.xml new file mode 100644 index 000000000..bac4948d3 --- /dev/null +++ b/app/src/quicksy/res/values-sq/strings.xml @@ -0,0 +1,12 @@ + + + Sasia e kohës që Quicksy nuk ndihet, pasi të ketë parë veprimtari në pajisje tjetër + Duke dërguar “stack traces” ndihmoni në zhvillimin e pandërprerë të Quicksy-t + Bëjuani të ditur krejt kontakteve tuaja, kur përdorni Quicksy-n + Që të vazhdoni të merrni njoftime, edhe kur ekrani juaj është i fikur, duhet të shtoni Quicksy-n te lista e aplikacioneve të mbrojtur. + Foto profili Quicksy + Quicksy s’mund të kihet në vendin tuaj. + S’arrihet të verifikohet identitet shërbyesi. + Gabim i panjohur sigurie. + Mbarim kohe teksa lidhej me shërbyesin. + \ No newline at end of file diff --git a/app/src/quicksy/res/values-sv/strings.xml b/app/src/quicksy/res/values-sv/strings.xml new file mode 100644 index 000000000..cd32d7395 --- /dev/null +++ b/app/src/quicksy/res/values-sv/strings.xml @@ -0,0 +1,12 @@ + + + Berätta för alla dina kontakter när du använder Quicksy + Quicksy är inte tillgängligt i ditt land. + Okänt säkerhetsfel. + Quicksy-profilbild + Det gick inte att verifiera serveridentiteten. + Timeout under anslutning till servern. + Genom att skicka in stack traces hjälper du den pågående utvecklingen av Quicksy + För att fortsätta ta emot aviseringar, även när skärmen är avstängd, måste du lägga till Quicksy i listan över skyddade appar. + Hur lång tid Quicksy håller tyst efter att ha sett aktivitet på en annan enhet + \ No newline at end of file diff --git a/app/src/quicksy/res/values-szl/strings.xml b/app/src/quicksy/res/values-szl/strings.xml new file mode 100644 index 000000000..d3f435559 --- /dev/null +++ b/app/src/quicksy/res/values-szl/strings.xml @@ -0,0 +1,12 @@ + + + Czas, jak dugo Quicksy je cichy po ôboczyniu aktywności na inkszyj maszinie + Jak wysyłosz sztreki sztapla, to pōmogosz przi budowaniu Quicksy + Informuj wszyske twoje kōntakty ô tym, kedy używosz Quicksy + Żeby durch dostować powiadōmiynia, nawet jak ekran je zgaszōny, musisz dodać Quicksy do listy chrōniōnych aplikacyji. + Profilowy ôbrozek Quicksy + Quicksy niy ma dostympne we twojim kraju. + Niy idzie zweryfikować tożsamości ôd serwera. + Niyznōmy feler bezpieczyństwa. + Przekroczynie czasu przi łōnczyniu ze serwerym. + diff --git a/app/src/quicksy/res/values-tr-rTR/strings.xml b/app/src/quicksy/res/values-tr-rTR/strings.xml new file mode 100644 index 000000000..12cef8d47 --- /dev/null +++ b/app/src/quicksy/res/values-tr-rTR/strings.xml @@ -0,0 +1,12 @@ + + + Başka bir aygıt üstünde etkinlik algılandığında Quicksy\'nin sessiz kalma süresi + Çöküş raporu göndermeniz Quicksy\'nin geliştirlmesinde katkıda bulunacaktır. + Tüm kişileriniz ne zaman Quicksy kullandığınızı görsün + Ekranınız kapalıyken bile bildirim almak için Quicksy\'i korunan uygulamalara eklemelisiniz. + Quicksy profil resmi + Quicksy ülkenizde kullanılamıyor. + Sunucu kimliği belirlenemiyor. + Bilinmeyen güvenlik hatası. + Sunucuya bağlanılırken zaman aşımına uğrandı. + diff --git a/app/src/quicksy/res/values-uk/strings.xml b/app/src/quicksy/res/values-uk/strings.xml new file mode 100644 index 000000000..e2aa92703 --- /dev/null +++ b/app/src/quicksy/res/values-uk/strings.xml @@ -0,0 +1,12 @@ + + + Час, протягом якого застосунок дотримується тиші після активності на іншому пристрої. + Надсилаючи траси стеків виклику, ви допомагаєте розробці цього застосунку. + Дозволити всім вашим контактам знати, коли ви використовуєте цю програму. + Щоб продовжувати отримувати сповіщення, навіть коли екран вимкнуто, потрібно додати цю програму до списку захищених. + Зображення профілю для Quicksy + Цей застосунок не доступний у вашій країні. + Автентичність сервера не підтверджено. + Невідома помилка безпеки. + Вичерпано час для встановлення з\'єднання із сервером. + diff --git a/app/src/quicksy/res/values-vi/strings.xml b/app/src/quicksy/res/values-vi/strings.xml new file mode 100644 index 000000000..99f454d24 --- /dev/null +++ b/app/src/quicksy/res/values-vi/strings.xml @@ -0,0 +1,12 @@ + + + Khoảng thời gian Quicksy giữ yên lặng sau khi xem hoạt động trên một thiết bị khác + Bằng việc gửi báo cáo hoạt động, bạn đang hỗ trợ sự phát triển liên tục của Quicksy + Để cho tất cả liên hệ của bạn biết khi bạn sử dụng Quicksy + Để 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 Quicksy vào danh sách các ứng dụng được bảo vệ. + Ảnh hồ sơ Quicksy + Quicksy không có sẵn ở quốc gia của bạn. + Không thể xác minh danh tính máy chủ. + Lỗi bảo mật không xác định. + Hết thời gian chờ khi kết nối đến máy chủ. + diff --git a/app/src/quicksy/res/values-zh-rCN/strings.xml b/app/src/quicksy/res/values-zh-rCN/strings.xml new file mode 100644 index 000000000..a93ac4a09 --- /dev/null +++ b/app/src/quicksy/res/values-zh-rCN/strings.xml @@ -0,0 +1,12 @@ + + + 发现在其它设备上的活动后,Conversations保持安静的时间 + 通过发送堆栈跟踪,您可以帮助 Quicksy 的持续开发 + 让你的所有联系人知道你使用Quicksy的时间 + 为了在屏幕关闭时也能收到消息提醒,您需要将 Quicksy 加入受保护的应用列表。 + Quicksy 个人资料图片 + Quicksy在您的国家无服务。 + 无法验证服务器身份。 + 未知安全错误。 + 连接到服务器时超时。 + \ No newline at end of file diff --git a/app/src/quicksy/res/values-zh-rTW/strings.xml b/app/src/quicksy/res/values-zh-rTW/strings.xml new file mode 100644 index 000000000..9846922b1 --- /dev/null +++ b/app/src/quicksy/res/values-zh-rTW/strings.xml @@ -0,0 +1,12 @@ + + + 發現在其它設備上的活動後,Quicksy 保持安靜的時間 + 發送堆疊跟蹤説明以幫助 Quicksy 持續開發 + 讓你的所有聯絡人知道你何時使用 Quicksy + 爲了在螢幕關閉時也能收到通知,你需要將 Quicksy 加入受保護的應用程式列表。 + Quicksy 設定檔圖片 + Quicksy 在您的國家無法使用。 + 無法驗證伺服器身分。 + 未知安全性錯誤。 + 連線伺服器逾時。 + diff --git a/app/src/quicksy/res/values/strings.xml b/app/src/quicksy/res/values/strings.xml new file mode 100644 index 000000000..e1462e916 --- /dev/null +++ b/app/src/quicksy/res/values/strings.xml @@ -0,0 +1,12 @@ + + + The length of time Quicksy keeps quiet after seeing activity on another device + By sending in stack traces you are helping the ongoing development of Quicksy + Let all your contacts know when you use Quicksy + To keep receiving notifications, even when the screen is turned off, you need to add Quicksy to the list of protected apps. + Quicksy profile picture + Quicksy is not available in your country. + Unable to verify server identity. + Unknown security error. + Timeout while connecting to server. + diff --git a/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java b/app/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java similarity index 100% rename from src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java rename to app/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java diff --git a/src/test/java/im/conversations/android/xmpp/PubSubTest.java b/app/src/test/java/im/conversations/android/xmpp/PubSubTest.java similarity index 100% rename from src/test/java/im/conversations/android/xmpp/PubSubTest.java rename to app/src/test/java/im/conversations/android/xmpp/PubSubTest.java diff --git a/src/test/java/im/conversations/android/xmpp/TimestampTest.java b/app/src/test/java/im/conversations/android/xmpp/TimestampTest.java similarity index 100% rename from src/test/java/im/conversations/android/xmpp/TimestampTest.java rename to app/src/test/java/im/conversations/android/xmpp/TimestampTest.java diff --git a/src/test/java/im/conversations/android/xmpp/XmlElementReaderTest.java b/app/src/test/java/im/conversations/android/xmpp/XmlElementReaderTest.java similarity index 100% rename from src/test/java/im/conversations/android/xmpp/XmlElementReaderTest.java rename to app/src/test/java/im/conversations/android/xmpp/XmlElementReaderTest.java diff --git a/build.gradle b/build.gradle index 05d30becd..5170846e3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,291 +1,28 @@ buildscript { - ext { - room_version = "2.5.0" - navVersion = '2.5.3' appcompatVersion = "1.6.1" + material = "1.8.0" lifecycleVersion = "2.2.0" + navVersion = '2.5.3' + roomVersion = "2.5.0" + espressoVersion = "3.5.1" } repositories { google() mavenCentral() } + + dependencies { classpath 'com.android.tools.build:gradle:7.4.1' - classpath "com.diffplug.spotless:spotless-plugin-gradle:6.13.0" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navVersion" + classpath "com.diffplug.spotless:spotless-plugin-gradle:6.13.0" } } - -apply plugin: 'com.android.application' -apply plugin: "com.diffplug.spotless" -apply plugin: "androidx.navigation.safeargs" - allprojects { repositories { google() mavenCentral() - maven { url "https://jitpack.io" } } -} - -repositories { - google() - mavenCentral() - maven { url "https://jitpack.io" } - jcenter() -} - -configurations { - playstoreImplementation - gitImplementation - implementation.exclude group: 'org.jetbrains' , module:'annotations' -} - -spotless { - ratchetFrom '2.12.2' - java { - target '**/*.java' - googleJavaFormat('1.8').aosp().reflowLongStrings() - } -} - -dependencies { - - - // Conversations 3.0 dependencies - - implementation project(':libs:annotation') - annotationProcessor project(':libs:annotation-processor') - - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.8' - - implementation "androidx.appcompat:appcompat:$rootProject.ext.appcompatVersion" - - implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.ext.lifecycleVersion" - - implementation "androidx.room:room-runtime:$room_version" - annotationProcessor "androidx.room:room-compiler:$room_version" - implementation "androidx.room:room-guava:$room_version" - - implementation "androidx.navigation:navigation-fragment:$rootProject.ext.navVersion" - implementation "androidx.navigation:navigation-ui:$rootProject.ext.navVersion" - - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - - - implementation "androidx.security:security-crypto:1.0.0" - - - implementation 'org.slf4j:slf4j-api:1.7.36' - implementation 'com.github.tony19:logback-android:2.0.1' - - testImplementation 'junit:junit:4.13.2' - testImplementation 'org.robolectric:robolectric:4.9' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test:runner:1.5.2' - - // legacy dependencies. Ideally everything below should be carefully reviewed and eventually moved up - - - implementation 'androidx.viewpager:viewpager:1.0.0' - - - playstoreImplementation('com.google.firebase:firebase-messaging:23.1.1') { - exclude group: 'com.google.firebase', module: 'firebase-core' - exclude group: 'com.google.firebase', module: 'firebase-analytics' - exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' - } - playstoreImplementation 'com.android.installreferrer:installreferrer:2.2' - implementation 'org.sufficientlysecure:openpgp-api:10.0' - implementation('com.theartofdev.edmodo:android-image-cropper:2.8.0') { - exclude group: 'com.android.support', module: 'appcompat-v7' - exclude group: 'com.android.support', module: 'exifinterface' - } - implementation 'im.conversations.webrtc:webrtc-android:104.0.0' - //implementation 'org.snikket:webrtc-android:107.0.0' - implementation 'org.jitsi:org.otr4j:0.23' - implementation 'org.bouncycastle:bcmail-jdk15on:1.64' - implementation 'org.gnu.inet:libidn:1.15' - implementation 'org.sufficientlysecure:openpgp-api:10.0' - implementation 'com.google.zxing:core:3.5.0' - implementation 'de.measite.minidns:minidns-hla:0.2.4' - implementation 'me.leolin:ShortcutBadger:1.1.22@aar' - implementation 'org.whispersystems:signal-protocol-android:2.6.2' - implementation 'com.makeramen:roundedimageview:2.3.0' - implementation 'jetty:javax.servlet:5.1.12' - implementation 'com.google.code.gson:gson:2.8.9' - implementation 'androidx.multidex:multidex:2.0.1' - implementation 'androidx.legacy:legacy-support-v13:1.0.0' - implementation 'androidx.exifinterface:exifinterface:1.3.6' - implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.emoji2:emoji2:1.2.0' - gitImplementation "androidx.emoji2:emoji2-bundled:1.2.0" - implementation 'androidx.recyclerview:recyclerview:1.2.1' - implementation 'com.google.android.material:material:1.8.0' - implementation 'androidx.cardview:cardview:1.0.0' // for compatibility - implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0' - implementation 'com.google.android.exoplayer:exoplayer-core:2.18.1' - implementation 'com.google.android.exoplayer:exoplayer-ui:2.18.1' - implementation 'com.google.android.exoplayer:extension-mediasession:2.18.1' - implementation 'pub.devrel:easypermissions:3.0.0' // version >= 3.0.0 needs android X libraries - implementation 'com.wefika:flowlayout:0.4.1' - implementation 'com.googlecode.ez-vcard:ez-vcard:0.11.3' - implementation 'org.jxmpp:jxmpp-jid:1.0.3' - implementation 'org.hsluv:hsluv:0.2' - implementation 'org.conscrypt:conscrypt-android:2.5.2' - implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.25' - implementation 'me.drakeet.support:toastcompat:1.1.0' - implementation 'org.osmdroid:osmdroid-android:6.1.14' - implementation 'com.leinardi.android:speed-dial:3.3.0' - implementation 'com.squareup.picasso:picasso:2.71828' - implementation "com.squareup.okhttp3:okhttp:4.10.0" - implementation 'com.squareup.retrofit2:retrofit:2.9.0' - implementation 'com.squareup.retrofit2:converter-gson:2.9.0' - implementation 'com.google.guava:guava:31.1-android' - implementation 'com.github.AppIntro:AppIntro:6.2.0' - implementation 'androidx.browser:browser:1.4.0' - implementation 'com.otaliastudios:transcoder:0.9.1' // 0.10.4 seems to be buggy - implementation 'me.saket:better-link-movement-method:2.2.0' - implementation project(':libs:AXML') -} - -ext { - preDexEnabled = System.getProperty("pre-dex", "true") -} - -android { - namespace 'eu.siacs.conversations' - //noinspection GradleCompatible - compileSdkVersion 33 - - defaultConfig { - minSdkVersion 23 - targetSdkVersion 32 - - //versionNameSuffix " beta_(2023-01-10)" // " beta_(XXXX-XX-XX)" // activate for beta versions - versionCode 128 - versionName "1.6.1" - //resConfigs "en" - - archivesBaseName += "-$versionName" - //archivesBaseName += "$versionNameSuffix" // activate for beta versions - applicationId "de.monocles.chat" - multiDexEnabled true - - buildConfigField("String", "LOGTAG", '"monocles chat"') - buildConfigField("String", "DOMAIN_LOCK", 'null') - buildConfigField("boolean", "SHOW_INTRO", 'true') - buildConfigField("String", "UPDATE_URL", '"https://monocles.eu/chat/update/"') - resValue "string", "applicationId", applicationId - def appName = "monocles chat" - resValue "string", "app_name", "monocles chat" - resValue "string", "short_app_name", "chat" - buildConfigField "String", "APP_NAME", "\"$appName\"" - - javaCompileOptions { - annotationProcessorOptions { - arguments += ["room.schemaLocation": "$projectDir/schemas".toString()] - } - } - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - testInstrumentationRunnerArguments clearPackageData: 'true' - - } - testOptions { - unitTests { - includeAndroidResources = false - } - } - - dataBinding { - enabled true - } - - packagingOptions { - jniLibs { - excludes += ['lib/x86/**', 'lib/x86_64/**'] - } - resources { - excludes += ['lib/x86/**', 'lib/x86_64/**', 'META-INF/BCKEY.DSA', 'META-INF/BCKEY.SF'] - } - } - - compileOptions { - coreLibraryDesugaringEnabled true - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 - } - - flavorDimensions("distribution") - - productFlavors { - playstore { - dimension "distribution" - versionNameSuffix "-playstore" - applicationId "de.monocles.chat" - buildConfigField("boolean", "SHOW_MIGRATION_INFO", 'false') - resValue "string", "applicationId", applicationId - } - git { - dimension "distribution" - buildConfigField("boolean", "SHOW_MIGRATION_INFO", 'true') - } - } - if (project.hasProperty('mStoreFile') && - project.hasProperty('mStorePassword') && - project.hasProperty('mKeyAlias') && - project.hasProperty('mKeyPassword')) { - signingConfigs { - release { - storeFile file(mStoreFile) - storePassword mStorePassword - keyAlias mKeyAlias - keyPassword mKeyPassword - } - } - buildTypes { - release { - debuggable false - signingConfig = signingConfigs.release - minifyEnabled true - shrinkResources true - runProguard true - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - - debug { - debuggable true - buildTypes.release.signingConfig = null - minifyEnabled true - shrinkResources true - runProguard true - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - - } - } - - subprojects { - - afterEvaluate { - if (getPlugins().hasPlugin('android') || - getPlugins().hasPlugin('android-library')) { - - configure(android.lintOptions) { - disable 'AndroidGradlePluginVersion', 'MissingTranslation' - } - } - - } - } - - lint { - abortOnError false - checkReleaseBuilds true - disable 'ExtraTranslation', 'MissingTranslation', 'InvalidPackage', 'MissingQuantity', 'AppCompatResource', 'RestrictedApi' - error 'StringFormatInvalid', 'StringFormatMatches' - } -} +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 51f9f7896..a03b35489 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,19 +1,21 @@ - -## For more details on how to configure your build environment visit +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html -# # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -# Default value: -Xmx1024m -XX:MaxPermSize=256m -# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -# +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -#Sat Jan 22 19:25:52 CET 2022 -org.gradle.parallel=true -org.gradle.jvmargs=-Xmx6144m -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -android.enableJetifier=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true -org.gradle.gradle-args=--max-workers=16 \ No newline at end of file +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true diff --git a/libs/annotation-processor/build.gradle b/libs/annotation-processor/build.gradle deleted file mode 100644 index 3bad25d2b..000000000 --- a/libs/annotation-processor/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - id 'java-library' -} - -repositories { - google() - mavenCentral() -} - -java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 -} -dependencies { - - implementation project(':libs:annotation') - - annotationProcessor 'com.google.auto.service:auto-service:1.0-rc5' - compileOnly 'com.google.auto.service:auto-service:1.0-rc5' - -} \ No newline at end of file diff --git a/libs/annotation/build.gradle b/libs/annotation/build.gradle deleted file mode 100644 index e493c42ff..000000000 --- a/libs/annotation/build.gradle +++ /dev/null @@ -1,8 +0,0 @@ -plugins { - id 'java-library' -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_7 - targetCompatibility = JavaVersion.VERSION_1_7 -} \ No newline at end of file diff --git a/proguard-rules.pro b/proguard-rules.pro deleted file mode 100644 index 00b331648..000000000 --- a/proguard-rules.pro +++ /dev/null @@ -1,92 +0,0 @@ --dontobfuscate - --keep class de.monocles.chat.** --keep class de.pixart.messenger.** --keep class eu.siacs.conversations.** --keep class org.whispersystems.** --keep class com.kyleduo.switchbutton.Configuration --keep class com.soundcloud.android.crop.** --keep class com.google.android.gms.** --keep class org.openintents.openpgp.* --keep public class * implements com.bumptech.glide.module.GlideModule --keep public class * extends com.bumptech.glide.module.AppGlideModule --keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** { - **[] $VALUES; - public *; -} --keep class com.squareup.okhttp.** { *; } --keep interface com.squareup.okhttp.** { *; } - -# Logger --keep class org.slf4j.** {*;} --keep class ch.qos.** {*;} - --dontwarn javax.mail.internet.MimeMessage --dontwarn javax.mail.internet.MimeBodyPart --dontwarn javax.mail.internet.SharedInputStream --dontwarn javax.activation.DataContentHandler --dontwarn org.bouncycastle.mail.** --dontwarn org.bouncycastle.x509.util.LDAPStoreHelper --dontwarn org.bouncycastle.jce.provider.X509LDAPCertStoreSpi --dontwarn org.bouncycastle.cert.dane.** --dontwarn rocks.xmpp.addr.** --dontwarn com.google.firebase.analytics.connector.AnalyticsConnector --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 ; -} - -# JSR 305 annotations are for embedding nullability information. --dontwarn javax.annotation.** - -# A resource is loaded with a relative path so the package of this class must be preserved. --keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase - -# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. --dontwarn org.codehaus.mojo.animal_sniffer.* - -# OkHttp platform used only on JVM and when Conscrypt dependency is available. --dontwarn okhttp3.internal.platform.ConscryptPlatform - - - -# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and -# EnclosingMethod is required to use InnerClasses. --keepattributes Signature, InnerClasses, EnclosingMethod - -# Retrofit does reflection on method and parameter annotations. --keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations - -# Retain service method parameters when optimizing. --keepclassmembers,allowshrinking,allowobfuscation interface * { - @retrofit2.http.* ; -} - -# Ignore annotation used for build tooling. --dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement - -# Ignore JSR 305 annotations for embedding nullability information. --dontwarn javax.annotation.** - -# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. --dontwarn kotlin.Unit - -# Top-level functions that can only be used by Kotlin. --dontwarn retrofit2.KotlinExtensions --dontwarn retrofit2.KotlinExtensions$* - -# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy -# and replaces all potential values with null. Explicitly keeping the interfaces prevents this. --if interface * { @retrofit2.http.* ; } --keep,allowobfuscation interface <1> \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index de0fe846e..c9722936c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,4 @@ -include ':libs:annotation' -include ':libs:annotation-processor' -include ':libs:AXML' -include ':libs:xmpp-addr' -rootProject.name = 'monocles chat' +rootProject.name = "Conversations" +include ':app' +include ':annotation' +include ':annotation-processor' diff --git a/src/conversations/java/eu/siacs/conversations/utils/ProvisioningUtils.java b/src/conversations/java/eu/siacs/conversations/utils/ProvisioningUtils.java deleted file mode 100644 index 3d639d2f7..000000000 --- a/src/conversations/java/eu/siacs/conversations/utils/ProvisioningUtils.java +++ /dev/null @@ -1,42 +0,0 @@ -package eu.siacs.conversations.utils; - -import android.app.Activity; -import android.content.Intent; - -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.AccountConfiguration; -import eu.siacs.conversations.persistance.DatabaseBackend; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.EditAccountActivity; -import eu.siacs.conversations.xmpp.Jid; - -public class ProvisioningUtils { - - public static void provision(final Activity activity, final String json) { - final AccountConfiguration accountConfiguration; - try { - accountConfiguration = AccountConfiguration.parse(json); - } catch (final IllegalArgumentException e) { - ToastCompat.makeText(activity, R.string.improperly_formatted_provisioning, ToastCompat.LENGTH_LONG).show(); - return; - } - final Jid jid = accountConfiguration.getJid(); - final List accounts = DatabaseBackend.getInstance(activity).getAccountJids(true); - if (accounts.contains(jid)) { - ToastCompat.makeText(activity, R.string.account_already_exists, ToastCompat.LENGTH_LONG).show(); - return; - } - final Intent serviceIntent = new Intent(activity, XmppConnectionService.class); - serviceIntent.setAction(XmppConnectionService.ACTION_PROVISION_ACCOUNT); - serviceIntent.putExtra("address", jid.asBareJid().toEscapedString()); - serviceIntent.putExtra("password", accountConfiguration.password); - Compatibility.startService(activity, serviceIntent); - final Intent intent = new Intent(activity, EditAccountActivity.class); - intent.putExtra("jid", jid.asBareJid().toEscapedString()); - intent.putExtra("init", true); - activity.startActivity(intent); - } - -} diff --git a/src/free/java/eu/siacs/conversations/services/EmojiInitializationService.java b/src/free/java/eu/siacs/conversations/services/EmojiInitializationService.java deleted file mode 100644 index 2618d3809..000000000 --- a/src/free/java/eu/siacs/conversations/services/EmojiInitializationService.java +++ /dev/null @@ -1,14 +0,0 @@ -package eu.siacs.conversations.services; - -import android.content.Context; - -import androidx.emoji2.bundled.BundledEmojiCompatConfig; -import androidx.emoji2.text.EmojiCompat; - -public class EmojiInitializationService { - - public static void execute(final Context context) { - EmojiCompat.init(new BundledEmojiCompatConfig(context).setReplaceAll(true)); - } - -} diff --git a/src/git/java/eu/siacs/conversations/utils/InstallReferrerUtils.java b/src/git/java/eu/siacs/conversations/utils/InstallReferrerUtils.java deleted file mode 100644 index 325caa8d0..000000000 --- a/src/git/java/eu/siacs/conversations/utils/InstallReferrerUtils.java +++ /dev/null @@ -1,13 +0,0 @@ -package eu.siacs.conversations.utils; - -import eu.siacs.conversations.ui.MagicCreateActivity; -import eu.siacs.conversations.ui.WelcomeActivity; - -public class InstallReferrerUtils { - - public InstallReferrerUtils(WelcomeActivity welcomeActivity) { - } - - public static void markInstallReferrerExecuted(MagicCreateActivity magicCreateActivity) { - } -} \ No newline at end of file diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml deleted file mode 100644 index 906e73bfa..000000000 --- a/src/main/AndroidManifest.xml +++ /dev/null @@ -1,522 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/assets/animate.min.css b/src/main/assets/animate.min.css deleted file mode 100644 index b6f612953..000000000 --- a/src/main/assets/animate.min.css +++ /dev/null @@ -1,11 +0,0 @@ -@charset "UTF-8"; - -/*! - * animate.css -http://daneden.me/animate - * Version - 3.5.1 - * Licensed under the MIT license - http://opensource.org/licenses/MIT - * - * Copyright (c) 2016 Daniel Eden - */ - -.animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.animated.infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animated.hinge{-webkit-animation-duration:2s;animation-duration:2s}.animated.bounceIn,.animated.bounceOut,.animated.flipOutX,.animated.flipOutY{-webkit-animation-duration:.75s;animation-duration:.75s}@-webkit-keyframes bounce{0%,20%,53%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}40%,43%,70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}70%{-webkit-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}@keyframes bounce{0%,20%,53%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}40%,43%,70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}70%{-webkit-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}.bounce{-webkit-animation-name:bounce;animation-name:bounce;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}.flash{-webkit-animation-name:flash;animation-name:flash}@-webkit-keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.pulse{-webkit-animation-name:pulse;animation-name:pulse}@-webkit-keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.rubberBand{-webkit-animation-name:rubberBand;animation-name:rubberBand}@-webkit-keyframes shake{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}@keyframes shake{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}.shake{-webkit-animation-name:shake;animation-name:shake}@-webkit-keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}.headShake{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-name:headShake;animation-name:headShake}@-webkit-keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}.swing{-webkit-transform-origin:top center;transform-origin:top center;-webkit-animation-name:swing;animation-name:swing}@-webkit-keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.tada{-webkit-animation-name:tada;animation-name:tada}@-webkit-keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:none;transform:none}}@keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:none;transform:none}}.wobble{-webkit-animation-name:wobble;animation-name:wobble}@-webkit-keyframes jello{0%,11.1%,to{-webkit-transform:none;transform:none}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}@keyframes jello{0%,11.1%,to{-webkit-transform:none;transform:none}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}.jello{-webkit-animation-name:jello;animation-name:jello;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}.bounceIn{-webkit-animation-name:bounceIn;animation-name:bounceIn}@-webkit-keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}to{-webkit-transform:none;transform:none}}.bounceInDown{-webkit-animation-name:bounceInDown;animation-name:bounceInDown}@-webkit-keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}to{-webkit-transform:none;transform:none}}.bounceInLeft{-webkit-animation-name:bounceInLeft;animation-name:bounceInLeft}@-webkit-keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:none;transform:none}}.bounceInRight{-webkit-animation-name:bounceInRight;animation-name:bounceInRight}@-webkit-keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.bounceInUp{-webkit-animation-name:bounceInUp;animation-name:bounceInUp}@-webkit-keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}.bounceOut{-webkit-animation-name:bounceOut;animation-name:bounceOut}@-webkit-keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.bounceOutDown{-webkit-animation-name:bounceOutDown;animation-name:bounceOutDown}@-webkit-keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.bounceOutLeft{-webkit-animation-name:bounceOutLeft;animation-name:bounceOutLeft}@-webkit-keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.bounceOutRight{-webkit-animation-name:bounceOutRight;animation-name:bounceOutRight}@-webkit-keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.bounceOutUp{-webkit-animation-name:bounceOutUp;animation-name:bounceOutUp}@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInDown{-webkit-animation-name:fadeInDown;animation-name:fadeInDown}@-webkit-keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInDownBig{-webkit-animation-name:fadeInDownBig;animation-name:fadeInDownBig}@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInLeft{-webkit-animation-name:fadeInLeft;animation-name:fadeInLeft}@-webkit-keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInLeftBig{-webkit-animation-name:fadeInLeftBig;animation-name:fadeInLeftBig}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInRight{-webkit-animation-name:fadeInRight;animation-name:fadeInRight}@-webkit-keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInRightBig{-webkit-animation-name:fadeInRightBig;animation-name:fadeInRightBig}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInUp{-webkit-animation-name:fadeInUp;animation-name:fadeInUp}@-webkit-keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInUpBig{-webkit-animation-name:fadeInUpBig;animation-name:fadeInUpBig}@-webkit-keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.fadeOutDown{-webkit-animation-name:fadeOutDown;animation-name:fadeOutDown}@-webkit-keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.fadeOutDownBig{-webkit-animation-name:fadeOutDownBig;animation-name:fadeOutDownBig}@-webkit-keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.fadeOutLeft{-webkit-animation-name:fadeOutLeft;animation-name:fadeOutLeft}@-webkit-keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.fadeOutLeftBig{-webkit-animation-name:fadeOutLeftBig;animation-name:fadeOutLeftBig}@-webkit-keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.fadeOutRight{-webkit-animation-name:fadeOutRight;animation-name:fadeOutRight}@-webkit-keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.fadeOutRightBig{-webkit-animation-name:fadeOutRightBig;animation-name:fadeOutRightBig}@-webkit-keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.fadeOutUp{-webkit-animation-name:fadeOutUp;animation-name:fadeOutUp}@-webkit-keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.fadeOutUpBig{-webkit-animation-name:fadeOutUpBig;animation-name:fadeOutUpBig}@-webkit-keyframes flip{0%{-webkit-transform:perspective(400px) rotateY(-1turn);transform:perspective(400px) rotateY(-1turn)}0%,40%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-190deg);transform:perspective(400px) translateZ(150px) rotateY(-190deg)}50%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-170deg);transform:perspective(400px) translateZ(150px) rotateY(-170deg)}50%,80%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95)}to{-webkit-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}@keyframes flip{0%{-webkit-transform:perspective(400px) rotateY(-1turn);transform:perspective(400px) rotateY(-1turn)}0%,40%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-190deg);transform:perspective(400px) translateZ(150px) rotateY(-190deg)}50%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-170deg);transform:perspective(400px) translateZ(150px) rotateY(-170deg)}50%,80%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95)}to{-webkit-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}.animated.flip{-webkit-backface-visibility:visible;backface-visibility:visible;-webkit-animation-name:flip;animation-name:flip}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg)}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg)}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.flipInX{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInX;animation-name:flipInX}@-webkit-keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg)}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg)}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.flipInY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInY;animation-name:flipInY}@-webkit-keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}@keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}.flipOutX{-webkit-animation-name:flipOutX;animation-name:flipOutX;-webkit-backface-visibility:visible!important;backface-visibility:visible!important}@-webkit-keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}@keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}.flipOutY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipOutY;animation-name:flipOutY}@-webkit-keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg)}60%,80%{opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:none;transform:none;opacity:1}}@keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg)}60%,80%{opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:none;transform:none;opacity:1}}.lightSpeedIn{-webkit-animation-name:lightSpeedIn;animation-name:lightSpeedIn;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedOut{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}@keyframes lightSpeedOut{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}.lightSpeedOut{-webkit-animation-name:lightSpeedOut;animation-name:lightSpeedOut;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes rotateIn{0%{transform-origin:center;-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateIn{0%{transform-origin:center;-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:none;transform:none;opacity:1}}.rotateIn{-webkit-animation-name:rotateIn;animation-name:rotateIn}@-webkit-keyframes rotateInDownLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInDownLeft{-webkit-animation-name:rotateInDownLeft;animation-name:rotateInDownLeft}@-webkit-keyframes rotateInDownRight{0%{transform-origin:right bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownRight{0%{transform-origin:right bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInDownRight{-webkit-animation-name:rotateInDownRight;animation-name:rotateInDownRight}@-webkit-keyframes rotateInUpLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInUpLeft{-webkit-animation-name:rotateInUpLeft;animation-name:rotateInUpLeft}@-webkit-keyframes rotateInUpRight{0%{transform-origin:right bottom;-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpRight{0%{transform-origin:right bottom;-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInUpRight{-webkit-animation-name:rotateInUpRight;animation-name:rotateInUpRight}@-webkit-keyframes rotateOut{0%{transform-origin:center;opacity:1}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}@keyframes rotateOut{0%{transform-origin:center;opacity:1}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}.rotateOut{-webkit-animation-name:rotateOut;animation-name:rotateOut}@-webkit-keyframes rotateOutDownLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}@keyframes rotateOutDownLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}.rotateOutDownLeft{-webkit-animation-name:rotateOutDownLeft;animation-name:rotateOutDownLeft}@-webkit-keyframes rotateOutDownRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutDownRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.rotateOutDownRight{-webkit-animation-name:rotateOutDownRight;animation-name:rotateOutDownRight}@-webkit-keyframes rotateOutUpLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutUpLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.rotateOutUpLeft{-webkit-animation-name:rotateOutUpLeft;animation-name:rotateOutUpLeft}@-webkit-keyframes rotateOutUpRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}@keyframes rotateOutUpRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}.rotateOutUpRight{-webkit-animation-name:rotateOutUpRight;animation-name:rotateOutUpRight}@-webkit-keyframes hinge{0%{transform-origin:top left}0%,20%,60%{-webkit-transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);transform-origin:top left}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}@keyframes hinge{0%{transform-origin:top left}0%,20%,60%{-webkit-transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);transform-origin:top left}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}.hinge{-webkit-animation-name:hinge;animation-name:hinge}@-webkit-keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:none;transform:none}}.rollIn{-webkit-animation-name:rollIn;animation-name:rollIn}@-webkit-keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}@keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}.rollOut{-webkit-animation-name:rollOut;animation-name:rollOut}@-webkit-keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}@keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}.zoomIn{-webkit-animation-name:zoomIn;animation-name:zoomIn}@-webkit-keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInDown{-webkit-animation-name:zoomInDown;animation-name:zoomInDown}@-webkit-keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInLeft{-webkit-animation-name:zoomInLeft;animation-name:zoomInLeft}@-webkit-keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInRight{-webkit-animation-name:zoomInRight;animation-name:zoomInRight}@-webkit-keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInUp{-webkit-animation-name:zoomInUp;animation-name:zoomInUp}@-webkit-keyframes zoomOut{0%{opacity:1}50%{-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%,to{opacity:0}}@keyframes zoomOut{0%{opacity:1}50%{-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%,to{opacity:0}}.zoomOut{-webkit-animation-name:zoomOut;animation-name:zoomOut}@-webkit-keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomOutDown{-webkit-animation-name:zoomOutDown;animation-name:zoomOutDown}@-webkit-keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;transform-origin:left center}}@keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;transform-origin:left center}}.zoomOutLeft{-webkit-animation-name:zoomOutLeft;animation-name:zoomOutLeft}@-webkit-keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;transform-origin:right center}}@keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;transform-origin:right center}}.zoomOutRight{-webkit-animation-name:zoomOutRight;animation-name:zoomOutRight}@-webkit-keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomOutUp{-webkit-animation-name:zoomOutUp;animation-name:zoomOutUp}@-webkit-keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInDown{-webkit-animation-name:slideInDown;animation-name:slideInDown}@-webkit-keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInLeft{-webkit-animation-name:slideInLeft;animation-name:slideInLeft}@-webkit-keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInRight{-webkit-animation-name:slideInRight;animation-name:slideInRight}@-webkit-keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInUp{-webkit-animation-name:slideInUp;animation-name:slideInUp}@-webkit-keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.slideOutDown{-webkit-animation-name:slideOutDown;animation-name:slideOutDown}@-webkit-keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.slideOutLeft{-webkit-animation-name:slideOutLeft;animation-name:slideOutLeft}@-webkit-keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.slideOutRight{-webkit-animation-name:slideOutRight;animation-name:slideOutRight}@-webkit-keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.slideOutUp{-webkit-animation-name:slideOutUp;animation-name:slideOutUp} \ No newline at end of file diff --git a/src/main/assets/font-awesome.min.css b/src/main/assets/font-awesome.min.css deleted file mode 100644 index 9b27f8ea8..000000000 --- a/src/main/assets/font-awesome.min.css +++ /dev/null @@ -1,4 +0,0 @@ -/*! - * Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.6.3');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.6.3') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.6.3') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.6.3') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.6.3') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.6.3#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/src/main/assets/fonts/weather.ttf b/src/main/assets/fonts/weather.ttf deleted file mode 100644 index 948f0a5d2..000000000 Binary files a/src/main/assets/fonts/weather.ttf and /dev/null differ diff --git a/src/main/assets/images/layers-2x.png b/src/main/assets/images/layers-2x.png deleted file mode 100644 index 200c333dc..000000000 Binary files a/src/main/assets/images/layers-2x.png and /dev/null differ diff --git a/src/main/assets/images/layers.png b/src/main/assets/images/layers.png deleted file mode 100644 index 1a72e5784..000000000 Binary files a/src/main/assets/images/layers.png and /dev/null differ diff --git a/src/main/assets/images/marker-icon-2x.png b/src/main/assets/images/marker-icon-2x.png deleted file mode 100644 index 88f9e5018..000000000 Binary files a/src/main/assets/images/marker-icon-2x.png and /dev/null differ diff --git a/src/main/assets/images/marker-icon.png b/src/main/assets/images/marker-icon.png deleted file mode 100644 index 950edf246..000000000 Binary files a/src/main/assets/images/marker-icon.png and /dev/null differ diff --git a/src/main/assets/images/marker-shadow.png b/src/main/assets/images/marker-shadow.png deleted file mode 100644 index 9fd297953..000000000 Binary files a/src/main/assets/images/marker-shadow.png and /dev/null differ diff --git a/src/main/assets/jquery.min.js b/src/main/assets/jquery.min.js deleted file mode 100644 index a1c07fd80..000000000 --- a/src/main/assets/jquery.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],E=C.document,r=Object.getPrototypeOf,s=t.slice,g=t.concat,u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.4.1",k=function(e,t){return new k.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function d(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp($),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&((e?e.ownerDocument||e:m)!==C&&T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!A[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&U.test(t)){(s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=k),o=(l=h(t)).length;while(o--)l[o]="#"+s+" "+xe(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){A(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[k]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),m!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",$)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===m&&y(m,e)?-1:t===C||t.ownerDocument===m&&y(m,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),d.matchesSelector&&E&&!A[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){A(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=p[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&p(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?k.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?k.grep(e,function(e){return e===n!==r}):"string"!=typeof n?k.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:L.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),D.test(r[1])&&k.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this)}).prototype=k.fn,q=k(E);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?k.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;nx",y.noCloneChecked=!!me.cloneNode(!0).lastChild.defaultValue;var Te=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ee=/^([^.]*)(?:\.(.+)|)/;function ke(){return!0}function Se(){return!1}function Ne(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ae(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ae(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Se;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return k().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=k.guid++)),e.each(function(){k.event.add(this,t,i,r,n)})}function De(e,i,o){o?(Q.set(e,i,!1),k.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Q.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(k.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Q.set(this,i,r),t=o(this,i),this[i](),r!==(n=Q.get(this,i))||t?Q.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(Q.set(this,i,{value:k.event.trigger(k.extend(r[0],k.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Q.get(e,i)&&k.event.add(e,i,ke)}k.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.get(t);if(v){n.handler&&(n=(o=n).handler,i=o.selector),i&&k.find.matchesSelector(ie,i),n.guid||(n.guid=k.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof k&&k.event.triggered!==e.type?k.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(R)||[""]).length;while(l--)d=g=(s=Ee.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=k.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=k.event.special[d]||{},c=k.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&k.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),k.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.hasData(e)&&Q.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(R)||[""]).length;while(l--)if(d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=k.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||k.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)k.event.remove(e,d+t[l],n,r,!0);k.isEmptyObject(u)&&Q.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=k.event.fix(e),u=new Array(arguments.length),l=(Q.get(this,"events")||{})[s.type]||[],c=k.event.special[s.type]||{};for(u[0]=s,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,qe=/\s*$/g;function Oe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&k(e).children("tbody")[0]||e}function Pe(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Re(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Me(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(Q.hasData(e)&&(o=Q.access(e),a=Q.set(t,o),l=o.events))for(i in delete a.handle,a.events={},l)for(n=0,r=l[i].length;n")},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=oe(e);if(!(y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||k.isXMLDoc(e)))for(a=ve(c),r=0,i=(o=ve(e)).length;r").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Vt,Gt=[],Yt=/(=)\?(?=&|$)|\?\?/;k.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Gt.pop()||k.expando+"_"+kt++;return this[e]=!0,e}}),k.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Yt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Yt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Yt,"$1"+r):!1!==e.jsonp&&(e.url+=(St.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||k.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?k(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Gt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Vt=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Vt.childNodes.length),k.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=D.exec(e))?[t.createElement(i[1])]:(i=we([e],t,o),o&&o.length&&k(o).remove(),k.merge([],i.childNodes)));var r,i,o},k.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(k.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},k.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){k.fn[t]=function(e){return this.on(t,e)}}),k.expr.pseudos.animated=function(t){return k.grep(k.timers,function(e){return t===e.elem}).length},k.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=k.css(e,"position"),c=k(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=k.css(e,"top"),u=k.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,k.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},k.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){k.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===k.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===k.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=k(e).offset()).top+=k.css(e,"borderTopWidth",!0),i.left+=k.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-k.css(r,"marginTop",!0),left:t.left-i.left-k.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===k.css(e,"position"))e=e.offsetParent;return e||ie})}}),k.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;k.fn[t]=function(e){return _(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),k.each(["top","left"],function(e,n){k.cssHooks[n]=ze(y.pixelPosition,function(e,t){if(t)return t=_e(e,n),$e.test(t)?k(e).position()[n]+"px":t})}),k.each({Height:"height",Width:"width"},function(a,s){k.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){k.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return _(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?k.css(e,t,i):k.style(e,t,n,i)},s,n?e:void 0,n)}})}),k.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){k.fn[n]=function(e,t){return 0 svg, -.leaflet-pane > canvas, -.leaflet-zoom-box, -.leaflet-image-layer, -.leaflet-layer { - position: absolute; - left: 0; - top: 0; - } -.leaflet-container { - overflow: hidden; - } -.leaflet-tile, -.leaflet-marker-icon, -.leaflet-marker-shadow { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - -webkit-user-drag: none; - } -/* Prevents IE11 from highlighting tiles in blue */ -.leaflet-tile::selection { - background: transparent; -} -/* Safari renders non-retina tile on retina better with this, but Chrome is worse */ -.leaflet-safari .leaflet-tile { - image-rendering: -webkit-optimize-contrast; - } -/* hack that prevents hw layers "stretching" when loading new tiles */ -.leaflet-safari .leaflet-tile-container { - width: 1600px; - height: 1600px; - -webkit-transform-origin: 0 0; - } -.leaflet-marker-icon, -.leaflet-marker-shadow { - display: block; - } -/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ -/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ -.leaflet-container .leaflet-overlay-pane svg, -.leaflet-container .leaflet-marker-pane img, -.leaflet-container .leaflet-shadow-pane img, -.leaflet-container .leaflet-tile-pane img, -.leaflet-container img.leaflet-image-layer, -.leaflet-container .leaflet-tile { - max-width: none !important; - max-height: none !important; - } - -.leaflet-container.leaflet-touch-zoom { - -ms-touch-action: pan-x pan-y; - touch-action: pan-x pan-y; - } -.leaflet-container.leaflet-touch-drag { - -ms-touch-action: pinch-zoom; - /* Fallback for FF which doesn't support pinch-zoom */ - touch-action: none; - touch-action: pinch-zoom; -} -.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { - -ms-touch-action: none; - touch-action: none; -} -.leaflet-container { - -webkit-tap-highlight-color: transparent; -} -.leaflet-container a { - -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); -} -.leaflet-tile { - filter: inherit; - visibility: hidden; - } -.leaflet-tile-loaded { - visibility: inherit; - } -.leaflet-zoom-box { - width: 0; - height: 0; - -moz-box-sizing: border-box; - box-sizing: border-box; - z-index: 800; - } -/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ -.leaflet-overlay-pane svg { - -moz-user-select: none; - } - -.leaflet-pane { z-index: 400; } - -.leaflet-tile-pane { z-index: 200; } -.leaflet-overlay-pane { z-index: 400; } -.leaflet-shadow-pane { z-index: 500; } -.leaflet-marker-pane { z-index: 600; } -.leaflet-tooltip-pane { z-index: 650; } -.leaflet-popup-pane { z-index: 700; } - -.leaflet-map-pane canvas { z-index: 100; } -.leaflet-map-pane svg { z-index: 200; } - -.leaflet-vml-shape { - width: 1px; - height: 1px; - } -.lvml { - behavior: url(#default#VML); - display: inline-block; - position: absolute; - } - - -/* control positioning */ - -.leaflet-control { - position: relative; - z-index: 800; - pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ - pointer-events: auto; - } -.leaflet-top, -.leaflet-bottom { - position: absolute; - z-index: 1000; - pointer-events: none; - } -.leaflet-top { - top: 0; - } -.leaflet-right { - right: 0; - } -.leaflet-bottom { - bottom: 0; - } -.leaflet-left { - left: 0; - } -.leaflet-control { - float: left; - clear: both; - } -.leaflet-right .leaflet-control { - float: right; - } -.leaflet-top .leaflet-control { - margin-top: 10px; - } -.leaflet-bottom .leaflet-control { - margin-bottom: 10px; - } -.leaflet-left .leaflet-control { - margin-left: 10px; - } -.leaflet-right .leaflet-control { - margin-right: 10px; - } - - -/* zoom and fade animations */ - -.leaflet-fade-anim .leaflet-tile { - will-change: opacity; - } -.leaflet-fade-anim .leaflet-popup { - opacity: 0; - -webkit-transition: opacity 0.2s linear; - -moz-transition: opacity 0.2s linear; - transition: opacity 0.2s linear; - } -.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { - opacity: 1; - } -.leaflet-zoom-animated { - -webkit-transform-origin: 0 0; - -ms-transform-origin: 0 0; - transform-origin: 0 0; - } -.leaflet-zoom-anim .leaflet-zoom-animated { - will-change: transform; - } -.leaflet-zoom-anim .leaflet-zoom-animated { - -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); - -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); - transition: transform 0.25s cubic-bezier(0,0,0.25,1); - } -.leaflet-zoom-anim .leaflet-tile, -.leaflet-pan-anim .leaflet-tile { - -webkit-transition: none; - -moz-transition: none; - transition: none; - } - -.leaflet-zoom-anim .leaflet-zoom-hide { - visibility: hidden; - } - - -/* cursors */ - -.leaflet-interactive { - cursor: pointer; - } -.leaflet-grab { - cursor: -webkit-grab; - cursor: -moz-grab; - cursor: grab; - } -.leaflet-crosshair, -.leaflet-crosshair .leaflet-interactive { - cursor: crosshair; - } -.leaflet-popup-pane, -.leaflet-control { - cursor: auto; - } -.leaflet-dragging .leaflet-grab, -.leaflet-dragging .leaflet-grab .leaflet-interactive, -.leaflet-dragging .leaflet-marker-draggable { - cursor: move; - cursor: -webkit-grabbing; - cursor: -moz-grabbing; - cursor: grabbing; - } - -/* marker & overlays interactivity */ -.leaflet-marker-icon, -.leaflet-marker-shadow, -.leaflet-image-layer, -.leaflet-pane > svg path, -.leaflet-tile-container { - pointer-events: none; - } - -.leaflet-marker-icon.leaflet-interactive, -.leaflet-image-layer.leaflet-interactive, -.leaflet-pane > svg path.leaflet-interactive, -svg.leaflet-image-layer.leaflet-interactive path { - pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ - pointer-events: auto; - } - -/* visual tweaks */ - -.leaflet-container { - background: #ddd; - outline: 0; - } -.leaflet-container a { - color: #0078A8; - } -.leaflet-container a.leaflet-active { - outline: 2px solid orange; - } -.leaflet-zoom-box { - border: 2px dotted #38f; - background: rgba(255,255,255,0.5); - } - - -/* general typography */ -.leaflet-container { - font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; - } - - -/* general toolbar styles */ - -.leaflet-bar { - box-shadow: 0 1px 5px rgba(0,0,0,0.65); - border-radius: 4px; - } -.leaflet-bar a, -.leaflet-bar a:hover { - background-color: #fff; - border-bottom: 1px solid #ccc; - width: 26px; - height: 26px; - line-height: 26px; - display: block; - text-align: center; - text-decoration: none; - color: black; - } -.leaflet-bar a, -.leaflet-control-layers-toggle { - background-position: 50% 50%; - background-repeat: no-repeat; - display: block; - } -.leaflet-bar a:hover { - background-color: #f4f4f4; - } -.leaflet-bar a:first-child { - border-top-left-radius: 4px; - border-top-right-radius: 4px; - } -.leaflet-bar a:last-child { - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; - border-bottom: none; - } -.leaflet-bar a.leaflet-disabled { - cursor: default; - background-color: #f4f4f4; - color: #bbb; - } - -.leaflet-touch .leaflet-bar a { - width: 30px; - height: 30px; - line-height: 30px; - } -.leaflet-touch .leaflet-bar a:first-child { - border-top-left-radius: 2px; - border-top-right-radius: 2px; - } -.leaflet-touch .leaflet-bar a:last-child { - border-bottom-left-radius: 2px; - border-bottom-right-radius: 2px; - } - -/* zoom control */ - -.leaflet-control-zoom-in, -.leaflet-control-zoom-out { - font: bold 18px 'Lucida Console', Monaco, monospace; - text-indent: 1px; - } - -.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { - font-size: 22px; - } - - -/* layers control */ - -.leaflet-control-layers { - box-shadow: 0 1px 5px rgba(0,0,0,0.4); - background: #fff; - border-radius: 5px; - } -.leaflet-control-layers-toggle { - background-image: url(images/layers.png); - width: 36px; - height: 36px; - } -.leaflet-retina .leaflet-control-layers-toggle { - background-image: url(images/layers-2x.png); - background-size: 26px 26px; - } -.leaflet-touch .leaflet-control-layers-toggle { - width: 44px; - height: 44px; - } -.leaflet-control-layers .leaflet-control-layers-list, -.leaflet-control-layers-expanded .leaflet-control-layers-toggle { - display: none; - } -.leaflet-control-layers-expanded .leaflet-control-layers-list { - display: block; - position: relative; - } -.leaflet-control-layers-expanded { - padding: 6px 10px 6px 6px; - color: #333; - background: #fff; - } -.leaflet-control-layers-scrollbar { - overflow-y: scroll; - overflow-x: hidden; - padding-right: 5px; - } -.leaflet-control-layers-selector { - margin-top: 2px; - position: relative; - top: 1px; - } -.leaflet-control-layers label { - display: block; - } -.leaflet-control-layers-separator { - height: 0; - border-top: 1px solid #ddd; - margin: 5px -10px 5px -6px; - } - -/* Default icon URLs */ -.leaflet-default-icon-path { - background-image: url(images/marker-icon.png); - } - - -/* attribution and scale controls */ - -.leaflet-container .leaflet-control-attribution { - background: #fff; - background: rgba(255, 255, 255, 0.7); - margin: 0; - } -.leaflet-control-attribution, -.leaflet-control-scale-line { - padding: 0 5px; - color: #333; - } -.leaflet-control-attribution a { - text-decoration: none; - } -.leaflet-control-attribution a:hover { - text-decoration: underline; - } -.leaflet-container .leaflet-control-attribution, -.leaflet-container .leaflet-control-scale { - font-size: 11px; - } -.leaflet-left .leaflet-control-scale { - margin-left: 5px; - } -.leaflet-bottom .leaflet-control-scale { - margin-bottom: 5px; - } -.leaflet-control-scale-line { - border: 2px solid #777; - border-top: none; - line-height: 1.1; - padding: 2px 5px 1px; - font-size: 11px; - white-space: nowrap; - overflow: hidden; - -moz-box-sizing: border-box; - box-sizing: border-box; - - background: #fff; - background: rgba(255, 255, 255, 0.5); - } -.leaflet-control-scale-line:not(:first-child) { - border-top: 2px solid #777; - border-bottom: none; - margin-top: -2px; - } -.leaflet-control-scale-line:not(:first-child):not(:last-child) { - border-bottom: 2px solid #777; - } - -.leaflet-touch .leaflet-control-attribution, -.leaflet-touch .leaflet-control-layers, -.leaflet-touch .leaflet-bar { - box-shadow: none; - } -.leaflet-touch .leaflet-control-layers, -.leaflet-touch .leaflet-bar { - border: 2px solid rgba(0,0,0,0.2); - background-clip: padding-box; - } - - -/* popup */ - -.leaflet-popup { - position: absolute; - text-align: center; - margin-bottom: 20px; - } -.leaflet-popup-content-wrapper { - padding: 1px; - text-align: left; - border-radius: 12px; - } -.leaflet-popup-content { - margin: 13px 19px; - line-height: 1.4; - } -.leaflet-popup-content p { - margin: 18px 0; - } -.leaflet-popup-tip-container { - width: 40px; - height: 20px; - position: absolute; - left: 50%; - margin-left: -20px; - overflow: hidden; - pointer-events: none; - } -.leaflet-popup-tip { - width: 17px; - height: 17px; - padding: 1px; - - margin: -10px auto 0; - - -webkit-transform: rotate(45deg); - -moz-transform: rotate(45deg); - -ms-transform: rotate(45deg); - transform: rotate(45deg); - } -.leaflet-popup-content-wrapper, -.leaflet-popup-tip { - background: white; - color: #333; - box-shadow: 0 3px 14px rgba(0,0,0,0.4); - } -.leaflet-container a.leaflet-popup-close-button { - position: absolute; - top: 0; - right: 0; - padding: 4px 4px 0 0; - border: none; - text-align: center; - width: 18px; - height: 14px; - font: 16px/14px Tahoma, Verdana, sans-serif; - color: #c3c3c3; - text-decoration: none; - font-weight: bold; - background: transparent; - } -.leaflet-container a.leaflet-popup-close-button:hover { - color: #999; - } -.leaflet-popup-scrolled { - overflow: auto; - border-bottom: 1px solid #ddd; - border-top: 1px solid #ddd; - } - -.leaflet-oldie .leaflet-popup-content-wrapper { - zoom: 1; - } -.leaflet-oldie .leaflet-popup-tip { - width: 24px; - margin: 0 auto; - - -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; - filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); - } -.leaflet-oldie .leaflet-popup-tip-container { - margin-top: -1px; - } - -.leaflet-oldie .leaflet-control-zoom, -.leaflet-oldie .leaflet-control-layers, -.leaflet-oldie .leaflet-popup-content-wrapper, -.leaflet-oldie .leaflet-popup-tip { - border: 1px solid #999; - } - - -/* div icon */ - -.leaflet-div-icon { - background: #fff; - border: 1px solid #666; - } - - -/* Tooltip */ -/* Base styles for the element that has a tooltip */ -.leaflet-tooltip { - position: absolute; - padding: 6px; - background-color: #fff; - border: 1px solid #fff; - border-radius: 3px; - color: #222; - white-space: nowrap; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - pointer-events: none; - box-shadow: 0 1px 3px rgba(0,0,0,0.4); - } -.leaflet-tooltip.leaflet-clickable { - cursor: pointer; - pointer-events: auto; - } -.leaflet-tooltip-top:before, -.leaflet-tooltip-bottom:before, -.leaflet-tooltip-left:before, -.leaflet-tooltip-right:before { - position: absolute; - pointer-events: none; - border: 6px solid transparent; - background: transparent; - content: ""; - } - -/* Directions */ - -.leaflet-tooltip-bottom { - margin-top: 6px; -} -.leaflet-tooltip-top { - margin-top: -6px; -} -.leaflet-tooltip-bottom:before, -.leaflet-tooltip-top:before { - left: 50%; - margin-left: -6px; - } -.leaflet-tooltip-top:before { - bottom: 0; - margin-bottom: -12px; - border-top-color: #fff; - } -.leaflet-tooltip-bottom:before { - top: 0; - margin-top: -12px; - margin-left: -6px; - border-bottom-color: #fff; - } -.leaflet-tooltip-left { - margin-left: -6px; -} -.leaflet-tooltip-right { - margin-left: 6px; -} -.leaflet-tooltip-left:before, -.leaflet-tooltip-right:before { - top: 50%; - margin-top: -6px; - } -.leaflet-tooltip-left:before { - right: 0; - margin-right: -12px; - border-left-color: #fff; - } -.leaflet-tooltip-right:before { - left: 0; - margin-left: -12px; - border-right-color: #fff; - } diff --git a/src/main/assets/leaflet.js b/src/main/assets/leaflet.js deleted file mode 100644 index bc9ef0f55..000000000 --- a/src/main/assets/leaflet.js +++ /dev/null @@ -1,5 +0,0 @@ -/* @preserve - * Leaflet 1.6.0+Detached: 0c81bdf904d864fd12a286e3d1979f47aba17991.0c81bdf, a JS library for interactive maps. http://leafletjs.com - * (c) 2010-2019 Vladimir Agafonkin, (c) 2010-2011 CloudMade - */ -!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?i(exports):"function"==typeof define&&define.amd?define(["exports"],i):i(t.L={})}(this,function(t){"use strict";var i=Object.freeze;function h(t){var i,e,n,o;for(e=1,n=arguments.length;e=this.min.x&&e.x<=this.max.x&&i.y>=this.min.y&&e.y<=this.max.y},intersects:function(t){t=R(t);var i=this.min,e=this.max,n=t.min,o=t.max,s=o.x>=i.x&&n.x<=e.x,r=o.y>=i.y&&n.y<=e.y;return s&&r},overlaps:function(t){t=R(t);var i=this.min,e=this.max,n=t.min,o=t.max,s=o.x>i.x&&n.xi.y&&n.y=n.lat&&e.lat<=o.lat&&i.lng>=n.lng&&e.lng<=o.lng},intersects:function(t){t=D(t);var i=this._southWest,e=this._northEast,n=t.getSouthWest(),o=t.getNorthEast(),s=o.lat>=i.lat&&n.lat<=e.lat,r=o.lng>=i.lng&&n.lng<=e.lng;return s&&r},overlaps:function(t){t=D(t);var i=this._southWest,e=this._northEast,n=t.getSouthWest(),o=t.getNorthEast(),s=o.lat>i.lat&&n.lati.lng&&n.lng';var i=t.firstChild;return i.style.behavior="url(#default#VML)",i&&"object"==typeof i.adj}catch(t){return!1}}();function Bt(t){return 0<=navigator.userAgent.toLowerCase().indexOf(t)}var At=(Object.freeze||Object)({ie:it,ielt9:et,edge:nt,webkit:ot,android:st,android23:rt,androidStock:ht,opera:ut,chrome:lt,gecko:ct,safari:_t,phantom:dt,opera12:pt,win:mt,ie3d:ft,webkit3d:gt,gecko3d:vt,any3d:yt,mobile:xt,mobileWebkit:wt,mobileWebkit3d:Pt,msPointer:Lt,pointer:bt,touch:Tt,mobileOpera:zt,mobileGecko:Mt,retina:Ct,passiveEvents:Et,canvas:St,svg:Zt,vml:kt}),It=Lt?"MSPointerDown":"pointerdown",Ot=Lt?"MSPointerMove":"pointermove",Rt=Lt?"MSPointerUp":"pointerup",Nt=Lt?"MSPointerCancel":"pointercancel",Dt=["INPUT","SELECT","OPTION"],jt={},Wt=!1,Ht=0;function Ft(t,i,e,n){return"touchstart"===i?function(t,i,e){var n=a(function(t){if("mouse"!==t.pointerType&&t.MSPOINTER_TYPE_MOUSE&&t.pointerType!==t.MSPOINTER_TYPE_MOUSE){if(!(Dt.indexOf(t.target.tagName)<0))return;ji(t)}Gt(t,i)});t["_leaflet_touchstart"+e]=n,t.addEventListener(It,n,!1),Wt||(document.documentElement.addEventListener(It,Ut,!0),document.documentElement.addEventListener(Ot,Vt,!0),document.documentElement.addEventListener(Rt,qt,!0),document.documentElement.addEventListener(Nt,qt,!0),Wt=!0)}(t,e,n):"touchmove"===i?function(t,i,e){function n(t){(t.pointerType!==t.MSPOINTER_TYPE_MOUSE&&"mouse"!==t.pointerType||0!==t.buttons)&&Gt(t,i)}t["_leaflet_touchmove"+e]=n,t.addEventListener(Ot,n,!1)}(t,e,n):"touchend"===i&&function(t,i,e){function n(t){Gt(t,i)}t["_leaflet_touchend"+e]=n,t.addEventListener(Rt,n,!1),t.addEventListener(Nt,n,!1)}(t,e,n),this}function Ut(t){jt[t.pointerId]=t,Ht++}function Vt(t){jt[t.pointerId]&&(jt[t.pointerId]=t)}function qt(t){delete jt[t.pointerId],Ht--}function Gt(t,i){for(var e in t.touches=[],jt)t.touches.push(jt[e]);t.changedTouches=[t],i(t)}var Kt=Lt?"MSPointerDown":bt?"pointerdown":"touchstart",Yt=Lt?"MSPointerUp":bt?"pointerup":"touchend",Xt="_leaflet_";function Jt(t,o,i){var s,r,a=!1;function e(t){var i;if(bt){if(!nt||"mouse"===t.pointerType)return;i=Ht}else i=t.touches.length;if(!(1this.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,i){this._enforcingBounds=!0;var e=this.getCenter(),n=this._limitCenter(e,this._zoom,D(t));return e.equals(n)||this.panTo(n,i),this._enforcingBounds=!1,this},panInside:function(t,i){var e=I((i=i||{}).paddingTopLeft||i.padding||[0,0]),n=I(i.paddingBottomRight||i.padding||[0,0]),o=this.getCenter(),s=this.project(o),r=this.project(t),a=this.getPixelBounds(),h=a.getSize().divideBy(2),u=R([a.min.add(e),a.max.subtract(n)]);if(!u.contains(r)){this._enforcingBounds=!0;var l=s.subtract(r),c=I(r.x+l.x,r.y+l.y);(r.xu.max.x)&&(c.x=s.x-l.x,0u.max.y)&&(c.y=s.y-l.y,0=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,i){for(var e,n=[],o="mouseout"===i||"mouseover"===i,s=t.target||t.srcElement,r=!1;s;){if((e=this._targets[u(s)])&&("click"===i||"preclick"===i)&&!t._simulated&&this._draggableMoved(e)){r=!0;break}if(e&&e.listens(i,!0)){if(o&&!Yi(s,t))break;if(n.push(e),o)break}if(s===this._container)break;s=s.parentNode}return n.length||r||o||!Yi(s,t)||(n=[this]),n},_handleDOMEvent:function(t){if(this._loaded&&!Ki(t)){var i=t.type;"mousedown"!==i&&"keypress"!==i&&"keyup"!==i&&"keydown"!==i||Mi(t.target||t.srcElement),this._fireDOMEvent(t,i)}},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,i,e){if("click"===t.type){var n=h({},t);n.type="preclick",this._fireDOMEvent(n,n.type,e)}if(!t._stopped&&(e=(e||[]).concat(this._findEventTargets(t,i))).length){var o=e[0];"contextmenu"===i&&o.listens(i,!0)&&ji(t);var s={originalEvent:t};if("keypress"!==t.type&&"keydown"!==t.type&&"keyup"!==t.type){var r=o.getLatLng&&(!o._radius||o._radius<=10);s.containerPoint=r?this.latLngToContainerPoint(o.getLatLng()):this.mouseEventToContainerPoint(t),s.layerPoint=this.containerPointToLayerPoint(s.containerPoint),s.latlng=r?o.getLatLng():this.layerPointToLatLng(s.layerPoint)}for(var a=0;athis.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(i),o=this._getCenterOffset(t)._divideBy(1-1/n);return!(!0!==e.animate&&!this.getSize().contains(o))&&(M(function(){this._moveStart(!0,!1)._animateZoom(t,i,!0)},this),!0)},_animateZoom:function(t,i,e,n){this._mapPane&&(e&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=i,mi(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:i,noUpdate:n}),setTimeout(a(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&fi(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom),M(function(){this._moveEnd(!0)},this))}});function Qi(t){return new te(t)}var te=S.extend({options:{position:"topright"},initialize:function(t){p(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var i=this._map;return i&&i.removeControl(this),this.options.position=t,i&&i.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var i=this._container=this.onAdd(t),e=this.getPosition(),n=t._controlCorners[e];return mi(i,"leaflet-control"),-1!==e.indexOf("bottom")?n.insertBefore(i,n.firstChild):n.appendChild(i),this._map.on("unload",this.remove,this),this},remove:function(){return this._map&&(li(this._container),this.onRemove&&this.onRemove(this._map),this._map.off("unload",this.remove,this),this._map=null),this},_refocusOnMap:function(t){this._map&&t&&0",n=document.createElement("div");return n.innerHTML=e,n.firstChild},_addItem:function(t){var i,e=document.createElement("label"),n=this._map.hasLayer(t.layer);t.overlay?((i=document.createElement("input")).type="checkbox",i.className="leaflet-control-layers-selector",i.defaultChecked=n):i=this._createRadioElement("leaflet-base-layers_"+u(this),n),this._layerControlInputs.push(i),i.layerId=u(t.layer),ki(i,"click",this._onInputClick,this);var o=document.createElement("span");o.innerHTML=" "+t.name;var s=document.createElement("div");return e.appendChild(s),s.appendChild(i),s.appendChild(o),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(e),this._checkDisabledLayers(),e},_onInputClick:function(){var t,i,e=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=e.length-1;0<=s;s--)t=e[s],i=this._getLayer(t.layerId).layer,t.checked?n.push(i):t.checked||o.push(i);for(s=0;si.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expand:function(){return this.expand()},_collapse:function(){return this.collapse()}}),ee=te.extend({options:{position:"topleft",zoomInText:"+",zoomInTitle:"Zoom in",zoomOutText:"−",zoomOutTitle:"Zoom out"},onAdd:function(t){var i="leaflet-control-zoom",e=ui("div",i+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,i+"-in",e,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,i+"-out",e,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),e},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,i,e,n,o){var s=ui("a",e,n);return s.innerHTML=t,s.href="#",s.title=i,s.setAttribute("role","button"),s.setAttribute("aria-label",i),Di(s),ki(s,"click",Wi),ki(s,"click",o,this),ki(s,"click",this._refocusOnMap,this),s},_updateDisabled:function(){var t=this._map,i="leaflet-disabled";fi(this._zoomInButton,i),fi(this._zoomOutButton,i),!this._disabled&&t._zoom!==t.getMinZoom()||mi(this._zoomOutButton,i),!this._disabled&&t._zoom!==t.getMaxZoom()||mi(this._zoomInButton,i)}});$i.mergeOptions({zoomControl:!0}),$i.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new ee,this.addControl(this.zoomControl))});var ne=te.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var i="leaflet-control-scale",e=ui("div",i),n=this.options;return this._addScales(n,i+"-line",e),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),e},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,i,e){t.metric&&(this._mScale=ui("div",i,e)),t.imperial&&(this._iScale=ui("div",i,e))},_update:function(){var t=this._map,i=t.getSize().y/2,e=t.distance(t.containerPointToLatLng([0,i]),t.containerPointToLatLng([this.options.maxWidth,i]));this._updateScales(e)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var i=this._getRoundNum(t),e=i<1e3?i+" m":i/1e3+" km";this._updateScale(this._mScale,e,i/t)},_updateImperial:function(t){var i,e,n,o=3.2808399*t;5280Leaflet'},initialize:function(t){p(this,t),this._attributions={}},onAdd:function(t){for(var i in(t.attributionControl=this)._container=ui("div","leaflet-control-attribution"),Di(this._container),t._layers)t._layers[i].getAttribution&&this.addAttribution(t._layers[i].getAttribution());return this._update(),this._container},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t&&(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update()),this},removeAttribution:function(t){return t&&this._attributions[t]&&(this._attributions[t]--,this._update()),this},_update:function(){if(this._map){var t=[];for(var i in this._attributions)this._attributions[i]&&t.push(i);var e=[];this.options.prefix&&e.push(this.options.prefix),t.length&&e.push(t.join(", ")),this._container.innerHTML=e.join(" | ")}}});$i.mergeOptions({attributionControl:!0}),$i.addInitHook(function(){this.options.attributionControl&&(new oe).addTo(this)});te.Layers=ie,te.Zoom=ee,te.Scale=ne,te.Attribution=oe,Qi.layers=function(t,i,e){return new ie(t,i,e)},Qi.zoom=function(t){return new ee(t)},Qi.scale=function(t){return new ne(t)},Qi.attribution=function(t){return new oe(t)};var se=S.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled||(this._enabled=!0,this.addHooks()),this},disable:function(){return this._enabled&&(this._enabled=!1,this.removeHooks()),this},enabled:function(){return!!this._enabled}});se.addTo=function(t,i){return t.addHandler(i,this),this};var re,ae={Events:Z},he=Tt?"touchstart mousedown":"mousedown",ue={mousedown:"mouseup",touchstart:"touchend",pointerdown:"touchend",MSPointerDown:"touchend"},le={mousedown:"mousemove",touchstart:"touchmove",pointerdown:"touchmove",MSPointerDown:"touchmove"},ce=k.extend({options:{clickTolerance:3},initialize:function(t,i,e,n){p(this,n),this._element=t,this._dragStartTarget=i||t,this._preventOutline=e},enable:function(){this._enabled||(ki(this._dragStartTarget,he,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(ce._dragging===this&&this.finishDrag(),Ai(this._dragStartTarget,he,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){if(!t._simulated&&this._enabled&&(this._moved=!1,!pi(this._element,"leaflet-zoom-anim")&&!(ce._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||((ce._dragging=this)._preventOutline&&Mi(this._element),Ti(),Qt(),this._moving)))){this.fire("down");var i=t.touches?t.touches[0]:t,e=Ei(this._element);this._startPoint=new B(i.clientX,i.clientY),this._parentScale=Si(e),ki(document,le[t.type],this._onMove,this),ki(document,ue[t.type],this._onUp,this)}},_onMove:function(t){if(!t._simulated&&this._enabled)if(t.touches&&1i.max.x&&(e|=2),t.yi.max.y&&(e|=8),e}function ge(t,i,e,n){var o,s=i.x,r=i.y,a=e.x-s,h=e.y-r,u=a*a+h*h;return 0this._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()t.y!=n.y>t.y&&t.x<(n.x-e.x)*(t.y-e.y)/(n.y-e.y)+e.x&&(u=!u);return u||je.prototype._containsPoint.call(this,t,!0)}});var He=ke.extend({initialize:function(t,i){p(this,i),this._layers={},t&&this.addData(t)},addData:function(t){var i,e,n,o=v(t)?t:t.features;if(o){for(i=0,e=o.length;iu.x&&(l=s.x+n-u.x+h.x),s.x-l-a.x<0&&(l=s.x-a.x),s.y+e+h.y>u.y&&(c=s.y+e-u.y+h.y),s.y-c-a.y<0&&(c=s.y-a.y),(l||c)&&t.fire("autopanstart").panBy([l,c])}},_onCloseButtonClick:function(t){this._close(),Wi(t)},_getAnchor:function(){return I(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}});$i.mergeOptions({closePopupOnClick:!0}),$i.include({openPopup:function(t,i,e){return t instanceof sn||(t=new sn(e).setContent(t)),i&&t.setLatLng(i),this.hasLayer(t)?this:(this._popup&&this._popup.options.autoClose&&this.closePopup(),this._popup=t,this.addLayer(t))},closePopup:function(t){return t&&t!==this._popup||(t=this._popup,this._popup=null),t&&this.removeLayer(t),this}}),Se.include({bindPopup:function(t,i){return t instanceof sn?(p(t,i),(this._popup=t)._source=this):(this._popup&&!i||(this._popup=new sn(i,this)),this._popup.setContent(t)),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t,i){return this._popup&&this._map&&(i=this._popup._prepareOpen(this,t,i),this._map.openPopup(this._popup,i)),this},closePopup:function(){return this._popup&&this._popup._close(),this},togglePopup:function(t){return this._popup&&(this._popup._map?this.closePopup():this.openPopup(t)),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var i=t.layer||t.target;this._popup&&this._map&&(Wi(t),i instanceof Re?this.openPopup(t.layer||t.target,t.latlng):this._map.hasLayer(this._popup)&&this._popup._source===i?this.closePopup():this.openPopup(i,t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}});var rn=on.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,interactive:!1,opacity:.9},onAdd:function(t){on.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&this._source.fire("tooltipopen",{tooltip:this},!0)},onRemove:function(t){on.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&this._source.fire("tooltipclose",{tooltip:this},!0)},getEvents:function(){var t=on.prototype.getEvents.call(this);return Tt&&!this.options.permanent&&(t.preclick=this._close),t},_close:function(){this._map&&this._map.closeTooltip(this)},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=ui("div",t)},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var i=this._map,e=this._container,n=i.latLngToContainerPoint(i.getCenter()),o=i.layerPointToContainerPoint(t),s=this.options.direction,r=e.offsetWidth,a=e.offsetHeight,h=I(this.options.offset),u=this._getAnchor();t="top"===s?t.add(I(-r/2+h.x,-a+h.y+u.y,!0)):"bottom"===s?t.subtract(I(r/2-h.x,-h.y,!0)):"center"===s?t.subtract(I(r/2+h.x,a/2-u.y+h.y,!0)):"right"===s||"auto"===s&&o.xthis.options.maxZoom||ethis.options.maxZoom||void 0!==this.options.minZoom&&oe.max.x)||!i.wrapLat&&(t.ye.max.y))return!1}if(!this.options.bounds)return!0;var n=this._tileCoordsToBounds(t);return D(this.options.bounds).overlaps(n)},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var i=this._map,e=this.getTileSize(),n=t.scaleBy(e),o=n.add(e);return[i.unproject(n,t.z),i.unproject(o,t.z)]},_tileCoordsToBounds:function(t){var i=this._tileCoordsToNwSe(t),e=new N(i[0],i[1]);return this.options.noWrap||(e=this._map.wrapLatLngBounds(e)),e},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var i=t.split(":"),e=new B(+i[0],+i[1]);return e.z=+i[2],e},_removeTile:function(t){var i=this._tiles[t];i&&(li(i.el),delete this._tiles[t],this.fire("tileunload",{tile:i.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){mi(t,"leaflet-tile");var i=this.getTileSize();t.style.width=i.x+"px",t.style.height=i.y+"px",t.onselectstart=l,t.onmousemove=l,et&&this.options.opacity<1&&yi(t,this.options.opacity),st&&!rt&&(t.style.WebkitBackfaceVisibility="hidden")},_addTile:function(t,i){var e=this._getTilePos(t),n=this._tileCoordsToKey(t),o=this.createTile(this._wrapCoords(t),a(this._tileReady,this,t));this._initTile(o),this.createTile.length<2&&M(a(this._tileReady,this,t,null,o)),Pi(o,e),this._tiles[n]={el:o,coords:t,current:!0},i.appendChild(o),this.fire("tileloadstart",{tile:o,coords:t})},_tileReady:function(t,i,e){i&&this.fire("tileerror",{error:i,tile:e,coords:t});var n=this._tileCoordsToKey(t);(e=this._tiles[n])&&(e.loaded=+new Date,this._map._fadeAnimated?(yi(e.el,0),C(this._fadeFrame),this._fadeFrame=M(this._updateOpacity,this)):(e.active=!0,this._pruneTiles()),i||(mi(e.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:e.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),et||!this._map._fadeAnimated?M(this._pruneTiles,this):setTimeout(a(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var i=new B(this._wrapX?r(t.x,this._wrapX):t.x,this._wrapY?r(t.y,this._wrapY):t.y);return i.z=t.z,i},_pxBoundsToTileRange:function(t){var i=this.getTileSize();return new O(t.min.unscaleBy(i).floor(),t.max.unscaleBy(i).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}});var un=hn.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1},initialize:function(t,i){this._url=t,(i=p(this,i)).detectRetina&&Ct&&0')}}catch(t){return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}}(),fn={_initContainer:function(){this._container=ui("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(_n.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var i=t._container=mn("shape");mi(i,"leaflet-vml-shape "+(this.options.className||"")),i.coordsize="1 1",t._path=mn("path"),i.appendChild(t._path),this._updateStyle(t),this._layers[u(t)]=t},_addPath:function(t){var i=t._container;this._container.appendChild(i),t.options.interactive&&t.addInteractiveTarget(i)},_removePath:function(t){var i=t._container;li(i),t.removeInteractiveTarget(i),delete this._layers[u(t)]},_updateStyle:function(t){var i=t._stroke,e=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(i||(i=t._stroke=mn("stroke")),o.appendChild(i),i.weight=n.weight+"px",i.color=n.color,i.opacity=n.opacity,n.dashArray?i.dashStyle=v(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):i.dashStyle="",i.endcap=n.lineCap.replace("butt","flat"),i.joinstyle=n.lineJoin):i&&(o.removeChild(i),t._stroke=null),n.fill?(e||(e=t._fill=mn("fill")),o.appendChild(e),e.color=n.fillColor||n.color,e.opacity=n.fillOpacity):e&&(o.removeChild(e),t._fill=null)},_updateCircle:function(t){var i=t._point.round(),e=Math.round(t._radius),n=Math.round(t._radiusY||e);this._setPath(t,t._empty()?"M0 0":"AL "+i.x+","+i.y+" "+e+","+n+" 0,23592600")},_setPath:function(t,i){t._path.v=i},_bringToFront:function(t){_i(t._container)},_bringToBack:function(t){di(t._container)}},gn=kt?mn:$,vn=_n.extend({getEvents:function(){var t=_n.prototype.getEvents.call(this);return t.zoomstart=this._onZoomStart,t},_initContainer:function(){this._container=gn("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=gn("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){li(this._container),Ai(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_onZoomStart:function(){this._update()},_update:function(){if(!this._map._animatingZoom||!this._bounds){_n.prototype._update.call(this);var t=this._bounds,i=t.getSize(),e=this._container;this._svgSize&&this._svgSize.equals(i)||(this._svgSize=i,e.setAttribute("width",i.x),e.setAttribute("height",i.y)),Pi(e,t.min),e.setAttribute("viewBox",[t.min.x,t.min.y,i.x,i.y].join(" ")),this.fire("update")}},_initPath:function(t){var i=t._path=gn("path");t.options.className&&mi(i,t.options.className),t.options.interactive&&mi(i,"leaflet-interactive"),this._updateStyle(t),this._layers[u(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){li(t._path),t.removeInteractiveTarget(t._path),delete this._layers[u(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var i=t._path,e=t.options;i&&(e.stroke?(i.setAttribute("stroke",e.color),i.setAttribute("stroke-opacity",e.opacity),i.setAttribute("stroke-width",e.weight),i.setAttribute("stroke-linecap",e.lineCap),i.setAttribute("stroke-linejoin",e.lineJoin),e.dashArray?i.setAttribute("stroke-dasharray",e.dashArray):i.removeAttribute("stroke-dasharray"),e.dashOffset?i.setAttribute("stroke-dashoffset",e.dashOffset):i.removeAttribute("stroke-dashoffset")):i.setAttribute("stroke","none"),e.fill?(i.setAttribute("fill",e.fillColor||e.color),i.setAttribute("fill-opacity",e.fillOpacity),i.setAttribute("fill-rule",e.fillRule||"evenodd")):i.setAttribute("fill","none"))},_updatePoly:function(t,i){this._setPath(t,Q(t._parts,i))},_updateCircle:function(t){var i=t._point,e=Math.max(Math.round(t._radius),1),n="a"+e+","+(Math.max(Math.round(t._radiusY),1)||e)+" 0 1,0 ",o=t._empty()?"M0 0":"M"+(i.x-e)+","+i.y+n+2*e+",0 "+n+2*-e+",0 ";this._setPath(t,o)},_setPath:function(t,i){t._path.setAttribute("d",i)},_bringToFront:function(t){_i(t._path)},_bringToBack:function(t){di(t._path)}});function yn(t){return Zt||kt?new vn(t):null}kt&&vn.include(fn),$i.include({getRenderer:function(t){var i=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer;return i||(i=this._renderer=this._createRenderer()),this.hasLayer(i)||this.addLayer(i),i},_getPaneRenderer:function(t){if("overlayPane"===t||void 0===t)return!1;var i=this._paneRenderers[t];return void 0===i&&(i=this._createRenderer({pane:t}),this._paneRenderers[t]=i),i},_createRenderer:function(t){return this.options.preferCanvas&&pn(t)||yn(t)}});var xn=We.extend({initialize:function(t,i){We.prototype.initialize.call(this,this._boundsToLatLngs(t),i)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return[(t=D(t)).getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});vn.create=gn,vn.pointsToPath=Q,He.geometryToLayer=Fe,He.coordsToLatLng=Ve,He.coordsToLatLngs=qe,He.latLngToCoords=Ge,He.latLngsToCoords=Ke,He.getFeature=Ye,He.asFeature=Xe,$i.mergeOptions({boxZoom:!0});var wn=se.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){ki(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){Ai(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){li(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),Qt(),Ti(),this._startPoint=this._map.mouseEventToContainerPoint(t),ki(document,{contextmenu:Wi,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=ui("div","leaflet-zoom-box",this._container),mi(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var i=new O(this._point,this._startPoint),e=i.getSize();Pi(this._box,i.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(li(this._box),fi(this._container,"leaflet-crosshair")),ti(),zi(),Ai(document,{contextmenu:Wi,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){if((1===t.which||1===t.button)&&(this._finish(),this._moved)){this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(a(this._resetState,this),0);var i=new N(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point));this._map.fitBounds(i).fire("boxzoomend",{boxZoomBounds:i})}},_onKeyDown:function(t){27===t.keyCode&&this._finish()}});$i.addInitHook("addHandler","boxZoom",wn),$i.mergeOptions({doubleClickZoom:!0});var Pn=se.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var i=this._map,e=i.getZoom(),n=i.options.zoomDelta,o=t.originalEvent.shiftKey?e-n:e+n;"center"===i.options.doubleClickZoom?i.setZoom(o):i.setZoomAround(t.containerPoint,o)}});$i.addInitHook("addHandler","doubleClickZoom",Pn),$i.mergeOptions({dragging:!0,inertia:!rt,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0});var Ln=se.extend({addHooks:function(){if(!this._draggable){var t=this._map;this._draggable=new ce(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))}mi(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){fi(this._map._container,"leaflet-grab"),fi(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t=this._map;if(t._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity){var i=D(this._map.options.maxBounds);this._offsetLimit=R(this._map.latLngToContainerPoint(i.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(i.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))}else this._offsetLimit=null;t.fire("movestart").fire("dragstart"),t.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){if(this._map.options.inertia){var i=this._lastTime=+new Date,e=this._lastPos=this._draggable._absPos||this._draggable._newPos;this._positions.push(e),this._times.push(i),this._prunePositions(i)}this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;1i.max.x&&(t.x=this._viscousLimit(t.x,i.max.x)),t.y>i.max.y&&(t.y=this._viscousLimit(t.y,i.max.y)),this._draggable._newPos=this._draggable._startPos.add(t)}},_onPreDragWrap:function(){var t=this._worldWidth,i=Math.round(t/2),e=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-i+e)%t+i-e,s=(n+i+e)%t-i-e,r=Math.abs(o+e)i.getMaxZoom()&&1 - - - - - - - - - - - - - - - - -
- - - - \ No newline at end of file diff --git a/src/main/ic_launcher-playstore.png b/src/main/ic_launcher-playstore.png deleted file mode 100644 index 95f86acf5..000000000 Binary files a/src/main/ic_launcher-playstore.png and /dev/null differ diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java deleted file mode 100644 index ba1c0586b..000000000 --- a/src/main/java/eu/siacs/conversations/Config.java +++ /dev/null @@ -1,276 +0,0 @@ -package eu.siacs.conversations; - -import android.graphics.Bitmap; -import android.net.Uri; -import android.util.Log; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Random; - -import eu.siacs.conversations.BuildConfig; -import eu.siacs.conversations.crypto.XmppDomainVerifier; -import eu.siacs.conversations.services.ProviderService; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.chatstate.ChatState; - -public final class Config { - - public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; - - private static final int UNENCRYPTED = 1; - private static final int OPENPGP = 2; - private static final int OTR = 4; - private static final int OMEMO = 8; - - private static final int ENCRYPTION_MASK = UNENCRYPTED | OPENPGP | OTR | OMEMO; - - public static boolean supportUnencrypted() { - return (ENCRYPTION_MASK & UNENCRYPTED) != 0; - } - - public static boolean supportOpenPgp() { - return (ENCRYPTION_MASK & OPENPGP) != 0; - } - - public static boolean supportOtr() { - return (ENCRYPTION_MASK & OTR) != 0; - } - - - public static boolean supportOmemo() { - return (ENCRYPTION_MASK & OMEMO) != 0; - } - - public static boolean omemoOnly() { - return !multipleEncryptionChoices() && supportOmemo(); - } - - public static boolean multipleEncryptionChoices() { - return (ENCRYPTION_MASK & (ENCRYPTION_MASK - 1)) != 0; - } - - public static String monocles() { - //if (Locale.getDefault().getLanguage().equalsIgnoreCase("de")) { - return "https://ocean.monocles.eu/apps/registration/"; - /*} else { - return "blabber.im/en.html"; - }*/ - } - - public static final String LOGTAG = BuildConfig.LOGTAG; - - public static final Jid BUG_REPORTS = Jid.of("support@monocles.de"); - public static final Uri HELP = Uri.parse("https://monocles.wiki"); - - public static final String inviteUserURL = monocles() + "/i/"; - public static final String inviteMUCURL = monocles() + "/j/"; - public static final String inviteHostURL = monocles(); // without http(s) - public static final String INVITE_DOMAIN = monocles(); - public static final String termsOfUseURL = "https://monocles.eu/legal-privacy/"; - public static final String privacyURL = "https://monocles.eu/legal-privacy/"; - public static final String migrationURL = Locale.getDefault().getLanguage().equalsIgnoreCase("de") ? "https://codeberg.org/Arne/monocles_chat" : "https://codeberg.org/Arne/monocles_chat"; - - public static final String CHANGELOG_URL = "https://codeberg.org/Arne/monocles_chat/src/branch/master/CHANGELOG.md"; - public static final String GIT_URL = "https://codeberg.org/Arne/monocles_chat"; - - public static final String PROVIDER_URL = ""; // https://invent.kde.org/melvo/xmpp-providers - //or https://invent.kde.org/melvo/xmpp-providers/-/raw/master/providers.json - public static final String XMPP_IP = null; //BuildConfig.XMPP_IP; // set to null means disable - public static final Integer[] XMPP_Ports = null; //BuildConfig.XMPP_Ports; // set to null means disable - public static final String DOMAIN_LOCK = BuildConfig.DOMAIN_LOCK; //only allow account creation for this domain - public static final String MAGIC_CREATE_DOMAIN = DOMAIN.getRandomServer(); - - public static final Jid QUICKSY_DOMAIN = Jid.of("quicksy.im"); - public static final String CHANNEL_DISCOVERY = "https://search.jabber.network"; - public static final boolean DISALLOW_REGISTRATION_IN_UI = false; //hide the register checkbox - public static final boolean SHOW_INTRO = BuildConfig.SHOW_INTRO; - public static final boolean SHOW_MIGRATION_INFO = BuildConfig.SHOW_MIGRATION_INFO; - - public static final boolean USE_RANDOM_RESOURCE_ON_EVERY_BIND = false; - - public static final boolean ALLOW_NON_TLS_CONNECTIONS = false; //very dangerous. you should have a good reason to set this to true - - public static final boolean FORCE_ORBOT = false; // always use TOR - - - public static final boolean QUICKSTART_ENABLED = true; - - //Notification settings - public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false; - - public static final boolean ALWAYS_NOTIFY_BY_DEFAULT = false; - public static final boolean SUPPRESS_ERROR_NOTIFICATION = false; - - public static final boolean DISABLE_BAN = false; // disables the ability to ban users from rooms - - public static final int PING_MAX_INTERVAL = 300; - public static final int IDLE_PING_INTERVAL = 600; //540 is minimum according to docs; - public static final int PING_MIN_INTERVAL = 30; - public static final int LOW_PING_TIMEOUT = 1; // used after push received - public static final int PING_TIMEOUT = 15; - public static final int SOCKET_TIMEOUT = 30; - public static final int CONNECT_TIMEOUT = 60; - public static final int POST_CONNECTIVITY_CHANGE_PING_INTERVAL = 30; - public static final int CONNECT_DISCO_TIMEOUT = 30; - public static final int MINI_GRACE_PERIOD = 750; - - public static final boolean XEP_0392 = true; //enables XEP-0392 v0.6.0 - - public static final int VIDEO_FAST_UPLOAD_SIZE = 10 * 1024 * 1024; - - public static final int AVATAR_SIZE = 480; - public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.JPEG; - public static final int AVATAR_CHAR_LIMIT = 9400; - - public static final Bitmap.CompressFormat IMAGE_FORMAT = Bitmap.CompressFormat.JPEG; - public static final int IMAGE_QUALITY = 65; - - public static final int MESSAGE_MERGE_WINDOW = 20; - - public static final int PAGE_SIZE = 50; - public static final int MAX_NUM_PAGES = 3; - public static final int MAX_SEARCH_RESULTS = 300; - - public static final int REFRESH_UI_INTERVAL = 500; - - public static final long OMEMO_AUTO_EXPIRY = 60 * MILLISECONDS_IN_DAY; // delete old OMEMO devices after 60 days of inactivity - public static final boolean REMOVE_BROKEN_DEVICES = false; - public static final boolean OMEMO_PADDING = false; - public static final boolean PUT_AUTH_TAG_INTO_KEY = true; - public static final boolean AUTOMATICALLY_COMPLETE_SESSIONS = true; - public static final boolean TWELVE_BYTE_IV = true; - - public static final int MAX_DISPLAY_MESSAGE_CHARS = 2 * 4096; //why only 4096? --> increased - public static final int MAX_STORAGE_MESSAGE_CHARS = 2 * 1024 * 1024; //2MB - - public static final boolean ExportLogs = true; // automatically export logs - public static final int ExportLogs_Hour = 4; //Time - hours: valid values from 0 to 23 - public static final int ExportLogs_Minute = 0; //Time - minutes: valid values from 0 to 59 - - public static final boolean USE_BOOKMARKS2 = false; - - public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb - public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true; - public static final boolean DISABLE_HTTP_UPLOAD = false; - public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts - public static final boolean BACKGROUND_STANZA_LOGGING = false; //log all stanzas that were received while the app is in background - public static final boolean RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE = true; //setting to true might increase power consumption - - public static final boolean ENCRYPT_ON_HTTP_UPLOADED = false; - - public static final boolean X509_VERIFICATION = false; //use x509 certificates to verify OMEMO keys - public static final boolean REQUIRE_RTP_VERIFICATION = false; //require a/v calls to be verified with OMEMO - - public static final boolean ONLY_INTERNAL_STORAGE = false; //use internal storage instead of sdcard to save attachments - - public static final boolean IGNORE_ID_REWRITE_IN_MUC = true; - public static final boolean MUC_LEAVE_BEFORE_JOIN = false; - - public static final boolean USE_LMC_VERSION_1_1 = true; - - public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY * 30; - public static final int MAM_MAX_MESSAGES = 750; - - public static final ChatState DEFAULT_CHAT_STATE = ChatState.ACTIVE; - public static final int TYPING_TIMEOUT = 5; - - public static final int EXPIRY_INTERVAL = 30 * 60 * 1000; // 30 minutes - - public static final String UPDATE_URL = BuildConfig.UPDATE_URL; - public static final long UPDATE_CHECK_TIMER = 24 * 60 * 60; // 24 h in seconds - - public static final String ISSUE_URL = "xmpp://support@conference.monocles.de?join"; - - //only allow secure tls chipers now - public static final String[] ENABLED_CIPHERS = { - "TLS_CHACHA20_POLY1305_SHA256", - "TLS_AES_256_GCM_SHA384", - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", - "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA512", - "TLS_DHE_RSA_WITH_AES_256_GCM_SHA512", - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", - }; - - public static final String[] WEAK_CIPHER_PATTERNS = { - "_NULL_", - "_EXPORT_", - "_anon_", - "_RC4_", - "_DES_", - "_MD5", - }; - - public static class OMEMO_EXCEPTIONS { - //if the own account matches one of the following domains OMEMO won’t be turned on automatically - public static final List ACCOUNT_DOMAINS = Collections.singletonList("s.ms"); - - //if the contacts domain matches one of the following domains OMEMO won’t be turned on automatically - //can be used for well known, widely used gateways - private static final List CONTACT_DOMAINS = Arrays.asList( - "cheogram.com", - "*.covid.monal.im" - ); - - public static boolean matchesContactDomain(final String domain) { - return XmppDomainVerifier.matchDomain(domain, CONTACT_DOMAINS); - } - } - - public static class DOMAIN { - // use this fallback server if provider list can't be updated automatically - public static final List DOMAINS = Arrays.asList( - "monocles.de", - "monocles.eu", - "monocles.fr", - "monocl.es", - "monocles.es", - "monocles.se", - "monocles.uk", - "monocles.ch", - "monocles.jp", - "monocles.us", - "monocles.cn" - ); - - // don't use these servers in provider list - public static final List BLACKLISTED_DOMAINS = Arrays.asList( - "blabber.im" - ); - - // choose a random server for registration - public static String getRandomServer() { - try { - new ProviderService().execute(); - final String domain = ProviderService.getProviders().get(new Random().nextInt(ProviderService.getProviders().size())); - Log.d(LOGTAG, "MagicCreate account on domain: " + domain); - return domain; - } catch (Exception e) { - Log.d(LOGTAG, "Error getting random server ", e); - } - return "monocles.de"; - } - } - - private Config() { - } - - - public static final class Map { - public final static double INITIAL_ZOOM_LEVEL = 4; - public final static double FINAL_ZOOM_LEVEL = 15; - public final static int MY_LOCATION_INDICATOR_SIZE = 15; - public final static int MY_LOCATION_INDICATOR_OUTLINE_SIZE = 5; - public final static long LOCATION_FIX_TIME_DELTA = 1000 * 10; // ms - public final static float LOCATION_FIX_SPACE_DELTA = 10; // m - public final static int LOCATION_FIX_SIGNIFICANT_TIME_DELTA = 1000 * 60 * 2; // ms - } - - // How deep nested quotes should be displayed. '2' means one quote nested in another. - public static final int QUOTE_MAX_DEPTH = 7; - // How deep nested quotes should be created on quoting a message. - public static final int QUOTING_MAX_DEPTH = 2; -} diff --git a/src/main/java/eu/siacs/conversations/android/AbstractPhoneContact.java b/src/main/java/eu/siacs/conversations/android/AbstractPhoneContact.java deleted file mode 100644 index b0d9132b3..000000000 --- a/src/main/java/eu/siacs/conversations/android/AbstractPhoneContact.java +++ /dev/null @@ -1,39 +0,0 @@ -package eu.siacs.conversations.android; - -import android.database.Cursor; -import android.net.Uri; -import android.provider.ContactsContract; -import android.text.TextUtils; - -public abstract class AbstractPhoneContact { - - private final Uri lookupUri; - private final String displayName; - private final String photoUri; - - - AbstractPhoneContact(Cursor cursor) { - int phoneId = cursor.getInt(cursor.getColumnIndex(ContactsContract.Data._ID)); - String lookupKey = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY)); - this.lookupUri = ContactsContract.Contacts.getLookupUri(phoneId, lookupKey); - this.displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DISPLAY_NAME)); - this.photoUri = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.PHOTO_URI)); - } - - public Uri getLookupUri() { - return lookupUri; - } - - public String getDisplayName() { - return displayName; - } - - public String getPhotoUri() { - return photoUri; - } - - - public int rating() { - return (TextUtils.isEmpty(displayName) ? 0 : 2) + (TextUtils.isEmpty(photoUri) ? 0 : 1); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/android/JabberIdContact.java b/src/main/java/eu/siacs/conversations/android/JabberIdContact.java deleted file mode 100644 index 8735fbe68..000000000 --- a/src/main/java/eu/siacs/conversations/android/JabberIdContact.java +++ /dev/null @@ -1,78 +0,0 @@ -package eu.siacs.conversations.android; - -import android.Manifest; -import android.content.Context; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.os.Build; -import android.provider.ContactsContract; -import android.util.Log; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.xmpp.Jid; - -public class JabberIdContact extends AbstractPhoneContact { - - private static final String[] PROJECTION = new String[]{ContactsContract.Data._ID, - ContactsContract.Data.DISPLAY_NAME, - ContactsContract.Data.PHOTO_URI, - ContactsContract.Data.LOOKUP_KEY, - ContactsContract.CommonDataKinds.Im.DATA - }; - private static final String SELECTION = ContactsContract.Data.MIMETYPE + "=? AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? or (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? and lower(" + ContactsContract.CommonDataKinds.Im.CUSTOM_PROTOCOL + ")=?))"; - - private static final String[] SELECTION_ARGS = { - ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE, - String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER), - String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_CUSTOM), - "xmpp" - }; - - private final Jid jid; - - private JabberIdContact(Cursor cursor) throws IllegalArgumentException { - super(cursor); - try { - this.jid = Jid.of(cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA))); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException(e); - } catch (NullPointerException e) { - throw new IllegalArgumentException(e); - } - } - - public Jid getJid() { - return jid; - } - - public static Map load(Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { - return Collections.emptyMap(); - } - 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"); - } - } - return contacts; - } catch (final Exception e) { - Log.d(Config.LOGTAG, "unable to query", e); - return Collections.emptyMap(); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/DomainHostnameVerifier.java b/src/main/java/eu/siacs/conversations/crypto/DomainHostnameVerifier.java deleted file mode 100644 index 402ca4f0b..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/DomainHostnameVerifier.java +++ /dev/null @@ -1,11 +0,0 @@ -package eu.siacs.conversations.crypto; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; - -public interface DomainHostnameVerifier extends HostnameVerifier { - - boolean verify(String domain, String hostname, SSLSession sslSession) throws SSLPeerUnverifiedException; - -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/OmemoSetting.java b/src/main/java/eu/siacs/conversations/crypto/OmemoSetting.java deleted file mode 100644 index 54456d7eb..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/OmemoSetting.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package eu.siacs.conversations.crypto; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; - -import com.google.common.base.Strings; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.ui.SettingsActivity; - -public class OmemoSetting { - - private static boolean always = false; - private static boolean never = false; - private static int encryption = Message.ENCRYPTION_AXOLOTL; - - public static boolean isAlways() { - return always; - } - - public static boolean isNever() { - return never; - } - - public static int getEncryption() { - return encryption; - } - - public static void load(final Context context, final SharedPreferences sharedPreferences) { - if (Config.omemoOnly()) { - always = true; - encryption = Message.ENCRYPTION_AXOLOTL; - return; - } - final String value = sharedPreferences.getString(SettingsActivity.OMEMO_SETTING, context.getResources().getString(R.string.omemo_setting_default)); - switch (Strings.nullToEmpty(value)) { - case "always": - always = true; - never = false; - encryption = Message.ENCRYPTION_AXOLOTL; - break; - case "default_on": - always = false; - never = false; - encryption = Message.ENCRYPTION_AXOLOTL; - break; - case "always_off": - always = false; - never = true; - encryption = Message.ENCRYPTION_NONE; - break; - default: - always = false; - never = false; - encryption = Message.ENCRYPTION_NONE; - break; - - } - } - - public static void load(final Context context) { - load(context, PreferenceManager.getDefaultSharedPreferences(context)); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/OtrService.java b/src/main/java/eu/siacs/conversations/crypto/OtrService.java deleted file mode 100644 index c7e718956..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/OtrService.java +++ /dev/null @@ -1,312 +0,0 @@ -package eu.siacs.conversations.crypto; - -import android.util.Log; - -import net.java.otr4j.OtrEngineHost; -import net.java.otr4j.OtrException; -import net.java.otr4j.OtrPolicy; -import net.java.otr4j.OtrPolicyImpl; -import net.java.otr4j.crypto.OtrCryptoEngineImpl; -import net.java.otr4j.crypto.OtrCryptoException; -import net.java.otr4j.session.FragmenterInstructions; -import net.java.otr4j.session.InstanceTag; -import net.java.otr4j.session.SessionID; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.math.BigInteger; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.spec.DSAPrivateKeySpec; -import java.security.spec.DSAPublicKeySpec; -import java.security.spec.InvalidKeySpecException; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.generator.MessageGenerator; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.chatstate.ChatState; -import eu.siacs.conversations.xmpp.jid.OtrJidHelper; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; - -public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost { - - private Account account; - private OtrPolicy otrPolicy; - private KeyPair keyPair; - private XmppConnectionService mXmppConnectionService; - - public OtrService(XmppConnectionService service, Account account) { - this.account = account; - this.otrPolicy = new OtrPolicyImpl(); - this.otrPolicy.setAllowV1(false); - this.otrPolicy.setAllowV2(true); - this.otrPolicy.setAllowV3(true); - this.keyPair = loadKey(account.getKeys()); - this.mXmppConnectionService = service; - } - - private KeyPair loadKey(final JSONObject keys) { - if (keys == null) { - return null; - } - synchronized (keys) { - try { - BigInteger x = new BigInteger(keys.getString("otr_x"), 16); - BigInteger y = new BigInteger(keys.getString("otr_y"), 16); - BigInteger p = new BigInteger(keys.getString("otr_p"), 16); - BigInteger q = new BigInteger(keys.getString("otr_q"), 16); - BigInteger g = new BigInteger(keys.getString("otr_g"), 16); - KeyFactory keyFactory = KeyFactory.getInstance("DSA"); - DSAPublicKeySpec pubKeySpec = new DSAPublicKeySpec(y, p, q, g); - DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g); - PublicKey publicKey = keyFactory.generatePublic(pubKeySpec); - PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec); - return new KeyPair(publicKey, privateKey); - } catch (JSONException e) { - return null; - } catch (NoSuchAlgorithmException e) { - return null; - } catch (InvalidKeySpecException e) { - return null; - } - } - } - - private void saveKey() { - PublicKey publicKey = keyPair.getPublic(); - PrivateKey privateKey = keyPair.getPrivate(); - KeyFactory keyFactory; - try { - keyFactory = KeyFactory.getInstance("DSA"); - DSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec( - privateKey, DSAPrivateKeySpec.class); - DSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey, - DSAPublicKeySpec.class); - this.account.setKey("otr_x", privateKeySpec.getX().toString(16)); - this.account.setKey("otr_g", privateKeySpec.getG().toString(16)); - this.account.setKey("otr_p", privateKeySpec.getP().toString(16)); - this.account.setKey("otr_q", privateKeySpec.getQ().toString(16)); - this.account.setKey("otr_y", publicKeySpec.getY().toString(16)); - } catch (final NoSuchAlgorithmException e) { - e.printStackTrace(); - } catch (final InvalidKeySpecException e) { - e.printStackTrace(); - } - - } - - @Override - public void askForSecret(SessionID id, InstanceTag instanceTag, String question) { - try { - final Jid jid = OtrJidHelper.fromSessionID(id); - Conversation conversation = this.mXmppConnectionService.find(this.account, jid); - if (conversation != null) { - conversation.smp().hint = question; - conversation.smp().status = Conversation.Smp.STATUS_CONTACT_REQUESTED; - mXmppConnectionService.updateConversationUi(); - } - } catch (IllegalArgumentException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": smp in invalid session " + id.toString()); - } - } - - @Override - public void finishedSessionMessage(SessionID arg0, String arg1) - throws OtrException { - - } - - @Override - public String getFallbackMessage(SessionID arg0) { - return MessageGenerator.OTR_FALLBACK_MESSAGE; - } - - @Override - public byte[] getLocalFingerprintRaw(SessionID arg0) { - try { - return getFingerprintRaw(getPublicKey()); - } catch (OtrCryptoException e) { - return null; - } - } - - public PublicKey getPublicKey() { - if (this.keyPair == null) { - return null; - } - return this.keyPair.getPublic(); - } - - @Override - public KeyPair getLocalKeyPair(SessionID arg0) throws OtrException { - if (this.keyPair == null) { - KeyPairGenerator kg; - try { - kg = KeyPairGenerator.getInstance("DSA"); - this.keyPair = kg.genKeyPair(); - this.saveKey(); - mXmppConnectionService.databaseBackend.updateAccount(account); - } catch (NoSuchAlgorithmException e) { - Log.d(Config.LOGTAG, - "error generating key pair " + e.getMessage()); - } - } - return this.keyPair; - } - - @Override - public String getReplyForUnreadableMessage(SessionID arg0) { - // TODO Auto-generated method stub - return null; - } - - @Override - public OtrPolicy getSessionPolicy(SessionID arg0) { - return otrPolicy; - } - - @Override - public void injectMessage(SessionID session, String body) - throws OtrException { - MessagePacket packet = new MessagePacket(); - packet.setFrom(account.getJid()); - if (session.getUserID().isEmpty()) { - packet.setAttribute("to", session.getAccountID()); - } else { - packet.setAttribute("to", session.getAccountID() + "/" + session.getUserID()); - } - packet.setBody(body); - MessageGenerator.addMessageHints(packet); - try { - Jid jid = OtrJidHelper.fromSessionID(session); - Conversation conversation = mXmppConnectionService.find(account, jid); - if (conversation != null && conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) { - if (mXmppConnectionService.sendChatStates()) { - packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); - } - } - } catch (final IllegalArgumentException ignored) { - - } - - packet.setType(MessagePacket.TYPE_CHAT); - packet.addChild("encryption", "urn:xmpp:eme:0").setAttribute("namespace", "urn:xmpp:otr:0"); - account.getXmppConnection().sendMessagePacket(packet); - } - - @Override - public void messageFromAnotherInstanceReceived(SessionID session) { - sendOtrErrorMessage(session, "Message from another OTR-instance received"); - } - - @Override - public void multipleInstancesDetected(SessionID arg0) { - // TODO Auto-generated method stub - - } - - @Override - public void requireEncryptedMessage(SessionID arg0, String arg1) - throws OtrException { - // TODO Auto-generated method stub - - } - - @Override - public void showError(SessionID arg0, String arg1) throws OtrException { - Log.d(Config.LOGTAG, "show error"); - } - - @Override - public void smpAborted(SessionID id) throws OtrException { - setSmpStatus(id, Conversation.Smp.STATUS_NONE); - } - - private void setSmpStatus(SessionID id, int status) { - try { - final Jid jid = OtrJidHelper.fromSessionID(id); - Conversation conversation = this.mXmppConnectionService.find(this.account, jid); - if (conversation != null) { - conversation.smp().status = status; - mXmppConnectionService.updateConversationUi(); - } - } catch (final IllegalArgumentException ignored) { - - } - } - - @Override - public void smpError(SessionID id, int arg1, boolean arg2) - throws OtrException { - setSmpStatus(id, Conversation.Smp.STATUS_NONE); - } - - @Override - public void unencryptedMessageReceived(SessionID arg0, String arg1) - throws OtrException { - throw new OtrException(new Exception("unencrypted message received")); - } - - @Override - public void unreadableMessageReceived(SessionID session) throws OtrException { - Log.d(Config.LOGTAG, "unreadable message received"); - sendOtrErrorMessage(session, "You sent me an unreadable OTR-encrypted message"); - } - - public void sendOtrErrorMessage(SessionID session, String errorText) { - try { - Jid jid = OtrJidHelper.fromSessionID(session); - Conversation conversation = mXmppConnectionService.find(account, jid); - String id = conversation == null ? null : conversation.getLastReceivedOtrMessageId(); - if (id != null) { - MessagePacket packet = mXmppConnectionService.getMessageGenerator() - .generateOtrError(jid, id, errorText); - packet.setFrom(account.getJid()); - mXmppConnectionService.sendMessagePacket(account, packet); - Log.d(Config.LOGTAG, packet.toString()); - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() - + ": unreadable OTR message in " + conversation.getName()); - } - } catch (IllegalArgumentException e) { - return; - } - } - - @Override - public void unverify(SessionID id, String arg1) { - setSmpStatus(id, Conversation.Smp.STATUS_FAILED); - } - - @Override - public void verify(SessionID id, String fingerprint, boolean approved) { - Log.d(Config.LOGTAG, "OtrService.verify(" + id.toString() + "," + fingerprint + "," + String.valueOf(approved) + ")"); - try { - final Jid jid = OtrJidHelper.fromSessionID(id); - Conversation conversation = this.mXmppConnectionService.find(this.account, jid); - if (conversation != null) { - if (approved) { - conversation.getContact().addOtrFingerprint(fingerprint); - } - conversation.smp().hint = null; - conversation.smp().status = Conversation.Smp.STATUS_VERIFIED; - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.syncRosterToDisk(conversation.getAccount()); - } - } catch (final IllegalArgumentException ignored) { - } - } - - @Override - public FragmenterInstructions getFragmenterInstructions(SessionID sessionID) { - return null; - } - -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java b/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java deleted file mode 100644 index ae8425ee0..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/PgpDecryptionService.java +++ /dev/null @@ -1,275 +0,0 @@ -package eu.siacs.conversations.crypto; - -import android.app.PendingIntent; -import android.content.Intent; -import android.util.Log; - -import org.openintents.openpgp.OpenPgpMetadata; -import org.openintents.openpgp.util.OpenPgpApi; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayDeque; -import java.util.HashSet; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.http.HttpConnectionManager; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.MimeUtils; - -public class PgpDecryptionService { - - protected final ArrayDeque messages = new ArrayDeque<>(); - protected final HashSet pendingNotifications = new HashSet<>(); - private final XmppConnectionService mXmppConnectionService; - private OpenPgpApi openPgpApi = null; - private Message currentMessage; - private PendingIntent pendingIntent; - private Intent userInteractionResult; - - - public PgpDecryptionService(XmppConnectionService service) { - this.mXmppConnectionService = service; - } - - public synchronized boolean decrypt(final Message message, boolean notify) { - messages.add(message); - if (notify && pendingIntent == null) { - pendingNotifications.add(message); - continueDecryption(); - return false; - } else { - continueDecryption(); - return notify; - } - } - - public synchronized void decrypt(final List list) { - for (Message message : list) { - if (message.getEncryption() == Message.ENCRYPTION_PGP) { - messages.add(message); - } - } - continueDecryption(); - } - - public synchronized void discard(List discards) { - this.messages.removeAll(discards); - this.pendingNotifications.removeAll(discards); - } - - public synchronized void discard(Message message) { - this.messages.remove(message); - this.pendingNotifications.remove(message); - } - - public void giveUpCurrentDecryption() { - Message message; - synchronized (this) { - if (currentMessage != null) { - return; - } - message = messages.peekFirst(); - if (message == null) { - return; - } - discard(message); - } - synchronized (message) { - if (message.getEncryption() == Message.ENCRYPTION_PGP) { - message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED); - } - } - mXmppConnectionService.updateMessage(message, false); - continueDecryption(true); - } - - protected synchronized void decryptNext() { - if (pendingIntent == null - && getOpenPgpApi() != null - && (currentMessage = messages.poll()) != null) { - new Thread(() -> { - executeApi(currentMessage); - decryptNext(); - }).start(); - } - } - - public synchronized void continueDecryption(boolean resetPending) { - if (resetPending) { - this.pendingIntent = null; - } - continueDecryption(); - } - - public synchronized void continueDecryption(Intent userInteractionResult) { - this.pendingIntent = null; - this.userInteractionResult = userInteractionResult; - continueDecryption(); - } - - public synchronized void continueDecryption() { - if (currentMessage == null) { - decryptNext(); - } - } - - private synchronized OpenPgpApi getOpenPgpApi() { - if (openPgpApi == null) { - this.openPgpApi = mXmppConnectionService.getOpenPgpApi(); - } - return this.openPgpApi; - } - - private void executeApi(Message message) { - boolean skipNotificationPush = false; - synchronized (message) { - Intent params = userInteractionResult != null ? userInteractionResult : new Intent(); - params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); - if (message.getType() == Message.TYPE_TEXT) { - InputStream is = new ByteArrayInputStream(message.getBody().getBytes()); - final OutputStream os = new ByteArrayOutputStream(); - Intent result = getOpenPgpApi().executeApi(params, is, os); - switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { - case OpenPgpApi.RESULT_CODE_SUCCESS: - try { - os.flush(); - final String body = os.toString(); - if (body == null) { - throw new IOException("body was null"); - } - message.setBody(body); - message.setEncryption(Message.ENCRYPTION_DECRYPTED); - final HttpConnectionManager manager = mXmppConnectionService.getHttpConnectionManager(); - if ((mXmppConnectionService.easyDownloader() || message.trusted()) - && message.treatAsDownloadable() - && manager.getAutoAcceptFileSize() > 0) { - manager.createNewDownloadConnection(message); - } - } catch (IOException e) { - message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED); - } - mXmppConnectionService.updateMessage(message); - break; - case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - synchronized (PgpDecryptionService.this) { - PendingIntent pendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT); - messages.addFirst(message); - currentMessage = null; - storePendingIntent(pendingIntent); - } - break; - case OpenPgpApi.RESULT_CODE_ERROR: - message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED); - mXmppConnectionService.updateMessage(message); - break; - } - } else if (message.isFileOrImage()) { - try { - final DownloadableFile inputFile = mXmppConnectionService.getFileBackend().getFile(message, false); - final DownloadableFile outputFile = mXmppConnectionService.getFileBackend().getFile(message, true); - if (outputFile.getParentFile().mkdirs()) { - Log.d(Config.LOGTAG, "created parent directories for " + outputFile.getAbsolutePath()); - } - outputFile.createNewFile(); - InputStream is = new FileInputStream(inputFile); - OutputStream os = new FileOutputStream(outputFile); - Intent result = getOpenPgpApi().executeApi(params, is, os); - switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { - case OpenPgpApi.RESULT_CODE_SUCCESS: - OpenPgpMetadata openPgpMetadata = result.getParcelableExtra(OpenPgpApi.RESULT_METADATA); - String originalFilename = openPgpMetadata.getFilename(); - String originalExtension = originalFilename == null ? null : MimeUtils.extractRelevantExtension(originalFilename); - if (originalExtension != null && MimeUtils.extractRelevantExtension(outputFile.getName()) == null) { - Log.d(Config.LOGTAG, "detected original filename during pgp decryption"); - final String mime = MimeUtils.guessMimeTypeFromExtension(originalExtension); - final String filename = outputFile.getName() + "." + originalExtension; - final File fixedFile = mXmppConnectionService.getFileBackend().getStorageLocation(filename, mime); - if (fixedFile.getParentFile().mkdirs()) { - Log.d(Config.LOGTAG, "created parent directories for " + fixedFile.getAbsolutePath()); - } - synchronized (mXmppConnectionService.FILENAMES_TO_IGNORE_DELETION) { - mXmppConnectionService.FILENAMES_TO_IGNORE_DELETION.add(outputFile.getAbsolutePath()); - } - if (outputFile.renameTo(fixedFile)) { - Log.d(Config.LOGTAG, "renamed " + outputFile.getAbsolutePath() + " to " + fixedFile.getAbsolutePath()); - message.setRelativeFilePath(fixedFile.getAbsolutePath()); - } - } - final String url = message.getFileParams().url; - message.setEncryption(Message.ENCRYPTION_DECRYPTED); - mXmppConnectionService.getFileBackend().updateFileParams(message, url); - mXmppConnectionService.updateMessage(message); - if (!inputFile.delete()) { - Log.w(Config.LOGTAG, "unable to delete pgp encrypted source file " + inputFile.getAbsolutePath()); - } - skipNotificationPush = true; - FileBackend.updateMediaScanner(mXmppConnectionService, outputFile, () -> notifyIfPending(message)); - break; - case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - synchronized (PgpDecryptionService.this) { - PendingIntent pendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT); - messages.addFirst(message); - currentMessage = null; - storePendingIntent(pendingIntent); - } - break; - case OpenPgpApi.RESULT_CODE_ERROR: - message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED); - mXmppConnectionService.updateMessage(message); - break; - } - } catch (final IOException e) { - message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED); - mXmppConnectionService.updateMessage(message); - } - } - } - if (!skipNotificationPush) { - notifyIfPending(message); - } - } - - private synchronized void notifyIfPending(Message message) { - if (pendingNotifications.remove(message)) { - mXmppConnectionService.getNotificationService().push(message); - } - } - - private void storePendingIntent(PendingIntent pendingIntent) { - this.pendingIntent = pendingIntent; - mXmppConnectionService.updateConversationUi(); - } - - public synchronized boolean hasPendingIntent(Conversation conversation) { - if (pendingIntent == null) { - return false; - } else { - for (Message message : messages) { - if (message.getConversation() == conversation) { - return true; - } - } - return false; - } - } - - public PendingIntent getPendingIntent() { - return pendingIntent; - } - - public boolean isConnected() { - return getOpenPgpApi() != null; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java deleted file mode 100644 index 420b96f26..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java +++ /dev/null @@ -1,290 +0,0 @@ -package eu.siacs.conversations.crypto; - -import android.app.PendingIntent; -import android.content.Intent; -import android.util.Log; - -import androidx.annotation.StringRes; - -import com.google.common.base.Joiner; -import com.google.common.base.Splitter; -import com.google.common.base.Strings; - -import org.openintents.openpgp.OpenPgpError; -import org.openintents.openpgp.OpenPgpSignatureResult; -import org.openintents.openpgp.util.OpenPgpApi; -import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.UiCallback; -import eu.siacs.conversations.utils.AsciiArmor; - -public class PgpEngine { - private OpenPgpApi api; - private XmppConnectionService mXmppConnectionService; - - public PgpEngine(OpenPgpApi api, XmppConnectionService service) { - this.api = api; - this.mXmppConnectionService = service; - } - - private static void logError(Account account, OpenPgpError error) { - if (error != null) { - error.describeContents(); - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": OpenKeychain error '" + error.getMessage() + "' code=" + error.getErrorId() + " class=" + error.getClass().getName()); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": OpenKeychain error with no message"); - } - } - - public void encrypt(final Message message, final UiCallback callback) { - Intent params = new Intent(); - params.setAction(OpenPgpApi.ACTION_ENCRYPT); - final Conversation conversation = (Conversation) message.getConversation(); - if (conversation.getMode() == Conversation.MODE_SINGLE) { - long[] keys = { - conversation.getContact().getPgpKeyId(), - conversation.getAccount().getPgpId() - }; - params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keys); - } else { - params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, conversation.getMucOptions().getPgpKeyIds()); - } - - if (!message.needsUploading()) { - params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); - String body; - if (message.hasFileOnRemoteHost()) { - body = message.getFileParams().url; - } else { - body = message.getBody(); - } - InputStream is = new ByteArrayInputStream(body.getBytes()); - final OutputStream os = new ByteArrayOutputStream(); - api.executeApiAsync(params, is, os, result -> { - switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { - case OpenPgpApi.RESULT_CODE_SUCCESS: - try { - os.flush(); - final ArrayList encryptedMessageBody = new ArrayList<>(); - final String[] lines = os.toString().split("\n"); - for (int i = 2; i < lines.length - 1; ++i) { - if (!lines[i].contains("Version")) { - encryptedMessageBody.add(lines[i].trim()); - } - } - message.setEncryptedBody(Joiner.on('\n').join(encryptedMessageBody)); - message.setEncryption(Message.ENCRYPTION_DECRYPTED); - mXmppConnectionService.sendMessage(message); - callback.success(message); - } catch (IOException e) { - callback.error(R.string.openpgp_error, message); - } - break; - case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message); - break; - case OpenPgpApi.RESULT_CODE_ERROR: - OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR); - String errorMessage = error != null ? error.getMessage() : null; - @StringRes final int res; - if (errorMessage != null && errorMessage.startsWith("Bad key for encryption")) { - res = R.string.bad_key_for_encryption; - } else { - res = R.string.openpgp_error; - } - logError(conversation.getAccount(), error); - callback.error(res, message); - break; - } - }); - } else { - try { - DownloadableFile inputFile = this.mXmppConnectionService - .getFileBackend().getFile(message, true); - DownloadableFile outputFile = this.mXmppConnectionService - .getFileBackend().getFile(message, false); - outputFile.getParentFile().mkdirs(); - outputFile.createNewFile(); - final InputStream is = new FileInputStream(inputFile); - final OutputStream os = new FileOutputStream(outputFile); - api.executeApiAsync(params, is, os, result -> { - switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { - case OpenPgpApi.RESULT_CODE_SUCCESS: - try { - os.flush(); - } catch (IOException ignored) { - //ignored - } - FileBackend.close(os); - mXmppConnectionService.sendMessage(message); - callback.success(message); - break; - case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message); - break; - case OpenPgpApi.RESULT_CODE_ERROR: - logError(conversation.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); - callback.error(R.string.openpgp_error, message); - break; - } - }); - } catch (final IOException e) { - callback.error(R.string.openpgp_error, message); - } - } - } - - public long fetchKeyId(final Account account, final String status, final String signature) { - if (signature == null || api == null) { - return 0; - } - final Intent params = new Intent(); - params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); - try { - params.putExtra(OpenPgpApi.RESULT_DETACHED_SIGNATURE, AsciiArmor.decode(signature)); - } catch (final Exception e) { - Log.d(Config.LOGTAG, "unable to parse signature", e); - return 0; - } - final InputStream is = new ByteArrayInputStream(Strings.nullToEmpty(status).getBytes()); - final ByteArrayOutputStream os = new ByteArrayOutputStream(); - final Intent result = api.executeApi(params, is, os); - switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, - OpenPgpApi.RESULT_CODE_ERROR)) { - case OpenPgpApi.RESULT_CODE_SUCCESS: - final OpenPgpSignatureResult sigResult = result.getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE); - //TODO unsure that sigResult.getResult() is either 1, 2 or 3 - if (sigResult != null) { - return sigResult.getKeyId(); - } else { - return 0; - } - case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - return 0; - case OpenPgpApi.RESULT_CODE_ERROR: - logError(account, result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); - return 0; - } - return 0; - } - - public void chooseKey(final Account account, final UiCallback callback) { - Intent p = new Intent(); - p.setAction(OpenPgpApi.ACTION_GET_SIGN_KEY_ID); - api.executeApiAsync(p, null, null, result -> { - switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) { - case OpenPgpApi.RESULT_CODE_SUCCESS: - callback.success(account); - return; - case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), account); - return; - case OpenPgpApi.RESULT_CODE_ERROR: - logError(account, result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); - callback.error(R.string.openpgp_error, account); - } - }); - } - - public void generateSignature(Intent intent, final Account account, String status, final UiCallback callback) { - if (account.getPgpId() == 0) { - return; - } - Intent params = intent == null ? new Intent() : intent; - params.setAction(OpenPgpApi.ACTION_CLEARTEXT_SIGN); - params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); - params.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, account.getPgpId()); - InputStream is = new ByteArrayInputStream(status.getBytes()); - final OutputStream os = new ByteArrayOutputStream(); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": signing status message \"" + status + "\""); - api.executeApiAsync(params, is, os, result -> { - switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) { - case OpenPgpApi.RESULT_CODE_SUCCESS: - final ArrayList signature = new ArrayList<>(); - try { - os.flush(); - boolean sig = false; - for (final String line : Splitter.on('\n').split(os.toString())) { - if (sig) { - if (line.contains("END PGP SIGNATURE")) { - sig = false; - } else { - if (!line.contains("Version")) { - signature.add(line.trim()); - } - } - } - if (line.contains("BEGIN PGP SIGNATURE")) { - sig = true; - } - } - } catch (IOException e) { - callback.error(R.string.openpgp_error, null); - return; - } - callback.success(Joiner.on('\n').join(signature)); - return; - case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), status); - return; - case OpenPgpApi.RESULT_CODE_ERROR: - OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR); - if (error != null && "signing subkey not found!".equals(error.getMessage())) { - callback.error(0, null); - } else { - logError(account, error); - callback.error(R.string.unable_to_connect_to_keychain, null); - } - } - }); - } - - public void hasKey(final Contact contact, final UiCallback callback) { - Intent params = new Intent(); - params.setAction(OpenPgpApi.ACTION_GET_KEY); - params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId()); - api.executeApiAsync(params, null, null, new IOpenPgpCallback() { - - @Override - public void onReturn(Intent result) { - switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) { - case OpenPgpApi.RESULT_CODE_SUCCESS: - callback.success(contact); - return; - case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: - callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), contact); - return; - case OpenPgpApi.RESULT_CODE_ERROR: - logError(contact.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR)); - callback.error(R.string.openpgp_error, contact); - } - } - }); - } - - public PendingIntent getIntentForKey(long pgpKeyId) { - Intent params = new Intent(); - params.setAction(OpenPgpApi.ACTION_GET_KEY); - params.putExtra(OpenPgpApi.EXTRA_KEY_ID, pgpKeyId); - Intent result = api.executeApi(params, null, null); - return (PendingIntent) result.getParcelableExtra(OpenPgpApi.RESULT_INTENT); - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java b/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java deleted file mode 100644 index fc8ede071..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java +++ /dev/null @@ -1,196 +0,0 @@ -package eu.siacs.conversations.crypto; - -import android.util.Log; -import android.util.Pair; - -import com.google.common.collect.ImmutableList; - -import org.bouncycastle.asn1.ASN1Primitive; -import org.bouncycastle.asn1.DERIA5String; -import org.bouncycastle.asn1.DERTaggedObject; -import org.bouncycastle.asn1.DERUTF8String; -import org.bouncycastle.asn1.DLSequence; -import org.bouncycastle.asn1.x500.RDN; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.bouncycastle.asn1.x500.style.IETFUtils; -import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; - -import java.io.IOException; -import java.net.IDN; -import java.security.cert.Certificate; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateParsingException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Locale; - -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; - -public class XmppDomainVerifier { - - private static final String LOGTAG = "XmppDomainVerifier"; - - private static final String SRV_NAME = "1.3.6.1.5.5.7.8.7"; - private static final String XMPP_ADDR = "1.3.6.1.5.5.7.8.5"; - - private static List getCommonNames(X509Certificate certificate) { - List domains = new ArrayList<>(); - try { - X500Name x500name = new JcaX509CertificateHolder(certificate).getSubject(); - RDN[] rdns = x500name.getRDNs(BCStyle.CN); - for (int i = 0; i < rdns.length; ++i) { - domains.add(IETFUtils.valueToString(x500name.getRDNs(BCStyle.CN)[i].getFirst().getValue())); - } - return domains; - } catch (CertificateEncodingException e) { - return domains; - } - } - - private static Pair parseOtherName(byte[] otherName) { - try { - ASN1Primitive asn1Primitive = ASN1Primitive.fromByteArray(otherName); - if (asn1Primitive instanceof DERTaggedObject) { - ASN1Primitive inner = ((DERTaggedObject) asn1Primitive).getObject(); - if (inner instanceof DLSequence) { - DLSequence sequence = (DLSequence) inner; - if (sequence.size() >= 2 && sequence.getObjectAt(1) instanceof DERTaggedObject) { - String oid = sequence.getObjectAt(0).toString(); - ASN1Primitive value = ((DERTaggedObject) sequence.getObjectAt(1)).getObject(); - if (value instanceof DERUTF8String) { - return new Pair<>(oid, ((DERUTF8String) value).getString()); - } else if (value instanceof DERIA5String) { - return new Pair<>(oid, ((DERIA5String) value).getString()); - } - } - } - } - return null; - } catch (IOException e) { - return null; - } - } - - public static boolean matchDomain(final String needle, final List haystack) { - for (final String entry : haystack) { - if (entry.startsWith("*.")) { - int offset = 0; - while (offset < needle.length()) { - int i = needle.indexOf('.', offset); - if (i < 0) { - break; - } - if (needle.substring(i).equalsIgnoreCase(entry.substring(1))) { - return true; - } - offset = i + 1; - } - } else { - if (entry.equalsIgnoreCase(needle)) { - return true; - } - } - } - return false; - } - - public boolean verify(final String unicodeDomain, final String unicodeHostname, SSLSession sslSession) throws SSLPeerUnverifiedException { - final String domain = IDN.toASCII(unicodeDomain); - final String hostname = unicodeHostname == null ? null : IDN.toASCII(unicodeHostname); - final Certificate[] chain = sslSession.getPeerCertificates(); - if (chain.length == 0 || !(chain[0] instanceof X509Certificate)) { - return false; - } - final X509Certificate certificate = (X509Certificate) chain[0]; - final List commonNames = getCommonNames(certificate); - if (isSelfSigned(certificate)) { - if (commonNames.size() == 1 && matchDomain(domain, commonNames)) { - Log.d(LOGTAG, "accepted CN in self signed cert as work around for " + domain); - return true; - } - } - try { - final ValidDomains validDomains = parseValidDomains(certificate); - Log.d(LOGTAG, "searching for " + domain + " in srvNames: " + validDomains.srvNames + " xmppAddrs: " + validDomains.xmppAddrs + " domains:" + validDomains.domains); - if (hostname != null) { - Log.d(LOGTAG, "also trying to verify hostname " + hostname); - } - return validDomains.xmppAddrs.contains(domain) - || validDomains.srvNames.contains("_xmpp-client." + domain) - || matchDomain(domain, validDomains.domains) - || (hostname != null && matchDomain(hostname, validDomains.domains)); - } catch (final Exception e) { - return false; - } - } - - public static ValidDomains parseValidDomains(final X509Certificate certificate) throws CertificateParsingException { - final List commonNames = getCommonNames(certificate); - final Collection> alternativeNames = certificate.getSubjectAlternativeNames(); - final List xmppAddrs = new ArrayList<>(); - final List srvNames = new ArrayList<>(); - final List domains = new ArrayList<>(); - if (alternativeNames != null) { - for (List san : alternativeNames) { - final Integer type = (Integer) san.get(0); - if (type == 0) { - final Pair otherName = parseOtherName((byte[]) san.get(1)); - if (otherName != null && otherName.first != null && otherName.second != null) { - switch (otherName.first) { - case SRV_NAME: - srvNames.add(otherName.second.toLowerCase(Locale.US)); - break; - case XMPP_ADDR: - xmppAddrs.add(otherName.second.toLowerCase(Locale.US)); - break; - default: - Log.d(LOGTAG, "oid: " + otherName.first + " value: " + otherName.second); - } - } - } else if (type == 2) { - final Object value = san.get(1); - if (value instanceof String) { - domains.add(((String) value).toLowerCase(Locale.US)); - } - } - } - } - if (srvNames.size() == 0 && xmppAddrs.size() == 0 && domains.size() == 0) { - domains.addAll(commonNames); - } - return new ValidDomains(xmppAddrs, srvNames, domains); - } - - public static final class ValidDomains { - final List xmppAddrs; - final List srvNames; - final List domains; - - private ValidDomains(List xmppAddrs, List srvNames, List domains) { - this.xmppAddrs = xmppAddrs; - this.srvNames = srvNames; - this.domains = domains; - } - - public List all() { - ImmutableList.Builder all = new ImmutableList.Builder<>(); - all.addAll(xmppAddrs); - all.addAll(srvNames); - all.addAll(domains); - return all.build(); - } - } - - private boolean isSelfSigned(X509Certificate certificate) { - try { - certificate.verify(certificate.getPublicKey()); - return true; - } catch (Exception e) { - return false; - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java deleted file mode 100644 index afa309ae3..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ /dev/null @@ -1,1774 +0,0 @@ -package eu.siacs.conversations.crypto.axolotl; - -import android.os.Bundle; -import android.security.KeyChain; -import android.util.Log; -import android.util.Pair; -import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; -import com.google.common.util.concurrent.SettableFuture; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.IdentityKeyPair; -import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.InvalidKeyIdException; -import org.whispersystems.libsignal.SessionBuilder; -import org.whispersystems.libsignal.SignalProtocolAddress; -import org.whispersystems.libsignal.UntrustedIdentityException; -import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.state.PreKeyBundle; -import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; -import org.whispersystems.libsignal.util.KeyHelper; - -import java.security.PrivateKey; -import java.security.Security; -import java.security.Signature; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.parser.IqParser; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.utils.SerialSingleThreadExecutor; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; -import eu.siacs.conversations.xmpp.jingle.OmemoVerification; -import eu.siacs.conversations.xmpp.jingle.OmemoVerifiedRtpContentMap; -import eu.siacs.conversations.xmpp.jingle.RtpContentMap; -import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; -import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo; -import eu.siacs.conversations.xmpp.pep.PublishOptions; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; - -public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { - - public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl"; - public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist"; - public static final String PEP_DEVICE_LIST_NOTIFY = PEP_DEVICE_LIST + "+notify"; - public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles"; - public static final String PEP_VERIFICATION = PEP_PREFIX + ".verification"; - public static final String PEP_OMEMO_WHITELISTED = PEP_PREFIX + ".whitelisted"; - - public static final String LOGPREFIX = "AxolotlService"; - - private static final int NUM_KEYS_TO_PUBLISH = 100; - private static final int publishTriesThreshold = 3; - - private final Account account; - private final XmppConnectionService mXmppConnectionService; - private final SQLiteAxolotlStore axolotlStore; - private final SessionMap sessions; - private final Map> deviceIds; - private final Map messageCache; - private final FetchStatusMap fetchStatusMap; - private final Map fetchDeviceListStatus = new HashMap<>(); - private final HashMap> fetchDeviceIdsMap = new HashMap<>(); - private final SerialSingleThreadExecutor executor; - private final Set healingAttempts = new HashSet<>(); - private final HashSet cleanedOwnDeviceIds = new HashSet<>(); - private final Set PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT = new HashSet<>(); - private int numPublishTriesOnEmptyPep = 0; - private boolean pepBroken = false; - private int lastDeviceListNotificationHash = 0; - private Set postponedSessions = new HashSet<>(); //sessions stored here will receive after mam catchup treatment - private Set postponedHealing = new HashSet<>(); //addresses stored here will need a healing notification after mam catchup - private AtomicBoolean changeAccessMode = new AtomicBoolean(false); - - public AxolotlService(Account account, XmppConnectionService connectionService) { - if (account == null || connectionService == null) { - throw new IllegalArgumentException("account and service cannot be null"); - } - if (Security.getProvider("BC") == null) { - Security.addProvider(new BouncyCastleProvider()); - } - this.mXmppConnectionService = connectionService; - this.account = account; - this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); - this.deviceIds = new HashMap<>(); - this.messageCache = new HashMap<>(); - this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account); - this.fetchStatusMap = new FetchStatusMap(); - this.executor = new SerialSingleThreadExecutor("Axolotl"); - } - - public static String getLogprefix(Account account) { - return LOGPREFIX + " (" + account.getJid().asBareJid().toString() + "): "; - } - - @Override - public void onAdvancedStreamFeaturesAvailable(Account account) { - if (Config.supportOmemo() - && account.getXmppConnection() != null - && account.getXmppConnection().getFeatures().pep()) { - publishBundlesIfNeeded(true, false); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping OMEMO initialization"); - } - } - - private boolean hasErrorFetchingDeviceList(Jid jid) { - Boolean status = fetchDeviceListStatus.get(jid); - return status != null && !status; - } - - public boolean hasErrorFetchingDeviceList(List jids) { - for (Jid jid : jids) { - if (hasErrorFetchingDeviceList(jid)) { - return true; - } - } - return false; - } - - public boolean fetchMapHasErrors(List jids) { - for (Jid jid : jids) { - if (deviceIds.get(jid) != null) { - for (Integer foreignId : this.deviceIds.get(jid)) { - SignalProtocolAddress address = new SignalProtocolAddress(jid.toString(), foreignId); - if (fetchStatusMap.getAll(address.getName()).containsValue(FetchStatus.ERROR)) { - return true; - } - } - } - } - return false; - } - - public void preVerifyFingerprint(Contact contact, String fingerprint) { - axolotlStore.preVerifyFingerprint(contact.getAccount(), contact.getJid().asBareJid().toString(), fingerprint); - } - - public void preVerifyFingerprint(Account account, String fingerprint) { - axolotlStore.preVerifyFingerprint(account, account.getJid().asBareJid().toString(), fingerprint); - } - - public boolean hasVerifiedKeys(String name) { - for (XmppAxolotlSession session : this.sessions.getAll(name).values()) { - if (session.getTrust().isVerified()) { - return true; - } - } - return false; - } - - public String getOwnFingerprint() { - return CryptoHelper.bytesToHex(axolotlStore.getIdentityKeyPair().getPublicKey().serialize()); - } - - public Set getKeysWithTrust(FingerprintStatus status) { - return axolotlStore.getContactKeysWithTrust(account.getJid().asBareJid().toString(), status); - } - - public Set getKeysWithTrust(FingerprintStatus status, Jid jid) { - return axolotlStore.getContactKeysWithTrust(jid.asBareJid().toString(), status); - } - - public Set getKeysWithTrust(FingerprintStatus status, List jids) { - Set keys = new HashSet<>(); - for (Jid jid : jids) { - keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toString(), status)); - } - return keys; - } - - public Set findCounterpartsBySourceId(int sid) { - return sessions.findCounterpartsForSourceId(sid); - } - - public long getNumTrustedKeys(Jid jid) { - return axolotlStore.getContactNumTrustedKeys(jid.asBareJid().toString()); - } - - public boolean anyTargetHasNoTrustedKeys(List jids) { - for (Jid jid : jids) { - if (axolotlStore.getContactNumTrustedKeys(jid.asBareJid().toString()) == 0) { - return true; - } - } - return false; - } - - private SignalProtocolAddress getAddressForJid(Jid jid) { - return new SignalProtocolAddress(jid.toString(), 0); - } - - public Collection findOwnSessions() { - SignalProtocolAddress ownAddress = getAddressForJid(account.getJid().asBareJid()); - ArrayList s = new ArrayList<>(this.sessions.getAll(ownAddress.getName()).values()); - Collections.sort(s); - return s; - } - - public Collection findSessionsForContact(Contact contact) { - SignalProtocolAddress contactAddress = getAddressForJid(contact.getJid()); - ArrayList s = new ArrayList<>(this.sessions.getAll(contactAddress.getName()).values()); - Collections.sort(s); - return s; - } - - private Set findSessionsForConversation(Conversation conversation) { - if (conversation.getContact().isSelf()) { - //will be added in findOwnSessions() - return Collections.emptySet(); - } - HashSet sessions = new HashSet<>(); - for (Jid jid : conversation.getAcceptedCryptoTargets()) { - sessions.addAll(this.sessions.getAll(getAddressForJid(jid).getName()).values()); - } - return sessions; - } - - private boolean hasAny(Jid jid) { - return sessions.hasAny(getAddressForJid(jid)); - } - - public boolean isPepBroken() { - return this.pepBroken; - } - - public void resetBrokenness() { - this.pepBroken = false; - this.numPublishTriesOnEmptyPep = 0; - this.lastDeviceListNotificationHash = 0; - this.healingAttempts.clear(); - } - - public void clearErrorsInFetchStatusMap(Jid jid) { - fetchStatusMap.clearErrorFor(jid); - fetchDeviceListStatus.remove(jid); - } - - public void regenerateKeys(boolean wipeOther) { - axolotlStore.regenerate(); - sessions.clear(); - fetchStatusMap.clear(); - fetchDeviceIdsMap.clear(); - fetchDeviceListStatus.clear(); - publishBundlesIfNeeded(true, wipeOther); - } - - public void destroy() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": destroying old axolotl service. no longer in use"); - mXmppConnectionService.databaseBackend.wipeAxolotlDb(account); - } - - public AxolotlService makeNew() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": make new axolotl service"); - return new AxolotlService(this.account, this.mXmppConnectionService); - } - - public int getOwnDeviceId() { - return axolotlStore.getLocalRegistrationId(); - } - - public SignalProtocolAddress getOwnAxolotlAddress() { - return new SignalProtocolAddress(account.getJid().asBareJid().toString(), getOwnDeviceId()); - } - - public Set getOwnDeviceIds() { - return this.deviceIds.get(account.getJid().asBareJid()); - } - - public void registerDevices(final Jid jid, @NonNull final Set deviceIds) { - final int hash = deviceIds.hashCode(); - final boolean me = jid.asBareJid().equals(account.getJid().asBareJid()); - if (me) { - if (hash != 0 && hash == this.lastDeviceListNotificationHash) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring duplicate own device id list"); - return; - } - this.lastDeviceListNotificationHash = hash; - } - boolean needsPublishing = me && !deviceIds.contains(getOwnDeviceId()); - if (me) { - deviceIds.remove(getOwnDeviceId()); - } - Set expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.asBareJid().toString())); - expiredDevices.removeAll(deviceIds); - for (Integer deviceId : expiredDevices) { - SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); - XmppAxolotlSession session = sessions.get(address); - if (session != null && session.getFingerprint() != null) { - if (session.getTrust().isActive()) { - session.setTrust(session.getTrust().toInactive()); - } - } - } - Set newDevices = new HashSet<>(deviceIds); - for (Integer deviceId : newDevices) { - SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); - XmppAxolotlSession session = sessions.get(address); - if (session != null && session.getFingerprint() != null) { - if (!session.getTrust().isActive()) { - Log.d(Config.LOGTAG, "reactivating device with fingerprint " + session.getFingerprint()); - session.setTrust(session.getTrust().toActive()); - } - } - } - if (me) { - if (Config.OMEMO_AUTO_EXPIRY != 0) { - needsPublishing |= deviceIds.removeAll(getExpiredDevices()); - } - needsPublishing |= this.changeAccessMode.get(); - for (Integer deviceId : deviceIds) { - SignalProtocolAddress ownDeviceAddress = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); - if (sessions.get(ownDeviceAddress) == null) { - FetchStatus status = fetchStatusMap.get(ownDeviceAddress); - if (status == null || status == FetchStatus.TIMEOUT) { - fetchStatusMap.put(ownDeviceAddress, FetchStatus.PENDING); - this.buildSessionFromPEP(ownDeviceAddress); - } - } - } - if (needsPublishing) { - publishOwnDeviceId(deviceIds); - } - } - final Set oldSet = this.deviceIds.get(jid); - final boolean changed = oldSet == null || oldSet.hashCode() != hash; - this.deviceIds.put(jid, deviceIds); - if (changed) { - mXmppConnectionService.updateConversationUi(); //update the lock icon - mXmppConnectionService.keyStatusUpdated(null); - if (me) { - mXmppConnectionService.updateAccountUi(); - } - } else { - Log.d(Config.LOGTAG, "skipped device list update because it hasn't changed"); - } - } - - public void wipeOtherPepDevices() { - if (pepBroken) { - Log.d(Config.LOGTAG, getLogprefix(account) + "wipeOtherPepDevices called, but PEP is broken. Ignoring... "); - return; - } - Set deviceIds = new HashSet<>(); - deviceIds.add(getOwnDeviceId()); - publishDeviceIdsAndRefineAccessModel(deviceIds); - } - - public void distrustFingerprint(final String fingerprint) { - final String fp = fingerprint.replaceAll("\\s", ""); - final FingerprintStatus fingerprintStatus = axolotlStore.getFingerprintStatus(fp); - axolotlStore.setFingerprintStatus(fp, fingerprintStatus.toUntrusted()); - } - - private void publishOwnDeviceIdIfNeeded() { - if (pepBroken) { - Log.d(Config.LOGTAG, getLogprefix(account) + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... "); - return; - } - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().asBareJid()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Timeout received while retrieving own Device Ids."); - } else { - //TODO consider calling registerDevices only after item-not-found to account for broken PEPs - Element item = mXmppConnectionService.getIqParser().getItem(packet); - Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved own device list: " + deviceIds); - registerDevices(account.getJid().asBareJid(), deviceIds); - } - } - }); - } - - private Set getExpiredDevices() { - Set devices = new HashSet<>(); - for (XmppAxolotlSession session : findOwnSessions()) { - if (session.getTrust().isActive()) { - long diff = System.currentTimeMillis() - session.getTrust().getLastActivation(); - if (diff > Config.OMEMO_AUTO_EXPIRY) { - long lastMessageDiff = System.currentTimeMillis() - mXmppConnectionService.databaseBackend.getLastTimeFingerprintUsed(account, session.getFingerprint()); - long hours = Math.round(lastMessageDiff / (1000 * 60.0 * 60.0)); - if (lastMessageDiff > Config.OMEMO_AUTO_EXPIRY) { - devices.add(session.getRemoteAddress().getDeviceId()); - session.setTrust(session.getTrust().toInactive()); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": added own device " + session.getFingerprint() + " to list of expired devices. Last message received " + hours + " hours ago"); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": own device " + session.getFingerprint() + " was active " + hours + " hours ago"); - } - } //TODO print last activation diff - } - } - return devices; - } - - private void publishOwnDeviceId(Set deviceIds) { - Set deviceIdsCopy = new HashSet<>(deviceIds); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "publishing own device ids"); - if (deviceIdsCopy.isEmpty()) { - if (numPublishTriesOnEmptyPep >= publishTriesThreshold) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Own device publish attempt threshold exceeded, aborting..."); - pepBroken = true; - return; - } else { - numPublishTriesOnEmptyPep++; - Log.w(Config.LOGTAG, getLogprefix(account) + "Own device list empty, attempting to publish (try " + numPublishTriesOnEmptyPep + ")"); - } - } else { - numPublishTriesOnEmptyPep = 0; - } - deviceIdsCopy.add(getOwnDeviceId()); - publishDeviceIdsAndRefineAccessModel(deviceIdsCopy); - } - - private void publishDeviceIdsAndRefineAccessModel(Set ids) { - publishDeviceIdsAndRefineAccessModel(ids, true); - } - - private void publishDeviceIdsAndRefineAccessModel(final Set ids, final boolean firstAttempt) { - final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null; - IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(ids, publishOptions); - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - final Element error = packet.getType() == IqPacket.TYPE.ERROR ? packet.findChild("error") : null; - final boolean preConditionNotMet = PublishOptions.preconditionNotMet(packet); - if (firstAttempt && preConditionNotMet) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for device list. pushing node configuration"); - mXmppConnectionService.pushNodeConfiguration(account, AxolotlService.PEP_DEVICE_LIST, publishOptions, new XmppConnectionService.OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - publishDeviceIdsAndRefineAccessModel(ids, false); - } - - @Override - public void onPushFailed() { - publishDeviceIdsAndRefineAccessModel(ids, false); - } - }); - } else { - if (AxolotlService.this.changeAccessMode.compareAndSet(true, false)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": done changing access mode"); - account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, false); - mXmppConnectionService.databaseBackend.updateAccount(account); - } - if (packet.getType() == IqPacket.TYPE.ERROR) { - if (preConditionNotMet) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": device list pre condition still not met on second attempt"); - } else if (error != null) { - pepBroken = true; - Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error")); - } - - } - } - } - }); - } - - public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord, - final Set preKeyRecords, - final boolean announceAfter, - final boolean wipe) { - try { - IdentityKey axolotlPublicKey = axolotlStore.getIdentityKeyPair().getPublicKey(); - PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias()); - X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias()); - Signature verifier = Signature.getInstance("sha256WithRSA"); - verifier.initSign(x509PrivateKey, SECURE_RANDOM); - verifier.update(axolotlPublicKey.serialize()); - byte[] signature = verifier.sign(); - IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId()); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": publish verification for device " + getOwnDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(final Account account, IqPacket packet) { - String node = AxolotlService.PEP_VERIFICATION + ":" + getOwnDeviceId(); - mXmppConnectionService.pushNodeConfiguration(account, node, PublishOptions.openAccess(), new XmppConnectionService.OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - Log.d(Config.LOGTAG, getLogprefix(account) + "configured verification node to be world readable"); - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe); - } - - @Override - public void onPushFailed() { - Log.d(Config.LOGTAG, getLogprefix(account) + "unable to set access model on verification node"); - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe); - } - }); - } - }); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public void publishBundlesIfNeeded(final boolean announce, final boolean wipe) { - if (pepBroken) { - Log.d(Config.LOGTAG, getLogprefix(account) + "publishBundlesIfNeeded called, but PEP is broken. Ignoring... "); - return; - } - - if (account.getXmppConnection().getFeatures().pepPublishOptions()) { - this.changeAccessMode.set(account.isOptionSet(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE)); - } else { - if (account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, true)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server doesn’t support publish-options. setting for later access mode change"); - mXmppConnectionService.databaseBackend.updateAccount(account); - } - } - if (this.changeAccessMode.get()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server gained publish-options capabilities. changing access model"); - } - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().asBareJid(), getOwnDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - return; //ignore timeout. do nothing - } - - if (packet.getType() == IqPacket.TYPE.ERROR) { - Element error = packet.findChild("error"); - if (error == null || !error.hasChild("item-not-found")) { - pepBroken = true; - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + packet); - return; - } - } - - PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); - Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); - boolean flush = false; - if (bundle == null) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + packet); - bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null); - flush = true; - } - if (keys == null) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + packet); - } - try { - boolean changed = false; - // Validate IdentityKey - IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair(); - if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); - changed = true; - } - - // Validate signedPreKeyRecord + ID - SignedPreKeyRecord signedPreKeyRecord; - int numSignedPreKeys = axolotlStore.getSignedPreKeysCount(); - try { - signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId()); - if (flush - || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) - || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); - signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); - axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); - changed = true; - } - } catch (InvalidKeyIdException e) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); - signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); - axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); - changed = true; - } - - // Validate PreKeys - Set preKeyRecords = new HashSet<>(); - if (keys != null) { - for (Integer id : keys.keySet()) { - try { - PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id); - if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) { - preKeyRecords.add(preKeyRecord); - } - } catch (InvalidKeyIdException ignored) { - } - } - } - int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size(); - if (newKeys > 0) { - List newRecords = KeyHelper.generatePreKeys( - axolotlStore.getCurrentPreKeyId() + 1, newKeys); - preKeyRecords.addAll(newRecords); - for (PreKeyRecord record : newRecords) { - axolotlStore.storePreKey(record.getId(), record); - } - changed = true; - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP."); - } - - - if (changed || changeAccessMode.get()) { - if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) { - mXmppConnectionService.publishDisplayName(account); - publishDeviceVerificationAndBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); - } else { - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); - } - } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "Bundle " + getOwnDeviceId() + " in PEP was current"); - if (wipe) { - wipeOtherPepDevices(); - } else if (announce) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); - publishOwnDeviceIdIfNeeded(); - } - } - } catch (InvalidKeyException e) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); - } - } - }); - } - - private void publishDeviceBundle(SignedPreKeyRecord signedPreKeyRecord, - Set preKeyRecords, - final boolean announceAfter, - final boolean wipe) { - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, true); - } - - private void publishDeviceBundle(final SignedPreKeyRecord signedPreKeyRecord, - final Set preKeyRecords, - final boolean announceAfter, - final boolean wipe, - final boolean firstAttempt) { - final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null; - final IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( - signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), - preKeyRecords, getOwnDeviceId(), publishOptions); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing..."); - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(final Account account, IqPacket packet) { - final boolean preconditionNotMet = PublishOptions.preconditionNotMet(packet); - if (firstAttempt && preconditionNotMet) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for bundle. pushing node configuration"); - final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId(); - mXmppConnectionService.pushNodeConfiguration(account, node, publishOptions, new XmppConnectionService.OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false); - } - - @Override - public void onPushFailed() { - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false); - } - }); - } else if (packet.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Successfully published bundle. "); - if (wipe) { - wipeOtherPepDevices(); - } else if (announceAfter) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); - publishOwnDeviceIdIfNeeded(); - } - } else if (packet.getType() == IqPacket.TYPE.ERROR) { - if (preconditionNotMet) { - Log.d(Config.LOGTAG, getLogprefix(account) + "bundle precondition still not met after second attempt"); - } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + packet.toString()); - } - pepBroken = true; - } - } - }); - } - - public void deleteOmemoIdentity() { - mXmppConnectionService.deletePepNode( - account, AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId()); - final Set ownDeviceIds = getOwnDeviceIds(); - publishDeviceIdsAndRefineAccessModel( - ownDeviceIds == null ? Collections.emptySet() : ownDeviceIds); - } - - public List getCryptoTargets(Conversation conversation) { - final List jids; - if (conversation.getMode() == Conversation.MODE_SINGLE) { - jids = new ArrayList<>(); - jids.add(conversation.getJid().asBareJid()); - } else { - jids = conversation.getMucOptions().getMembers(false); - } - return jids; - } - - public FingerprintStatus getFingerprintTrust(String fingerprint) { - return axolotlStore.getFingerprintStatus(fingerprint); - } - - public X509Certificate getFingerprintCertificate(String fingerprint) { - return axolotlStore.getFingerprintCertificate(fingerprint); - } - - public void setFingerprintTrust(String fingerprint, FingerprintStatus status) { - axolotlStore.setFingerprintStatus(fingerprint, status); - } - - private ListenableFuture verifySessionWithPEP(final XmppAxolotlSession session) { - Log.d(Config.LOGTAG, "trying to verify fresh session (" + session.getRemoteAddress().getName() + ") with pep"); - final SignalProtocolAddress address = session.getRemoteAddress(); - final IdentityKey identityKey = session.getIdentityKey(); - final Jid jid; - try { - jid = Jid.of(address.getName()); - } catch (final IllegalArgumentException e) { - fetchStatusMap.put(address, FetchStatus.SUCCESS); - finishBuildingSessionsFromPEP(address); - return Futures.immediateFuture(session); - } - final SettableFuture future = SettableFuture.create(); - final IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(jid, address.getDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, (account, response) -> { - Pair verification = mXmppConnectionService.getIqParser().verification(response); - if (verification != null) { - try { - Signature verifier = Signature.getInstance("sha256WithRSA"); - verifier.initVerify(verification.first[0]); - verifier.update(identityKey.serialize()); - if (verifier.verify(verification.second)) { - try { - mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA"); - String fingerprint = session.getFingerprint(); - Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: " + fingerprint); - setFingerprintTrust(fingerprint, FingerprintStatus.createActiveVerified(true)); - axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]); - fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED); - Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]); - try { - final String cn = information.getString("subject_cn"); - final Jid jid1 = Jid.of(address.getName()); - Log.d(Config.LOGTAG, "setting common name for " + jid1 + " to " + cn); - account.getRoster().getContact(jid1).setCommonName(cn); - } catch (final IllegalArgumentException ignored) { - //ignored - } - finishBuildingSessionsFromPEP(address); - future.set(session); - return; - } catch (Exception e) { - Log.d(Config.LOGTAG, "could not verify certificate"); - } - } - } catch (Exception e) { - Log.d(Config.LOGTAG, "error during verification " + e.getMessage()); - } - } else { - Log.d(Config.LOGTAG, "no verification found"); - } - fetchStatusMap.put(address, FetchStatus.SUCCESS); - finishBuildingSessionsFromPEP(address); - future.set(session); - }); - return future; - } - - private void finishBuildingSessionsFromPEP(final SignalProtocolAddress address) { - SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().asBareJid().toString(), 0); - Map own = fetchStatusMap.getAll(ownAddress.getName()); - Map remote = fetchStatusMap.getAll(address.getName()); - if (!own.containsValue(FetchStatus.PENDING) && !remote.containsValue(FetchStatus.PENDING)) { - FetchStatus report = null; - if (own.containsValue(FetchStatus.SUCCESS) || remote.containsValue(FetchStatus.SUCCESS)) { - report = FetchStatus.SUCCESS; - } else if (own.containsValue(FetchStatus.SUCCESS_VERIFIED) || remote.containsValue(FetchStatus.SUCCESS_VERIFIED)) { - report = FetchStatus.SUCCESS_VERIFIED; - } else if (own.containsValue(FetchStatus.SUCCESS_TRUSTED) || remote.containsValue(FetchStatus.SUCCESS_TRUSTED)) { - report = FetchStatus.SUCCESS_TRUSTED; - } else if (own.containsValue(FetchStatus.ERROR) || remote.containsValue(FetchStatus.ERROR)) { - report = FetchStatus.ERROR; - } - mXmppConnectionService.keyStatusUpdated(report); - } - if (Config.REMOVE_BROKEN_DEVICES) { - Set ownDeviceIds = new HashSet<>(getOwnDeviceIds()); - boolean publish = false; - for (Map.Entry entry : own.entrySet()) { - int id = entry.getKey(); - if (entry.getValue() == FetchStatus.ERROR && PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT.add(id) && ownDeviceIds.remove(id)) { - publish = true; - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error fetching own device with id " + id + ". removing from announcement"); - } - } - if (publish) { - publishOwnDeviceId(ownDeviceIds); - } - } - } - - public boolean hasEmptyDeviceList(Jid jid) { - return !hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty()); - } - - public void fetchDeviceIds(final Jid jid) { - fetchDeviceIds(jid, null); - } - - private void fetchDeviceIds(final Jid jid, OnDeviceIdsFetched callback) { - IqPacket packet; - synchronized (this.fetchDeviceIdsMap) { - List callbacks = this.fetchDeviceIdsMap.get(jid); - if (callbacks != null) { - if (callback != null) { - callbacks.add(callback); - } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching device ids for " + jid + " already running. adding callback"); - packet = null; - } else { - callbacks = new ArrayList<>(); - if (callback != null) { - callbacks.add(callback); - } - this.fetchDeviceIdsMap.put(jid, callbacks); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching device ids for " + jid); - packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(jid); - } - } - if (packet != null) { - mXmppConnectionService.sendIqPacket(account, packet, (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - fetchDeviceListStatus.put(jid, true); - Element item = mXmppConnectionService.getIqParser().getItem(response); - Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); - registerDevices(jid, deviceIds); - final List callbacks; - synchronized (fetchDeviceIdsMap) { - callbacks = fetchDeviceIdsMap.remove(jid); - } - if (callbacks != null) { - for (OnDeviceIdsFetched c : callbacks) { - c.fetched(jid, deviceIds); - } - } - } else { - if (response.getType() == IqPacket.TYPE.TIMEOUT) { - fetchDeviceListStatus.remove(jid); - } else { - fetchDeviceListStatus.put(jid, false); - } - final List callbacks; - synchronized (fetchDeviceIdsMap) { - callbacks = fetchDeviceIdsMap.remove(jid); - } - if (callbacks != null) { - for (OnDeviceIdsFetched c : callbacks) { - c.fetched(jid, null); - } - } - } - }); - } - } - - private void fetchDeviceIds(List jids, final OnMultipleDeviceIdFetched callback) { - final ArrayList unfinishedJids = new ArrayList<>(jids); - synchronized (unfinishedJids) { - for (Jid jid : unfinishedJids) { - fetchDeviceIds(jid, (j, deviceIds) -> { - synchronized (unfinishedJids) { - unfinishedJids.remove(j); - if (unfinishedJids.size() == 0 && callback != null) { - callback.fetched(); - } - } - }); - } - } - } - - private ListenableFuture buildSessionFromPEP(final SignalProtocolAddress address) { - return buildSessionFromPEP(address, null); - } - - private ListenableFuture buildSessionFromPEP(final SignalProtocolAddress address, OnSessionBuildFromPep callback) { - final SettableFuture sessionSettableFuture = SettableFuture.create(); - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new session for " + address.toString()); - if (address.equals(getOwnAxolotlAddress())) { - throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!"); - } - final Jid jid = Jid.of(address.getName()); - final boolean oneOfOurs = jid.asBareJid().equals(account.getJid().asBareJid()); - IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(jid, address.getDeviceId()); - mXmppConnectionService.sendIqPacket(account, bundlesPacket, (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - fetchStatusMap.put(address, FetchStatus.TIMEOUT); - sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. Timeout")); - } else if (packet.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing..."); - final IqParser parser = mXmppConnectionService.getIqParser(); - final List preKeyBundleList = parser.preKeys(packet); - final PreKeyBundle bundle = parser.bundle(packet); - if (preKeyBundleList.isEmpty() || bundle == null) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet); - fetchStatusMap.put(address, FetchStatus.ERROR); - finishBuildingSessionsFromPEP(address); - if (callback != null) { - callback.onSessionBuildFailed(); - } - sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. IQ Packet Invalid")); - return; - } - Random random = new Random(); - final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); - if (preKey == null) { - //should never happen - fetchStatusMap.put(address, FetchStatus.ERROR); - finishBuildingSessionsFromPEP(address); - if (callback != null) { - callback.onSessionBuildFailed(); - } - sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. No suitable PreKey found")); - return; - } - - final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(), - preKey.getPreKeyId(), preKey.getPreKey(), - bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), - bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); - - try { - SessionBuilder builder = new SessionBuilder(axolotlStore, address); - builder.process(preKeyBundle); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey()); - sessions.put(address, session); - if (Config.X509_VERIFICATION) { - sessionSettableFuture.setFuture(verifySessionWithPEP(session)); //TODO; maybe inject callback in here too - } else { - FingerprintStatus status = getFingerprintTrust(CryptoHelper.bytesToHex(bundle.getIdentityKey().getPublicKey().serialize())); - FetchStatus fetchStatus; - if (status != null && status.isVerified()) { - fetchStatus = FetchStatus.SUCCESS_VERIFIED; - } else if (status != null && status.isTrusted()) { - fetchStatus = FetchStatus.SUCCESS_TRUSTED; - } else { - fetchStatus = FetchStatus.SUCCESS; - } - fetchStatusMap.put(address, fetchStatus); - finishBuildingSessionsFromPEP(address); - if (callback != null) { - callback.onSessionBuildSuccessful(); - } - sessionSettableFuture.set(session); - } - } catch (UntrustedIdentityException | InvalidKeyException e) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": " - + e.getClass().getName() + ", " + e.getMessage()); - fetchStatusMap.put(address, FetchStatus.ERROR); - finishBuildingSessionsFromPEP(address); - if (oneOfOurs && cleanedOwnDeviceIds.add(address.getDeviceId())) { - removeFromDeviceAnnouncement(address.getDeviceId()); - } - if (callback != null) { - callback.onSessionBuildFailed(); - } - sessionSettableFuture.setException(new CryptoFailedException(e)); - } - } else { - fetchStatusMap.put(address, FetchStatus.ERROR); - Element error = packet.findChild("error"); - boolean itemNotFound = error != null && error.hasChild("item-not-found"); - Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while building session:" + packet.findChild("error")); - finishBuildingSessionsFromPEP(address); - if (oneOfOurs && itemNotFound && cleanedOwnDeviceIds.add(address.getDeviceId())) { - removeFromDeviceAnnouncement(address.getDeviceId()); - } - if (callback != null) { - callback.onSessionBuildFailed(); - } - sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. IQ Packet Error")); - } - }); - return sessionSettableFuture; - } - - private void removeFromDeviceAnnouncement(Integer id) { - HashSet temp = new HashSet<>(getOwnDeviceIds()); - if (temp.remove(id)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + " remove own device id " + id + " from announcement. devices left:" + temp); - publishOwnDeviceId(temp); - } - } - - public Set findDevicesWithoutSession(final Conversation conversation) { - Set addresses = new HashSet<>(); - for (Jid jid : getCryptoTargets(conversation)) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + jid); - final Set ids = deviceIds.get(jid); - if (ids != null && !ids.isEmpty()) { - for (Integer foreignId : ids) { - SignalProtocolAddress address = new SignalProtocolAddress(jid.toString(), foreignId); - if (sessions.get(address) == null) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); - if (identityKey != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); - sessions.put(address, session); - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + jid + ":" + foreignId); - if (fetchStatusMap.get(address) != FetchStatus.ERROR) { - addresses.add(address); - } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken"); - } - } - } - } - } else { - mXmppConnectionService.keyStatusUpdated(FetchStatus.ERROR); - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!"); - } - } - Set ownIds = this.deviceIds.get(account.getJid().asBareJid()); - for (Integer ownId : (ownIds != null ? ownIds : new HashSet())) { - SignalProtocolAddress address = new SignalProtocolAddress(account.getJid().asBareJid().toString(), ownId); - if (sessions.get(address) == null) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); - if (identityKey != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); - sessions.put(address, session); - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().asBareJid() + ":" + ownId); - if (fetchStatusMap.get(address) != FetchStatus.ERROR) { - addresses.add(address); - } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken"); - } - } - } - } - - return addresses; - } - - public boolean createSessionsIfNeeded(final Conversation conversation) { - final List jidsWithEmptyDeviceList = getCryptoTargets(conversation); - for (Iterator iterator = jidsWithEmptyDeviceList.iterator(); iterator.hasNext(); ) { - final Jid jid = iterator.next(); - if (!hasEmptyDeviceList(jid)) { - iterator.remove(); - } - } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": createSessionsIfNeeded() - jids with empty device list: " + jidsWithEmptyDeviceList); - if (jidsWithEmptyDeviceList.size() > 0) { - fetchDeviceIds(jidsWithEmptyDeviceList, () -> createSessionsIfNeededActual(conversation)); - return true; - } else { - return createSessionsIfNeededActual(conversation); - } - } - - private boolean createSessionsIfNeededActual(final Conversation conversation) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed..."); - boolean newSessions = false; - Set addresses = findDevicesWithoutSession(conversation); - for (SignalProtocolAddress address : addresses) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString()); - FetchStatus status = fetchStatusMap.get(address); - if (status == null || status == FetchStatus.TIMEOUT) { - fetchStatusMap.put(address, FetchStatus.PENDING); - this.buildSessionFromPEP(address); - newSessions = true; - } else if (status == FetchStatus.PENDING) { - newSessions = true; - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString()); - } - } - - return newSessions; - } - - public boolean trustedSessionVerified(final Conversation conversation) { - final Set sessions = new HashSet<>(); - sessions.addAll(findSessionsForConversation(conversation)); - sessions.addAll(findOwnSessions()); - boolean verified = false; - for (XmppAxolotlSession session : sessions) { - if (session.getTrust().isTrustedAndActive()) { - if (session.getTrust().getTrust() == FingerprintStatus.Trust.VERIFIED_X509) { - verified = true; - } else { - return false; - } - } - } - return verified; - } - - public boolean hasPendingKeyFetches(List jids) { - SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().asBareJid().toString(), 0); - if (fetchStatusMap.getAll(ownAddress.getName()).containsValue(FetchStatus.PENDING)) { - return true; - } - synchronized (this.fetchDeviceIdsMap) { - for (Jid jid : jids) { - SignalProtocolAddress foreignAddress = new SignalProtocolAddress(jid.asBareJid().toString(), 0); - if (fetchStatusMap.getAll(foreignAddress.getName()).containsValue(FetchStatus.PENDING) || this.fetchDeviceIdsMap.containsKey(jid)) { - return true; - } - } - } - return false; - } - - @Nullable - private boolean buildHeader(XmppAxolotlMessage axolotlMessage, Conversation c) { - Set remoteSessions = findSessionsForConversation(c); - final boolean acceptEmpty = (c.getMode() == Conversation.MODE_MULTI && c.getMucOptions().getUserCount() == 0) || c.getContact().isSelf(); - Collection ownSessions = findOwnSessions(); - if (remoteSessions.isEmpty() && !acceptEmpty) { - return false; - } - for (XmppAxolotlSession session : remoteSessions) { - axolotlMessage.addDevice(session); - } - for (XmppAxolotlSession session : ownSessions) { - axolotlMessage.addDevice(session); - } - - return true; - } - - //this is being used for private muc messages only - private boolean buildHeader(XmppAxolotlMessage axolotlMessage, Jid jid) { - if (jid == null) { - return false; - } - HashSet sessions = new HashSet<>(); - sessions.addAll(this.sessions.getAll(getAddressForJid(jid).getName()).values()); - if (sessions.isEmpty()) { - return false; - } - sessions.addAll(findOwnSessions()); - for (XmppAxolotlSession session : sessions) { - axolotlMessage.addDevice(session); - } - return true; - } - - @Nullable - public XmppAxolotlMessage encrypt(Message message) { - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); - final String content; - if (message.hasFileOnRemoteHost()) { - content = message.getFileParams().url; - } else { - content = message.getBody(); - } - try { - axolotlMessage.encrypt(content); - } catch (CryptoFailedException e) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage()); - return null; - } - - final boolean success; - if (message.isPrivateMessage()) { - success = buildHeader(axolotlMessage, message.getTrueCounterpart()); - } else { - success = buildHeader(axolotlMessage, (Conversation) message.getConversation()); - } - return success ? axolotlMessage : null; - } - - public void preparePayloadMessage(final Message message, final boolean delay) { - executor.execute(new Runnable() { - @Override - public void run() { - XmppAxolotlMessage axolotlMessage = encrypt(message); - if (axolotlMessage == null) { - mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); - //mXmppConnectionService.updateConversationUi(); - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Generated message, caching: " + message.getUuid()); - messageCache.put(message.getUuid(), axolotlMessage); - mXmppConnectionService.resendMessage(message, delay); - } - } - }); - } - - private OmemoVerifiedIceUdpTransportInfo encrypt(final IceUdpTransportInfo element, final XmppAxolotlSession session) throws CryptoFailedException { - final OmemoVerifiedIceUdpTransportInfo transportInfo = new OmemoVerifiedIceUdpTransportInfo(); - transportInfo.setAttributes(element.getAttributes()); - for (final Element child : element.getChildren()) { - if ("fingerprint".equals(child.getName()) && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) { - final Element fingerprint = new Element("fingerprint", Namespace.OMEMO_DTLS_SRTP_VERIFICATION); - fingerprint.setAttribute("setup", child.getAttribute("setup")); - fingerprint.setAttribute("hash", child.getAttribute("hash")); - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); - final String content = child.getContent(); - axolotlMessage.encrypt(content); - axolotlMessage.addDevice(session, true); - fingerprint.addChild(axolotlMessage.toElement()); - transportInfo.addChild(fingerprint); - } else { - transportInfo.addChild(child); - } - } - return transportInfo; - } - - - public ListenableFuture> encrypt(final RtpContentMap rtpContentMap, final Jid jid, final int deviceId) { - return Futures.transformAsync( - getSession(jid, deviceId), - session -> encrypt(rtpContentMap, session), - MoreExecutors.directExecutor() - ); - } - - private ListenableFuture> encrypt(final RtpContentMap rtpContentMap, final XmppAxolotlSession session) { - if (Config.REQUIRE_RTP_VERIFICATION) { - requireVerification(session); - } - final ImmutableMap.Builder descriptionTransportBuilder = new ImmutableMap.Builder<>(); - final OmemoVerification omemoVerification = new OmemoVerification(); - omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId()); - omemoVerification.setSessionFingerprint(session.getFingerprint()); - for (final Map.Entry content : rtpContentMap.contents.entrySet()) { - final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue(); - final OmemoVerifiedIceUdpTransportInfo encryptedTransportInfo; - try { - encryptedTransportInfo = encrypt(descriptionTransport.transport, session); - } catch (final CryptoFailedException e) { - return Futures.immediateFailedFuture(e); - } - descriptionTransportBuilder.put( - content.getKey(), - new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo) - ); - } - return Futures.immediateFuture( - new OmemoVerifiedPayload<>( - omemoVerification, - new OmemoVerifiedRtpContentMap(rtpContentMap.group, descriptionTransportBuilder.build()) - )); - } - - private ListenableFuture getSession(final Jid jid, final int deviceId) { - final SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); - final XmppAxolotlSession session = sessions.get(address); - if (session == null) { - return buildSessionFromPEP(address); - } - return Futures.immediateFuture(session); - } - - public ListenableFuture> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) { - final ImmutableMap.Builder descriptionTransportBuilder = new ImmutableMap.Builder<>(); - final OmemoVerification omemoVerification = new OmemoVerification(); - final ImmutableList.Builder> pepVerificationFutures = new ImmutableList.Builder<>(); - for (final Map.Entry content : omemoVerifiedRtpContentMap.contents.entrySet()) { - final RtpContentMap.DescriptionTransport descriptionTransport = content.getValue(); - final OmemoVerifiedPayload decryptedTransport; - try { - decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from, pepVerificationFutures); - } catch (CryptoFailedException e) { - return Futures.immediateFailedFuture(e); - } - omemoVerification.setOrEnsureEqual(decryptedTransport); - descriptionTransportBuilder.put( - content.getKey(), - new RtpContentMap.DescriptionTransport(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload) - ); - } - processPostponed(); - final ImmutableList> sessionFutures = pepVerificationFutures.build(); - return Futures.transform( - Futures.allAsList(sessionFutures), - sessions -> { - if (Config.REQUIRE_RTP_VERIFICATION) { - for (XmppAxolotlSession session : sessions) { - requireVerification(session); - } - } - return new OmemoVerifiedPayload<>( - omemoVerification, - new RtpContentMap(omemoVerifiedRtpContentMap.group, descriptionTransportBuilder.build()) - ); - - }, - MoreExecutors.directExecutor() - ); - } - - private OmemoVerifiedPayload decrypt(final OmemoVerifiedIceUdpTransportInfo verifiedIceUdpTransportInfo, final Jid from, ImmutableList.Builder> pepVerificationFutures) throws CryptoFailedException { - final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); - transportInfo.setAttributes(verifiedIceUdpTransportInfo.getAttributes()); - final OmemoVerification omemoVerification = new OmemoVerification(); - for (final Element child : verifiedIceUdpTransportInfo.getChildren()) { - if ("fingerprint".equals(child.getName()) && Namespace.OMEMO_DTLS_SRTP_VERIFICATION.equals(child.getNamespace())) { - final Element fingerprint = new Element("fingerprint", Namespace.JINGLE_APPS_DTLS); - fingerprint.setAttribute("setup", child.getAttribute("setup")); - fingerprint.setAttribute("hash", child.getAttribute("hash")); - final Element encrypted = child.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); - final XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, from.asBareJid()); - final XmppAxolotlSession session = getReceivingSession(xmppAxolotlMessage); - final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintext = xmppAxolotlMessage.decrypt(session, getOwnDeviceId()); - final Integer preKeyId = session.getPreKeyIdAndReset(); - if (preKeyId != null) { - postponedSessions.add(session); - } - if (session.isFresh()) { - pepVerificationFutures.add(putFreshSession(session)); - } else if (Config.REQUIRE_RTP_VERIFICATION) { - pepVerificationFutures.add(Futures.immediateFuture(session)); - } - fingerprint.setContent(plaintext.getPlaintext()); - omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId()); - omemoVerification.setSessionFingerprint(plaintext.getFingerprint()); - transportInfo.addChild(fingerprint); - } else { - transportInfo.addChild(child); - } - } - return new OmemoVerifiedPayload<>(omemoVerification, transportInfo); - } - - private static void requireVerification(final XmppAxolotlSession session) { - if (session.getTrust().isVerified()) { - return; - } - throw new NotVerifiedException(String.format( - "session with %s was not verified", - session.getFingerprint() - )); - } - - public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) { - executor.execute(new Runnable() { - @Override - public void run() { - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); - if (buildHeader(axolotlMessage, conversation)) { - onMessageCreatedCallback.run(axolotlMessage); - } else { - onMessageCreatedCallback.run(null); - } - } - }); - } - - public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) { - XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid()); - if (axolotlMessage != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache hit: " + message.getUuid()); - messageCache.remove(message.getUuid()); - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache miss: " + message.getUuid()); - } - return axolotlMessage; - } - - private XmppAxolotlSession recreateUncachedSession(SignalProtocolAddress address) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); - return (identityKey != null) - ? new XmppAxolotlSession(account, axolotlStore, address, identityKey) - : null; - } - - private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) { - SignalProtocolAddress senderAddress = new SignalProtocolAddress(message.getFrom().toString(), message.getSenderDeviceId()); - return getReceivingSession(senderAddress); - - } - - private XmppAxolotlSession getReceivingSession(SignalProtocolAddress senderAddress) { - XmppAxolotlSession session = sessions.get(senderAddress); - if (session == null) { - session = recreateUncachedSession(senderAddress); - if (session == null) { - session = new XmppAxolotlSession(account, axolotlStore, senderAddress); - } - } - return session; - } - - public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message, boolean postponePreKeyMessageHandling) throws NotEncryptedForThisDeviceException, BrokenSessionException, OutdatedSenderException { - XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; - - XmppAxolotlSession session = getReceivingSession(message); - int ownDeviceId = getOwnDeviceId(); - try { - plaintextMessage = message.decrypt(session, ownDeviceId); - Integer preKeyId = session.getPreKeyIdAndReset(); - if (preKeyId != null) { - postPreKeyMessageHandling(session, postponePreKeyMessageHandling); - } - } catch (NotEncryptedForThisDeviceException e) { - if (account.getJid().asBareJid().equals(message.getFrom().asBareJid()) && message.getSenderDeviceId() == ownDeviceId) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Reflected omemo message received"); - } else { - throw e; - } - } catch (final BrokenSessionException e) { - throw e; - } catch (final OutdatedSenderException e) { - Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage()); - throw e; - } catch (CryptoFailedException e) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from " + message.getFrom(), e); - } - - if (session.isFresh() && plaintextMessage != null) { - putFreshSession(session); - } - - return plaintextMessage; - } - - public void reportBrokenSessionException(BrokenSessionException e, boolean postpone) { - Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": broken session with " + e.getSignalProtocolAddress().toString() + " detected", e); - if (postpone) { - postponedHealing.add(e.getSignalProtocolAddress()); - } else { - notifyRequiresHealing(e.getSignalProtocolAddress()); - } - } - - private void notifyRequiresHealing(final SignalProtocolAddress signalProtocolAddress) { - if (healingAttempts.add(signalProtocolAddress)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": attempt to heal " + signalProtocolAddress); - buildSessionFromPEP(signalProtocolAddress, new OnSessionBuildFromPep() { - @Override - public void onSessionBuildSuccessful() { - Log.d(Config.LOGTAG, "successfully build new session from pep after detecting broken session"); - completeSession(getReceivingSession(signalProtocolAddress)); - } - - @Override - public void onSessionBuildFailed() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to build new session from pep after detecting broken session"); - } - }); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": do not attempt to heal " + signalProtocolAddress + " again"); - } - } - - private void postPreKeyMessageHandling(final XmppAxolotlSession session, final boolean postpone) { - if (postpone) { - postponedSessions.add(session); - } else { - if (axolotlStore.flushPreKeys()) { - publishBundlesIfNeeded(false, false); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": nothing to flush. Not republishing key"); - } - if (trustedOrPreviouslyResponded(session) && Config.AUTOMATICALLY_COMPLETE_SESSIONS) { - completeSession(session); - } - } - } - - public void processPostponed() { - if (postponedSessions.size() > 0) { - if (axolotlStore.flushPreKeys()) { - publishBundlesIfNeeded(false, false); - } - } - final Iterator iterator = postponedSessions.iterator(); - while (iterator.hasNext()) { - final XmppAxolotlSession session = iterator.next(); - if (trustedOrPreviouslyResponded(session) && Config.AUTOMATICALLY_COMPLETE_SESSIONS) { - completeSession(session); - } - iterator.remove(); - } - final Iterator postponedHealingAttemptsIterator = postponedHealing.iterator(); - while (postponedHealingAttemptsIterator.hasNext()) { - notifyRequiresHealing(postponedHealingAttemptsIterator.next()); - postponedHealingAttemptsIterator.remove(); - } - } - - private boolean trustedOrPreviouslyResponded(XmppAxolotlSession session) { - try { - return trustedOrPreviouslyResponded(Jid.of(session.getRemoteAddress().getName())); - } catch (IllegalArgumentException e) { - return false; - } - } - - public boolean trustedOrPreviouslyResponded(Jid jid) { - final Contact contact = account.getRoster().getContact(jid); - if (contact.showInRoster() || contact.isSelf()) { - return true; - } - final Conversation conversation = mXmppConnectionService.find(account, jid); - return conversation != null && conversation.sentMessagesCount() > 0; - } - - private void completeSession(XmppAxolotlSession session) { - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); - axolotlMessage.addDevice(session, true); - try { - final Jid jid = Jid.of(session.getRemoteAddress().getName()); - MessagePacket packet = mXmppConnectionService.getMessageGenerator().generateKeyTransportMessage(jid, axolotlMessage); - mXmppConnectionService.sendMessagePacket(account, packet); - } catch (IllegalArgumentException e) { - throw new Error("Remote addresses are created from jid and should convert back to jid", e); - } - } - - public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message, final boolean postponePreKeyMessageHandling) { - final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage; - final XmppAxolotlSession session = getReceivingSession(message); - try { - keyTransportMessage = message.getParameters(session, getOwnDeviceId()); - Integer preKeyId = session.getPreKeyIdAndReset(); - if (preKeyId != null) { - postPreKeyMessageHandling(session, postponePreKeyMessageHandling); - } - } catch (CryptoFailedException e) { - Log.d(Config.LOGTAG, "could not decrypt keyTransport message " + e.getMessage()); - return null; - } - - if (session.isFresh() && keyTransportMessage != null) { - putFreshSession(session); - } - - return keyTransportMessage; - } - - private ListenableFuture putFreshSession(XmppAxolotlSession session) { - sessions.put(session); - if (Config.X509_VERIFICATION) { - if (session.getIdentityKey() != null) { - return verifySessionWithPEP(session); - } else { - Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": identity key was empty after reloading for x509 verification"); - } - } - return Futures.immediateFuture(session); - } - - public enum FetchStatus { - PENDING, - SUCCESS, - SUCCESS_VERIFIED, - TIMEOUT, - SUCCESS_TRUSTED, - ERROR - } - - public interface OnDeviceIdsFetched { - void fetched(Jid jid, Set deviceIds); - } - - - public interface OnMultipleDeviceIdFetched { - void fetched(); - } - - interface OnSessionBuildFromPep { - void onSessionBuildSuccessful(); - - void onSessionBuildFailed(); - } - - private static class AxolotlAddressMap { - protected final Object MAP_LOCK = new Object(); - protected Map> map; - - public AxolotlAddressMap() { - this.map = new HashMap<>(); - } - - public void put(SignalProtocolAddress address, T value) { - synchronized (MAP_LOCK) { - Map devices = map.get(address.getName()); - if (devices == null) { - devices = new HashMap<>(); - map.put(address.getName(), devices); - } - devices.put(address.getDeviceId(), value); - } - } - - public T get(SignalProtocolAddress address) { - synchronized (MAP_LOCK) { - Map devices = map.get(address.getName()); - if (devices == null) { - return null; - } - return devices.get(address.getDeviceId()); - } - } - - public Map getAll(String name) { - synchronized (MAP_LOCK) { - Map devices = map.get(name); - if (devices == null) { - return new HashMap<>(); - } - return devices; - } - } - - public boolean hasAny(SignalProtocolAddress address) { - synchronized (MAP_LOCK) { - Map devices = map.get(address.getName()); - return devices != null && !devices.isEmpty(); - } - } - - public void clear() { - map.clear(); - } - - } - - private static class SessionMap extends AxolotlAddressMap { - private final XmppConnectionService xmppConnectionService; - private final Account account; - - public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) { - super(); - this.xmppConnectionService = service; - this.account = account; - this.fillMap(store); - } - - public Set findCounterpartsForSourceId(Integer sid) { - Set candidates = new HashSet<>(); - synchronized (MAP_LOCK) { - for (Map.Entry> entry : map.entrySet()) { - String key = entry.getKey(); - if (entry.getValue().containsKey(sid)) { - candidates.add(Jid.of(key)); - } - } - } - return candidates; - } - - private void putDevicesForJid(String bareJid, List deviceIds, SQLiteAxolotlStore store) { - for (Integer deviceId : deviceIds) { - SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(bareJid, deviceId); - IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey(); - if (Config.X509_VERIFICATION) { - X509Certificate certificate = store.getFingerprintCertificate(CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize())); - if (certificate != null) { - Bundle information = CryptoHelper.extractCertificateInformation(certificate); - try { - final String cn = information.getString("subject_cn"); - final Jid jid = Jid.of(bareJid); - Log.d(Config.LOGTAG, "setting common name for " + jid + " to " + cn); - account.getRoster().getContact(jid).setCommonName(cn); - } catch (final IllegalArgumentException ignored) { - //ignored - } - } - } - this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey)); - } - } - - private void fillMap(SQLiteAxolotlStore store) { - List deviceIds = store.getSubDeviceSessions(account.getJid().asBareJid().toString()); - putDevicesForJid(account.getJid().asBareJid().toString(), deviceIds, store); - for (String address : store.getKnownAddresses()) { - deviceIds = store.getSubDeviceSessions(address); - putDevicesForJid(address, deviceIds, store); - } - } - - @Override - public void put(SignalProtocolAddress address, XmppAxolotlSession value) { - super.put(address, value); - value.setNotFresh(); - } - - public void put(XmppAxolotlSession session) { - this.put(session.getRemoteAddress(), session); - } - } - - private static class FetchStatusMap extends AxolotlAddressMap { - - public void clearErrorFor(Jid jid) { - synchronized (MAP_LOCK) { - Map devices = this.map.get(jid.asBareJid().toString()); - if (devices == null) { - return; - } - for (Map.Entry entry : devices.entrySet()) { - if (entry.getValue() == FetchStatus.ERROR) { - Log.d(Config.LOGTAG, "resetting error for " + jid.asBareJid() + "(" + entry.getKey() + ")"); - entry.setValue(FetchStatus.TIMEOUT); - } - } - } - } - } - - public static class OmemoVerifiedPayload { - private final int deviceId; - private final String fingerprint; - private final T payload; - - private OmemoVerifiedPayload(OmemoVerification omemoVerification, T payload) { - this.deviceId = omemoVerification.getDeviceId(); - this.fingerprint = omemoVerification.getFingerprint(); - this.payload = payload; - } - - public int getDeviceId() { - return deviceId; - } - - public String getFingerprint() { - return fingerprint; - } - - public T getPayload() { - return payload; - } - } - - public static class NotVerifiedException extends SecurityException { - - public NotVerifiedException(String message) { - super(message); - } - - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/BrokenSessionException.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/BrokenSessionException.java deleted file mode 100644 index 1096a8556..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/BrokenSessionException.java +++ /dev/null @@ -1,18 +0,0 @@ -package eu.siacs.conversations.crypto.axolotl; - -import org.whispersystems.libsignal.SignalProtocolAddress; - -public class BrokenSessionException extends CryptoFailedException { - - private final SignalProtocolAddress signalProtocolAddress; - - public BrokenSessionException(SignalProtocolAddress address, Exception e) { - super(e); - this.signalProtocolAddress = address; - - } - - public SignalProtocolAddress getSignalProtocolAddress() { - return signalProtocolAddress; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/CryptoFailedException.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/CryptoFailedException.java deleted file mode 100644 index 0a7a90757..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/CryptoFailedException.java +++ /dev/null @@ -1,16 +0,0 @@ -package eu.siacs.conversations.crypto.axolotl; - -public class CryptoFailedException extends Exception { - - public CryptoFailedException(String msg) { - super(msg); - } - - public CryptoFailedException(String msg, Exception e) { - super(msg, e); - } - - public CryptoFailedException(Exception e) { - super(e); - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java deleted file mode 100644 index 6ba46d004..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java +++ /dev/null @@ -1,184 +0,0 @@ -package eu.siacs.conversations.crypto.axolotl; - -import android.content.ContentValues; -import android.database.Cursor; - -public class FingerprintStatus implements Comparable { - - private static final long DO_NOT_OVERWRITE = -1; - - private Trust trust = Trust.UNTRUSTED; - private boolean active = false; - private long lastActivation = DO_NOT_OVERWRITE; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - FingerprintStatus that = (FingerprintStatus) o; - - return active == that.active && trust == that.trust; - } - - @Override - public int hashCode() { - int result = trust.hashCode(); - result = 31 * result + (active ? 1 : 0); - return result; - } - - private FingerprintStatus() { - - - } - - public ContentValues toContentValues() { - final ContentValues contentValues = new ContentValues(); - contentValues.put(SQLiteAxolotlStore.TRUST, trust.toString()); - contentValues.put(SQLiteAxolotlStore.ACTIVE, active ? 1 : 0); - if (lastActivation != DO_NOT_OVERWRITE) { - contentValues.put(SQLiteAxolotlStore.LAST_ACTIVATION,lastActivation); - } - return contentValues; - } - - public static FingerprintStatus fromCursor(Cursor cursor) { - final FingerprintStatus status = new FingerprintStatus(); - try { - status.trust = Trust.valueOf(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.TRUST))); - } catch (IllegalArgumentException e) { - status.trust = Trust.UNTRUSTED; - } - status.active = cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.ACTIVE)) > 0; - status.lastActivation = cursor.getLong(cursor.getColumnIndex(SQLiteAxolotlStore.LAST_ACTIVATION)); - return status; - } - - public static FingerprintStatus createActiveUndecided() { - final FingerprintStatus status = new FingerprintStatus(); - status.trust = Trust.UNDECIDED; - status.active = true; - status.lastActivation = System.currentTimeMillis(); - return status; - } - - public static FingerprintStatus createActiveTrusted() { - final FingerprintStatus status = new FingerprintStatus(); - status.trust = Trust.TRUSTED; - status.active = true; - status.lastActivation = System.currentTimeMillis(); - return status; - } - - public static FingerprintStatus createActiveVerified(boolean x509) { - final FingerprintStatus status = new FingerprintStatus(); - status.trust = x509 ? Trust.VERIFIED_X509 : Trust.VERIFIED; - status.active = true; - return status; - } - - public static FingerprintStatus createActive(Boolean trusted) { - return createActive(trusted != null && trusted); - } - - public static FingerprintStatus createActive(boolean trusted) { - final FingerprintStatus status = new FingerprintStatus(); - status.trust = trusted ? Trust.TRUSTED : Trust.UNTRUSTED; - status.active = true; - return status; - } - - public boolean isTrustedAndActive() { - return active && isTrusted(); - } - - public boolean isTrusted() { - return trust == Trust.TRUSTED || isVerified(); - } - - public boolean isVerified() { - return trust == Trust.VERIFIED || trust == Trust.VERIFIED_X509; - } - - public boolean isCompromised() { - return trust == Trust.COMPROMISED; - } - - public boolean isActive() { - return active; - } - - public FingerprintStatus toActive() { - FingerprintStatus status = new FingerprintStatus(); - status.trust = trust; - if (!status.active) { - status.lastActivation = System.currentTimeMillis(); - } - status.active = true; - return status; - } - - public FingerprintStatus toInactive() { - FingerprintStatus status = new FingerprintStatus(); - status.trust = trust; - status.active = false; - return status; - } - - public Trust getTrust() { - return trust; - } - - public FingerprintStatus toVerified() { - FingerprintStatus status = new FingerprintStatus(); - status.active = active; - status.trust = Trust.VERIFIED; - return status; - } - - public FingerprintStatus toUntrusted() { - FingerprintStatus status = new FingerprintStatus(); - status.active = active; - status.trust = Trust.UNTRUSTED; - return status; - } - - public static FingerprintStatus createInactiveVerified() { - final FingerprintStatus status = new FingerprintStatus(); - status.trust = Trust.VERIFIED; - status.active = false; - return status; - } - - @Override - public int compareTo(FingerprintStatus o) { - if (active == o.active) { - if (lastActivation > o.lastActivation) { - return -1; - } else if (lastActivation < o.lastActivation) { - return 1; - } else { - return 0; - } - } else if (active){ - return -1; - } else { - return 1; - } - } - - public long getLastActivation() { - return lastActivation; - } - - public enum Trust { - COMPROMISED, - UNDECIDED, - UNTRUSTED, - TRUSTED, - VERIFIED, - VERIFIED_X509 - } - -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java deleted file mode 100644 index 7d6bfc1bd..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java +++ /dev/null @@ -1,4 +0,0 @@ -package eu.siacs.conversations.crypto.axolotl; - -public class NoSessionsCreatedException extends Throwable { -} diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/NotEncryptedForThisDeviceException.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/NotEncryptedForThisDeviceException.java deleted file mode 100644 index 5910d7b29..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/NotEncryptedForThisDeviceException.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package eu.siacs.conversations.crypto.axolotl; - -public class NotEncryptedForThisDeviceException extends CryptoFailedException { - public NotEncryptedForThisDeviceException() { - super("Message was not encrypted for this device"); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/OnMessageCreatedCallback.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/OnMessageCreatedCallback.java deleted file mode 100644 index 5d30915df..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/OnMessageCreatedCallback.java +++ /dev/null @@ -1,5 +0,0 @@ -package eu.siacs.conversations.crypto.axolotl; - -public interface OnMessageCreatedCallback { - void run(XmppAxolotlMessage message); -} diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/OutdatedSenderException.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/OutdatedSenderException.java deleted file mode 100644 index 7a9605bb2..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/OutdatedSenderException.java +++ /dev/null @@ -1,8 +0,0 @@ -package eu.siacs.conversations.crypto.axolotl; - -public class OutdatedSenderException extends CryptoFailedException { - - public OutdatedSenderException(final String msg) { - super(msg); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java deleted file mode 100644 index 99429ca1f..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java +++ /dev/null @@ -1,478 +0,0 @@ -package eu.siacs.conversations.crypto.axolotl; - -import android.util.Log; -import android.util.LruCache; - -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.IdentityKeyPair; -import org.whispersystems.libsignal.InvalidKeyIdException; -import org.whispersystems.libsignal.SignalProtocolAddress; -import org.whispersystems.libsignal.ecc.Curve; -import org.whispersystems.libsignal.ecc.ECKeyPair; -import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.SessionRecord; -import org.whispersystems.libsignal.state.SignalProtocolStore; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; -import org.whispersystems.libsignal.util.KeyHelper; - -import java.security.cert.X509Certificate; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.CryptoHelper; - -public class SQLiteAxolotlStore implements SignalProtocolStore { - - public static final String PREKEY_TABLENAME = "prekeys"; - public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys"; - public static final String SESSION_TABLENAME = "sessions"; - public static final String IDENTITIES_TABLENAME = "identities"; - public static final String ACCOUNT = "account"; - public static final String DEVICE_ID = "device_id"; - public static final String ID = "id"; - public static final String KEY = "key"; - public static final String FINGERPRINT = "fingerprint"; - public static final String NAME = "name"; - public static final String TRUSTED = "trusted"; //no longer used - public static final String TRUST = "trust"; - public static final String ACTIVE = "active"; - public static final String LAST_ACTIVATION = "last_activation"; - public static final String OWN = "ownkey"; - public static final String CERTIFICATE = "certificate"; - - public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; - public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id"; - - private static final int NUM_TRUSTS_TO_CACHE = 100; - - private final Account account; - private final XmppConnectionService mXmppConnectionService; - - private IdentityKeyPair identityKeyPair; - private int localRegistrationId; - private int currentPreKeyId = 0; - - private final HashSet preKeysMarkedForRemoval = new HashSet<>(); - - private final LruCache trustCache = - new LruCache(NUM_TRUSTS_TO_CACHE) { - @Override - protected FingerprintStatus create(String fingerprint) { - return mXmppConnectionService.databaseBackend.getFingerprintStatus(account, fingerprint); - } - }; - - private static IdentityKeyPair generateIdentityKeyPair() { - Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl IdentityKeyPair..."); - ECKeyPair identityKeyPairKeys = Curve.generateKeyPair(); - return new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()), - identityKeyPairKeys.getPrivateKey()); - } - - private static int generateRegistrationId() { - Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl registration ID..."); - return KeyHelper.generateRegistrationId(true); - } - - public SQLiteAxolotlStore(Account account, XmppConnectionService service) { - this.account = account; - this.mXmppConnectionService = service; - this.localRegistrationId = loadRegistrationId(); - this.currentPreKeyId = loadCurrentPreKeyId(); - } - - public int getCurrentPreKeyId() { - return currentPreKeyId; - } - - // -------------------------------------- - // IdentityKeyStore - // -------------------------------------- - - private IdentityKeyPair loadIdentityKeyPair() { - synchronized (mXmppConnectionService) { - IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account); - - if (ownKey != null) { - return ownKey; - } else { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve own IdentityKeyPair"); - ownKey = generateIdentityKeyPair(); - mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownKey); - } - return ownKey; - } - } - - private int loadRegistrationId() { - return loadRegistrationId(false); - } - - private int loadRegistrationId(boolean regenerate) { - String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID); - int reg_id; - if (!regenerate && regIdString != null) { - reg_id = Integer.valueOf(regIdString); - } else { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve axolotl registration id for account " + account.getJid()); - reg_id = generateRegistrationId(); - boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id)); - if (success) { - mXmppConnectionService.databaseBackend.updateAccount(account); - } else { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new key to the database!"); - } - } - return reg_id; - } - - private int loadCurrentPreKeyId() { - String prekeyIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID); - int prekey_id; - if (prekeyIdString != null) { - prekey_id = Integer.valueOf(prekeyIdString); - } else { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve current prekey id for account " + account.getJid()); - prekey_id = 0; - } - return prekey_id; - } - - public void regenerate() { - mXmppConnectionService.databaseBackend.wipeAxolotlDb(account); - trustCache.evictAll(); - account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0)); - identityKeyPair = loadIdentityKeyPair(); - localRegistrationId = loadRegistrationId(true); - currentPreKeyId = 0; - mXmppConnectionService.updateAccountUi(); - } - - /** - * Get the local client's identity key pair. - * - * @return The local client's persistent identity key pair. - */ - @Override - public IdentityKeyPair getIdentityKeyPair() { - if (identityKeyPair == null) { - identityKeyPair = loadIdentityKeyPair(); - } - return identityKeyPair; - } - - /** - * Return the local client's registration ID. - *

- * Clients should maintain a registration ID, a random number - * between 1 and 16380 that's generated once at install time. - * - * @return the local client's registration ID. - */ - @Override - public int getLocalRegistrationId() { - return localRegistrationId; - } - - /** - * Save a remote client's identity key - *

- * Store a remote client's identity key as trusted. - * - * @param address The address of the remote client. - * @param identityKey The remote client's identity key. - * @return true on success - */ - @Override - public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { - if (!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, address.getName()).contains(identityKey)) { - String fingerprint = CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()); - FingerprintStatus status = getFingerprintStatus(fingerprint); - if (status == null) { - if (mXmppConnectionService.blindTrustBeforeVerification() && !account.getAxolotlService().hasVerifiedKeys(address.getName())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": blindly trusted " + fingerprint + " of " + address.getName()); - status = FingerprintStatus.createActiveTrusted(); - } else { - status = FingerprintStatus.createActiveUndecided(); - } - } else { - status = status.toActive(); - } - mXmppConnectionService.databaseBackend.storeIdentityKey(account, address.getName(), identityKey, status); - trustCache.remove(fingerprint); - } - return true; - } - - /** - * Verify a remote client's identity key. - *

- * Determine whether a remote client's identity is trusted. Convention is - * that the TextSecure protocol is 'trust on first use.' This means that - * an identity key is considered 'trusted' if there is no entry for the recipient - * in the local store, or if it matches the saved key for a recipient in the local - * store. Only if it mismatches an entry in the local store is it considered - * 'untrusted.' - * - * @param identityKey The identity key to verify. - * @return true if trusted, false if untrusted. - */ - @Override - public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) { - return true; - } - - public FingerprintStatus getFingerprintStatus(String fingerprint) { - return (fingerprint == null) ? null : trustCache.get(fingerprint); - } - - public void setFingerprintStatus(String fingerprint, FingerprintStatus status) { - mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, status); - trustCache.remove(fingerprint); - } - - public void setFingerprintCertificate(String fingerprint, X509Certificate x509Certificate) { - mXmppConnectionService.databaseBackend.setIdentityKeyCertificate(account, fingerprint, x509Certificate); - } - - public X509Certificate getFingerprintCertificate(String fingerprint) { - return mXmppConnectionService.databaseBackend.getIdentityKeyCertifcate(account, fingerprint); - } - - public Set getContactKeysWithTrust(String bareJid, FingerprintStatus status) { - return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, status); - } - - public long getContactNumTrustedKeys(String bareJid) { - return mXmppConnectionService.databaseBackend.numTrustedKeys(account, bareJid); - } - - // -------------------------------------- - // SessionStore - // -------------------------------------- - - /** - * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple, - * or a new SessionRecord if one does not currently exist. - *

- * It is important that implementations return a copy of the current durable information. The - * returned SessionRecord may be modified, but those changes should not have an effect on the - * durable session state (what is returned by subsequent calls to this method) without the - * store method being called here first. - * - * @param address The name and device ID of the remote client. - * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or - * a new SessionRecord if one does not currently exist. - */ - @Override - public SessionRecord loadSession(SignalProtocolAddress address) { - SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address); - return (session != null) ? session : new SessionRecord(); - } - - /** - * Returns all known devices with active sessions for a recipient - * - * @param name the name of the client. - * @return all known sub-devices with active sessions. - */ - @Override - public List getSubDeviceSessions(String name) { - return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account, - new SignalProtocolAddress(name, 0)); - } - - - public List getKnownAddresses() { - return mXmppConnectionService.databaseBackend.getKnownSignalAddresses(account); - } - - /** - * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple. - * - * @param address the address of the remote client. - * @param record the current SessionRecord for the remote client. - */ - @Override - public void storeSession(SignalProtocolAddress address, SessionRecord record) { - mXmppConnectionService.databaseBackend.storeSession(account, address, record); - } - - /** - * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple. - * - * @param address the address of the remote client. - * @return true if a {@link SessionRecord} exists, false otherwise. - */ - @Override - public boolean containsSession(SignalProtocolAddress address) { - return mXmppConnectionService.databaseBackend.containsSession(account, address); - } - - /** - * Remove a {@link SessionRecord} for a recipientId + deviceId tuple. - * - * @param address the address of the remote client. - */ - @Override - public void deleteSession(SignalProtocolAddress address) { - mXmppConnectionService.databaseBackend.deleteSession(account, address); - } - - /** - * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId. - * - * @param name the name of the remote client. - */ - @Override - public void deleteAllSessions(String name) { - SignalProtocolAddress address = new SignalProtocolAddress(name, 0); - mXmppConnectionService.databaseBackend.deleteAllSessions(account, - address); - } - - // -------------------------------------- - // PreKeyStore - // -------------------------------------- - - /** - * Load a local PreKeyRecord. - * - * @param preKeyId the ID of the local PreKeyRecord. - * @return the corresponding PreKeyRecord. - * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord. - */ - @Override - public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { - PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); - if (record == null) { - throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId); - } - return record; - } - - /** - * Store a local PreKeyRecord. - * - * @param preKeyId the ID of the PreKeyRecord to store. - * @param record the PreKeyRecord. - */ - @Override - public void storePreKey(int preKeyId, PreKeyRecord record) { - mXmppConnectionService.databaseBackend.storePreKey(account, record); - currentPreKeyId = preKeyId; - boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId)); - if (success) { - mXmppConnectionService.databaseBackend.updateAccount(account); - } else { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new prekey id to the database!"); - } - } - - /** - * @param preKeyId A PreKeyRecord ID. - * @return true if the store has a record for the preKeyId, otherwise false. - */ - @Override - public boolean containsPreKey(int preKeyId) { - return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId); - } - - /** - * Delete a PreKeyRecord from local storage. - * - * @param preKeyId The ID of the PreKeyRecord to remove. - */ - @Override - public void removePreKey(int preKeyId) { - Log.d(Config.LOGTAG, "mark prekey for removal " + preKeyId); - synchronized (preKeysMarkedForRemoval) { - preKeysMarkedForRemoval.add(preKeyId); - } - } - - - public boolean flushPreKeys() { - Log.d(Config.LOGTAG, "flushing pre keys"); - int count = 0; - synchronized (preKeysMarkedForRemoval) { - for (Integer preKeyId : preKeysMarkedForRemoval) { - count += mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId); - } - preKeysMarkedForRemoval.clear(); - } - return count > 0; - } - - // -------------------------------------- - // SignedPreKeyStore - // -------------------------------------- - - /** - * Load a local SignedPreKeyRecord. - * - * @param signedPreKeyId the ID of the local SignedPreKeyRecord. - * @return the corresponding SignedPreKeyRecord. - * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord. - */ - @Override - public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { - SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); - if (record == null) { - throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId); - } - return record; - } - - /** - * Load all local SignedPreKeyRecords. - * - * @return All stored SignedPreKeyRecords. - */ - @Override - public List loadSignedPreKeys() { - return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account); - } - - public int getSignedPreKeysCount() { - return mXmppConnectionService.databaseBackend.getSignedPreKeysCount(account); - } - - /** - * Store a local SignedPreKeyRecord. - * - * @param signedPreKeyId the ID of the SignedPreKeyRecord to store. - * @param record the SignedPreKeyRecord. - */ - @Override - public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { - mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record); - } - - /** - * @param signedPreKeyId A SignedPreKeyRecord ID. - * @return true if the store has a record for the signedPreKeyId, otherwise false. - */ - @Override - public boolean containsSignedPreKey(int signedPreKeyId) { - return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId); - } - - /** - * Delete a SignedPreKeyRecord from local storage. - * - * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove. - */ - @Override - public void removeSignedPreKey(int signedPreKeyId) { - mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId); - } - - public void preVerifyFingerprint(Account account, String name, String fingerprint) { - mXmppConnectionService.databaseBackend.storePreVerification(account, name, fingerprint, FingerprintStatus.createInactiveVerified()); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java deleted file mode 100644 index 0c583dae3..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java +++ /dev/null @@ -1,318 +0,0 @@ -package eu.siacs.conversations.crypto.axolotl; - -import android.util.Base64; -import android.util.Log; - -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.List; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.KeyGenerator; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.Compatibility; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.Jid; - -public class XmppAxolotlMessage { - public static final String CONTAINERTAG = "encrypted"; - private static final String HEADER = "header"; - private static final String SOURCEID = "sid"; - private static final String KEYTAG = "key"; - private static final String REMOTEID = "rid"; - private static final String IVTAG = "iv"; - private static final String PAYLOAD = "payload"; - - private static final String KEYTYPE = "AES"; - private static final String CIPHERMODE = "AES/GCM/NoPadding"; - private static final String PROVIDER = "BC"; - private final List keys; - private final Jid from; - private final int sourceDeviceId; - private byte[] innerKey; - private byte[] ciphertext = null; - private byte[] authtagPlusInnerKey = null; - private byte[] iv = null; - - private XmppAxolotlMessage(final Element axolotlMessage, final Jid from) throws IllegalArgumentException { - this.from = from; - Element header = axolotlMessage.findChild(HEADER); - try { - this.sourceDeviceId = Integer.parseInt(header.getAttribute(SOURCEID)); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("invalid source id"); - } - List keyElements = header.getChildren(); - this.keys = new ArrayList<>(); - for (Element keyElement : keyElements) { - switch (keyElement.getName()) { - case KEYTAG: - try { - int recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID)); - byte[] key = Base64.decode(keyElement.getContent().trim(), Base64.DEFAULT); - boolean isPreKey = keyElement.getAttributeAsBoolean("prekey"); - this.keys.add(new XmppAxolotlSession.AxolotlKey(recipientId, key, isPreKey)); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("invalid remote id"); - } - break; - case IVTAG: - if (this.iv != null) { - throw new IllegalArgumentException("Duplicate iv entry"); - } - iv = Base64.decode(keyElement.getContent().trim(), Base64.DEFAULT); - break; - default: - Log.w(Config.LOGTAG, "Unexpected element in header: " + keyElement.toString()); - break; - } - } - final Element payloadElement = axolotlMessage.findChildEnsureSingle(PAYLOAD, AxolotlService.PEP_PREFIX); - if (payloadElement != null) { - ciphertext = Base64.decode(payloadElement.getContent().trim(), Base64.DEFAULT); - } - } - - XmppAxolotlMessage(Jid from, int sourceDeviceId) { - this.from = from; - this.sourceDeviceId = sourceDeviceId; - this.keys = new ArrayList<>(); - this.iv = generateIv(); - this.innerKey = generateKey(); - } - - public static int parseSourceId(final Element axolotlMessage) throws IllegalArgumentException { - final Element header = axolotlMessage.findChild(HEADER); - if (header == null) { - throw new IllegalArgumentException("No header found"); - } - try { - return Integer.parseInt(header.getAttribute(SOURCEID)); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("invalid source id"); - } - } - - public static XmppAxolotlMessage fromElement(Element element, Jid from) { - return new XmppAxolotlMessage(element, from); - } - - private static byte[] generateKey() { - try { - KeyGenerator generator = KeyGenerator.getInstance(KEYTYPE); - generator.init(128); - return generator.generateKey().getEncoded(); - } catch (NoSuchAlgorithmException e) { - Log.e(Config.LOGTAG, e.getMessage()); - return null; - } - } - - private static byte[] generateIv() { - final SecureRandom random = new SecureRandom(); - final byte[] iv = new byte[12]; - random.nextBytes(iv); - return iv; - } - - private static byte[] getPaddedBytes(String plaintext) { - int plainLength = plaintext.getBytes().length; - int pad = Math.max(64, (plainLength / 32 + 1) * 32) - plainLength; - SecureRandom random = new SecureRandom(); - int left = random.nextInt(pad); - int right = pad - left; - StringBuilder builder = new StringBuilder(plaintext); - for (int i = 0; i < left; ++i) { - builder.insert(0, random.nextBoolean() ? "\t" : " "); - } - for (int i = 0; i < right; ++i) { - builder.append(random.nextBoolean() ? "\t" : " "); - } - return builder.toString().getBytes(); - } - - public boolean hasPayload() { - return ciphertext != null; - } - - void encrypt(final String plaintext) throws CryptoFailedException { - try { - SecretKey secretKey = new SecretKeySpec(innerKey, KEYTYPE); - IvParameterSpec ivSpec = new IvParameterSpec(iv); - Cipher cipher = Compatibility.runsTwentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER); - cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); - this.ciphertext = cipher.doFinal(Config.OMEMO_PADDING ? getPaddedBytes(plaintext) : plaintext.getBytes()); - if (Config.PUT_AUTH_TAG_INTO_KEY && this.ciphertext != null) { - this.authtagPlusInnerKey = new byte[16 + 16]; - byte[] ciphertext = new byte[this.ciphertext.length - 16]; - System.arraycopy(this.ciphertext, 0, ciphertext, 0, ciphertext.length); - System.arraycopy(this.ciphertext, ciphertext.length, authtagPlusInnerKey, 16, 16); - System.arraycopy(this.innerKey, 0, authtagPlusInnerKey, 0, this.innerKey.length); - this.ciphertext = ciphertext; - } - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException - | IllegalBlockSizeException | BadPaddingException | NoSuchProviderException - | InvalidAlgorithmParameterException e) { - throw new CryptoFailedException(e); - } - } - - public Jid getFrom() { - return this.from; - } - - int getSenderDeviceId() { - return sourceDeviceId; - } - - void addDevice(XmppAxolotlSession session) { - addDevice(session, false); - } - - void addDevice(XmppAxolotlSession session, boolean ignoreSessionTrust) { - XmppAxolotlSession.AxolotlKey key; - if (authtagPlusInnerKey != null) { - key = session.processSending(authtagPlusInnerKey, ignoreSessionTrust); - } else { - key = session.processSending(innerKey, ignoreSessionTrust); - } - if (key != null) { - keys.add(key); - } - } - - public byte[] getInnerKey() { - return innerKey; - } - - public byte[] getIV() { - return this.iv; - } - - public Element toElement() { - Element encryptionElement = new Element(CONTAINERTAG, AxolotlService.PEP_PREFIX); - Element headerElement = encryptionElement.addChild(HEADER); - headerElement.setAttribute(SOURCEID, sourceDeviceId); - for (XmppAxolotlSession.AxolotlKey key : keys) { - Element keyElement = new Element(KEYTAG); - keyElement.setAttribute(REMOTEID, key.deviceId); - if (key.prekey) { - keyElement.setAttribute("prekey", "true"); - } - keyElement.setContent(Base64.encodeToString(key.key, Base64.NO_WRAP)); - headerElement.addChild(keyElement); - } - headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.NO_WRAP)); - if (ciphertext != null) { - Element payload = encryptionElement.addChild(PAYLOAD); - payload.setContent(Base64.encodeToString(ciphertext, Base64.NO_WRAP)); - } - return encryptionElement; - } - - private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException { - ArrayList possibleKeys = new ArrayList<>(); - for (XmppAxolotlSession.AxolotlKey key : keys) { - if (key.deviceId == sourceDeviceId) { - possibleKeys.add(key); - } - } - if (possibleKeys.size() == 0) { - throw new NotEncryptedForThisDeviceException(); - } - return session.processReceiving(possibleKeys); - } - - XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException { - return new XmppAxolotlKeyTransportMessage(session.getFingerprint(), unpackKey(session, sourceDeviceId), getIV()); - } - - public XmppAxolotlPlaintextMessage decrypt(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException { - XmppAxolotlPlaintextMessage plaintextMessage = null; - byte[] key = unpackKey(session, sourceDeviceId); - if (key != null) { - try { - if (key.length < 32) { - throw new OutdatedSenderException("Key did not contain auth tag. Sender needs to update their OMEMO client"); - } - final int authTagLength = key.length - 16; - byte[] newCipherText = new byte[key.length - 16 + ciphertext.length]; - byte[] newKey = new byte[16]; - System.arraycopy(ciphertext, 0, newCipherText, 0, ciphertext.length); - System.arraycopy(key, 16, newCipherText, ciphertext.length, authTagLength); - System.arraycopy(key, 0, newKey, 0, newKey.length); - ciphertext = newCipherText; - key = newKey; - - final Cipher cipher = Compatibility.runsTwentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER); - SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE); - IvParameterSpec ivSpec = new IvParameterSpec(iv); - - cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); - - String plaintext = new String(cipher.doFinal(ciphertext)); - plaintextMessage = new XmppAxolotlPlaintextMessage(Config.OMEMO_PADDING ? plaintext.trim() : plaintext, session.getFingerprint()); - - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException - | InvalidAlgorithmParameterException | IllegalBlockSizeException - | BadPaddingException | NoSuchProviderException e) { - throw new CryptoFailedException(e); - } - } - return plaintextMessage; - } - - public static class XmppAxolotlPlaintextMessage { - private final String plaintext; - private final String fingerprint; - - XmppAxolotlPlaintextMessage(String plaintext, String fingerprint) { - this.plaintext = plaintext; - this.fingerprint = fingerprint; - } - - public String getPlaintext() { - return plaintext; - } - - - public String getFingerprint() { - return fingerprint; - } - } - - public static class XmppAxolotlKeyTransportMessage { - private final String fingerprint; - private final byte[] key; - private final byte[] iv; - - XmppAxolotlKeyTransportMessage(String fingerprint, byte[] key, byte[] iv) { - this.fingerprint = fingerprint; - this.key = key; - this.iv = iv; - } - - public String getFingerprint() { - return fingerprint; - } - - public byte[] getKey() { - return key; - } - - public byte[] getIv() { - return iv; - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java deleted file mode 100644 index d89688f76..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java +++ /dev/null @@ -1,192 +0,0 @@ -package eu.siacs.conversations.crypto.axolotl; - -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.whispersystems.libsignal.DuplicateMessageException; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.InvalidKeyIdException; -import org.whispersystems.libsignal.InvalidMessageException; -import org.whispersystems.libsignal.InvalidVersionException; -import org.whispersystems.libsignal.LegacyMessageException; -import org.whispersystems.libsignal.NoSessionException; -import org.whispersystems.libsignal.SessionCipher; -import org.whispersystems.libsignal.SignalProtocolAddress; -import org.whispersystems.libsignal.UntrustedIdentityException; -import org.whispersystems.libsignal.protocol.CiphertextMessage; -import org.whispersystems.libsignal.protocol.PreKeySignalMessage; -import org.whispersystems.libsignal.protocol.SignalMessage; -import org.whispersystems.libsignal.util.guava.Optional; - -import java.util.Iterator; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.utils.CryptoHelper; - -public class XmppAxolotlSession implements Comparable { - private final SessionCipher cipher; - private final SQLiteAxolotlStore sqLiteAxolotlStore; - private final SignalProtocolAddress remoteAddress; - private final Account account; - private IdentityKey identityKey; - private Integer preKeyId = null; - private boolean fresh = true; - - public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, SignalProtocolAddress remoteAddress, IdentityKey identityKey) { - this(account, store, remoteAddress); - this.identityKey = identityKey; - } - - public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, SignalProtocolAddress remoteAddress) { - this.cipher = new SessionCipher(store, remoteAddress); - this.remoteAddress = remoteAddress; - this.sqLiteAxolotlStore = store; - this.account = account; - } - - public Integer getPreKeyIdAndReset() { - final Integer preKeyId = this.preKeyId; - this.preKeyId = null; - return preKeyId; - } - - public String getFingerprint() { - return identityKey == null ? null : CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()); - } - - public IdentityKey getIdentityKey() { - return identityKey; - } - - public SignalProtocolAddress getRemoteAddress() { - return remoteAddress; - } - - public boolean isFresh() { - return fresh; - } - - public void setNotFresh() { - this.fresh = false; - } - - protected void setTrust(FingerprintStatus status) { - sqLiteAxolotlStore.setFingerprintStatus(getFingerprint(), status); - } - - public FingerprintStatus getTrust() { - FingerprintStatus status = sqLiteAxolotlStore.getFingerprintStatus(getFingerprint()); - return (status == null) ? FingerprintStatus.createActiveUndecided() : status; - } - - @Nullable - byte[] processReceiving(List possibleKeys) throws CryptoFailedException { - byte[] plaintext = null; - FingerprintStatus status = getTrust(); - if (!status.isCompromised()) { - Iterator iterator = possibleKeys.iterator(); - while (iterator.hasNext()) { - AxolotlKey encryptedKey = iterator.next(); - try { - if (encryptedKey.prekey) { - PreKeySignalMessage preKeySignalMessage = new PreKeySignalMessage(encryptedKey.key); - Optional optionalPreKeyId = preKeySignalMessage.getPreKeyId(); - IdentityKey identityKey = preKeySignalMessage.getIdentityKey(); - if (!optionalPreKeyId.isPresent()) { - if (iterator.hasNext()) { - continue; - } - throw new CryptoFailedException("PreKeyWhisperMessage did not contain a PreKeyId"); - } - preKeyId = optionalPreKeyId.get(); - if (this.identityKey != null && !this.identityKey.equals(identityKey)) { - if (iterator.hasNext()) { - continue; - } - throw new CryptoFailedException("Received PreKeyWhisperMessage but preexisting identity key changed."); - } - this.identityKey = identityKey; - plaintext = cipher.decrypt(preKeySignalMessage); - } else { - SignalMessage signalMessage = new SignalMessage(encryptedKey.key); - try { - plaintext = cipher.decrypt(signalMessage); - } catch (InvalidMessageException e) { - if (iterator.hasNext()) { - Log.w(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring crypto exception because possible keys left to try", e); - continue; - } - throw new BrokenSessionException(this.remoteAddress, e); - } catch (NoSessionException e) { - if (iterator.hasNext()) { - Log.w(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring crypto exception because possible keys left to try", e); - continue; - } - throw new BrokenSessionException(this.remoteAddress, e); - } - preKeyId = null; //better safe than sorry because we use that to do special after prekey handling - } - } catch (InvalidVersionException | InvalidKeyException | LegacyMessageException | InvalidMessageException | DuplicateMessageException | InvalidKeyIdException | UntrustedIdentityException e) { - if (iterator.hasNext()) { - Log.w(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring crypto exception because possible keys left to try", e); - continue; - } - throw new CryptoFailedException("Error decrypting SignalMessage", e); - } - if (iterator.hasNext()) { - break; - } - } - if (!status.isActive()) { - setTrust(status.toActive()); - //TODO: also (re)add to device list? - } - } else { - throw new CryptoFailedException("not encrypting omemo message from fingerprint " + getFingerprint() + " because it was marked as compromised"); - } - return plaintext; - } - - @Nullable - public AxolotlKey processSending(@NonNull byte[] outgoingMessage, boolean ignoreSessionTrust) { - FingerprintStatus status = getTrust(); - if (ignoreSessionTrust || status.isTrustedAndActive()) { - try { - CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage); - return new AxolotlKey(getRemoteAddress().getDeviceId(), ciphertextMessage.serialize(), ciphertextMessage.getType() == CiphertextMessage.PREKEY_TYPE); - } catch (UntrustedIdentityException e) { - return null; - } - } else { - return null; - } - } - - public Account getAccount() { - return account; - } - - @Override - public int compareTo(XmppAxolotlSession o) { - return getTrust().compareTo(o.getTrust()); - } - - public static class AxolotlKey { - - - public final byte[] key; - public final boolean prekey; - public final int deviceId; - - public AxolotlKey(int deviceId, byte[] key, boolean prekey) { - this.deviceId = deviceId; - this.key = key; - this.prekey = prekey; - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java deleted file mode 100644 index 0c61f424d..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java +++ /dev/null @@ -1,26 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import javax.net.ssl.SSLSocket; - -import eu.siacs.conversations.entities.Account; -public class Anonymous extends SaslMechanism { - - public static final String MECHANISM = "ANONYMOUS";public Anonymous(final Account account) { - super(account); - } - - @Override - public int getPriority() { - return 0; - } - - @Override - public String getMechanism() { - return MECHANISM; - } - - @Override - public String getClientFirstMessage(final SSLSocket sslSocket) { - return ""; - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java deleted file mode 100644 index f7ff3c071..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java +++ /dev/null @@ -1,118 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import android.util.Log; - -import com.google.common.base.CaseFormat; -import com.google.common.base.Strings; -import com.google.common.base.Preconditions; -import com.google.common.base.Predicates; -import com.google.common.collect.Collections2; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import com.google.common.collect.BiMap; -import com.google.common.collect.ImmutableBiMap; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.SSLSockets; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xml.Namespace; - -public enum ChannelBinding { - NONE, - TLS_EXPORTER, - TLS_SERVER_END_POINT, - TLS_UNIQUE; - - public static final BiMap SHORT_NAMES; - - static { - final ImmutableBiMap.Builder builder = ImmutableBiMap.builder(); - for (final ChannelBinding cb : values()) { - builder.put(cb, shortName(cb)); - } - SHORT_NAMES = builder.build(); - } - - - public static Collection of(final Element channelBinding) { - Preconditions.checkArgument( - channelBinding == null - || ("sasl-channel-binding".equals(channelBinding.getName()) - && Namespace.CHANNEL_BINDING.equals(channelBinding.getNamespace())), - "pass null or a valid channel binding stream feature"); - return Collections2.filter( - Collections2.transform( - Collections2.filter( - channelBinding == null - ? Collections.emptyList() - : channelBinding.getChildren(), - c -> c != null && "channel-binding".equals(c.getName())), - c -> c == null ? null : ChannelBinding.of(c.getAttribute("type"))), - Predicates.notNull()); - } - - private static ChannelBinding of(final String type) { - if (type == null) { - return null; - } - try { - return valueOf( - CaseFormat.LOWER_HYPHEN.converterTo(CaseFormat.UPPER_UNDERSCORE).convert(type)); - } catch (final IllegalArgumentException e) { - Log.d(Config.LOGTAG, type + " is not a known channel binding"); - return null; - } - } - - public static ChannelBinding get(final String name) { - if (Strings.isNullOrEmpty(name)) { - return NONE; - } - try { - return valueOf(name); - } catch (final IllegalArgumentException e) { - return NONE; - } - } - public static ChannelBinding best( - final Collection bindings, final SSLSockets.Version sslVersion) { - if (sslVersion == SSLSockets.Version.NONE) { - return NONE; - } - if (bindings.contains(TLS_EXPORTER) && sslVersion == SSLSockets.Version.TLS_1_3) { - return TLS_EXPORTER; - } else if (bindings.contains(TLS_UNIQUE) - && Arrays.asList( - SSLSockets.Version.TLS_1_0, - SSLSockets.Version.TLS_1_1, - SSLSockets.Version.TLS_1_2) - .contains(sslVersion)) { - return TLS_UNIQUE; - } else if (bindings.contains(TLS_SERVER_END_POINT)) { - return TLS_SERVER_END_POINT; - } else { - return NONE; - } - } - - public static boolean isAvailable( - final ChannelBinding channelBinding, final SSLSockets.Version sslVersion) { - return ChannelBinding.best(Collections.singleton(channelBinding), sslVersion) - == channelBinding; - } - - private static String shortName(final ChannelBinding channelBinding) { - switch (channelBinding) { - case TLS_UNIQUE: - return "UNIQ"; - case TLS_EXPORTER: - return "EXPR"; - case TLS_SERVER_END_POINT: - return "ENDP"; - case NONE: - return "NONE"; - default: - throw new AssertionError("Missing short name for " + channelBinding); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java deleted file mode 100644 index b94210a60..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java +++ /dev/null @@ -1,100 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import org.bouncycastle.jcajce.provider.digest.SHA256; -import org.conscrypt.Conscrypt; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.cert.Certificate; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; - -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocket; - -public interface ChannelBindingMechanism { - - String EXPORTER_LABEL = "EXPORTER-Channel-Binding"; - - ChannelBinding getChannelBinding(); - - static byte[] getChannelBindingData(final SSLSocket sslSocket, final ChannelBinding channelBinding) - throws SaslMechanism.AuthenticationException { - if (sslSocket == null) { - throw new SaslMechanism.AuthenticationException("Channel binding attempt on non secure socket"); - } - if (channelBinding == ChannelBinding.TLS_EXPORTER) { - final byte[] keyingMaterial; - try { - keyingMaterial = - Conscrypt.exportKeyingMaterial(sslSocket, EXPORTER_LABEL, new byte[0], 32); - } catch (final SSLException e) { - throw new SaslMechanism.AuthenticationException("Could not export keying material"); - } - if (keyingMaterial == null) { - throw new SaslMechanism.AuthenticationException( - "Could not export keying material. Socket not ready"); - } - return keyingMaterial; - } else if (channelBinding == ChannelBinding.TLS_UNIQUE) { - final byte[] unique = Conscrypt.getTlsUnique(sslSocket); - if (unique == null) { - throw new SaslMechanism.AuthenticationException( - "Could not retrieve tls unique. Socket not ready"); - } - return unique; - } else if (channelBinding == ChannelBinding.TLS_SERVER_END_POINT) { - return getServerEndPointChannelBinding(sslSocket.getSession()); - } else { - throw new SaslMechanism.AuthenticationException( - String.format("%s is not a valid channel binding", channelBinding)); - } - } - - static byte[] getServerEndPointChannelBinding(final SSLSession session) - throws SaslMechanism.AuthenticationException { - final Certificate[] certificates; - try { - certificates = session.getPeerCertificates(); - } catch (final SSLPeerUnverifiedException e) { - throw new SaslMechanism.AuthenticationException("Could not verify peer certificates"); - } - if (certificates == null || certificates.length == 0) { - throw new SaslMechanism.AuthenticationException("Could not retrieve peer certificate"); - } - final X509Certificate certificate; - if (certificates[0] instanceof X509Certificate) { - certificate = (X509Certificate) certificates[0]; - } else { - throw new SaslMechanism.AuthenticationException("Certificate was not X509"); - } - final String algorithm = certificate.getSigAlgName(); - final int withIndex = algorithm.indexOf("with"); - if (withIndex <= 0) { - throw new SaslMechanism.AuthenticationException("Unable to parse SigAlgName"); - } - final String hashAlgorithm = algorithm.substring(0, withIndex); - final MessageDigest messageDigest; - // https://www.rfc-editor.org/rfc/rfc5929#section-4.1 - if ("MD5".equalsIgnoreCase(hashAlgorithm) || "SHA1".equalsIgnoreCase(hashAlgorithm)) { - messageDigest = new SHA256.Digest(); - } else { - try { - messageDigest = MessageDigest.getInstance(hashAlgorithm); - } catch (final NoSuchAlgorithmException e) { - throw new SaslMechanism.AuthenticationException( - "Could not instantiate message digest for " + hashAlgorithm); - } - } - final byte[] encodedCertificate; - try { - encodedCertificate = certificate.getEncoded(); - } catch (final CertificateEncodingException e) { - throw new SaslMechanism.AuthenticationException("Could not encode certificate"); - } - messageDigest.update(encodedCertificate); - return messageDigest.digest(); - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java deleted file mode 100644 index 5ed65b2c4..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java +++ /dev/null @@ -1,112 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import android.util.Base64; - -import java.nio.charset.Charset; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import javax.net.ssl.SSLSocket; - -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.utils.CryptoHelper; - -public class DigestMd5 extends SaslMechanism { - public static final String MECHANISM = "DIGEST-MD5";private State state = State.INITIAL; - - public DigestMd5(final Account account) { - super(account); - } - - @Override - public int getPriority() { - return 10; - } - - @Override - public String getMechanism() { - return MECHANISM; - } - - @Override - public String getResponse(final String challenge, final SSLSocket sslSocket) - throws AuthenticationException { - switch (state) { - case INITIAL: - state = State.RESPONSE_SENT; - final String encodedResponse; - try { - final Tokenizer tokenizer = - new Tokenizer(Base64.decode(challenge, Base64.DEFAULT)); - String nonce = ""; - for (final String token : tokenizer) { - final String[] parts = token.split("=", 2); - if (parts[0].equals("nonce")) { - nonce = parts[1].replace("\"", ""); - } else if (parts[0].equals("rspauth")) { - return ""; - } - } - final String digestUri = "xmpp/" + account.getServer(); - final String nonceCount = "00000001"; - final String x = - account.getUsername() - + ":" - + account.getServer() - + ":" - + account.getPassword(); - final MessageDigest md = MessageDigest.getInstance("MD5"); - final byte[] y = md.digest(x.getBytes(Charset.defaultCharset())); - final String cNonce = CryptoHelper.random(100); - final byte[] a1 = - CryptoHelper.concatenateByteArrays( - y, - (":" + nonce + ":" + cNonce) - .getBytes(Charset.defaultCharset())); - final String a2 = "AUTHENTICATE:" + digestUri; - final String ha1 = CryptoHelper.bytesToHex(md.digest(a1)); - final String ha2 = - CryptoHelper.bytesToHex( - md.digest(a2.getBytes(Charset.defaultCharset()))); - final String kd = - ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce + ":auth:" + ha2; - final String response = - CryptoHelper.bytesToHex( - md.digest(kd.getBytes(Charset.defaultCharset()))); - final String saslString = - "username=\"" - + account.getUsername() - + "\",realm=\"" - + account.getServer() - + "\",nonce=\"" - + nonce - + "\",cnonce=\"" - + cNonce - + "\",nc=" - + nonceCount - + ",qop=auth,digest-uri=\"" - + digestUri - + "\",response=" - + response - + ",charset=utf-8"; - encodedResponse = - Base64.encodeToString( - saslString.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); - } catch (final NoSuchAlgorithmException e) { - throw new AuthenticationException(e); - } - - return encodedResponse; - case RESPONSE_SENT: - state = State.VALID_SERVER_RESPONSE; - break; - case VALID_SERVER_RESPONSE: - if (challenge == null) { - return null; // everything is fine - } - default: - throw new InvalidStateException(state); - } - return null; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/External.java b/src/main/java/eu/siacs/conversations/crypto/sasl/External.java deleted file mode 100644 index 690056c22..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/External.java +++ /dev/null @@ -1,29 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import android.util.Base64; - -import javax.net.ssl.SSLSocket; - -import eu.siacs.conversations.entities.Account; -public class External extends SaslMechanism { - - public static final String MECHANISM = "EXTERNAL";public External(final Account account) { - super(account); - } - - @Override - public int getPriority() { - return 25; - } - - @Override - public String getMechanism() { - return MECHANISM; - } - - @Override - public String getClientFirstMessage(final SSLSocket sslSocket) { - return Base64.encodeToString( - account.getJid().asBareJid().toEscapedString().getBytes(), Base64.NO_WRAP); - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java deleted file mode 100644 index d3595b9e4..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java +++ /dev/null @@ -1,190 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import android.util.Base64; -import android.util.Log; - -import com.google.common.base.MoreObjects; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMultimap; -import com.google.common.collect.Multimap; -import com.google.common.hash.HashFunction; -import com.google.common.primitives.Bytes; - -import org.jetbrains.annotations.NotNull; - -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; - -import javax.net.ssl.SSLSocket; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.utils.SSLSockets; - -public abstract class HashedToken extends SaslMechanism implements ChannelBindingMechanism { - - private static final String PREFIX = "HT"; - - private static final List HASH_FUNCTIONS = Arrays.asList("SHA-512", "SHA-256"); - private static final byte[] INITIATOR = "Initiator".getBytes(StandardCharsets.UTF_8); - private static final byte[] RESPONDER = "Responder".getBytes(StandardCharsets.UTF_8); - - protected final ChannelBinding channelBinding; - - protected HashedToken(final Account account, final ChannelBinding channelBinding) { - super(account); - this.channelBinding = channelBinding; - } - - @Override - public int getPriority() { - throw new UnsupportedOperationException(); - } - - @Override - public String getClientFirstMessage(final SSLSocket sslSocket) { - final String token = Strings.nullToEmpty(this.account.getFastToken()); - final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8)); - final byte[] cbData = getChannelBindingData(sslSocket); - final byte[] initiatorHashedToken = - hashing.hashBytes(Bytes.concat(INITIATOR, cbData)).asBytes(); - final byte[] firstMessage = - Bytes.concat( - account.getUsername().getBytes(StandardCharsets.UTF_8), - new byte[] {0x00}, - initiatorHashedToken); - return Base64.encodeToString(firstMessage, Base64.NO_WRAP); - } - - private byte[] getChannelBindingData(final SSLSocket sslSocket) { - if (this.channelBinding == ChannelBinding.NONE) { - return new byte[0]; - } - try { - return ChannelBindingMechanism.getChannelBindingData(sslSocket, this.channelBinding); - } catch (final AuthenticationException e) { - Log.e( - Config.LOGTAG, - account.getJid().asBareJid() - + ": unable to retrieve channel binding data for " - + getMechanism(), - e); - return new byte[0]; - } - } - - @Override - public String getResponse(final String challenge, final SSLSocket socket) - throws AuthenticationException { - final byte[] responderMessage; - try { - responderMessage = Base64.decode(challenge, Base64.NO_WRAP); - } catch (final Exception e) { - throw new AuthenticationException("Unable to decode responder message", e); - } - final String token = Strings.nullToEmpty(this.account.getFastToken()); - final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8)); - final byte[] cbData = getChannelBindingData(socket); - final byte[] expectedResponderMessage = - hashing.hashBytes(Bytes.concat(RESPONDER, cbData)).asBytes(); - if (Arrays.equals(responderMessage, expectedResponderMessage)) { - return null; - } - throw new AuthenticationException("Responder message did not match"); - } - - protected abstract HashFunction getHashFunction(final byte[] key); - - public abstract Mechanism getTokenMechanism(); - - @Override - public String getMechanism() { - return getTokenMechanism().name(); - } - - public static final class Mechanism { - public final String hashFunction; - public final ChannelBinding channelBinding; - - public Mechanism(String hashFunction, ChannelBinding channelBinding) { - this.hashFunction = hashFunction; - this.channelBinding = channelBinding; - } - - public static Mechanism of(final String mechanism) { - final int first = mechanism.indexOf('-'); - final int last = mechanism.lastIndexOf('-'); - if (last <= first || mechanism.length() <= last) { - throw new IllegalArgumentException("Not a valid HashedToken name"); - } - if (mechanism.substring(0, first).equals(PREFIX)) { - final String hashFunction = mechanism.substring(first + 1, last); - final String cbShortName = mechanism.substring(last + 1); - final ChannelBinding channelBinding = - ChannelBinding.SHORT_NAMES.inverse().get(cbShortName); - if (channelBinding == null) { - throw new IllegalArgumentException("Unknown channel binding " + cbShortName); - } - return new Mechanism(hashFunction, channelBinding); - } else { - throw new IllegalArgumentException("HashedToken name does not start with HT"); - } - } - - public static Mechanism ofOrNull(final String mechanism) { - try { - return mechanism == null ? null : of(mechanism); - } catch (final IllegalArgumentException e) { - return null; - } - } - - public static Multimap of(final Collection mechanisms) { - final ImmutableMultimap.Builder builder = - ImmutableMultimap.builder(); - for (final String name : mechanisms) { - try { - final Mechanism mechanism = Mechanism.of(name); - builder.put(mechanism.hashFunction, mechanism.channelBinding); - } catch (final IllegalArgumentException ignored) { - } - } - return builder.build(); - } - - public static Mechanism best( - final Collection mechanisms, final SSLSockets.Version sslVersion) { - final Multimap multimap = of(mechanisms); - for (final String hashFunction : HASH_FUNCTIONS) { - final Collection channelBindings = multimap.get(hashFunction); - if (channelBindings.isEmpty()) { - continue; - } - final ChannelBinding cb = ChannelBinding.best(channelBindings, sslVersion); - return new Mechanism(hashFunction, cb); - } - return null; - } - - @NotNull - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("hashFunction", hashFunction) - .add("channelBinding", channelBinding) - .toString(); - } - - public String name() { - return String.format( - "%s-%s-%s", - PREFIX, hashFunction, ChannelBinding.SHORT_NAMES.get(channelBinding)); - } - } - - public ChannelBinding getChannelBinding() { - return this.channelBinding; - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java deleted file mode 100644 index aef19d72a..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java +++ /dev/null @@ -1,23 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import com.google.common.hash.HashFunction; -import com.google.common.hash.Hashing; - -import eu.siacs.conversations.entities.Account; - -public class HashedTokenSha256 extends HashedToken { - - public HashedTokenSha256(final Account account, final ChannelBinding channelBinding) { - super(account, channelBinding); - } - - @Override - protected HashFunction getHashFunction(final byte[] key) { - return Hashing.hmacSha256(key); - } - - @Override - public Mechanism getTokenMechanism() { - return new Mechanism("SHA-256", channelBinding); - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java b/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java deleted file mode 100644 index 6f48b5444..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java +++ /dev/null @@ -1,23 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import com.google.common.hash.HashFunction; -import com.google.common.hash.Hashing; - -import eu.siacs.conversations.entities.Account; - -public class HashedTokenSha512 extends HashedToken { - - public HashedTokenSha512(final Account account, final ChannelBinding channelBinding) { - super(account, channelBinding); - } - - @Override - protected HashFunction getHashFunction(final byte[] key) { - return Hashing.hmacSha512(key); - } - - @Override - public Mechanism getTokenMechanism() { - return new Mechanism("SHA-512", this.channelBinding); - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java deleted file mode 100644 index 3da5908d9..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java +++ /dev/null @@ -1,35 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import android.util.Base64; - -import java.nio.charset.Charset; - -import javax.net.ssl.SSLSocket; - -import eu.siacs.conversations.entities.Account; - -public class Plain extends SaslMechanism { - public static final String MECHANISM = "PLAIN";public Plain(final Account account) { - super(account); - } - - public static String getMessage(String username, String password) { - final String message = '\u0000' + username + '\u0000' + password; - return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); - } - - @Override - public int getPriority() { - return 10; - } - - @Override - public String getMechanism() { - return MECHANISM; - } - - @Override - public String getClientFirstMessage(final SSLSocket sslSocket) { - return getMessage(account.getUsername(), account.getPassword()); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java deleted file mode 100644 index f30002ad1..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ /dev/null @@ -1,195 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import com.google.common.base.Strings; - -import java.util.Collection; -import java.util.Collections; -import android.util.Log; -import javax.net.ssl.SSLSocket; -import com.google.common.base.Preconditions; -import com.google.common.collect.Collections2; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.SSLSockets; - -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xml.Namespace; - -public abstract class SaslMechanism { - - protected final Account account; - - protected SaslMechanism(final Account account) { - this.account = account; - } - - public static String namespace(final Version version) { - if (version == Version.SASL) { - return Namespace.SASL; - } else { - return Namespace.SASL_2; - } - } - - /** - * The priority is used to pin the authentication mechanism. If authentication fails, it MAY be - * retried with another mechanism of the same priority, but MUST NOT be tried with a mechanism - * of lower priority (to prevent downgrade attacks). - * - * @return An arbitrary int representing the priority - */ - public abstract int getPriority(); - - public abstract String getMechanism(); - - public String getClientFirstMessage(final SSLSocket sslSocket) { - return ""; - } - - public String getResponse(final String challenge, final SSLSocket sslSocket) - throws AuthenticationException { - return ""; - } - public static Collection mechanisms(final Element authElement) { - if (authElement == null) { - return Collections.emptyList(); - } - return Collections2.transform( - Collections2.filter( - authElement.getChildren(), - c -> c != null && "mechanism".equals(c.getName())), - c -> c == null ? null : c.getContent()); - } - - - protected enum State { - INITIAL, - AUTH_TEXT_SENT, - RESPONSE_SENT, - VALID_SERVER_RESPONSE, - } - - public enum Version { - SASL, - SASL_2; - - public static Version of(final Element element) { - switch (Strings.nullToEmpty(element.getNamespace())) { - case Namespace.SASL: - return SASL; - case Namespace.SASL_2: - return SASL_2; - default: - throw new IllegalArgumentException("Unrecognized SASL namespace"); - } - } - } - - public static class AuthenticationException extends Exception { - public AuthenticationException(final String message) { - super(message); - } - - public AuthenticationException(final Exception inner) { - super(inner); - } - - public AuthenticationException(final String message, final Exception exception) { - super(message, exception); - } - } - - public static class InvalidStateException extends AuthenticationException { - public InvalidStateException(final String message) { - super(message); - } - - public InvalidStateException(final State state) { - this("Invalid state: " + state.toString()); - } - } - - public static final class Factory { - - private final Account account; - - public Factory(final Account account) { - this.account = account; - } - - private SaslMechanism of( - final Collection mechanisms, final ChannelBinding channelBinding) { - Preconditions.checkNotNull(channelBinding, "Use ChannelBinding.NONE instead of null"); - if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { - return new External(account); - } else if (mechanisms.contains(ScramSha512Plus.MECHANISM) - && channelBinding != ChannelBinding.NONE) { - return new ScramSha512Plus(account, channelBinding); - } else if (mechanisms.contains(ScramSha256Plus.MECHANISM) - && channelBinding != ChannelBinding.NONE) { - return new ScramSha256Plus(account, channelBinding); - } else if (mechanisms.contains(ScramSha1Plus.MECHANISM) - && channelBinding != ChannelBinding.NONE) { - return new ScramSha1Plus(account, channelBinding); - } else if (mechanisms.contains(ScramSha512.MECHANISM)) { - return new ScramSha512(account); - } else if (mechanisms.contains(ScramSha256.MECHANISM)) { - return new ScramSha256(account); - } else if (mechanisms.contains(ScramSha1.MECHANISM)) { - return new ScramSha1(account); - } else if (mechanisms.contains(Plain.MECHANISM) - && !account.getServer().equals("nimbuzz.com")) { - return new Plain(account); - } else if (mechanisms.contains(DigestMd5.MECHANISM)) { - return new DigestMd5(account); - } else if (mechanisms.contains(Anonymous.MECHANISM)) { - return new Anonymous(account); - } else { - return null; - } - } - - public SaslMechanism of( - final Collection mechanisms, - final Collection bindings, - final Version version, - final SSLSockets.Version sslVersion) { - final HashedToken fastMechanism = account.getFastMechanism(); - if (version == Version.SASL_2 && fastMechanism != null) { - return fastMechanism; - } - final ChannelBinding channelBinding = ChannelBinding.best(bindings, sslVersion); - return of(mechanisms, channelBinding); - } - - - public SaslMechanism of(final String mechanism, final ChannelBinding channelBinding) { - return of(Collections.singleton(mechanism), channelBinding); - } - } - - public static SaslMechanism ensureAvailable( - final SaslMechanism mechanism, final SSLSockets.Version sslVersion) { - if (mechanism instanceof ChannelBindingMechanism) { - final ChannelBinding cb = ((ChannelBindingMechanism) mechanism).getChannelBinding(); - if (ChannelBinding.isAvailable(cb, sslVersion)) { - return mechanism; - } else { - Log.d( - Config.LOGTAG, - "pinned channel binding method " + cb + " no longer available"); - return null; - } - } else { - return mechanism; - } - } - - public static boolean hashedToken(final SaslMechanism saslMechanism) { - return saslMechanism instanceof HashedToken; - } - - public static boolean pin(final SaslMechanism saslMechanism) { - return !hashedToken(saslMechanism); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java deleted file mode 100644 index 2e395f38e..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ /dev/null @@ -1,316 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import android.util.Base64; - -import com.google.common.base.CaseFormat; -import com.google.common.base.Objects; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.hash.HashFunction; - -import java.nio.charset.Charset; -import java.security.InvalidKeyException; -import java.util.concurrent.ExecutionException; - -import javax.crypto.SecretKey; -import javax.net.ssl.SSLSocket; - -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.utils.CryptoHelper; - -abstract class ScramMechanism extends SaslMechanism { - - public static final SecretKey EMPTY_KEY = - new SecretKey() { - @Override - public String getAlgorithm() { - return "HMAC"; - } - - @Override - public String getFormat() { - return "RAW"; - } - - @Override - public byte[] getEncoded() { - return new byte[0]; - } - }; - - private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes(); - private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); - private static final Cache CACHE = - CacheBuilder.newBuilder().maximumSize(10).build(); - protected final ChannelBinding channelBinding; - private final String gs2Header; - private final String clientNonce; - protected State state = State.INITIAL; - private String clientFirstMessageBare; - private byte[] serverSignature = null; - - ScramMechanism(final Account account, final ChannelBinding channelBinding) { - super(account); - this.channelBinding = channelBinding; - if (channelBinding == ChannelBinding.NONE) { - // TODO this needs to be changed to "y,," for the scram internal down grade protection - // but we might risk compatibility issues if the server supports a binding that we don’t - // support - this.gs2Header = "n,,"; - } else { - this.gs2Header = - String.format( - "p=%s,,", - CaseFormat.UPPER_UNDERSCORE - .converterTo(CaseFormat.LOWER_HYPHEN) - .convert(channelBinding.toString())); - } - // This nonce should be different for each authentication attempt. - this.clientNonce = CryptoHelper.random(100); - clientFirstMessageBare = ""; - } - - protected abstract HashFunction getHMac(final byte[] key); - - protected abstract HashFunction getDigest(); - - private KeyPair getKeyPair(final String password, final String salt, final int iterations) - throws ExecutionException { - return CACHE.get( - new CacheKey(getMechanism(), password, salt, iterations), - () -> { - final byte[] saltedPassword, serverKey, clientKey; - saltedPassword = - hi( - password.getBytes(), - Base64.decode(salt, Base64.DEFAULT), - iterations); - serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); - clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); - return new KeyPair(clientKey, serverKey); - }); - } - - private byte[] hmac(final byte[] key, final byte[] input) throws InvalidKeyException { - return getHMac(key).hashBytes(input).asBytes(); - } - - private byte[] digest(final byte[] bytes) { - return getDigest().hashBytes(bytes).asBytes(); - } - - /* - * Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the - * pseudorandom function (PRF) and with dkLen == output length of - * HMAC() == output length of H(). - */ - private byte[] hi(final byte[] key, final byte[] salt, final int iterations) - throws InvalidKeyException { - byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE)); - byte[] out = u.clone(); - for (int i = 1; i < iterations; i++) { - u = hmac(key, u); - for (int j = 0; j < u.length; j++) { - out[j] ^= u[j]; - } - } - return out; - } - - @Override - public String getClientFirstMessage(final SSLSocket sslSocket) { - if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) { - clientFirstMessageBare = - "n=" - + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) - + ",r=" - + this.clientNonce; - state = State.AUTH_TEXT_SENT; - } - return Base64.encodeToString( - (gs2Header + clientFirstMessageBare).getBytes(Charset.defaultCharset()), - Base64.NO_WRAP); - } - - public String getResponse(final String challenge, final SSLSocket socket) - throws AuthenticationException { - switch (state) { - case AUTH_TEXT_SENT: - if (challenge == null) { - throw new AuthenticationException("challenge can not be null"); - } - byte[] serverFirstMessage; - try { - serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT); - } catch (IllegalArgumentException e) { - throw new AuthenticationException("Unable to decode server challenge", e); - } - final Tokenizer tokenizer = new Tokenizer(serverFirstMessage); - String nonce = ""; - int iterationCount = -1; - String salt = ""; - for (final String token : tokenizer) { - if (token.charAt(1) == '=') { - switch (token.charAt(0)) { - case 'i': - try { - iterationCount = Integer.parseInt(token.substring(2)); - } catch (final NumberFormatException e) { - throw new AuthenticationException(e); - } - break; - case 's': - salt = token.substring(2); - break; - case 'r': - nonce = token.substring(2); - break; - case 'm': - /* - * RFC 5802: - * m: This attribute is reserved for future extensibility. In this - * version of SCRAM, its presence in a client or a server message - * MUST cause authentication failure when the attribute is parsed by - * the other end. - */ - throw new AuthenticationException( - "Server sent reserved token: `m'"); - } - } - } - - if (iterationCount < 0) { - throw new AuthenticationException("Server did not send iteration count"); - } - if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) { - throw new AuthenticationException( - "Server nonce does not contain client nonce: " + nonce); - } - if (salt.isEmpty()) { - throw new AuthenticationException("Server sent empty salt"); - } - - final byte[] channelBindingData = getChannelBindingData(socket); - - final int gs2Len = this.gs2Header.getBytes().length; - final byte[] cMessage = new byte[gs2Len + channelBindingData.length]; - System.arraycopy(this.gs2Header.getBytes(), 0, cMessage, 0, gs2Len); - System.arraycopy( - channelBindingData, 0, cMessage, gs2Len, channelBindingData.length); - - final String clientFinalMessageWithoutProof = - "c=" + Base64.encodeToString(cMessage, Base64.NO_WRAP) + ",r=" + nonce; - - final byte[] authMessage = - (clientFirstMessageBare - + ',' - + new String(serverFirstMessage) - + ',' - + clientFinalMessageWithoutProof) - .getBytes(); - - final KeyPair keys; - try { - keys = - getKeyPair( - CryptoHelper.saslPrep(account.getPassword()), - salt, - iterationCount); - } catch (ExecutionException e) { - throw new AuthenticationException("Invalid keys generated"); - } - final byte[] clientSignature; - try { - serverSignature = hmac(keys.serverKey, authMessage); - final byte[] storedKey = digest(keys.clientKey); - - clientSignature = hmac(storedKey, authMessage); - - } catch (final InvalidKeyException e) { - throw new AuthenticationException(e); - } - - final byte[] clientProof = new byte[keys.clientKey.length]; - - if (clientSignature.length < keys.clientKey.length) { - throw new AuthenticationException( - "client signature was shorter than clientKey"); - } - - for (int i = 0; i < clientProof.length; i++) { - clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]); - } - - final String clientFinalMessage = - clientFinalMessageWithoutProof - + ",p=" - + Base64.encodeToString(clientProof, Base64.NO_WRAP); - state = State.RESPONSE_SENT; - return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP); - case RESPONSE_SENT: - try { - final String clientCalculatedServerFinalMessage = - "v=" + Base64.encodeToString(serverSignature, Base64.NO_WRAP); - if (!clientCalculatedServerFinalMessage.equals( - new String(Base64.decode(challenge, Base64.DEFAULT)))) { - throw new Exception(); - } - state = State.VALID_SERVER_RESPONSE; - return ""; - } catch (Exception e) { - throw new AuthenticationException( - "Server final message does not match calculated final message"); - } - default: - throw new InvalidStateException(state); - } - } - - protected byte[] getChannelBindingData(final SSLSocket sslSocket) - throws AuthenticationException { - if (this.channelBinding == ChannelBinding.NONE) { - return new byte[0]; - } - throw new AssertionError("getChannelBindingData needs to be overwritten"); - } - - private static class CacheKey { - final String algorithm; - final String password; - final String salt; - final int iterations; - - private CacheKey(String algorithm, String password, String salt, int iterations) { - this.algorithm = algorithm; - this.password = password; - this.salt = salt; - this.iterations = iterations; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CacheKey cacheKey = (CacheKey) o; - return iterations == cacheKey.iterations - && Objects.equal(algorithm, cacheKey.algorithm) - && Objects.equal(password, cacheKey.password) - && Objects.equal(salt, cacheKey.salt); - } - - @Override - public int hashCode() { - return Objects.hashCode(algorithm, password, salt, iterations); - } - } - - private static class KeyPair { - final byte[] clientKey; - final byte[] serverKey; - - KeyPair(final byte[] clientKey, final byte[] serverKey) { - this.clientKey = clientKey; - this.serverKey = serverKey; - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java deleted file mode 100644 index 0c836933f..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java +++ /dev/null @@ -1,23 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import javax.net.ssl.SSLSocket; - -import eu.siacs.conversations.entities.Account; - -public abstract class ScramPlusMechanism extends ScramMechanism implements ChannelBindingMechanism { - - ScramPlusMechanism(Account account, ChannelBinding channelBinding) { - super(account, channelBinding); - } - - @Override - protected byte[] getChannelBindingData(final SSLSocket sslSocket) - throws AuthenticationException { - return ChannelBindingMechanism.getChannelBindingData(sslSocket, this.channelBinding); - } - - @Override - public ChannelBinding getChannelBinding() { - return this.channelBinding; - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java deleted file mode 100644 index 1e0fc32b2..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java +++ /dev/null @@ -1,37 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import com.google.common.hash.HashFunction; -import com.google.common.hash.Hashing; - -import eu.siacs.conversations.entities.Account; - -public class ScramSha1 extends ScramMechanism { - - public static final String MECHANISM = "SCRAM-SHA-1"; - - public ScramSha1(final Account account) { - super(account, ChannelBinding.NONE); - } - - @Override - protected HashFunction getHMac(final byte[] key) { - return (key == null || key.length == 0) - ? Hashing.hmacSha1(EMPTY_KEY) - : Hashing.hmacSha1(key); - } - - @Override - protected HashFunction getDigest() { - return Hashing.sha1(); - } - - @Override - public int getPriority() { - return 20; - } - - @Override - public String getMechanism() { - return MECHANISM; - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java deleted file mode 100644 index 2ca27570f..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1Plus.java +++ /dev/null @@ -1,37 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import com.google.common.hash.HashFunction; -import com.google.common.hash.Hashing; - -import eu.siacs.conversations.entities.Account; - -public class ScramSha1Plus extends ScramPlusMechanism { - - public static final String MECHANISM = "SCRAM-SHA-1-PLUS"; - - public ScramSha1Plus(final Account account, final ChannelBinding channelBinding) { - super(account, channelBinding); - } - - @Override - protected HashFunction getHMac(final byte[] key) { - return (key == null || key.length == 0) - ? Hashing.hmacSha1(EMPTY_KEY) - : Hashing.hmacSha1(key); - } - - @Override - protected HashFunction getDigest() { - return Hashing.sha1(); - } - - @Override - public int getPriority() { - return 35; // higher than SCRAM-SHA512 (30) - } - - @Override - public String getMechanism() { - return MECHANISM; - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java deleted file mode 100644 index bb7acc136..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java +++ /dev/null @@ -1,37 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import com.google.common.hash.HashFunction; -import com.google.common.hash.Hashing; - -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.digests.SHA256Digest; -import org.bouncycastle.crypto.macs.HMac; - -import eu.siacs.conversations.entities.Account; - -public class ScramSha256 extends ScramMechanism { - public static final String MECHANISM = "SCRAM-SHA-256";public ScramSha256(final Account account) { - super(account, ChannelBinding.NONE); - } - - @Override - protected HashFunction getHMac(final byte[] key) { - return (key == null || key.length == 0) - ? Hashing.hmacSha256(EMPTY_KEY) - : Hashing.hmacSha256(key); - } - - @Override - protected HashFunction getDigest() { - return Hashing.sha256(); - } - @Override - public int getPriority() { - return 25; - } - - @Override - public String getMechanism() { - return MECHANISM; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java deleted file mode 100644 index 4db33a2fa..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256Plus.java +++ /dev/null @@ -1,37 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import com.google.common.hash.HashFunction; -import com.google.common.hash.Hashing; - -import eu.siacs.conversations.entities.Account; - -public class ScramSha256Plus extends ScramPlusMechanism { - - public static final String MECHANISM = "SCRAM-SHA-256-PLUS"; - - public ScramSha256Plus(final Account account, final ChannelBinding channelBinding) { - super(account, channelBinding); - } - - @Override - protected HashFunction getHMac(final byte[] key) { - return (key == null || key.length == 0) - ? Hashing.hmacSha256(EMPTY_KEY) - : Hashing.hmacSha256(key); - } - - @Override - protected HashFunction getDigest() { - return Hashing.sha256(); - } - - @Override - public int getPriority() { - return 40; - } - - @Override - public String getMechanism() { - return MECHANISM; - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java deleted file mode 100644 index 006d3d9ff..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java +++ /dev/null @@ -1,41 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import com.google.common.hash.HashFunction; -import com.google.common.hash.Hashing; - -import org.bouncycastle.crypto.Digest; -import org.bouncycastle.crypto.digests.SHA512Digest; -import org.bouncycastle.crypto.macs.HMac; - -import eu.siacs.conversations.entities.Account; - -public class ScramSha512 extends ScramMechanism { - - public static final String MECHANISM = "SCRAM-SHA-512"; - - public ScramSha512(final Account account) { - super(account, ChannelBinding.NONE); - } - - @Override - protected HashFunction getHMac(final byte[] key) { - return (key == null || key.length == 0) - ? Hashing.hmacSha512(EMPTY_KEY) - : Hashing.hmacSha512(key); - } - - @Override - protected HashFunction getDigest() { - return Hashing.sha512(); - } - - @Override - public int getPriority() { - return 30; - } - - @Override - public String getMechanism() { - return MECHANISM; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java deleted file mode 100644 index 5d8461973..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512Plus.java +++ /dev/null @@ -1,37 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import com.google.common.hash.HashFunction; -import com.google.common.hash.Hashing; - -import eu.siacs.conversations.entities.Account; - -public class ScramSha512Plus extends ScramPlusMechanism { - - public static final String MECHANISM = "SCRAM-SHA-512-PLUS"; - - public ScramSha512Plus(final Account account, final ChannelBinding channelBinding) { - super(account, channelBinding); - } - - @Override - protected HashFunction getHMac(final byte[] key) { - return (key == null || key.length == 0) - ? Hashing.hmacSha512(EMPTY_KEY) - : Hashing.hmacSha512(key); - } - - @Override - protected HashFunction getDigest() { - return Hashing.sha512(); - } - - @Override - public int getPriority() { - return 45; - } - - @Override - public String getMechanism() { - return MECHANISM; - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java deleted file mode 100644 index 9cb170c5a..000000000 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java +++ /dev/null @@ -1,77 +0,0 @@ -package eu.siacs.conversations.crypto.sasl; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; - -/** A tokenizer for GS2 header strings */ -public final class Tokenizer implements Iterator, Iterable { - private final List parts; - private int index; - - public Tokenizer(final byte[] challenge) { - final String challengeString = new String(challenge); - parts = new ArrayList<>(Arrays.asList(challengeString.split(","))); - // Trim parts. - for (int i = 0; i < parts.size(); i++) { - parts.set(i, parts.get(i).trim()); - } - index = 0; - } - - /** - * Returns true if there is at least one more element, false otherwise. - * - * @see #next - */ - @Override - public boolean hasNext() { - return parts.size() != index + 1; - } - - /** - * Returns the next object and advances the iterator. - * - * @return the next object. - * @throws java.util.NoSuchElementException if there are no more elements. - * @see #hasNext - */ - @Override - public String next() { - if (hasNext()) { - return parts.get(index++); - } else { - throw new NoSuchElementException("No such element. Size is: " + parts.size()); - } - } - - /** - * Removes the last object returned by {@code next} from the collection. This method can only be - * called once between each call to {@code next}. - * - * @throws UnsupportedOperationException if removing is not supported by the collection being - * iterated. - * @throws IllegalStateException if {@code next} has not been called, or {@code remove} has - * already been called after the last call to {@code next}. - */ - @Override - public void remove() { - if (index <= 0) { - throw new IllegalStateException( - "You can't delete an element before first next() method call"); - } - parts.remove(--index); - } - - /** - * Returns an {@link java.util.Iterator} for the elements in this object. - * - * @return An {@code Iterator} instance. - */ - @Override - public Iterator iterator() { - return parts.iterator(); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java b/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java deleted file mode 100644 index a80712b55..000000000 --- a/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java +++ /dev/null @@ -1,20 +0,0 @@ -package eu.siacs.conversations.entities; - -import android.content.ContentValues; - -public abstract class AbstractEntity { - - public static final String UUID = "uuid"; - - protected String uuid; - - public String getUuid() { - return this.uuid; - } - - public abstract ContentValues getContentValues(); - - public boolean equals(AbstractEntity entity) { - return this.getUuid().equals(entity.getUuid()); - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java deleted file mode 100644 index 2b2e44d30..000000000 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ /dev/null @@ -1,928 +0,0 @@ -package eu.siacs.conversations.entities; - -import android.content.ContentValues; -import android.database.Cursor; -import android.os.SystemClock; -import android.util.Log; - -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; - -import net.java.otr4j.crypto.OtrCryptoEngineImpl; -import net.java.otr4j.crypto.OtrCryptoException; -import java.security.PublicKey; -import java.security.interfaces.DSAPublicKey; -import java.util.Locale; - -import eu.siacs.conversations.crypto.sasl.HashedToken; -import eu.siacs.conversations.crypto.sasl.HashedTokenSha256; -import eu.siacs.conversations.crypto.sasl.HashedTokenSha512; -import eu.siacs.conversations.crypto.sasl.ChannelBindingMechanism; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.PgpDecryptionService; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; -import eu.siacs.conversations.crypto.OtrService; -import eu.siacs.conversations.services.AvatarService; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.XmppConnection; -import eu.siacs.conversations.xmpp.jingle.RtpCapability; -import eu.siacs.conversations.crypto.sasl.ChannelBinding; -import eu.siacs.conversations.crypto.sasl.SaslMechanism; -import eu.siacs.conversations.crypto.sasl.ScramPlusMechanism; - -public class Account extends AbstractEntity implements AvatarService.Avatarable { - - public static final String TABLENAME = "accounts"; - - public static final String USERNAME = "username"; - public static final String SERVER = "server"; - public static final String PASSWORD = "password"; - public static final String OPTIONS = "options"; - public static final String ROSTERVERSION = "rosterversion"; - public static final String KEYS = "keys"; - public static final String AVATAR = "avatar"; - public static final String DISPLAY_NAME = "display_name"; - public static final String HOSTNAME = "hostname"; - public static final String PORT = "port"; - public static final String STATUS = "status"; - public static final String STATUS_MESSAGE = "status_message"; - public static final String RESOURCE = "resource"; - public static final String PINNED_MECHANISM = "pinned_mechanism"; - public static final String PINNED_CHANNEL_BINDING = "pinned_channel_binding"; - public static final String FAST_MECHANISM = "fast_mechanism"; - public static final String FAST_TOKEN = "fast_token"; - - public static final int OPTION_DISABLED = 1; - public static final int OPTION_REGISTER = 2; - public static final int OPTION_MAGIC_CREATE = 4; - public static final int OPTION_REQUIRES_ACCESS_MODE_CHANGE = 5; - public static final int OPTION_LOGGED_IN_SUCCESSFULLY = 6; - public static final int OPTION_HTTP_UPLOAD_AVAILABLE = 7; - public static final int OPTION_UNVERIFIED = 8; - public static final int OPTION_FIXED_USERNAME = 9; - public static final int OPTION_QUICKSTART_AVAILABLE = 10; - - private static final String KEY_PGP_SIGNATURE = "pgp_signature"; - private static final String KEY_PGP_ID = "pgp_id"; - private static final String KEY_PINNED_MECHANISM = "pinned_mechanism"; - public static final String KEY_PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration"; - - - protected final JSONObject keys; - private final Roster roster = new Roster(this); - private final Collection blocklist = new CopyOnWriteArraySet<>(); - public final Set pendingConferenceJoins = new HashSet<>(); - public final Set pendingConferenceLeaves = new HashSet<>(); - public final Set inProgressConferenceJoins = new HashSet<>(); - public final Set inProgressConferencePings = new HashSet<>(); - protected Jid jid; - protected String password; - protected int options = 0; - protected State status = State.OFFLINE; - private State lastErrorStatus = State.OFFLINE; - protected String resource; - protected String avatar; - protected String hostname = null; - protected int port = 5222; - protected boolean online = false; - private OtrService mOtrService = null; - private String rosterVersion; - private String displayName = null; - private AxolotlService axolotlService = null; - private PgpDecryptionService pgpDecryptionService = null; - private XmppConnection xmppConnection = null; - private long mEndGracePeriod = 0L; - private String otrFingerprint; - private final Map bookmarks = new HashMap<>(); - private Presence.Status presenceStatus; - private String presenceStatusMessage; - private String pinnedMechanism; - private String pinnedChannelBinding; - private String fastMechanism; - private String fastToken; - - public Account(final Jid jid, final String password) { - this( - java.util.UUID.randomUUID().toString(), - jid, - password, - 0, - null, - "", - null, - null, - null, - 5222, - Presence.Status.ONLINE, - null, - null, - null, - null, - null); - } - - private Account( - final String uuid, - final Jid jid, - final String password, - final int options, - final String rosterVersion, - final String keys, - final String avatar, - String displayName, - String hostname, - int port, - final Presence.Status status, - String statusMessage, - final String pinnedMechanism, - final String pinnedChannelBinding, - final String fastMechanism, - final String fastToken) { - this.uuid = uuid; - this.jid = jid; - this.password = password; - this.options = options; - this.rosterVersion = rosterVersion; - JSONObject tmp; - try { - tmp = new JSONObject(keys); - } catch (JSONException e) { - tmp = new JSONObject(); - } - this.keys = tmp; - this.avatar = avatar; - this.displayName = displayName; - this.hostname = hostname; - this.port = port; - this.presenceStatus = status; - this.presenceStatusMessage = statusMessage; - this.pinnedMechanism = pinnedMechanism; - this.pinnedChannelBinding = pinnedChannelBinding; - this.fastMechanism = fastMechanism; - this.fastToken = fastToken; - } - - public static Account fromCursor(final Cursor cursor) { - final Jid jid; - try { - final String resource = cursor.getString(cursor.getColumnIndexOrThrow(RESOURCE)); - jid = - Jid.of( - cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)), - cursor.getString(cursor.getColumnIndexOrThrow(SERVER)), - resource == null || resource.trim().isEmpty() ? null : resource); - } catch (final IllegalArgumentException e) { - Log.d( - Config.LOGTAG, - cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)) - + "@" - + cursor.getString(cursor.getColumnIndexOrThrow(SERVER))); - throw new AssertionError(e); - } - return new Account( - cursor.getString(cursor.getColumnIndexOrThrow(UUID)), - jid, - cursor.getString(cursor.getColumnIndexOrThrow(PASSWORD)), - cursor.getInt(cursor.getColumnIndexOrThrow(OPTIONS)), - cursor.getString(cursor.getColumnIndexOrThrow(ROSTERVERSION)), - cursor.getString(cursor.getColumnIndexOrThrow(KEYS)), - cursor.getString(cursor.getColumnIndexOrThrow(AVATAR)), - cursor.getString(cursor.getColumnIndexOrThrow(DISPLAY_NAME)), - cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME)), - cursor.getInt(cursor.getColumnIndexOrThrow(PORT)), - Presence.Status.fromShowString( - cursor.getString(cursor.getColumnIndexOrThrow(STATUS))), - cursor.getString(cursor.getColumnIndexOrThrow(STATUS_MESSAGE)), - cursor.getString(cursor.getColumnIndexOrThrow(PINNED_MECHANISM)), - cursor.getString(cursor.getColumnIndexOrThrow(PINNED_CHANNEL_BINDING)), - cursor.getString(cursor.getColumnIndexOrThrow(FAST_MECHANISM)), - cursor.getString(cursor.getColumnIndexOrThrow(FAST_TOKEN))); - } - - public boolean httpUploadAvailable(long size) { - return xmppConnection != null && xmppConnection.getFeatures().httpUpload(size); - } - - public boolean httpUploadAvailable() { - return isOptionSet(OPTION_HTTP_UPLOAD_AVAILABLE) || httpUploadAvailable(0); - } - - public String getDisplayName() { - return displayName; - } - - public void setDisplayName(String displayName) { - this.displayName = displayName; - } - - public Contact getSelfContact() { - return getRoster().getContact(jid); - } - - public boolean hasPendingPgpIntent(Conversation conversation) { - return pgpDecryptionService != null && pgpDecryptionService.hasPendingIntent(conversation); - } - - public boolean isPgpDecryptionServiceConnected() { - return pgpDecryptionService != null && pgpDecryptionService.isConnected(); - } - - public boolean setShowErrorNotification(boolean newValue) { - boolean oldValue = showErrorNotification(); - setKey("show_error", Boolean.toString(newValue)); - return newValue != oldValue; - } - - public boolean showErrorNotification() { - String key = getKey("show_error"); - return key == null || Boolean.parseBoolean(key); - } - - public boolean isEnabled() { - return !isOptionSet(Account.OPTION_DISABLED); - } - - public boolean isOptionSet(final int option) { - return ((options & (1 << option)) != 0); - } - - public boolean setOption(final int option, final boolean value) { - final int before = this.options; - if (value) { - this.options |= 1 << option; - } else { - this.options &= ~(1 << option); - } - return before != this.options; - } - - public String getUsername() { - return jid.getEscapedLocal(); - } - - public boolean setJid(final Jid next) { - final Jid previousFull = this.jid; - final Jid prev = this.jid != null ? this.jid.asBareJid() : null; - final boolean changed = prev == null || (next != null && !prev.equals(next.asBareJid())); - if (changed) { - final AxolotlService oldAxolotlService = this.axolotlService; - if (oldAxolotlService != null) { - oldAxolotlService.destroy(); - this.jid = next; - this.axolotlService = oldAxolotlService.makeNew(); - } - } - this.jid = next; - return next != null && !next.equals(previousFull); - } - - public Jid getDomain() { - return jid.getDomain(); - } - - public String getServer() { - return jid.getDomain().toEscapedString(); - } - - public String getPassword() { - return password; - } - - public void setPassword(final String password) { - this.password = password; - } - - public String getHostname() { - return Strings.nullToEmpty(this.hostname); - } - - public void setHostname(String hostname) { - this.hostname = hostname; - } - - public boolean isOnion() { - final String server = getServer(); - return server != null && server.endsWith(".onion"); - } - - public boolean isI2P() { - final String server = getServer(); - return server != null && server.endsWith(".i2p"); - } - - public int getPort() { - return this.port; - } - - public void setPort(int port) { - this.port = port; - } - - public State getStatus() { - if (isOptionSet(OPTION_DISABLED)) { - return State.DISABLED; - } else { - return this.status; - } - } - - public State getLastErrorStatus() { - return this.lastErrorStatus; - } - - public void setStatus(final State status) { - this.status = status; - if (status.isError || status == State.ONLINE) { - this.lastErrorStatus = status; - } - } - public void setPinnedMechanism(final SaslMechanism mechanism) { - this.pinnedMechanism = mechanism.getMechanism(); - if (mechanism instanceof ChannelBindingMechanism) { - this.pinnedChannelBinding = - ((ChannelBindingMechanism) mechanism).getChannelBinding().toString(); - } else { - this.pinnedChannelBinding = null; - } - } - public void setFastToken(final HashedToken.Mechanism mechanism, final String token) { - this.fastMechanism = mechanism.name(); - this.fastToken = token; - } - - public void resetFastToken() { - this.fastMechanism = null; - this.fastToken = null; - } - - - public void resetPinnedMechanism() { - this.pinnedMechanism = null; - this.pinnedChannelBinding = null; - setKey(Account.KEY_PINNED_MECHANISM, String.valueOf(-1)); - } - - public int getPinnedMechanismPriority() { - final int fallback = getKeyAsInt(KEY_PINNED_MECHANISM, -1); - if (Strings.isNullOrEmpty(this.pinnedMechanism)) { - return fallback; - } - final SaslMechanism saslMechanism = getPinnedMechanism(); - if (saslMechanism == null) { - return fallback; - } else { - return saslMechanism.getPriority(); - } - } - - private SaslMechanism getPinnedMechanism() { - final String mechanism = Strings.nullToEmpty(this.pinnedMechanism); - final ChannelBinding channelBinding = ChannelBinding.get(this.pinnedChannelBinding); - return new SaslMechanism.Factory(this).of(mechanism, channelBinding); - } - public HashedToken getFastMechanism() { - final HashedToken.Mechanism fastMechanism = HashedToken.Mechanism.ofOrNull(this.fastMechanism); - final String token = this.fastToken; - if (fastMechanism == null || Strings.isNullOrEmpty(token)) { - return null; - } - if (fastMechanism.hashFunction.equals("SHA-256")) { - return new HashedTokenSha256(this, fastMechanism.channelBinding); - } else if (fastMechanism.hashFunction.equals("SHA-512")) { - return new HashedTokenSha512(this, fastMechanism.channelBinding); - } else { - return null; - } - } - - public SaslMechanism getQuickStartMechanism() { - final HashedToken hashedTokenMechanism = getFastMechanism(); - if (hashedTokenMechanism != null) { - return hashedTokenMechanism; - } - return getPinnedMechanism(); - } - - public String getFastToken() { - return this.fastToken; - } - - - public State getTrueStatus() { - return this.status; - } - - public boolean errorStatus() { - return getStatus().isError(); - } - - public boolean hasErrorStatus() { - return getXmppConnection() != null - && (getStatus().isError() || getStatus() == State.CONNECTING) - && getXmppConnection().getAttempt() >= 3; - } - - public Presence.Status getPresenceStatus() { - return this.presenceStatus; - } - - public void setPresenceStatus(Presence.Status status) { - this.presenceStatus = status; - } - - public String getPresenceStatusMessage() { - return this.presenceStatusMessage; - } - - public void setPresenceStatusMessage(String message) { - this.presenceStatusMessage = message; - } - - public String getResource() { - return jid.getResource(); - } - - public void setResource(final String resource) { - this.jid = this.jid.withResource(resource); - } - - public Jid getJid() { - return jid; - } - - public JSONObject getKeys() { - return keys; - } - - public String getKey(final String name) { - synchronized (this.keys) { - return this.keys.optString(name, null); - } - } - - public int getKeyAsInt(final String name, int defaultValue) { - String key = getKey(name); - try { - return key == null ? defaultValue : Integer.parseInt(key); - } catch (NumberFormatException e) { - return defaultValue; - } - } - - public boolean setKey(final String keyName, final String keyValue) { - synchronized (this.keys) { - try { - this.keys.put(keyName, keyValue); - return true; - } catch (final JSONException e) { - return false; - } - } - } - - public void setPrivateKeyAlias(final String alias) { - setKey("private_key_alias", alias); - } - - public String getPrivateKeyAlias() { - return getKey("private_key_alias"); - } - - @Override - public ContentValues getContentValues() { - final ContentValues values = new ContentValues(); - values.put(UUID, uuid); - values.put(USERNAME, jid.getLocal()); - values.put(SERVER, jid.getDomain().toEscapedString()); - values.put(PASSWORD, password); - values.put(OPTIONS, options); - synchronized (this.keys) { - values.put(KEYS, this.keys.toString()); - } - values.put(ROSTERVERSION, rosterVersion); - values.put(AVATAR, avatar); - values.put(DISPLAY_NAME, displayName); - values.put(HOSTNAME, hostname); - values.put(PORT, port); - values.put(STATUS, presenceStatus.toShowString()); - values.put(STATUS_MESSAGE, presenceStatusMessage); - values.put(RESOURCE, jid.getResource()); - values.put(PINNED_MECHANISM, pinnedMechanism); - values.put(PINNED_CHANNEL_BINDING, pinnedChannelBinding); - values.put(FAST_MECHANISM, this.fastMechanism); - values.put(FAST_TOKEN, this.fastToken); - return values; - } - - public AxolotlService getAxolotlService() { - return axolotlService; - } - - public void initAccountServices(final XmppConnectionService context) { - this.mOtrService = new OtrService(context, this); - this.axolotlService = new AxolotlService(this, context); - this.pgpDecryptionService = new PgpDecryptionService(context); - if (xmppConnection != null) { - xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService); - } - } - public OtrService getOtrService() { - return this.mOtrService; - } - - public PgpDecryptionService getPgpDecryptionService() { - return this.pgpDecryptionService; - } - - public XmppConnection getXmppConnection() { - return this.xmppConnection; - } - - public void setXmppConnection(final XmppConnection connection) { - this.xmppConnection = connection; - } - - public String getOtrFingerprint() { - if (this.otrFingerprint == null) { - try { - if (this.mOtrService == null) { - return null; - } - final PublicKey publicKey = this.mOtrService.getPublicKey(); - if (publicKey == null || !(publicKey instanceof DSAPublicKey)) { - return null; - } - this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey).toLowerCase(Locale.US); - return this.otrFingerprint; - } catch (final OtrCryptoException ignored) { - return null; - } - } else { - return this.otrFingerprint; - } - } - - public String getRosterVersion() { - if (this.rosterVersion == null) { - return ""; - } else { - return this.rosterVersion; - } - } - - public void setRosterVersion(final String version) { - this.rosterVersion = version; - } - - public int countPresences() { - return this.getSelfContact().getPresences().size(); - } - - public int activeDevicesWithRtpCapability() { - int i = 0; - for (Presence presence : getSelfContact().getPresences().getPresences()) { - if (RtpCapability.check(presence) != RtpCapability.Capability.NONE) { - i++; - } - } - return i; - } - - public String getPgpSignature() { - return getKey(KEY_PGP_SIGNATURE); - } - - public boolean setPgpSignature(String signature) { - return setKey(KEY_PGP_SIGNATURE, signature); - } - - public boolean unsetPgpSignature() { - synchronized (this.keys) { - return keys.remove(KEY_PGP_SIGNATURE) != null; - } - } - - public long getPgpId() { - synchronized (this.keys) { - if (keys.has(KEY_PGP_ID)) { - try { - return keys.getLong(KEY_PGP_ID); - } catch (JSONException e) { - return 0; - } - } else { - return 0; - } - } - } - - public boolean setPgpSignId(long pgpID) { - synchronized (this.keys) { - try { - if (pgpID == 0) { - keys.remove(KEY_PGP_ID); - } else { - keys.put(KEY_PGP_ID, pgpID); - } - } catch (JSONException e) { - return false; - } - return true; - } - } - - public Roster getRoster() { - return this.roster; - } - - public Collection getBookmarks() { - synchronized (this.bookmarks) { - return ImmutableList.copyOf(this.bookmarks.values()); - } - } - - public void setBookmarks(final Map bookmarks) { - synchronized (this.bookmarks) { - this.bookmarks.clear(); - this.bookmarks.putAll(bookmarks); - } - } - - public void putBookmark(final Bookmark bookmark) { - synchronized (this.bookmarks) { - this.bookmarks.put(bookmark.getJid(), bookmark); - } - } - - public void removeBookmark(Bookmark bookmark) { - synchronized (this.bookmarks) { - this.bookmarks.remove(bookmark.getJid()); - } - } - - public void removeBookmark(Jid jid) { - synchronized (this.bookmarks) { - this.bookmarks.remove(jid); - } - } - - public Set getBookmarkedJids() { - synchronized (this.bookmarks) { - return new HashSet<>(this.bookmarks.keySet()); - } - } - - public Bookmark getBookmark(final Jid jid) { - synchronized (this.bookmarks) { - return this.bookmarks.get(jid.asBareJid()); - } - } - - public boolean setAvatar(final String filename) { - if (this.avatar != null && this.avatar.equals(filename)) { - return false; - } else { - this.avatar = filename; - return true; - } - } - - public String getAvatar() { - return this.avatar; - } - - public void activateGracePeriod(final long duration) { - if (duration > 0) { - this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration; - } - } - - public void deactivateGracePeriod() { - this.mEndGracePeriod = 0L; - } - - public boolean inGracePeriod() { - return SystemClock.elapsedRealtime() < this.mEndGracePeriod; - } - - public String getShareableUri() { - List fingerprints = this.getFingerprints(); - String uri = "xmpp:" + this.getJid().asBareJid().toEscapedString(); - if (fingerprints.size() > 0) { - return XmppUri.getFingerprintUri(uri, fingerprints, ';'); - } else { - return uri; - } - } - - public String getShareableLink() { - List fingerprints = this.getFingerprints(); - String uri = Config.inviteUserURL + XmppUri.lameUrlEncode(this.getJid().asBareJid().toEscapedString()); - if (fingerprints.size() > 0) { - return XmppUri.getFingerprintUri(uri, fingerprints, '&'); - } else { - return uri; - } - } - - private List getFingerprints() { - ArrayList fingerprints = new ArrayList<>(); - final String otr = this.getOtrFingerprint(); - if (otr != null) { - fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OTR, otr)); - } - if (axolotlService == null) { - return fingerprints; - } - fingerprints.add( - new XmppUri.Fingerprint( - XmppUri.FingerprintType.OMEMO, - axolotlService.getOwnFingerprint().substring(2), - axolotlService.getOwnDeviceId())); - for (XmppAxolotlSession session : axolotlService.findOwnSessions()) { - if (session.getTrust().isVerified() && session.getTrust().isActive()) { - fingerprints.add( - new XmppUri.Fingerprint( - XmppUri.FingerprintType.OMEMO, - session.getFingerprint().substring(2).replaceAll("\\s", ""), - session.getRemoteAddress().getDeviceId())); - } - } - return fingerprints; - } - - public boolean isBlocked(final ListItem contact) { - final Jid jid = contact.getJid(); - return jid != null - && (blocklist.contains(jid.asBareJid()) || blocklist.contains(jid.getDomain())); - } - - public boolean isBlocked(final Jid jid) { - return jid != null && blocklist.contains(jid.asBareJid()); - } - - public Collection getBlocklist() { - return this.blocklist; - } - - public void clearBlocklist() { - getBlocklist().clear(); - } - - public boolean isOnlineAndConnected() { - return this.getStatus() == State.ONLINE && this.getXmppConnection() != null; - } - - @Override - public int getAvatarBackgroundColor() { - return UIHelper.getColorForName(jid.asBareJid().toString()); - } - - @Override - public String getAvatarName() { - throw new IllegalStateException("This method should not be called"); - } - - public enum State { - DISABLED(false, false), - OFFLINE(false), - CONNECTING(false), - ONLINE(false), - NO_INTERNET(false), - UNAUTHORIZED, - TEMPORARY_AUTH_FAILURE, - SERVER_NOT_FOUND, - REGISTRATION_SUCCESSFUL(false), - REGISTRATION_FAILED(true, false), - REGISTRATION_WEB(true, false), - REGISTRATION_CONFLICT(true, false), - REGISTRATION_NOT_SUPPORTED(true, false), - REGISTRATION_PLEASE_WAIT(true, false), - REGISTRATION_INVALID_TOKEN(true, false), - REGISTRATION_PASSWORD_TOO_WEAK(true, false), - TLS_ERROR, - TLS_ERROR_DOMAIN, - INCOMPATIBLE_SERVER, - INCOMPATIBLE_CLIENT, - TOR_NOT_AVAILABLE, - I2P_NOT_AVAILABLE, - DOWNGRADE_ATTACK, - SESSION_FAILURE, - BIND_FAILURE, - HOST_UNKNOWN, - STREAM_ERROR, - STREAM_OPENING_ERROR, - POLICY_VIOLATION, - PAYMENT_REQUIRED, - MISSING_INTERNET_PERMISSION(false); - - private final boolean isError; - private final boolean attemptReconnect; - - State(final boolean isError) { - this(isError, true); - } - - State(final boolean isError, final boolean reconnect) { - this.isError = isError; - this.attemptReconnect = reconnect; - } - - State() { - this(true, true); - } - - public boolean isError() { - return this.isError; - } - - public boolean isAttemptReconnect() { - return this.attemptReconnect; - } - - public int getReadableId() { - switch (this) { - case DISABLED: - return R.string.account_status_disabled; - case ONLINE: - return R.string.account_status_online; - case CONNECTING: - return R.string.account_status_connecting; - case OFFLINE: - return R.string.account_status_offline; - case UNAUTHORIZED: - return R.string.account_status_unauthorized; - case SERVER_NOT_FOUND: - return R.string.account_status_not_found; - case NO_INTERNET: - return R.string.account_status_no_internet; - case REGISTRATION_FAILED: - return R.string.account_status_regis_fail; - case REGISTRATION_WEB: - return R.string.account_status_regis_web; - case REGISTRATION_CONFLICT: - return R.string.account_status_regis_conflict; - case REGISTRATION_SUCCESSFUL: - return R.string.account_status_regis_success; - case REGISTRATION_NOT_SUPPORTED: - return R.string.account_status_regis_not_sup; - case REGISTRATION_INVALID_TOKEN: - return R.string.account_status_regis_invalid_token; - case TLS_ERROR: - return R.string.account_status_tls_error; - case TLS_ERROR_DOMAIN: - return R.string.account_status_tls_error_domain; - case INCOMPATIBLE_SERVER: - return R.string.account_status_incompatible_server; - case INCOMPATIBLE_CLIENT: - return R.string.account_status_incompatible_client; - case TOR_NOT_AVAILABLE: - return R.string.account_status_tor_unavailable; - case I2P_NOT_AVAILABLE: - return R.string.account_status_i2p_unavailable; - case BIND_FAILURE: - return R.string.account_status_bind_failure; - case SESSION_FAILURE: - return R.string.session_failure; - case DOWNGRADE_ATTACK: - return R.string.sasl_downgrade; - case HOST_UNKNOWN: - return R.string.account_status_host_unknown; - case POLICY_VIOLATION: - return R.string.account_status_policy_violation; - case REGISTRATION_PLEASE_WAIT: - return R.string.registration_please_wait; - case REGISTRATION_PASSWORD_TOO_WEAK: - return R.string.registration_password_too_weak; - case STREAM_ERROR: - return R.string.account_status_stream_error; - case STREAM_OPENING_ERROR: - return R.string.account_status_stream_opening_error; - case PAYMENT_REQUIRED: - return R.string.payment_required; - case MISSING_INTERNET_PERMISSION: - return R.string.missing_internet_permission; - default: - return R.string.account_status_unknown; - } - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/entities/AccountConfiguration.java b/src/main/java/eu/siacs/conversations/entities/AccountConfiguration.java deleted file mode 100644 index 65b6804f9..000000000 --- a/src/main/java/eu/siacs/conversations/entities/AccountConfiguration.java +++ /dev/null @@ -1,50 +0,0 @@ -package eu.siacs.conversations.entities; - -import com.google.common.base.Preconditions; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonSyntaxException; -import com.google.gson.annotations.SerializedName; - -import eu.siacs.conversations.xmpp.Jid; - -public class AccountConfiguration { - - private static final Gson GSON = new GsonBuilder().create(); - - public Protocol protocol; - public String address; - public String password; - - public Jid getJid() { - return Jid.ofEscaped(address); - } - - public static AccountConfiguration parse(final String input) { - final AccountConfiguration c; - try { - c = GSON.fromJson(input, AccountConfiguration.class); - } catch (JsonSyntaxException e) { - throw new IllegalArgumentException("Not a valid JSON string", e); - } - Preconditions.checkArgument( - c.protocol == Protocol.XMPP, - "Protocol must be XMPP" - ); - Preconditions.checkArgument( - c.address != null && c.getJid().isBareJid() && !c.getJid().isDomainJid(), - "Invalid XMPP address" - ); - Preconditions.checkArgument( - c.password != null && c.password.length() > 0, - "No password specified" - ); - return c; - } - - public enum Protocol { - @SerializedName("xmpp") XMPP, - } - -} - diff --git a/src/main/java/eu/siacs/conversations/entities/Blockable.java b/src/main/java/eu/siacs/conversations/entities/Blockable.java deleted file mode 100644 index 43504a4f1..000000000 --- a/src/main/java/eu/siacs/conversations/entities/Blockable.java +++ /dev/null @@ -1,15 +0,0 @@ -package eu.siacs.conversations.entities; - -import eu.siacs.conversations.xmpp.Jid; - -public interface Blockable { - boolean isBlocked(); - - boolean isDomainBlocked(); - - Jid getBlockedJid(); - - Jid getJid(); - - Account getAccount(); -} diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java deleted file mode 100644 index 1f7dc3ef0..000000000 --- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java +++ /dev/null @@ -1,267 +0,0 @@ -package eu.siacs.conversations.entities; - -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.utils.StringUtils; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.InvalidJid; -import eu.siacs.conversations.xmpp.Jid; - -public class Bookmark extends Element implements ListItem { - - private Account account; - private WeakReference conversation; - private Jid jid; - - public Bookmark(final Account account, final Jid jid) { - super("conference"); - this.jid = jid; - this.setAttribute("jid", jid); - this.account = account; - } - - private Bookmark(Account account) { - super("conference"); - this.account = account; - } - - public static Map parseFromStorage(Element storage, Account account) { - if (storage == null) { - return Collections.emptyMap(); - } - final HashMap bookmarks = new HashMap<>(); - for (final Element item : storage.getChildren()) { - if (item.getName().equals("conference")) { - final Bookmark bookmark = Bookmark.parse(item, account); - if (bookmark != null) { - final Bookmark old = bookmarks.put(bookmark.jid, bookmark); - if (old != null && old.getBookmarkName() != null && bookmark.getBookmarkName() == null) { - bookmark.setBookmarkName(old.getBookmarkName()); - } - } - } - } - return bookmarks; - } - - public static Map parseFromPubsub(Element pubsub, Account account) { - if (pubsub == null) { - return Collections.emptyMap(); - } - final Element items = pubsub.findChild("items"); - if (items != null && Namespace.BOOKMARKS2.equals(items.getAttribute("node"))) { - final Map bookmarks = new HashMap<>(); - for (Element item : items.getChildren()) { - if (item.getName().equals("item")) { - final Bookmark bookmark = Bookmark.parseFromItem(item, account); - if (bookmark != null) { - bookmarks.put(bookmark.jid, bookmark); - } - } - } - return bookmarks; - } - return Collections.emptyMap(); - } - - public static Bookmark parse(Element element, Account account) { - Bookmark bookmark = new Bookmark(account); - bookmark.setAttributes(element.getAttributes()); - bookmark.setChildren(element.getChildren()); - bookmark.jid = InvalidJid.getNullForInvalid(bookmark.getAttributeAsJid("jid")); - if (bookmark.jid == null) { - return null; - } - return bookmark; - } - - public static Bookmark parseFromItem(Element item, Account account) { - final Element conference = item.findChild("conference", Namespace.BOOKMARKS2); - if (conference == null) { - return null; - } - final Bookmark bookmark = new Bookmark(account); - bookmark.jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("id")); - if (bookmark.jid == null) { - return null; - } - bookmark.setBookmarkName(conference.getAttribute("name")); - bookmark.setAutojoin(conference.getAttributeAsBoolean("autojoin")); - bookmark.setNick(conference.findChildContent("nick")); - return bookmark; - } - - public void setAutojoin(boolean autojoin) { - if (autojoin) { - this.setAttribute("autojoin", "true"); - } else { - this.setAttribute("autojoin", "false"); - } - } - - @Override - public int compareTo(final @NonNull ListItem another) { - return this.getDisplayName().compareToIgnoreCase( - another.getDisplayName()); - } - - @Override - public String getDisplayName() { - final Conversation c = getConversation(); - final String name = getBookmarkName(); - if (c != null) { - return c.getName().toString(); - } else if (printableValue(name, false)) { - return name.trim(); - } else { - Jid jid = this.getJid(); - return jid != null && jid.getLocal() != null ? jid.getLocal() : ""; - } - } - - @Override - public int getOffline() { - return 0; - } - - public static boolean printableValue(@Nullable String value, boolean permitNone) { - return value != null && !value.trim().isEmpty() && (permitNone || !"None".equals(value)); - } - - public static boolean printableValue(@Nullable String value) { - return printableValue(value, true); - } - - @Override - public Jid getJid() { - return this.jid; - } - - public Jid getFullJid() { - final String nick = getNick(); - return jid == null || nick == null || nick.trim().isEmpty() ? jid : jid.withResource(nick); - } - - @Override - public List getTags(Context context) { - ArrayList tags = new ArrayList<>(); - for (Element element : getChildren()) { - if (element.getName().equals("group") && element.getContent() != null) { - String group = element.getContent(); - tags.add(new Tag(group, UIHelper.getColorForName(group, true), 0, account, false)); - } - } - return tags; - } - - @Override - public boolean getActive() { - return false; - } - - public String getNick() { - return this.findChildContent("nick"); - } - - public void setNick(String nick) { - Element element = this.findChild("nick"); - if (element == null) { - element = this.addChild("nick"); - } - element.setContent(nick); - } - - public boolean autojoin() { - return this.getAttributeAsBoolean("autojoin"); - } - - public String getPassword() { - return this.findChildContent("password"); - } - - public void setPassword(String password) { - Element element = this.findChild("password"); - if (element != null) { - element.setContent(password); - } - } - - @Override - public boolean match(Context context, String needle) { - if (needle == null) { - return true; - } - needle = needle.toLowerCase(Locale.US); - final Jid jid = getJid(); - return (jid != null && jid.toString().contains(needle)) || - getDisplayName().toLowerCase(Locale.US).contains(needle) || - matchInTag(context, needle); - } - - private boolean matchInTag(Context context, String needle) { - needle = needle.toLowerCase(Locale.US); - for (Tag tag : getTags(context)) { - if (tag.getName().toLowerCase(Locale.US).contains(needle)) { - return true; - } - } - return false; - } - - public Account getAccount() { - return this.account; - } - - public synchronized Conversation getConversation() { - return this.conversation != null ? this.conversation.get() : null; - } - - public synchronized void setConversation(Conversation conversation) { - if (this.conversation != null) { - this.conversation.clear(); - } - if (conversation == null) { - this.conversation = null; - } else { - this.conversation = new WeakReference<>(conversation); - conversation.getMucOptions().notifyOfBookmarkNick(getNick()); - } - } - - public String getBookmarkName() { - return this.getAttribute("name"); - } - - public boolean setBookmarkName(String name) { - String before = getBookmarkName(); - if (name != null) { - this.setAttribute("name", name); - } else { - this.removeAttribute("name"); - } - return StringUtils.changed(before, name); - } - - @Override - public int getAvatarBackgroundColor() { - return UIHelper.getColorForName(jid != null ? jid.asBareJid().toString() : getDisplayName()); - } - - @Override - public String getAvatarName() { - return getDisplayName(); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java deleted file mode 100644 index c0f5b5f04..000000000 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ /dev/null @@ -1,686 +0,0 @@ -package eu.siacs.conversations.entities; - -import android.annotation.SuppressLint; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.text.TextUtils; - -import androidx.annotation.NonNull; - -import com.google.common.base.Strings; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Objects; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.android.AbstractPhoneContact; -import eu.siacs.conversations.android.JabberIdContact; -import eu.siacs.conversations.services.QuickConversationsService; -import eu.siacs.conversations.utils.JidHelper; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.jingle.RtpCapability; -import eu.siacs.conversations.xmpp.pep.Avatar; - -public class Contact implements ListItem, Blockable { - public static final String TABLENAME = "contacts"; - - public static final String SYSTEMNAME = "systemname"; - public static final String SERVERNAME = "servername"; - public static final String PRESENCE_NAME = "presence_name"; - public static final String JID = "jid"; - public static final String OPTIONS = "options"; - public static final String SYSTEMACCOUNT = "systemaccount"; - public static final String PHOTOURI = "photouri"; - public static final String KEYS = "pgpkey"; - public static final String ACCOUNT = "accountUuid"; - public static final String AVATAR = "avatar"; - public static final String LAST_PRESENCE = "last_presence"; - public static final String LAST_TIME = "last_time"; - public static final String GROUPS = "groups"; - public static final String RTP_CAPABILITY = "rtpCapability"; - private String accountUuid; - private String systemName; - private String serverName; - private String presenceName; - private String commonName; - protected Jid jid; - private int subscription = 0; - private Uri systemAccount; - private String photoUri; - private final JSONObject keys; - private JSONArray groups = new JSONArray(); - private final Presences presences = new Presences(); - protected Account account; - protected Avatar avatar; - - private boolean mActive = false; - private long mLastseen = 0; - private String mLastPresence = null; - private RtpCapability.Capability rtpCapability; - - public Contact(final String account, final String systemName, final String serverName, final String presenceName, - final Jid jid, final int subscription, final String photoUri, - final Uri systemAccount, final String keys, final String avatar, final long lastseen, - final String presence, final String groups, final RtpCapability.Capability rtpCapability) { - this.accountUuid = account; - this.systemName = systemName; - this.serverName = serverName; - this.presenceName = presenceName; - this.jid = jid; - this.subscription = subscription; - this.photoUri = photoUri; - this.systemAccount = systemAccount; - JSONObject tmpJsonObject; - try { - tmpJsonObject = (keys == null ? new JSONObject("") : new JSONObject(keys)); - } catch (JSONException e) { - tmpJsonObject = new JSONObject(); - } - this.keys = tmpJsonObject; - if (avatar != null) { - this.avatar = new Avatar(); - this.avatar.sha1sum = avatar; - this.avatar.origin = Avatar.Origin.VCARD; //always assume worst - } - try { - this.groups = (groups == null ? new JSONArray() : new JSONArray(groups)); - } catch (JSONException e) { - this.groups = new JSONArray(); - } - this.mLastseen = lastseen; - this.mLastPresence = presence; - this.rtpCapability = rtpCapability; - } - - public Contact(final Jid jid) { - this.jid = jid; - this.keys = new JSONObject(); - } - - @SuppressLint("Range") - public static Contact fromCursor(final Cursor cursor) { - final Jid jid; - try { - jid = Jid.of(cursor.getString(cursor.getColumnIndex(JID))); - } catch (final IllegalArgumentException e) { - // TODO: Borked DB... handle this somehow? - return null; - } - Uri systemAccount; - try { - systemAccount = Uri.parse(cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT))); - } catch (Exception e) { - systemAccount = null; - } - return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)), - cursor.getString(cursor.getColumnIndex(SYSTEMNAME)), - cursor.getString(cursor.getColumnIndex(SERVERNAME)), - cursor.getString(cursor.getColumnIndex(PRESENCE_NAME)), - jid, - cursor.getInt(cursor.getColumnIndex(OPTIONS)), - cursor.getString(cursor.getColumnIndex(PHOTOURI)), - systemAccount, - cursor.getString(cursor.getColumnIndex(KEYS)), - cursor.getString(cursor.getColumnIndex(AVATAR)), - cursor.getLong(cursor.getColumnIndex(LAST_TIME)), - cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)), - cursor.getString(cursor.getColumnIndex(GROUPS)), - RtpCapability.Capability.of(cursor.getString(cursor.getColumnIndex(RTP_CAPABILITY)))); - } - - public String getDisplayName() { - if (isSelf()) { - final String displayName = account.getDisplayName(); - if (!Strings.isNullOrEmpty(displayName)) { - return displayName; - } - } - if (Config.X509_VERIFICATION && !TextUtils.isEmpty(this.commonName)) { - return this.commonName; - } else if (!TextUtils.isEmpty(this.systemName)) { - return this.systemName; - } else if (!TextUtils.isEmpty(this.serverName)) { - return this.serverName; - } else if (!TextUtils.isEmpty(this.presenceName) && ((QuickConversationsService.isQuicksy() && JidHelper.isQuicksyDomain(jid.getDomain())) || mutualPresenceSubscription())) { - return this.presenceName; - } else if (jid.getLocal() != null) { - return JidHelper.localPartOrFallback(jid); - } else { - return jid.getDomain().toEscapedString(); - } - } - - @Override - public int getOffline() { - return 0; - } - - public String getPublicDisplayName() { - if (!TextUtils.isEmpty(this.presenceName)) { - return this.presenceName; - } else if (jid.getLocal() != null) { - return JidHelper.localPartOrFallback(jid); - } else { - return jid.getDomain().toEscapedString(); - } - } - - public String getProfilePhoto() { - return this.photoUri; - } - - public Jid getJid() { - return jid; - } - - @Override - public List getTags(Context context) { - final ArrayList tags = new ArrayList<>(); - for (final String group : getGroups(true)) { - tags.add(new Tag(group, UIHelper.getColorForName(group), 0, account, isActive())); - } - Presence.Status status = getShownStatus(); - tags.add(UIHelper.getTagForStatus(context, status, account, isActive())); - if (isBlocked()) { - tags.add(new Tag(context.getString(R.string.blocked), 0xff2e2f3b, 0, account, isActive())); - } - return tags; - } - - @Override - public boolean getActive() { - return isActive(); - } - - public boolean match(Context context, String needle) { - if (TextUtils.isEmpty(needle)) { - return true; - } - needle = needle.toLowerCase(Locale.US).trim(); - String[] parts = needle.split("\\s+"); - if (parts.length > 1) { - for (String part : parts) { - if (!match(context, part)) { - return false; - } - } - return true; - } else { - return jid.toString().contains(needle) || - getDisplayName().toLowerCase(Locale.US).contains(needle) || - matchInTag(context, needle); - } - } - - private boolean matchInTag(Context context, String needle) { - needle = needle.toLowerCase(Locale.US); - for (Tag tag : getTags(context)) { - if (tag.getName().toLowerCase(Locale.US).contains(needle)) { - return true; - } - } - return false; - } - - public ContentValues getContentValues() { - synchronized (this.keys) { - final ContentValues values = new ContentValues(); - values.put(ACCOUNT, accountUuid); - values.put(SYSTEMNAME, systemName); - values.put(SERVERNAME, serverName); - values.put(PRESENCE_NAME, presenceName); - values.put(JID, jid.toString()); - values.put(OPTIONS, subscription); - values.put(SYSTEMACCOUNT, systemAccount != null ? systemAccount.toString() : null); - values.put(PHOTOURI, photoUri); - values.put(KEYS, keys.toString()); - values.put(AVATAR, avatar == null ? null : avatar.getFilename()); - values.put(LAST_PRESENCE, mLastPresence); - values.put(LAST_TIME, mLastseen); - values.put(GROUPS, groups.toString()); - values.put(RTP_CAPABILITY, rtpCapability == null ? null : rtpCapability.toString()); - return values; - } - } - - public Account getAccount() { - return this.account; - } - - public void setAccount(Account account) { - this.account = account; - this.accountUuid = account.getUuid(); - } - - public Presences getPresences() { - return this.presences; - } - - public void updatePresence(final String resource, final Presence presence) { - this.presences.updatePresence(resource, presence); - } - - public void removePresence(final String resource) { - this.presences.removePresence(resource); - } - - public void clearPresences() { - this.presences.clearPresences(); - this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST); - } - - public Presence.Status getShownStatus() { - return this.presences.getShownStatus(); - } - - public String getMostAvailableResource() { - return this.presences.getMostAvailableResource(); - } - - public boolean setPhotoUri(String uri) { - if (uri != null && !uri.equals(this.photoUri)) { - this.photoUri = uri; - return true; - } else if (this.photoUri != null && uri == null) { - this.photoUri = null; - return true; - } else { - return false; - } - } - - public void setServerName(String serverName) { - this.serverName = serverName; - } - - public boolean setSystemName(String systemName) { - final String old = getDisplayName(); - this.systemName = systemName; - return !old.equals(getDisplayName()); - } - - public boolean setPresenceName(String presenceName) { - final String old = getDisplayName(); - this.presenceName = presenceName; - return !old.equals(getDisplayName()); - } - - public Uri getSystemAccount() { - return systemAccount; - } - - public void setSystemAccount(Uri lookupUri) { - this.systemAccount = lookupUri; - } - - private Collection getGroups(final boolean unique) { - final Collection groups = unique ? new HashSet<>() : new ArrayList<>(); - for (int i = 0; i < this.groups.length(); ++i) { - try { - groups.add(this.groups.getString(i)); - } catch (final JSONException ignored) { - } - } - return groups; - } - - public ArrayList getOtrFingerprints() { - synchronized (this.keys) { - final ArrayList fingerprints = new ArrayList(); - try { - if (this.keys.has("otr_fingerprints")) { - final JSONArray prints = this.keys.getJSONArray("otr_fingerprints"); - for (int i = 0; i < prints.length(); ++i) { - final String print = prints.isNull(i) ? null : prints.getString(i); - if (print != null && !print.isEmpty()) { - fingerprints.add(prints.getString(i).toLowerCase(Locale.US)); - } - } - } - } catch (final JSONException ignored) { - - } - return fingerprints; - } - } - - public boolean addOtrFingerprint(String print) { - synchronized (this.keys) { - if (getOtrFingerprints().contains(print)) { - return false; - } - try { - JSONArray fingerprints; - if (!this.keys.has("otr_fingerprints")) { - fingerprints = new JSONArray(); - } else { - fingerprints = this.keys.getJSONArray("otr_fingerprints"); - } - fingerprints.put(print); - this.keys.put("otr_fingerprints", fingerprints); - return true; - } catch (final JSONException ignored) { - return false; - } - } - } - - public long getPgpKeyId() { - synchronized (this.keys) { - if (this.keys.has("pgp_keyid")) { - try { - return this.keys.getLong("pgp_keyid"); - } catch (JSONException e) { - return 0; - } - } else { - return 0; - } - } - } - - public boolean setPgpKeyId(long keyId) { - final long previousKeyId = getPgpKeyId(); - synchronized (this.keys) { - try { - this.keys.put("pgp_keyid", keyId); - return previousKeyId != keyId; - } catch (final JSONException ignored) { - } - } - return false; - } - - public void setOption(int option) { - this.subscription |= 1 << option; - } - - public void resetOption(int option) { - this.subscription &= ~(1 << option); - } - - public boolean getOption(int option) { - return ((this.subscription & (1 << option)) != 0); - } - - public boolean showInRoster() { - return (this.getOption(Contact.Options.IN_ROSTER) && (!this - .getOption(Contact.Options.DIRTY_DELETE))) - || (this.getOption(Contact.Options.DIRTY_PUSH)); - } - - public boolean showInContactList() { - return showInRoster() - || getOption(Options.SYNCED_VIA_OTHER) - || (QuickConversationsService.isQuicksy() && systemAccount != null); - } - - public void parseSubscriptionFromElement(Element item) { - String ask = item.getAttribute("ask"); - String subscription = item.getAttribute("subscription"); - - if (subscription == null) { - this.resetOption(Options.FROM); - this.resetOption(Options.TO); - } else { - switch (subscription) { - case "to": - this.resetOption(Options.FROM); - this.setOption(Options.TO); - break; - case "from": - this.resetOption(Options.TO); - this.setOption(Options.FROM); - this.resetOption(Options.PREEMPTIVE_GRANT); - this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST); - break; - case "both": - this.setOption(Options.TO); - this.setOption(Options.FROM); - this.resetOption(Options.PREEMPTIVE_GRANT); - this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST); - break; - case "none": - this.resetOption(Options.FROM); - this.resetOption(Options.TO); - break; - } - } - - // do NOT override asking if pending push request - if (!this.getOption(Contact.Options.DIRTY_PUSH)) { - if ((ask != null) && (ask.equals("subscribe"))) { - this.setOption(Contact.Options.ASKING); - } else { - this.resetOption(Contact.Options.ASKING); - } - } - } - - public void parseGroupsFromElement(Element item) { - this.groups = new JSONArray(); - for (Element element : item.getChildren()) { - if (element.getName().equals("group") && element.getContent() != null) { - this.groups.put(element.getContent()); - } - } - } - - public Element asElement() { - final Element item = new Element("item"); - item.setAttribute("jid", this.jid); - if (this.serverName != null) { - item.setAttribute("name", this.serverName); - } - for (String group : getGroups(false)) { - item.addChild("group").setContent(group); - } - return item; - } - - @Override - public int compareTo(@NonNull final ListItem another) { - return this.getDisplayName().compareToIgnoreCase( - another.getDisplayName()); - } - - public String getServer() { - return getJid().getDomain().toEscapedString(); - } - - public void setAvatar(Avatar avatar) { - setAvatar(avatar, false); - } - - public void setAvatar(Avatar avatar, boolean previouslyOmittedPepFetch) { - if (this.avatar != null && this.avatar.equals(avatar)) { - return; - } - if (!previouslyOmittedPepFetch && this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) { - return; - } - this.avatar = avatar; - } - - public String getAvatarFilename() { - return avatar == null ? null : avatar.getFilename(); - } - - public Avatar getAvatar() { - return avatar; - } - - public boolean deleteOtrFingerprint(String fingerprint) { - synchronized (this.keys) { - boolean success = false; - try { - if (this.keys.has("otr_fingerprints")) { - JSONArray newPrints = new JSONArray(); - JSONArray oldPrints = this.keys - .getJSONArray("otr_fingerprints"); - for (int i = 0; i < oldPrints.length(); ++i) { - if (!oldPrints.getString(i).equals(fingerprint)) { - newPrints.put(oldPrints.getString(i)); - } else { - success = true; - } - } - this.keys.put("otr_fingerprints", newPrints); - } - return success; - } catch (JSONException e) { - return false; - } - } - } - - - public boolean mutualPresenceSubscription() { - return getOption(Options.FROM) && getOption(Options.TO); - } - - @Override - public boolean isBlocked() { - return getAccount().isBlocked(this); - } - - @Override - public boolean isDomainBlocked() { - return getAccount().isBlocked(this.getJid().getDomain()); - } - - @Override - public Jid getBlockedJid() { - if (isDomainBlocked()) { - return getJid().getDomain(); - } else { - return getJid(); - } - } - - public boolean isSelf() { - return account.getJid().asBareJid().equals(jid.asBareJid()); - } - - boolean isOwnServer() { - return account.getJid().getDomain().equals(jid.asBareJid()); - } - - public void setCommonName(String cn) { - this.commonName = cn; - } - - public void flagActive() { - this.mActive = true; - } - - public void flagInactive() { - this.mActive = false; - } - - public boolean isActive() { - return this.mActive && account.isOnlineAndConnected(); - } - - public boolean setLastseen(long timestamp) { - if (timestamp > this.mLastseen) { - this.mLastseen = timestamp; - return true; - } else { - return false; - } - } - - public long getLastseen() { - return this.mLastseen; - } - - public void setLastResource(String resource) { - this.mLastPresence = resource; - } - - public String getLastResource() { - return this.mLastPresence; - } - - public String getServerName() { - return serverName; - } - - public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) { - setOption(getOption(phoneContact.getClass())); - setSystemAccount(phoneContact.getLookupUri()); - boolean changed = setSystemName(phoneContact.getDisplayName()); - changed |= setPhotoUri(phoneContact.getPhotoUri()); - return changed; - } - - public synchronized boolean unsetPhoneContact(Class clazz) { - resetOption(getOption(clazz)); - boolean changed = false; - if (!getOption(Options.SYNCED_VIA_ADDRESSBOOK) && !getOption(Options.SYNCED_VIA_OTHER)) { - setSystemAccount(null); - changed |= setPhotoUri(null); - changed |= setSystemName(null); - } - return changed; - } - - public static int getOption(Class clazz) { - if (clazz == JabberIdContact.class) { - return Options.SYNCED_VIA_ADDRESSBOOK; - } else { - return Options.SYNCED_VIA_OTHER; - } - } - - @Override - public int getAvatarBackgroundColor() { - return UIHelper.getColorForName(jid != null ? jid.asBareJid().toString() : getDisplayName()); - } - - @Override - public String getAvatarName() { - return getDisplayName(); - } - - public boolean hasAvatarOrPresenceName() { - return (avatar != null && avatar.getFilename() != null) || presenceName != null; - } - - public boolean refreshRtpCapability() { - final RtpCapability.Capability previous = this.rtpCapability; - this.rtpCapability = RtpCapability.check(this, false); - return !Objects.equals(previous, this.rtpCapability); - } - - public RtpCapability.Capability getRtpCapability() { - return this.rtpCapability == null ? RtpCapability.Capability.NONE : this.rtpCapability; - } - - public static final class Options { - public static final int TO = 0; - public static final int FROM = 1; - public static final int ASKING = 2; - public static final int PREEMPTIVE_GRANT = 3; - public static final int IN_ROSTER = 4; - public static final int PENDING_SUBSCRIPTION_REQUEST = 5; - public static final int DIRTY_PUSH = 6; - public static final int DIRTY_DELETE = 7; - private static final int SYNCED_VIA_ADDRESSBOOK = 8; - public static final int SYNCED_VIA_OTHER = 9; - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java deleted file mode 100644 index 967772f14..000000000 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ /dev/null @@ -1,1443 +0,0 @@ -package eu.siacs.conversations.entities; - -import static eu.siacs.conversations.entities.Bookmark.printableValue; - -import android.content.ContentValues; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.preference.PreferenceManager; -import android.text.TextUtils; - -import net.java.otr4j.OtrException; -import net.java.otr4j.crypto.OtrCryptoException; -import net.java.otr4j.session.SessionID; -import net.java.otr4j.session.SessionImpl; -import net.java.otr4j.session.SessionStatus; - -import java.security.interfaces.DSAPublicKey; -import java.util.Locale; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.common.collect.ComparisonChain; -import com.google.common.collect.Lists; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; -import java.util.concurrent.atomic.AtomicBoolean; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.crypto.OmemoSetting; -import eu.siacs.conversations.crypto.PgpDecryptionService; -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; -import eu.siacs.conversations.xmpp.mam.MamReference; - - -public class Conversation extends AbstractEntity implements Blockable, Comparable, Conversational, AvatarService.Avatarable { - public static final String TABLENAME = "conversations"; - - public static final int STATUS_AVAILABLE = 0; - public static final int STATUS_ARCHIVED = 1; - - public static final String NAME = "name"; - public static final String ACCOUNT = "accountUuid"; - public static final String CONTACT = "contactUuid"; - public static final String CONTACTJID = "contactJid"; - public static final String STATUS = "status"; - public static final String CREATED = "created"; - public static final String MODE = "mode"; - public static final String ATTRIBUTES = "attributes"; - - public static final String ATTRIBUTE_MUTED_TILL = "muted_till"; - public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify"; - public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history"; - public static final String ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS = "formerly_private_non_anonymous"; - public static final String ATTRIBUTE_PINNED_ON_TOP = "pinned_on_top"; - static final String ATTRIBUTE_MUC_PASSWORD = "muc_password"; - static final String ATTRIBUTE_MEMBERS_ONLY = "members_only"; - static final String ATTRIBUTE_MODERATED = "moderated"; - static final String ATTRIBUTE_NON_ANONYMOUS = "non_anonymous"; - private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message"; - private static final String ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP = "next_message_timestamp"; - private static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets"; - private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption"; - private static final String ATTRIBUTE_CORRECTING_MESSAGE = "correcting_message"; - protected final ArrayList messages = new ArrayList<>(); - public AtomicBoolean messagesLoaded = new AtomicBoolean(true); - protected Account account = null; - private String draftMessage; - private String name; - private String contactUuid; - private String accountUuid; - private Jid contactJid; - private int status; - private long created; - private int mode; - private JSONObject attributes; - private Jid nextCounterpart; - private transient SessionImpl otrSession; - private transient String otrFingerprint = null; - private Smp mSmp = new Smp(); - private transient MucOptions mucOptions = null; - private byte[] symmetricKey; - private boolean messagesLeftOnServer = true; - private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE; - private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE; - private String mLastReceivedOtrMessageId = null; - private String mFirstMamReference = null; - - public Conversation(final String name, final Account account, final Jid contactJid, - final int mode) { - this(java.util.UUID.randomUUID().toString(), name, null, account - .getUuid(), contactJid, System.currentTimeMillis(), - STATUS_AVAILABLE, mode, ""); - this.account = account; - } - - public Conversation(final String uuid, final String name, final String contactUuid, - final String accountUuid, final Jid contactJid, final long created, final int status, - final int mode, final String attributes) { - this.uuid = uuid; - this.name = name; - this.contactUuid = contactUuid; - this.accountUuid = accountUuid; - this.contactJid = contactJid; - this.created = created; - this.status = status; - this.mode = mode; - try { - this.attributes = new JSONObject(attributes == null ? "" : attributes); - } catch (JSONException e) { - this.attributes = new JSONObject(); - } - } - - public static Conversation fromCursor(Cursor cursor) { - return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)), - cursor.getString(cursor.getColumnIndex(NAME)), - cursor.getString(cursor.getColumnIndex(CONTACT)), - cursor.getString(cursor.getColumnIndex(ACCOUNT)), - JidHelper.parseOrFallbackToInvalid(cursor.getString(cursor.getColumnIndex(CONTACTJID))), - cursor.getLong(cursor.getColumnIndex(CREATED)), - cursor.getInt(cursor.getColumnIndex(STATUS)), - cursor.getInt(cursor.getColumnIndex(MODE)), - cursor.getString(cursor.getColumnIndex(ATTRIBUTES))); - } - - public static Message getLatestMarkableMessage(final List messages, boolean isPrivateAndNonAnonymousMuc) { - for (int i = messages.size() - 1; i >= 0; --i) { - final Message message = messages.get(i); - if (message.getStatus() <= Message.STATUS_RECEIVED - && (message.markable || isPrivateAndNonAnonymousMuc) - && !message.isPrivateMessage()) { - return message; - } - } - return null; - } - - public static boolean suitableForOmemoByDefault(final Conversation conversation) { - if (conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)) { - return false; - } - if (conversation.getContact().isOwnServer()) { - return false; - } - final String contact = conversation.getJid().getDomain().toEscapedString(); - final String account = conversation.getAccount().getServer(); - if (Config.OMEMO_EXCEPTIONS.matchesContactDomain(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) { - return false; - } - return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false); - } - - public boolean hasMessagesLeftOnServer() { - return messagesLeftOnServer; - } - - public void setHasMessagesLeftOnServer(boolean value) { - this.messagesLeftOnServer = value; - } - - public void deleteMessage(Message message) { - synchronized (this.messages) { - this.messages.remove(message); - } - } - - public Message getFirstUnreadMessage() { - Message first = null; - synchronized (this.messages) { - for (int i = messages.size() - 1; i >= 0; --i) { - if (messages.get(i).isRead()) { - return first; - } else { - first = messages.get(i); - } - } - } - return first; - } - - public String findMostRecentRemoteDisplayableId() { - final boolean multi = mode == Conversation.MODE_MULTI; - synchronized (this.messages) { - for(final Message message : Lists.reverse(this.messages)) { - if (message.getStatus() == Message.STATUS_RECEIVED) { - final String serverMsgId = message.getServerMsgId(); - if (serverMsgId != null && multi) { - return serverMsgId; - } - return message.getRemoteMsgId(); - } - } - } - return null; - } - - public int countFailedDeliveries() { - int count = 0; - synchronized (this.messages) { - for(final Message message : this.messages) { - if (message.getStatus() == Message.STATUS_SEND_FAILED) { - ++count; - } - } - } - return count; - } - - public Message getLastEditableMessage() { - synchronized (this.messages) { - for(final Message message : Lists.reverse(this.messages)) { - if (message.isEditable()) { - if (message.isGeoUri() || message.getType() != Message.TYPE_TEXT) { - return null; - } - return message; - } - } - } - return null; - } - - - public Message findUnsentMessageWithUuid(String uuid) { - synchronized (this.messages) { - for (final Message message : this.messages) { - final int s = message.getStatus(); - if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) { - return message; - } - } - } - return null; - } - - public void findWaitingMessages(OnMessageFound onMessageFound) { - final ArrayList results = new ArrayList<>(); - synchronized (this.messages) { - for (Message message : this.messages) { - if (message.getStatus() == Message.STATUS_WAITING) { - results.add(message); - } - } - } - for (Message result : results) { - onMessageFound.onMessageFound(result); - } - } - public void findResendAbleFailedMessage(OnMessageFound onMessageFound) { - final ArrayList results = new ArrayList<>(); - synchronized (this.messages) { - for (final Message message : this.messages) { - if (message.getStatus() == Message.STATUS_SEND_FAILED && !message.isFileOrImage()) { - results.add(message); - } - } - } - for (final Message result : results) { - onMessageFound.onMessageFound(result); - } - } - - public void findUnreadMessagesAndCalls(OnMessageFound onMessageFound) { - final ArrayList results = new ArrayList<>(); - synchronized (this.messages) { - for (final Message message : this.messages) { - if (message.isRead()) { - continue; - } - results.add(message); - } - } - for (final Message result : results) { - onMessageFound.onMessageFound(result); - } - } - - public void findFailedMessagesWithFiles(final OnMessageFound onMessageFound) { - final ArrayList results = new ArrayList<>(); - synchronized (this.messages) { - for (final Message m : this.messages) { - if (m.isFileOrImage() && m.getEncryption() != Message.ENCRYPTION_PGP) { - if (m.getStatus() == Message.STATUS_SEND_FAILED && !m.isFileDeleted() && m.needsUploading()) { - results.add(m); - } - } - } - } - for (Message result : results) { - onMessageFound.onMessageFound(result); - } - } - - public void findDeletedMessages(final OnMessageFound onMessageFound) { - final ArrayList results = new ArrayList<>(); - synchronized (this.messages) { - for (final Message m : this.messages) { - if (m.isFileDeleted()) { - results.add(m); - } - } - } - for (Message result : results) { - onMessageFound.onMessageFound(result); - } - } - - 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() || 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; - } - } - } - return null; - } - - public boolean markAsDeleted(final List uuids) { - boolean deleted = false; - final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService(); - synchronized (this.messages) { - for (Message message : this.messages) { - if (uuids.contains(message.getUuid())) { - message.setFileDeleted(true); - deleted = true; - if (message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) { - pgpDecryptionService.discard(message); - } - } - } - } - return deleted; - } - - - public boolean markAsChanged(final List files) { - boolean changed = false; - final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService(); - synchronized (this.messages) { - for (Message message : this.messages) { - for (final DatabaseBackend.FilePathInfo file : files) - if (file.uuid.toString().equals(message.getUuid())) { - message.setFileDeleted(file.FileDeleted); - changed = true; - if (file.FileDeleted && message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) { - pgpDecryptionService.discard(message); - } - } - } - } - return changed; - } - - public void clearMessages() { - synchronized (this.messages) { - this.messages.clear(); - } - } - - public boolean setIncomingChatState(ChatState state) { - if (this.mIncomingChatState == state) { - return false; - } - this.mIncomingChatState = state; - return true; - } - - public ChatState getIncomingChatState() { - return this.mIncomingChatState; - } - - public boolean setOutgoingChatState(ChatState state) { - if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) { - if (this.mOutgoingChatState != state) { - this.mOutgoingChatState = state; - return true; - } - } - return false; - } - - public ChatState getOutgoingChatState() { - return this.mOutgoingChatState; - } - - public void trim() { - synchronized (this.messages) { - final int size = messages.size(); - int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES; - if (getAccount() != null && getAccount().getXmppConnection() != null && getAccount().getXmppConnection().getXmppConnectionService() != null) { - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getAccount().getXmppConnection().getXmppConnectionService()); - int pagesize = Integer.parseInt(pref.getString("pagesize", String.valueOf(Config.PAGE_SIZE))); - int maxnumpages = Integer.parseInt(pref.getString("max_num_pages", String.valueOf(Config.MAX_NUM_PAGES))); - maxsize = pagesize * maxnumpages; - } - if (size > maxsize) { - List discards = this.messages.subList(0, size - maxsize); - final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService(); - if (pgpDecryptionService != null) { - pgpDecryptionService.discard(discards); - } - discards.clear(); - untieMessages(); - } - } - } - - public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) { - synchronized (this.messages) { - for (Message message : this.messages) { - if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING) - && (message.getEncryption() == encryptionType)) { - onMessageFound.onMessageFound(message); - } - } - } - } - - - public void findUnsentTextMessages(OnMessageFound onMessageFound) { - final ArrayList results = new ArrayList<>(); - synchronized (this.messages) { - for (Message message : this.messages) { - if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) && message.getStatus() == Message.STATUS_UNSEND) { - results.add(message); - } - } - } - for (Message result : results) { - onMessageFound.onMessageFound(result); - } - } - - public Message findSentMessageWithUuidOrRemoteId(String id) { - return findSentMessageWithUuidOrRemoteId(id, false, false); - } - - public Message findSentMessageWithUuidOrRemoteId(String id, boolean ignorestatus, boolean withedits) { - synchronized (this.messages) { - for (Message message : this.messages) { - - if (id.equals(message.getUuid()) || ((message.getStatus() >= Message.STATUS_SEND || ignorestatus) && id.equals(message.getRemoteMsgId()))) { - return message; - } - - if (withedits) { - for (Edit itm : message.edits) { - if (id.equals(itm.getEditedId())) { - return message; - } - } - } - } - } - return null; - } - - public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) { - synchronized (this.messages) { - for (int i = this.messages.size() - 1; i >= 0; --i) { - final Message message = messages.get(i); - final Jid mcp = message.getCounterpart(); - if (mcp == null) { - continue; - } - if (mcp.equals(counterpart) && ((message.getStatus() == Message.STATUS_RECEIVED) == received) - && (carbon == message.isCarbon() || received)) { - final boolean idMatch = id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id); - if (idMatch && !message.isFileOrImage() && !message.treatAsDownloadable()) { - return message; - } - } - } - } - return null; - } - - public Message findSentMessageWithUuid(String id) { - synchronized (this.messages) { - for (Message message : this.messages) { - if (id.equals(message.getUuid())) { - return message; - } - } - } - return null; - } - - public Message findMessageWithRemoteId(String id, Jid counterpart) { - synchronized (this.messages) { - for (Message message : this.messages) { - if (counterpart.equals(message.getCounterpart()) - && (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) { - return message; - } - } - } - return null; - } - - public Message findMessageWithServerMsgId(String id) { - synchronized (this.messages) { - for (Message message : this.messages) { - if (id != null && id.equals(message.getServerMsgId())) { - return message; - } - } - } - return null; - } - - public boolean hasMessageWithCounterpart(Jid counterpart) { - synchronized (this.messages) { - for (Message message : this.messages) { - if (counterpart.equals(message.getCounterpart())) { - return true; - } - } - } - return false; - } - - public List filterDuplicates(List list) { - HashMap items = new HashMap(); - for (Message item : list) { - items.put(item.getUuid(), item); - } - - ArrayList result = new ArrayList(items.values()); - Collections.sort(result, (o1, o2) -> { - if (o1.getTimeSent() < o2.getTimeSent()) - return -1; - if (o1.getTimeSent() > o2.getTimeSent()) - return 1; - return 0; - }); - return result; - } - - public void populateWithMessages(final List messages) { - synchronized (this.messages) { - messages.clear(); - messages.addAll(filterDuplicates(this.messages)); - - for (int n = 0; n < messages.size(); n++) { - if (messages.get(n).isMessageDeleted()) { - messages.remove(n); - n--; - continue; - } - - if (messages.get(n).getRetractId() != null) { - if (messages.get(n).getStatus() != Message.STATUS_RECEIVED) { - messages.remove(n); - n--; - continue; - } - } - } - - for (Message itm : messages) { - if (itm.isMessageDeleted()) { - if (itm.getEditedList().size() > 0) { - itm.setTime(itm.getEditedList().get(0).getTimeSent()); - } - } - } - } - for (Iterator iterator = messages.iterator(); iterator.hasNext(); ) { - if (iterator.next().wasMergedIntoPrevious()) { - iterator.remove(); - } - } - } - - @Override - public boolean isBlocked() { - return getContact().isBlocked(); - } - - @Override - public boolean isDomainBlocked() { - return getContact().isDomainBlocked(); - } - - @Override - public Jid getBlockedJid() { - return getContact().getBlockedJid(); - } - - public String getLastReceivedOtrMessageId() { - return this.mLastReceivedOtrMessageId; - } - - public void setLastReceivedOtrMessageId(String id) { - this.mLastReceivedOtrMessageId = id; - } - - - public int countMessages() { - synchronized (this.messages) { - return this.messages.size(); - } - } - - public String getFirstMamReference() { - return this.mFirstMamReference; - } - - public void setFirstMamReference(String reference) { - this.mFirstMamReference = reference; - } - - public void setLastClearHistory(long time, String reference) { - if (reference != null) { - setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, String.valueOf(time) + ":" + reference); - } else { - setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time); - } - } - - public MamReference getLastClearHistory() { - return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY)); - } - - public List getAcceptedCryptoTargets() { - if (mode == MODE_SINGLE) { - return Collections.singletonList(getJid().asBareJid()); - } else { - return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS); - } - } - - public void setAcceptedCryptoTargets(List acceptedTargets) { - setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets); - } - - public boolean setCorrectingMessage(Message correctingMessage) { - setAttribute(ATTRIBUTE_CORRECTING_MESSAGE, correctingMessage == null ? null : correctingMessage.getUuid()); - return correctingMessage == null && draftMessage != null; - } - - public Message getCorrectingMessage() { - final String uuid = getAttribute(ATTRIBUTE_CORRECTING_MESSAGE); - return uuid == null ? null : findSentMessageWithUuid(uuid); - } - - public boolean withSelf() { - return getContact().isSelf(); - } - - @Override - public int compareTo(@NonNull Conversation another) { - return ComparisonChain.start() - .compareFalseFirst(another.getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false), getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP,false)) - .compareFalseFirst(another.getAccount().isEnabled(), getAccount().isEnabled()) - .compare(another.getSortableTime(), getSortableTime()) - .result(); - } - - private long getSortableTime() { - Draft draft = getDraft(); - final long messageTime = getLatestMessage().getTimeSent(); - if (draft == null) { - return messageTime; - } else { - return Math.max(messageTime, draft.getTimestamp()); - } - } - - public String getDraftMessage() { - return draftMessage; - } - - public void setDraftMessage(String draftMessage) { - this.draftMessage = draftMessage; - } - - public boolean isRead() { - return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead(); - } - - public List markRead(String upToUuid) { - final List unread = new ArrayList<>(); - synchronized (this.messages) { - for (Message message : this.messages) { - if (!message.isRead()) { - message.markRead(); - unread.add(message); - } - if (message.getUuid().equals(upToUuid)) { - return unread; - } - } - } - return unread; - } - - public Message getLatestMessage() { - synchronized (this.messages) { - if (this.messages.size() == 0) { - Message message = new Message(this, "", Message.ENCRYPTION_NONE); - message.setType(Message.TYPE_STATUS); - message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp())); - return message; - } else { - return this.messages.get(this.messages.size() - 1); - } - } - } - - public @NonNull - CharSequence getName() { - if (getMode() == MODE_MULTI) { - final String roomName = getMucOptions().getName(); - final String subject = getMucOptions().getSubject(); - final Bookmark bookmark = getBookmark(); - final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null; - if (printableValue(roomName)) { - return roomName; - } else if (printableValue(subject)) { - return subject; - } else if (printableValue(bookmarkName, false)) { - return bookmarkName; - } else { - final String generatedName = getMucOptions().createNameFromParticipants(); - if (printableValue(generatedName)) { - return generatedName; - } else { - return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid; - } - } - } else if ((QuickConversationsService.isConversations() || !JidHelper.isQuicksyDomain(contactJid.getDomain())) && isWithStranger()) { - return contactJid; - } else { - return this.getContact().getDisplayName(); - } - } - - public String getAccountUuid() { - return this.accountUuid; - } - - public Account getAccount() { - return this.account; - } - - public void setAccount(final Account account) { - this.account = account; - } - - public Contact getContact() { - return this.account.getRoster().getContact(this.contactJid); - } - - @Override - public Jid getJid() { - return this.contactJid; - } - - public int getStatus() { - return this.status; - } - - public void setStatus(int status) { - this.status = status; - } - - public long getCreated() { - return this.created; - } - - public ContentValues getContentValues() { - ContentValues values = new ContentValues(); - values.put(UUID, uuid); - values.put(NAME, name); - values.put(CONTACT, contactUuid); - values.put(ACCOUNT, accountUuid); - values.put(CONTACTJID, contactJid.toString()); - values.put(CREATED, created); - values.put(STATUS, status); - values.put(MODE, mode); - synchronized (this.attributes) { - values.put(ATTRIBUTES, attributes.toString()); - } - return values; - } - - public int getMode() { - return this.mode; - } - - public void setMode(int mode) { - this.mode = mode; - } - public SessionImpl startOtrSession(String presence, boolean sendStart) { - if (this.otrSession != null) { - return this.otrSession; - } else { - final SessionID sessionId = new SessionID(this.getJid().asBareJid().toString(), - presence, - "xmpp"); - this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService()); - try { - if (sendStart) { - this.otrSession.startSession(); - return this.otrSession; - } - return this.otrSession; - } catch (OtrException e) { - return null; - } - } - - } - - public SessionImpl getOtrSession() { - return this.otrSession; - } - - public void resetOtrSession() { - this.otrFingerprint = null; - this.otrSession = null; - this.mSmp.hint = null; - this.mSmp.secret = null; - this.mSmp.status = Smp.STATUS_NONE; - } - - public Smp smp() { - return mSmp; - } - - public boolean startOtrIfNeeded() { - if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) { - try { - this.otrSession.startSession(); - return true; - } catch (OtrException e) { - this.resetOtrSession(); - return false; - } - } else { - return true; - } - } - - public boolean endOtrIfNeeded() { - if (this.otrSession != null) { - if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) { - try { - this.otrSession.endSession(); - this.resetOtrSession(); - return true; - } catch (OtrException e) { - this.resetOtrSession(); - return false; - } - } else { - this.resetOtrSession(); - return false; - } - } else { - return false; - } - } - - public boolean hasValidOtrSession() { - return this.otrSession != null; - } - - public synchronized String getOtrFingerprint() { - if (this.otrFingerprint == null) { - try { - if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) { - return null; - } - DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey(); - this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey).toLowerCase(Locale.US); - } catch (final OtrCryptoException ignored) { - return null; - } catch (final UnsupportedOperationException ignored) { - return null; - } - } - return this.otrFingerprint; - } - - public boolean verifyOtrFingerprint() { - final String fingerprint = getOtrFingerprint(); - if (fingerprint != null) { - getContact().addOtrFingerprint(fingerprint); - return true; - } else { - return false; - } - } - - public boolean isOtrFingerprintVerified() { - return getContact().getOtrFingerprints().contains(getOtrFingerprint()); - } - - /** - * short for is Private and Non-anonymous - */ - public boolean isSingleOrPrivateAndNonAnonymous() { - return mode == MODE_SINGLE || isPrivateAndNonAnonymous(); - } - - public boolean isPrivateAndNonAnonymous() { - return getMucOptions().isPrivateAndNonAnonymous(); - } - - public synchronized MucOptions getMucOptions() { - if (this.mucOptions == null) { - this.mucOptions = new MucOptions(this); - } - return this.mucOptions; - } - - public void resetMucOptions() { - this.mucOptions = null; - } - - public void setContactJid(final Jid jid) { - this.contactJid = jid; - } - - public Jid getNextCounterpart() { - return this.nextCounterpart; - } - - public void setNextCounterpart(Jid jid) { - this.nextCounterpart = jid; - } - - public int getNextEncryption() { - if (!Config.supportOmemo() && !Config.supportOpenPgp() && !Config.supportOtr()) { - return Message.ENCRYPTION_NONE; - } - if (OmemoSetting.isAlways()) { - return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE; - } - if (OmemoSetting.isNever()) { - return Message.ENCRYPTION_NONE; - } - final int defaultEncryption; - if (suitableForOmemoByDefault(this)) { - defaultEncryption = OmemoSetting.getEncryption(); - } else { - defaultEncryption = Message.ENCRYPTION_NONE; - } - int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption); - if (encryption < 0) { - return defaultEncryption; - } else if (encryption == Message.ENCRYPTION_OTR) { - return encryption; - } else { - return encryption; - } - } - - public boolean setNextEncryption(int encryption) { - return this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, encryption); - } - - public String getNextMessage() { - final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE); - return nextMessage == null ? "" : nextMessage; - } - - public boolean smpRequested() { - return smp().status == Smp.STATUS_CONTACT_REQUESTED; - } - - - public @Nullable - Draft getDraft() { - final long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0); - if (timestamp > getLatestMessage().getTimeSent()) { - String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE); - if (!TextUtils.isEmpty(message) && timestamp != 0) { - return new Draft(message, timestamp); - } - } - return null; - } - - public boolean setNextMessage(final String input) { - final String message = input == null || input.trim().isEmpty() ? null : input; - boolean changed = !getNextMessage().equals(message); - this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message); - if (changed) { - this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis()); - } - return changed; - } - - public void setSymmetricKey(byte[] key) { - this.symmetricKey = key; - } - - public byte[] getSymmetricKey() { - return this.symmetricKey; - } - - - public Bookmark getBookmark() { - return this.account.getBookmark(this.contactJid); - } - - public Message findDuplicateMessage(Message message, boolean withremoteid) { - synchronized (this.messages) { - for (int i = this.messages.size() - 1; i >= 0; --i) { - if (this.messages.get(i).similar(message)) { - return this.messages.get(i); - } - if (withremoteid) { - if (this.messages.get(i).remoteMsgId != null && message.getRemoteMsgId() != null && this.messages.get(i).remoteMsgId.equals(message.getRemoteMsgId())) { - return this.messages.get(i); - } - } - } - } - return null; - } - - public boolean hasDuplicateMessage(Message message) { - return findDuplicateMessage(message) != null; - } - - public Message findSentMessageWithBody(String body) { - synchronized (this.messages) { - for (int i = this.messages.size() - 1; i >= 0; --i) { - Message message = this.messages.get(i); - if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) { - String otherBody; - if (message.hasFileOnRemoteHost()) { - otherBody = message.getFileParams().url; - } else { - otherBody = message.body; - } - if (otherBody != null && otherBody.equals(body)) { - return message; - } - } - } - return null; - } - } - - public Message findRtpSession(final String sessionId, final int s) { - synchronized (this.messages) { - for (int i = this.messages.size() - 1; i >= 0; --i) { - final Message message = this.messages.get(i); - if ((message.getStatus() == s) && (message.getType() == Message.TYPE_RTP_SESSION) && sessionId.equals(message.getRemoteMsgId())) { - return message; - } - } - } - return null; - } - - public boolean possibleDuplicate(final String serverMsgId, final String remoteMsgId) { - if (serverMsgId == null || remoteMsgId == null) { - return false; - } - synchronized (this.messages) { - for (Message message : this.messages) { - if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) { - return true; - } - } - } - return false; - } - - public MamReference getLastMessageTransmitted() { - final MamReference lastClear = getLastClearHistory(); - MamReference lastReceived = new MamReference(0); - synchronized (this.messages) { - for (int i = this.messages.size() - 1; i >= 0; --i) { - final Message message = this.messages.get(i); - if (message.isPrivateMessage()) { - continue; //it's unsafe to use private messages as anchor. They could be coming from user archive - } - if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) { - lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId()); - break; - } - } - } - return MamReference.max(lastClear, lastReceived); - } - - public void setMutedTill(long value) { - this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value)); - } - - public boolean isMuted() { - return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0); - } - - public boolean alwaysNotify() { - return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous()); - } - - public boolean setAttribute(String key, boolean value) { - return setAttribute(key, String.valueOf(value)); - } - - private boolean setAttribute(String key, long value) { - return setAttribute(key, Long.toString(value)); - } - - private boolean setAttribute(String key, int value) { - return setAttribute(key, String.valueOf(value)); - } - - public boolean setAttribute(String key, String value) { - synchronized (this.attributes) { - try { - if (value == null) { - if (this.attributes.has(key)) { - this.attributes.remove(key); - return true; - } else { - return false; - } - } else { - final String prev = this.attributes.optString(key, null); - this.attributes.put(key, value); - return !value.equals(prev); - } - } catch (JSONException e) { - throw new AssertionError(e); - } - } - } - - public boolean setAttribute(String key, List jids) { - JSONArray array = new JSONArray(); - for (Jid jid : jids) { - array.put(jid.asBareJid().toString()); - } - synchronized (this.attributes) { - try { - this.attributes.put(key, array); - return true; - } catch (JSONException e) { - return false; - } - } - } - - public String getAttribute(String key) { - synchronized (this.attributes) { - return this.attributes.optString(key, null); - } - } - - private List getJidListAttribute(String key) { - ArrayList list = new ArrayList<>(); - synchronized (this.attributes) { - try { - JSONArray array = this.attributes.getJSONArray(key); - for (int i = 0; i < array.length(); ++i) { - try { - list.add(Jid.of(array.getString(i))); - } catch (IllegalArgumentException e) { - //ignored - } - } - } catch (JSONException e) { - //ignored - } - } - return list; - } - - private int getIntAttribute(String key, int defaultValue) { - String value = this.getAttribute(key); - if (value == null) { - return defaultValue; - } else { - try { - return Integer.parseInt(value); - } catch (NumberFormatException e) { - return defaultValue; - } - } - } - - public long getLongAttribute(String key, long defaultValue) { - String value = this.getAttribute(key); - if (value == null) { - return defaultValue; - } else { - try { - return Long.parseLong(value); - } catch (NumberFormatException e) { - return defaultValue; - } - } - } - - public boolean getBooleanAttribute(String key, boolean defaultValue) { - String value = this.getAttribute(key); - if (value == null) { - return defaultValue; - } else { - return Boolean.parseBoolean(value); - } - } - - public void add(Message message) { - synchronized (this.messages) { - this.messages.add(message); - } - } - - public void prepend(int offset, Message message) { - synchronized (this.messages) { - this.messages.add(Math.min(offset, this.messages.size()), message); - } - } - - public void addAll(int index, List messages) { - synchronized (this.messages) { - this.messages.addAll(index, messages); - } - account.getPgpDecryptionService().decrypt(messages); - } - - public void expireOldMessages(long timestamp) { - synchronized (this.messages) { - for (ListIterator iterator = this.messages.listIterator(); iterator.hasNext(); ) { - if (iterator.next().getTimeSent() < timestamp) { - iterator.remove(); - } - } - untieMessages(); - } - } - - public void sort() { - synchronized (this.messages) { - Collections.sort(this.messages, (left, right) -> { - if (left.getTimeSent() < right.getTimeSent()) { - return -1; - } else if (left.getTimeSent() > right.getTimeSent()) { - return 1; - } else { - return 0; - } - }); - untieMessages(); - } - } - - private void untieMessages() { - for (Message message : this.messages) { - message.untie(); - } - } - - public int unreadCount() { - synchronized (this.messages) { - int count = 0; - for (int i = this.messages.size() - 1; i >= 0; --i) { - if (this.messages.get(i).isRead()) { - return count; - } - ++count; - } - return count; - } - } - - public int failedCount() { - synchronized (this.messages) { - int count = 0; - for (int i = this.messages.size() - 1; i >= 0; --i) { - Message message = this.messages.get(i); - if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) && message.getEncryption() != Message.ENCRYPTION_PGP) { - if (message.getStatus() == Message.STATUS_SEND_FAILED && !message.isFileDeleted() && message.needsUploading()) { - ++count; - } - } - } - return count; - } - } - - public int receivedMessagesCount() { - int count = 0; - synchronized (this.messages) { - for (Message message : messages) { - if (message.getStatus() == Message.STATUS_RECEIVED) { - ++count; - } - } - } - return count; - } - - public int sentMessagesCount() { - int count = 0; - synchronized (this.messages) { - for (Message message : messages) { - if (message.getStatus() != Message.STATUS_RECEIVED) { - ++count; - } - } - } - return count; - } - - public boolean isWithStranger() { - final Contact contact = getContact(); - return mode == MODE_SINGLE - && !contact.isOwnServer() - && !contact.showInContactList() - && !contact.isSelf() - && !JidHelper.isQuicksyDomain(contact.getJid()) - && sentMessagesCount() == 0; - } - - public int getReceivedMessagesCountSinceUuid(String uuid) { - if (uuid == null) { - return 0; - } - int count = 0; - synchronized (this.messages) { - for (int i = messages.size() - 1; i >= 0; i--) { - final Message message = messages.get(i); - if (uuid.equals(message.getUuid())) { - return count; - } - if (message.getStatus() <= Message.STATUS_RECEIVED) { - ++count; - } - } - } - return 0; - } - - @Override - public int getAvatarBackgroundColor() { - return UIHelper.getColorForName(getName().toString()); - } - - @Override - public String getAvatarName() { - return getName().toString(); - } - - public interface OnMessageFound { - void onMessageFound(final Message message); - } - - public static class Draft { - private final String message; - private final long timestamp; - - private Draft(String message, long timestamp) { - this.message = message; - this.timestamp = timestamp; - } - - public long getTimestamp() { - return timestamp; - } - - public String getMessage() { - return message; - } - } - - public class Smp { - public static final int STATUS_NONE = 0; - public static final int STATUS_CONTACT_REQUESTED = 1; - public static final int STATUS_WE_REQUESTED = 2; - public static final int STATUS_FAILED = 3; - public static final int STATUS_VERIFIED = 4; - - public String secret = null; - public String hint = null; - public int status = 0; - } - - - public Message findDuplicateMessage(Message message) { - return findDuplicateMessage(message, false); - } - - public boolean hasDuplicateMessage(Message message, boolean withremoteid) { - return findDuplicateMessage(message, withremoteid) != null; - } - - public Message findMessageWithUuidOrRemoteId(final String id) { - synchronized (this.messages) { - for (final Message message : this.messages) { - if (message.getRemoteMsgId() != null && message.getRemoteMsgId().equals(id) || message.getUuid().equals(id)) { - return message; - } - } - } - return null; - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/Conversational.java b/src/main/java/eu/siacs/conversations/entities/Conversational.java deleted file mode 100644 index d1703bc8a..000000000 --- a/src/main/java/eu/siacs/conversations/entities/Conversational.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package eu.siacs.conversations.entities; - -import eu.siacs.conversations.xmpp.Jid; - -public interface Conversational { - - int MODE_MULTI = 1; - int MODE_SINGLE = 0; - - Account getAccount(); - - Contact getContact(); - - Jid getJid(); - - int getMode(); - - String getUuid(); -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java deleted file mode 100644 index 2be498ff5..000000000 --- a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java +++ /dev/null @@ -1,95 +0,0 @@ -package eu.siacs.conversations.entities; - -import android.util.Log; - -import java.io.File; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.MimeUtils; - -public class DownloadableFile extends File { - - private static final long serialVersionUID = 2247012619505115863L; - - private long expectedSize = 0; - private byte[] sha1sum; - private byte[] aeskey; - - private byte[] iv; - - public DownloadableFile(String path) { - super(path); - } - - public DownloadableFile(final File parent, final String file) { - super(parent, file); - } - - public long getSize() { - return super.length(); - } - - public long getExpectedSize() { - return this.expectedSize; - } - - public String getMimeType() { - String path = this.getAbsolutePath(); - int start = path.lastIndexOf('.') + 1; - if (start < path.length()) { - String mime = MimeUtils.guessMimeTypeFromExtension(path.substring(start)); - return mime == null ? "" : mime; - } else { - return ""; - } - } - - public void setExpectedSize(long size) { - this.expectedSize = size; - } - - public byte[] getSha1Sum() { - return this.sha1sum; - } - - public void setSha1Sum(byte[] sum) { - this.sha1sum = sum; - } - - public void setKeyAndIv(byte[] keyIvCombo) { - // originally, we used a 16 byte IV, then found for aes-gcm a 12 byte IV is ideal - // this code supports reading either length, with sending 12 byte IV to be done in future - if (keyIvCombo.length == 48) { - this.aeskey = new byte[32]; - this.iv = new byte[16]; - System.arraycopy(keyIvCombo, 0, this.iv, 0, 16); - System.arraycopy(keyIvCombo, 16, this.aeskey, 0, 32); - } else if (keyIvCombo.length == 44) { - this.aeskey = new byte[32]; - this.iv = new byte[12]; - System.arraycopy(keyIvCombo, 0, this.iv, 0, 12); - System.arraycopy(keyIvCombo, 12, this.aeskey, 0, 32); - } else if (keyIvCombo.length >= 32) { - this.aeskey = new byte[32]; - this.iv = new byte[]{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf}; - System.arraycopy(keyIvCombo, 0, aeskey, 0, 32); - } - Log.d(Config.LOGTAG, "using " + this.iv.length + "-byte IV for file transmission"); - } - - public void setKey(byte[] key) { - this.aeskey = key; - } - - public void setIv(byte[] iv) { - this.iv = iv; - } - - public byte[] getKey() { - return this.aeskey; - } - - public byte[] getIv() { - return this.iv; - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/Edit.java b/src/main/java/eu/siacs/conversations/entities/Edit.java deleted file mode 100644 index 324b983de..000000000 --- a/src/main/java/eu/siacs/conversations/entities/Edit.java +++ /dev/null @@ -1,115 +0,0 @@ -package eu.siacs.conversations.entities; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.List; - -public class Edit { - - private final String editedId; - private final String serverMsgId; - private String body; - private final long timeSent; - - Edit(String editedId, String serverMsgId, String body, long timeSent) { - this.editedId = editedId; - this.serverMsgId = serverMsgId; - this.body = body; - this.timeSent = timeSent; - } - - static String toJson(List edits, boolean hidebody) throws JSONException { - JSONArray jsonArray = new JSONArray(); - for (Edit edit : edits) { - jsonArray.put(edit.toJson(hidebody)); - } - return jsonArray.toString(); - } - - static boolean wasPreviouslyEditedRemoteMsgId(List edits, String remoteMsgId) { - for (Edit edit : edits) { - if (edit.editedId != null && edit.editedId.equals(remoteMsgId)) { - return true; - } - } - return false; - } - - static boolean wasPreviouslyEditedServerMsgId(List edits, String serverMsgId) { - for (Edit edit : edits) { - if (edit.serverMsgId != null && edit.serverMsgId.equals(serverMsgId)) { - return true; - } - } - return false; - } - - private static Edit fromJson(JSONObject jsonObject) throws JSONException { - String edited = jsonObject.has("edited_id") ? jsonObject.getString("edited_id") : null; - String serverMsgId = jsonObject.has("server_msg_id") ? jsonObject.getString("server_msg_id") : null; - String body = jsonObject.has("body") ? jsonObject.getString("body") : null; - long timeSent = jsonObject.has("timeSent") ? jsonObject.getLong("timeSent") : null; - return new Edit(edited, serverMsgId, body, timeSent); - } - - static List fromJson(String input) { - final ArrayList list = new ArrayList<>(); - if (input == null) { - return list; - } - try { - final JSONArray jsonArray = new JSONArray(input); - for (int i = 0; i < jsonArray.length(); ++i) { - list.add(fromJson(jsonArray.getJSONObject(i))); - } - return list; - } catch (JSONException e) { - return list; - } - } - - private JSONObject toJson(boolean hidebody) throws JSONException { - JSONObject jsonObject = new JSONObject(); - jsonObject.put("edited_id", editedId); - jsonObject.put("server_msg_id", serverMsgId); - jsonObject.put("body", hidebody ? "" : body); - jsonObject.put("timeSent", timeSent); - return jsonObject; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Edit edit = (Edit) o; - - if (editedId != null ? !editedId.equals(edit.editedId) : edit.editedId != null) - return false; - return serverMsgId != null ? serverMsgId.equals(edit.serverMsgId) : edit.serverMsgId == null; - } - - @Override - public int hashCode() { - int result = editedId != null ? editedId.hashCode() : 0; - result = 31 * result + (serverMsgId != null ? serverMsgId.hashCode() : 0); - return result; - } - - public String getServerMsgId() { - return serverMsgId; - } - - public String getBody() { - return body; - } - - public String getEditedId() { return editedId; } - - public long getTimeSent() { - return timeSent; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java b/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java deleted file mode 100644 index a8ecb35a3..000000000 --- a/src/main/java/eu/siacs/conversations/entities/IndividualMessage.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package eu.siacs.conversations.entities; - -import android.database.Cursor; - -import java.util.Set; - -import eu.siacs.conversations.ui.adapter.MessageAdapter; -import eu.siacs.conversations.xmpp.Jid; - -public class IndividualMessage extends Message { - - private IndividualMessage(Conversational conversation) { - super(conversation); - } - - private IndividualMessage(Conversational conversation, String uuid, String conversationUUid, Jid counterpart, Jid trueCounterpart, String body, long timeSent, int encryption, int status, int type, boolean carbon, String remoteMsgId, String relativeFilePath, String serverMsgId, String fingerprint, boolean read, boolean deleted, String edited, boolean oob, String errorMessage, Set readByMarkers, boolean markable, boolean file_deleted, String bodyLanguage, String retractId) { - super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, deleted, edited, oob, errorMessage, readByMarkers, markable, file_deleted, bodyLanguage,retractId); - } - - public static Message createDateSeparator(Message message) { - final Message separator = new IndividualMessage(message.getConversation()); - separator.setType(Message.TYPE_STATUS); - separator.body = MessageAdapter.DATE_SEPARATOR_BODY; - separator.setTime(message.getTimeSent()); - return separator; - } - - public static Message fromCursor(Cursor cursor, Conversational conversation) { - Jid jid; - try { - String value = cursor.getString(cursor.getColumnIndex(COUNTERPART)); - if (value != null) { - jid = Jid.of(value); - } else { - jid = null; - } - } catch (IllegalArgumentException e) { - jid = null; - } catch (IllegalStateException e) { - return null; // message too long? - } - Jid trueCounterpart; - try { - String value = cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART)); - if (value != null) { - trueCounterpart = Jid.of(value); - } else { - trueCounterpart = null; - } - } catch (IllegalArgumentException e) { - trueCounterpart = null; - } - return new IndividualMessage(conversation, - cursor.getString(cursor.getColumnIndex(UUID)), - cursor.getString(cursor.getColumnIndex(CONVERSATION)), - jid, - trueCounterpart, - cursor.getString(cursor.getColumnIndex(BODY)), - cursor.getLong(cursor.getColumnIndex(TIME_SENT)), - cursor.getInt(cursor.getColumnIndex(ENCRYPTION)), - cursor.getInt(cursor.getColumnIndex(STATUS)), - cursor.getInt(cursor.getColumnIndex(TYPE)), - cursor.getInt(cursor.getColumnIndex(CARBON)) > 0, - cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)), - cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)), - cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)), - cursor.getString(cursor.getColumnIndex(FINGERPRINT)), - cursor.getInt(cursor.getColumnIndex(READ)) > 0, - cursor.getInt(cursor.getColumnIndex(DELETED)) > 0, - cursor.getString(cursor.getColumnIndex(EDITED)), - cursor.getInt(cursor.getColumnIndex(OOB)) > 0, - cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)), - ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))), - cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0, - cursor.getInt(cursor.getColumnIndex(FILE_DELETED)) > 0, - cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)), - cursor.getString(cursor.getColumnIndex(RETRACT_ID)) - ); - } - - @Override - public Message next() { - return null; - } - - @Override - public Message prev() { - return null; - } - - @Override - public boolean isValidInSession() { - return true; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/entities/ListItem.java b/src/main/java/eu/siacs/conversations/entities/ListItem.java deleted file mode 100644 index 83bbc10e3..000000000 --- a/src/main/java/eu/siacs/conversations/entities/ListItem.java +++ /dev/null @@ -1,60 +0,0 @@ -package eu.siacs.conversations.entities; - -import android.content.Context; - -import java.util.List; - -import eu.siacs.conversations.services.AvatarService; -import eu.siacs.conversations.xmpp.Jid; - -public interface ListItem extends Comparable, AvatarService.Avatarable { - String getDisplayName(); - - int getOffline(); - - Jid getJid(); - - Account getAccount(); - - List getTags(Context context); - - boolean getActive(); - - final class Tag { - private final String name; - private final int color; - private final int offline; - private final Account account; - private final boolean active; - - public Tag(final String name, final int color, final int offline, final Account account, final boolean active) { - this.name = name; - this.color = color; - this.offline = offline; - this.account = account; - this.active = active; - } - - public int getColor() { - return this.color; - } - - public String getName() { - return this.name; - } - - public int getOffline() { - return this.offline; - } - - public Account getAccount() { - return this.account; - } - - public boolean getActive() { - return this.active; - } - } - - boolean match(Context context, final String needle); -} diff --git a/src/main/java/eu/siacs/conversations/entities/MTMDecision.java b/src/main/java/eu/siacs/conversations/entities/MTMDecision.java deleted file mode 100644 index fe939c0ae..000000000 --- a/src/main/java/eu/siacs/conversations/entities/MTMDecision.java +++ /dev/null @@ -1,33 +0,0 @@ -/* MemorizingTrustManager - a TrustManager which asks the user about invalid - * certificates and memorizes their decision. - * - * Copyright (c) 2010 Georg Lukas - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package eu.siacs.conversations.entities; - -public class MTMDecision { - public final static int DECISION_INVALID = 0; - public final static int DECISION_ABORT = 1; - public final static int DECISION_ONCE = 2; - public final static int DECISION_ALWAYS = 3; - - public int state = DECISION_INVALID; -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java deleted file mode 100644 index e19e89176..000000000 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ /dev/null @@ -1,1207 +0,0 @@ -package eu.siacs.conversations.entities; - -import android.content.ContentValues; -import android.database.Cursor; -import android.graphics.Color; -import android.text.SpannableStringBuilder; -import android.util.Log; - -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableSet; -import com.google.common.primitives.Longs; - -import org.json.JSONException; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; -import eu.siacs.conversations.http.URL; -import eu.siacs.conversations.services.AvatarService; -import eu.siacs.conversations.ui.util.PresenceSelector; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.Emoticons; -import eu.siacs.conversations.utils.GeoHelper; -import eu.siacs.conversations.utils.MessageUtils; -import eu.siacs.conversations.utils.MimeUtils; -import eu.siacs.conversations.utils.Patterns; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.xmpp.Jid; - -public class Message extends AbstractEntity implements AvatarService.Avatarable { - - public static final String TABLENAME = "messages"; - - public static final int STATUS_RECEIVED = 0; - public static final int STATUS_UNSEND = 1; - public static final int STATUS_SEND = 2; - public static final int STATUS_SEND_FAILED = 3; - public static final int STATUS_WAITING = 5; - public static final int STATUS_OFFERED = 6; - public static final int STATUS_SEND_RECEIVED = 7; - public static final int STATUS_SEND_DISPLAYED = 8; - - public static final int ENCRYPTION_NONE = 0; - public static final int ENCRYPTION_PGP = 1; - public static final int ENCRYPTION_OTR = 2; - public static final int ENCRYPTION_DECRYPTED = 3; - public static final int ENCRYPTION_DECRYPTION_FAILED = 4; - public static final int ENCRYPTION_AXOLOTL = 5; - public static final int ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE = 6; - public static final int ENCRYPTION_AXOLOTL_FAILED = 7; - - public static final int TYPE_TEXT = 0; - public static final int TYPE_IMAGE = 1; - public static final int TYPE_FILE = 2; - public static final int TYPE_STATUS = 3; - public static final int TYPE_PRIVATE = 4; - public static final int TYPE_PRIVATE_FILE = 5; - public static final int TYPE_RTP_SESSION = 6; - - public static final String CONVERSATION = "conversationUuid"; - public static final String COUNTERPART = "counterpart"; - public static final String TRUE_COUNTERPART = "trueCounterpart"; - public static final String BODY = "body"; - public static final String BODY_LANGUAGE = "bodyLanguage"; - public static final String TIME_SENT = "timeSent"; - public static final String ENCRYPTION = "encryption"; - public static final String STATUS = "status"; - public static final String TYPE = "type"; - public static final String CARBON = "carbon"; - public static final String OOB = "oob"; - public static final String EDITED = "edited"; - public static final String REMOTE_MSG_ID = "remoteMsgId"; - public static final String SERVER_MSG_ID = "serverMsgId"; - public static final String RELATIVE_FILE_PATH = "relativeFilePath"; - public static final String FINGERPRINT = "axolotl_fingerprint"; - public static final String READ = "read"; - public static final String DELETED = "deleted"; - public static final String ERROR_MESSAGE = "errorMsg"; - public static final String READ_BY_MARKERS = "readByMarkers"; - public static final String MARKABLE = "markable"; - public static final String FILE_DELETED = "file_deleted"; - public static final String ME_COMMAND = "/me"; - public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled"; - public static final String DELETED_MESSAGE_BODY = "eu.siacs.conversations.message_deleted"; - public static final String DELETED_MESSAGE_BODY_OLD = "de.pixart.messenger.message_deleted"; - public static final String RETRACT_ID = "retractId"; - - public boolean markable = false; - protected String conversationUuid; - protected Jid counterpart; - protected Jid trueCounterpart; - protected String body; - protected String encryptedBody; - protected long timeSent; - protected int encryption; - protected int status; - protected int type; - protected boolean file_deleted = false; - protected boolean carbon = false; - protected boolean oob = false; - protected List edits = new ArrayList<>(); - protected String relativeFilePath; - protected boolean read = true; - protected boolean deleted = false; - protected String remoteMsgId = null; - - private String bodyLanguage = null; - protected String serverMsgId = null; - private final Conversational conversation; - protected Transferable transferable = null; - private Message mNextMessage = null; - private Message mPreviousMessage = null; - private String axolotlFingerprint = null; - private String errorMessage = null; - private Set readByMarkers = new CopyOnWriteArraySet<>(); - private String retractId = null; - protected int resendCount = 0; - - private Boolean isGeoUri = null; - private Boolean isXmppUri = null; - private Boolean isWebUri = null; - private String WebUri = null; - private Boolean isEmojisOnly = null; - private Boolean treatAsDownloadable = null; - private FileParams fileParams = null; - private List counterparts; - private WeakReference user; - - protected Message(Conversational conversation) { - this.conversation = conversation; - } - - public Message(Conversational conversation, String body, int encryption) { - this(conversation, body, encryption, STATUS_UNSEND); - } - - public Message(Conversational conversation, String body, int encryption, int status) { - this(conversation, java.util.UUID.randomUUID().toString(), - conversation.getUuid(), - conversation.getJid() == null ? null : conversation.getJid().asBareJid(), - null, - body, - System.currentTimeMillis(), - encryption, - status, - TYPE_TEXT, - false, - null, - null, - null, - null, - true, - false, - null, - false, - null, - null, - false, - false, - null, - null); - } - - public Message(Conversation conversation, int status, int type, final String remoteMsgId) { - this(conversation, java.util.UUID.randomUUID().toString(), - conversation.getUuid(), - conversation.getJid() == null ? null : conversation.getJid().asBareJid(), - null, - null, - System.currentTimeMillis(), - Message.ENCRYPTION_NONE, - status, - type, - false, - remoteMsgId, - null, - null, - null, - true, - false, - null, - false, - null, - null, - false, - false, - null, - null); - } - - protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart, - final Jid trueCounterpart, final String body, final long timeSent, - final int encryption, final int status, final int type, final boolean carbon, - final String remoteMsgId, final String relativeFilePath, - final String serverMsgId, final String fingerprint, final boolean read, final boolean deleted, - final String edited, final boolean oob, final String errorMessage, final Set readByMarkers, - final boolean markable, final boolean file_deleted, final String bodyLanguage, final String retractId) { - this.conversation = conversation; - this.uuid = uuid; - this.conversationUuid = conversationUUid; - this.counterpart = counterpart; - this.trueCounterpart = trueCounterpart; - this.body = body == null ? "" : body; - this.timeSent = timeSent; - this.encryption = encryption; - this.status = status; - this.type = type; - this.carbon = carbon; - this.remoteMsgId = remoteMsgId; - this.relativeFilePath = relativeFilePath; - this.serverMsgId = serverMsgId; - this.axolotlFingerprint = fingerprint; - this.read = read; - this.deleted = deleted; - this.edits = Edit.fromJson(edited); - this.oob = oob; - this.errorMessage = errorMessage; - this.readByMarkers = readByMarkers == null ? new CopyOnWriteArraySet<>() : readByMarkers; - this.markable = markable; - this.file_deleted = file_deleted; - this.bodyLanguage = bodyLanguage; - this.retractId = retractId; - } - - public static Message fromCursor(Cursor cursor, Conversation conversation) { - return new Message(conversation, - cursor.getString(cursor.getColumnIndex(UUID)), - cursor.getString(cursor.getColumnIndex(CONVERSATION)), - fromString(cursor.getString(cursor.getColumnIndex(COUNTERPART))), - fromString(cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART))), - cursor.getString(cursor.getColumnIndex(BODY)), - cursor.getLong(cursor.getColumnIndex(TIME_SENT)), - cursor.getInt(cursor.getColumnIndex(ENCRYPTION)), - cursor.getInt(cursor.getColumnIndex(STATUS)), - cursor.getInt(cursor.getColumnIndex(TYPE)), - cursor.getInt(cursor.getColumnIndex(CARBON)) > 0, - cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)), - cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)), - cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)), - cursor.getString(cursor.getColumnIndex(FINGERPRINT)), - cursor.getInt(cursor.getColumnIndex(READ)) > 0, - cursor.getInt(cursor.getColumnIndex(DELETED)) > 0, - cursor.getString(cursor.getColumnIndex(EDITED)), - cursor.getInt(cursor.getColumnIndex(OOB)) > 0, - cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)), - ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))), - cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0, - cursor.getInt(cursor.getColumnIndex(FILE_DELETED)) > 0, - cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)), - cursor.getString(cursor.getColumnIndex(RETRACT_ID)) - ); - } - - private static Jid fromString(String value) { - try { - if (value != null) { - return Jid.of(value); - } - } catch (IllegalArgumentException e) { - return null; - } - return null; - } - - public static Message createStatusMessage(Conversation conversation, String body) { - final Message message = new Message(conversation); - message.setType(Message.TYPE_STATUS); - message.setStatus(Message.STATUS_RECEIVED); - message.body = body; - return message; - } - - public static Message createLoadMoreMessage(Conversation conversation) { - final Message message = new Message(conversation); - message.setType(Message.TYPE_STATUS); - message.body = "LOAD_MORE"; - return message; - } - - @Override - public ContentValues getContentValues() { - ContentValues values = new ContentValues(); - values.put(UUID, uuid); - values.put(CONVERSATION, conversationUuid); - if (counterpart == null) { - values.putNull(COUNTERPART); - } else { - values.put(COUNTERPART, counterpart.toString()); - } - if (trueCounterpart == null) { - values.putNull(TRUE_COUNTERPART); - } else { - values.put(TRUE_COUNTERPART, trueCounterpart.toString()); - } - values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body); - values.put(TIME_SENT, timeSent); - values.put(ENCRYPTION, encryption); - values.put(STATUS, status); - values.put(TYPE, type); - values.put(CARBON, carbon ? 1 : 0); - values.put(REMOTE_MSG_ID, remoteMsgId); - values.put(RELATIVE_FILE_PATH, relativeFilePath); - values.put(SERVER_MSG_ID, serverMsgId); - values.put(FINGERPRINT, axolotlFingerprint); - values.put(READ, read ? 1 : 0); - values.put(DELETED, deleted ? 1 : 0); - try { - values.put(EDITED, Edit.toJson(edits, retractId != null || deleted)); - } catch (JSONException e) { - Log.e(Config.LOGTAG, "error persisting json for edits", e); - } - values.put(OOB, oob ? 1 : 0); - values.put(ERROR_MESSAGE, errorMessage); - values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString()); - values.put(MARKABLE, markable ? 1 : 0); - values.put(FILE_DELETED, file_deleted ? 1 : 0); - values.put(BODY_LANGUAGE, bodyLanguage); - values.put(RETRACT_ID, retractId); - return values; - } - - public String getConversationUuid() { - return conversationUuid; - } - - public Conversational getConversation() { - return this.conversation; - } - - public Jid getCounterpart() { - return counterpart; - } - - public void setCounterpart(final Jid counterpart) { - this.counterpart = counterpart; - } - - public Contact getContact() { - if (this.conversation.getMode() == Conversation.MODE_SINGLE) { - return this.conversation.getContact(); - } else { - if (this.trueCounterpart == null) { - return null; - } else { - return this.conversation.getAccount().getRoster() - .getContactFromContactList(this.trueCounterpart); - } - } - } - - public String getBody() { - return body; - } - - public synchronized void setBody(String body) { - if (body == null) { - throw new Error("You should not set the message body to null"); - } - this.body = body; - this.isGeoUri = null; - this.isXmppUri = null; - this.isWebUri = null; - this.isEmojisOnly = null; - this.treatAsDownloadable = null; - this.fileParams = null; - } - - public void setMucUser(MucOptions.User user) { - this.user = new WeakReference<>(user); - } - - public boolean sameMucUser(Message otherMessage) { - final MucOptions.User thisUser = this.user == null ? null : this.user.get(); - final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get(); - return thisUser != null && thisUser == otherUser; - } - - public String getErrorMessage() { - return errorMessage; - } - - public boolean setErrorMessage(String message) { - boolean changed = (message != null && !message.equals(errorMessage)) - || (message == null && errorMessage != null); - this.errorMessage = message; - return changed; - } - - public long getTimeSent() { - return timeSent; - } - - public int getEncryption() { - return encryption; - } - - public void setEncryption(int encryption) { - this.encryption = encryption; - } - - public int getStatus() { - return status; - } - - public void setStatus(int status) { - this.status = status; - } - - public String getRelativeFilePath() { - return this.relativeFilePath; - } - - public void setRelativeFilePath(String path) { - this.relativeFilePath = path; - } - - public String getRemoteMsgId() { - return this.remoteMsgId; - } - - public void setRemoteMsgId(String id) { - this.remoteMsgId = id; - } - - public String getServerMsgId() { - return this.serverMsgId; - } - - public void setServerMsgId(String id) { - this.serverMsgId = id; - } - - public boolean isRead() { - return this.read; - } - - public boolean isMessageDeleted() { - return this.deleted; - } - - public void setMessageDeleted(boolean deleted) { - this.deleted = deleted; - } - - public boolean isFileDeleted() { - return this.file_deleted; - } - - public void setFileDeleted(boolean file_deleted) { - this.file_deleted = file_deleted; - } - - public void markRead() { - this.read = true; - } - - public void markUnread() { - this.read = false; - } - - public void setTime(long time) { - this.timeSent = time; - } - - public String getEncryptedBody() { - return this.encryptedBody; - } - - public void setEncryptedBody(String body) { - this.encryptedBody = body; - } - - public int getType() { - return this.type; - } - - public void setType(int type) { - this.type = type; - } - - public boolean isCarbon() { - return carbon; - } - - public void setCarbon(boolean carbon) { - this.carbon = carbon; - } - - public void putEdited(String edited, String serverMsgId, String body, long timeSent) { - final Edit edit = new Edit(edited, serverMsgId, body, timeSent); - if (this.edits.size() < 128 && !this.edits.contains(edit)) { - this.edits.add(edit); - } - } - - boolean remoteMsgIdMatchInEdit(String id) { - for (Edit edit : this.edits) { - if (id.equals(edit.getEditedId())) { - return true; - } - } - return false; - } - - public String getBodyLanguage() { - return this.bodyLanguage; - } - - public void setBodyLanguage(String language) { - this.bodyLanguage = language; - } - - public boolean edited() { - return this.edits.size() > 0; - } - - public void setTrueCounterpart(Jid trueCounterpart) { - this.trueCounterpart = trueCounterpart; - } - - public Jid getTrueCounterpart() { - return this.trueCounterpart; - } - - public Transferable getTransferable() { - return this.transferable; - } - - public String getRetractId() { - return this.retractId; - } - - public void setRetractId(String id) { - this.retractId = id; - } - - public synchronized void setTransferable(Transferable transferable) { - this.fileParams = null; - this.transferable = transferable; - } - - public boolean addReadByMarker(ReadByMarker readByMarker) { - if (readByMarker.getRealJid() != null) { - if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) { - Log.d(Config.LOGTAG, "trying to add read marker by " + readByMarker.getRealJid() + " to " + body); - return false; - } - } else if (readByMarker.getFullJid() != null) { - if (readByMarker.getFullJid().equals(counterpart)) { - Log.d(Config.LOGTAG, "trying to add read marker by " + readByMarker.getFullJid() + " to " + body); - return false; - } - } - if (this.readByMarkers.add(readByMarker)) { - if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) { - Iterator iterator = this.readByMarkers.iterator(); - while (iterator.hasNext()) { - ReadByMarker marker = iterator.next(); - if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) { - iterator.remove(); - } - } - } - return true; - } else { - return false; - } - } - - public Set getReadByMarkers() { - return ImmutableSet.copyOf(this.readByMarkers); - } - - boolean similar(Message message) { - if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) { - return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId()); - } else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) { - return true; - } else if (this.body == null || this.counterpart == null) { - return false; - } else { - String body, otherBody; - if (this.hasFileOnRemoteHost()) { - body = getFileParams().url; - otherBody = message.body == null ? null : message.body.trim(); - } else { - body = this.body; - otherBody = message.body; - } - final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart()); - if (message.getRemoteMsgId() != null) { - final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches(); - if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) { - return true; - } - return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid)) - && matchingCounterpart - && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid)); - } else { - return this.remoteMsgId == null - && matchingCounterpart - && body.equals(otherBody) - && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000; - } - } - } - - public Message next() { - if (this.conversation instanceof Conversation) { - final Conversation conversation = (Conversation) this.conversation; - synchronized (conversation.messages) { - if (this.mNextMessage == null) { - int index = conversation.messages.indexOf(this); - if (index < 0 || index >= conversation.messages.size() - 1) { - this.mNextMessage = null; - } else { - this.mNextMessage = conversation.messages.get(index + 1); - } - } - return this.mNextMessage; - } - } else { - throw new AssertionError("Calling next should be disabled for stubs"); - } - } - - public Message prev() { - if (this.conversation instanceof Conversation) { - final Conversation conversation = (Conversation) this.conversation; - synchronized (conversation.messages) { - if (this.mPreviousMessage == null) { - int index = conversation.messages.indexOf(this); - if (index <= 0 || index > conversation.messages.size()) { - this.mPreviousMessage = null; - } else { - this.mPreviousMessage = conversation.messages.get(index - 1); - } - } - } - return this.mPreviousMessage; - } else { - throw new AssertionError("Calling prev should be disabled for stubs"); - } - } - - public boolean isLastCorrectableMessage() { - Message next = next(); - while (next != null) { - if (next.isEditable()) { - return false; - } - next = next.next(); - } - return isEditable(); - } - - public boolean isEditable() { - return status != STATUS_RECEIVED && type != Message.TYPE_RTP_SESSION; - } - - public boolean mergeable(final Message message) { - try { - boolean mergeAllowed = this.conversation.getAccount().getXmppConnection().getXmppConnectionService().allowMergeMessages(); - return mergeAllowed && (message != null && - (message.getType() == Message.TYPE_TEXT && - this.getTransferable() == null && - message.getTransferable() == null && - message.getEncryption() != Message.ENCRYPTION_PGP && - message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED && - this.getType() == message.getType() && - //this.getStatus() == message.getStatus() && - isStatusMergeable(this.getStatus(), message.getStatus()) && - this.getEncryption() == message.getEncryption() && - this.getCounterpart() != null && - this.getCounterpart().equals(message.getCounterpart()) && - this.edited() == message.edited() && - !this.isMessageDeleted() == !message.isMessageDeleted() && - (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && - this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS && - !message.isGeoUri() && - !this.isGeoUri() && - !message.isWebUri() && - !this.isWebUri() && - !message.isOOb() && - !this.isOOb() && - !message.treatAsDownloadable() && - !this.treatAsDownloadable() && - !message.hasMeCommand() && - !this.hasMeCommand() && - !message.bodyIsOnlyEmojis() && - !this.bodyIsOnlyEmojis() && - !message.isXmppUri() && - !this.isXmppUri() && - !message.hasDeletedBody() && - !this.hasDeletedBody() && - ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) && - UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) && - this.getReadByMarkers().equals(message.getReadByMarkers()) && - !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS) - )); - } catch (Exception e) { - e.printStackTrace(); - } - return false; - } - - private static boolean isStatusMergeable(int a, int b) { - return a == b || ( - (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND) - || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND) - || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING) - || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND) - || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING) - ); - } - - public void setCounterparts(List counterparts) { - this.counterparts = counterparts; - } - - public List getCounterparts() { - return this.counterparts; - } - - @Override - public int getAvatarBackgroundColor() { - if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) { - return Color.TRANSPARENT; - } else { - return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this)); - } - } - - @Override - public String getAvatarName() { - return UIHelper.getMessageDisplayName(this); - } - - public boolean isOOb() { - return oob; - } - - public static class MergeSeparator { - } - - public SpannableStringBuilder getMergedBody() { - SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim()); - Message current = this; - while (current.mergeable(current.next())) { - current = current.next(); - if (current == null) { - break; - } - body.append("\n\n"); - body.setSpan(new MergeSeparator(), body.length() - 2, body.length(), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE); - body.append(MessageUtils.filterLtrRtl(current.getBody()).trim()); - } - return body; - } - - public boolean hasMeCommand() { - return this.body.trim().startsWith(ME_COMMAND); - } - - public boolean hasDeletedBody() { - return this.body.trim().equals(DELETED_MESSAGE_BODY) || this.body.trim().equals(DELETED_MESSAGE_BODY_OLD); - } - - public int getMergedStatus() { - int status = this.status; - Message current = this; - while (current.mergeable(current.next())) { - current = current.next(); - if (current == null) { - break; - } - status = current.status; - } - return status; - } - - public long getMergedTimeSent() { - long time = this.timeSent; - Message current = this; - while (current.mergeable(current.next())) { - current = current.next(); - if (current == null) { - break; - } - time = current.timeSent; - } - return time; - } - - public boolean wasMergedIntoPrevious() { - Message prev = this.prev(); - return prev != null && prev.mergeable(this); - } - - public boolean trusted() { - Contact contact = this.getContact(); - return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf())); - } - - public boolean fixCounterpart() { - final Presences presences = conversation.getContact().getPresences(); - if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) { - return true; - } else if (presences.size() >= 1) { - counterpart = PresenceSelector.getNextCounterpart(getContact(),presences.toResourceArray()[0]); - return true; - } else { - counterpart = null; - return false; - } - } - - public void setUuid(String uuid) { - this.uuid = uuid; - } - - public String getEditedId() { - if (edits.size() > 0) { - return edits.get(edits.size() - 1).getEditedId(); - } else { - throw new IllegalStateException("Attempting to store unedited message"); - } - } - - public List getEditedList() { - return edits; - } - - public String getEditedIdWireFormat() { - if (edits.size() > 0) { - return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId(); - } else { - throw new IllegalStateException("Attempting to store unedited message"); - } - } - - public void setOob(boolean isOob) { - this.oob = isOob; - } - - public String getMimeType() { - String extension; - if (relativeFilePath != null) { - extension = MimeUtils.extractRelevantExtension(relativeFilePath); - } else { - try { - final String url = URL.tryParse(body.split("\n")[0]); - if (url == null) { - return null; - } - extension = MimeUtils.extractRelevantExtension(url); - } catch (Exception e) { - return null; - } - } - return MimeUtils.guessMimeTypeFromExtension(extension); - } - - public synchronized boolean treatAsDownloadable() { - if (treatAsDownloadable == null) { - treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob); - } - return treatAsDownloadable; - } - - public synchronized boolean bodyIsOnlyEmojis() { - if (isEmojisOnly == null) { - isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", "")); - } - return isEmojisOnly; - } - - public synchronized boolean isXmppUri() { - if (isXmppUri == null) { - isXmppUri = XmppUri.XMPP_URI.matcher(body).matches(); - } - return isXmppUri; - } - - public synchronized boolean isGeoUri() { - if (isGeoUri == null) { - isGeoUri = GeoHelper.GEO_URI.matcher(body).matches(); - } - return isGeoUri; - } - - public synchronized boolean isWebUri() { - if (isWebUri == null) { - isWebUri = Patterns.WEB_URL.matcher(body).matches(); - } - return isWebUri; - } - - public synchronized String getWebUri() { - final Pattern urlPattern = Pattern.compile( - "(?:(?:https?):\\/\\/|www\\.)(?:\\([-A-Z0-9+&@#\\/%=~_|$?!:,.]*\\)|[-A-Z0-9+&@#\\/%=~_|$?!:,.])*(?:\\([-A-Z0-9+&@#\\/%=~_|$?!:,.]*\\)|[A-Z0-9+&@#\\/%=~_|$])", - Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL); - Matcher m = urlPattern.matcher(body); - while (m.find()) { - if (WebUri == null) { - WebUri = m.group(0); - Log.d(Config.LOGTAG, "Weburi Message: " + WebUri); - return WebUri; - } - } - return WebUri; - } - - public synchronized void resetFileParams() { - this.fileParams = null; - } - - public synchronized FileParams getFileParams() { - if (fileParams == null) { - fileParams = new FileParams(); - if (this.transferable != null) { - fileParams.size = this.transferable.getFileSize(); - } - final String[] parts = body == null ? new String[0] : body.split("\\|"); - switch (parts.length) { - case 1: - try { - fileParams.size = Long.parseLong(parts[0]); - } catch (final NumberFormatException e) { - fileParams.url = URL.tryParse(parts[0]); - } - break; - case 5: - fileParams.width = parseInt(parts[2]); - fileParams.height = parseInt(parts[3]); - fileParams.runtime = parseInt(parts[4]); - case 4: - fileParams.width = parseInt(parts[2]); - fileParams.height = parseInt(parts[3]); - case 2: - fileParams.url = URL.tryParse(parts[0]); - fileParams.size = Longs.tryParse(parts[1]); - break; - case 3: - fileParams.size = Longs.tryParse(parts[0]); - fileParams.width = parseInt(parts[1]); - fileParams.height = parseInt(parts[2]); - break; - case 6: - fileParams.url = URL.tryParse(parts[0]); - fileParams.size = Longs.tryParse(parts[1]); - fileParams.runtime = parseInt(parts[4]); - fileParams.subject = parseString(parts[5]); - break; - - } - Log.d(Config.LOGTAG, "FileParams: " + body); - } - return fileParams; - } - - private static int parseInt(String value) { - try { - return Integer.parseInt(value); - } catch (NumberFormatException e) { - return 0; - } - } - - private static String parseString(String value) { - try { - return value; - } catch (Exception e) { - return ""; - } - } - - public void untie() { - this.mNextMessage = null; - this.mPreviousMessage = null; - } - - public boolean isPrivateMessage() { - return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE; - } - - public boolean isFileOrImage() { - return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE; - } - - - public boolean isTypeText() { - return type == TYPE_TEXT || type == TYPE_PRIVATE; - } - - public boolean hasFileOnRemoteHost() { - return isFileOrImage() && getFileParams().url != null; - } - - public boolean needsUploading() { - return isFileOrImage() && getFileParams().url == null; - } - - public boolean fileIsTransferring() { - return transferable.getStatus() == Transferable.STATUS_DOWNLOADING || transferable.getStatus() == Transferable.STATUS_UPLOADING || transferable.getStatus() == Transferable.STATUS_WAITING; - } - - public static class FileParams { - public String url; - public Long size = null; - public int width = 0; - public int height = 0; - public int runtime = 0; - public String subject = ""; - public long getSize() { - return size == null ? 0 : size; - } - } - - public void setFingerprint(String fingerprint) { - this.axolotlFingerprint = fingerprint; - } - - public String getFingerprint() { - return axolotlFingerprint; - } - - public boolean isTrusted() { - final AxolotlService axolotlService = conversation.getAccount().getAxolotlService(); - final FingerprintStatus s = axolotlService != null ? axolotlService.getFingerprintTrust(axolotlFingerprint) : null; - return s != null && s.isTrusted(); - } - - private int getPreviousEncryption() { - for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) { - if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) { - continue; - } - return iterator.getEncryption(); - } - return ENCRYPTION_NONE; - } - - private int getNextEncryption() { - if (this.conversation instanceof Conversation) { - Conversation conversation = (Conversation) this.conversation; - for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) { - if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) { - continue; - } - return iterator.getEncryption(); - } - return conversation.getNextEncryption(); - } else { - throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs"); - } - } - - public boolean isValidInSession() { - int pastEncryption = getCleanedEncryption(this.getPreviousEncryption()); - int futureEncryption = getCleanedEncryption(this.getNextEncryption()); - - boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE - || futureEncryption == ENCRYPTION_NONE - || pastEncryption != futureEncryption; - - return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption; - } - - private static int getCleanedEncryption(int encryption) { - if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) { - return ENCRYPTION_PGP; - } - if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) { - return ENCRYPTION_AXOLOTL; - } - return encryption; - } - - public static boolean configurePrivateMessage(final Message message) { - return configurePrivateMessage(message, false); - } - - public static boolean configurePrivateFileMessage(final Message message) { - return configurePrivateMessage(message, true); - } - - private static boolean configurePrivateMessage(final Message message, final boolean isFile) { - final Conversation conversation; - if (message.conversation instanceof Conversation) { - conversation = (Conversation) message.conversation; - } else { - return false; - } - if (conversation.getMode() == Conversation.MODE_MULTI) { - final Jid nextCounterpart = conversation.getNextCounterpart(); - 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; - } - - public int getResendCount(){ - return resendCount; - } - public int increaseResendCount(){ - return ++resendCount; - } - - - /** - * Checks whether message should show an avatar next to it. Mainly used to hide the avatar - * when succeeding messages have the same author. Thus it defaults to true. - * - * @return boolean - */ - public boolean isAvatarable() { - if (this.next() == null) { - return true; - } - Message next = this.next(); - - // same status (particularly sent vs received) - if (this.getStatus() == next.getStatus()) { - // same user - if (this.getAvatarName().equals(next.getAvatarName())) { - // same encryption - if (this.getEncryption() != next.getEncryption()) { - return true; - } - // same day - if (!UIHelper.sameDay(this.getTimeSent(), next.getTimeSent())){ - return true; - } - // if merged, ask merged - if (next.wasMergedIntoPrevious()) { - return next.isAvatarable(); - } - return false; - } - } - return true; - } - - /** - * Checks whether message should show a username next to it. Mainly used to hide the username - * when succeeding messages have the same author. Thus it defaults to true. - * - * @return boolean - */ - public boolean showUsername() { - if (this.prev() == null) { - return true; - } - Message prev = this.prev(); - - // same status (particularly sent vs received) - if (this.getStatus() == prev.getStatus()) { - // same user - if (this.getAvatarName().equals(prev.getAvatarName())) { - // same encryption - if (this.getEncryption() != prev.getEncryption()) { - return true; - } - // same day - if (!UIHelper.sameDay(this.getTimeSent(), prev.getTimeSent())){ - return true; - } - // if merged, ask merged - if (prev.wasMergedIntoPrevious()) { - return prev.isAvatarable(); - } - return false; - } - } - return true; - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java deleted file mode 100644 index 296dbc09d..000000000 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ /dev/null @@ -1,915 +0,0 @@ -package eu.siacs.conversations.entities; - -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.services.AvatarService; -import eu.siacs.conversations.services.MessageArchiveService; -import eu.siacs.conversations.utils.JidHelper; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.chatstate.ChatState; -import eu.siacs.conversations.xmpp.forms.Data; -import eu.siacs.conversations.xmpp.forms.Field; -import eu.siacs.conversations.xmpp.pep.Avatar; - -public class MucOptions { - - public static final String STATUS_CODE_SELF_PRESENCE = "110"; - public static final String STATUS_CODE_ROOM_CREATED = "201"; - public static final String STATUS_CODE_BANNED = "301"; - public static final String STATUS_CODE_CHANGED_NICK = "303"; - public static final String STATUS_CODE_KICKED = "307"; - public static final String STATUS_CODE_AFFILIATION_CHANGE = "321"; - public static final String STATUS_CODE_LOST_MEMBERSHIP = "322"; - public static final String STATUS_CODE_SHUTDOWN = "332"; - public static final String STATUS_CODE_TECHNICAL_REASONS = "333"; - private final Set users = new HashSet<>(); - private final Conversation conversation; - public OnRenameListener onRenameListener = null; - private boolean mAutoPushConfiguration = true; - private final Account account; - private ServiceDiscoveryResult serviceDiscoveryResult; - private boolean isOnline = false; - private Error error = Error.NONE; - private User self; - private String password = null; - private boolean tookProposedNickFromBookmark = false; - - public MucOptions(Conversation conversation) { - this.account = conversation.getAccount(); - this.conversation = conversation; - this.self = new User(this, createJoinJid(getProposedNick())); - this.self.affiliation = Affiliation.of(conversation.getAttribute("affiliation")); - this.self.role = Role.of(conversation.getAttribute("role")); - } - - public Account getAccount() { - return this.conversation.getAccount(); - } - - public boolean setSelf(User user) { - this.self = user; - final boolean roleChanged = this.conversation.setAttribute("role", user.role.toString()); - final boolean affiliationChanged = this.conversation.setAttribute("affiliation", user.affiliation.toString()); - return roleChanged || affiliationChanged; - } - - public void changeAffiliation(Jid jid, Affiliation affiliation) { - User user = findUserByRealJid(jid); - synchronized (users) { - if (user != null && user.getRole() == Role.NONE) { - users.remove(user); - if (affiliation.ranks(Affiliation.MEMBER)) { - user.affiliation = affiliation; - users.add(user); - } - } - } - } - - public void flagNoAutoPushConfiguration() { - mAutoPushConfiguration = false; - } - - public boolean autoPushConfiguration() { - return mAutoPushConfiguration; - } - - public boolean isSelf(Jid counterpart) { - return counterpart.equals(self.getFullJid()); - } - - public void resetChatState() { - synchronized (users) { - for (User user : users) { - user.chatState = Config.DEFAULT_CHAT_STATE; - } - } - } - - public boolean isTookProposedNickFromBookmark() { - return tookProposedNickFromBookmark; - } - - void notifyOfBookmarkNick(final String nick) { - final String normalized = normalize(account.getJid(), nick); - if (normalized != null && normalized.equals(getSelf().getFullJid().getResource())) { - this.tookProposedNickFromBookmark = true; - } - } - - public boolean mamSupport() { - return MessageArchiveService.Version.has(getFeatures()); - } - - public boolean updateConfiguration(ServiceDiscoveryResult serviceDiscoveryResult) { - this.serviceDiscoveryResult = serviceDiscoveryResult; - String name; - Field roomConfigName = getRoomInfoForm().getFieldByName("muc#roomconfig_roomname"); - if (roomConfigName != null) { - name = roomConfigName.getValue(); - } else { - List identities = serviceDiscoveryResult.getIdentities(); - String identityName = identities.size() > 0 ? identities.get(0).getName() : null; - final Jid jid = conversation.getJid(); - if (identityName != null && !identityName.equals(jid == null ? null : jid.getEscapedLocal())) { - name = identityName; - } else { - name = null; - } - } - boolean changed = conversation.setAttribute("muc_name", name); - changed |= conversation.setAttribute(Conversation.ATTRIBUTE_MEMBERS_ONLY, this.hasFeature("muc_membersonly")); - changed |= conversation.setAttribute(Conversation.ATTRIBUTE_MODERATED, this.hasFeature("muc_moderated")); - changed |= conversation.setAttribute(Conversation.ATTRIBUTE_NON_ANONYMOUS, this.hasFeature("muc_nonanonymous")); - return changed; - } - - private Data getRoomInfoForm() { - final List forms = serviceDiscoveryResult == null ? Collections.emptyList() : serviceDiscoveryResult.forms; - return forms.size() == 0 ? new Data() : forms.get(0); - } - - public String getAvatar() { - return account.getRoster().getContact(conversation.getJid()).getAvatarFilename(); - } - - public boolean hasFeature(String feature) { - return this.serviceDiscoveryResult != null && this.serviceDiscoveryResult.features.contains(feature); - } - - public boolean hasVCards() { - return hasFeature("vcard-temp"); - } - - public boolean canInvite() { - final boolean hasPermission = !membersOnly() || self.getRole().ranks(Role.MODERATOR) || allowInvites(); - return hasPermission && online(); - } - - public boolean allowInvites() { - final Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowinvites"); - return field != null && "1".equals(field.getValue()); - } - - public boolean canChangeSubject() { - return self.getRole().ranks(Role.MODERATOR) || participantsCanChangeSubject(); - } - - public boolean participantsCanChangeSubject() { - final Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_changesubject"); - return field != null && "1".equals(field.getValue()); - } - - public boolean allowPm() { - final Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowpm"); - if (field == null) { - return true; //fall back if field does not exists - } - if ("anyone".equals(field.getValue())) { - return true; - } else if ("participants".equals(field.getValue())) { - return self.getRole().ranks(Role.PARTICIPANT); - } else if ("moderators".equals(field.getValue())) { - return self.getRole().ranks(Role.MODERATOR); - } else { - return false; - } - } - - public boolean participating() { - return self.getRole().ranks(Role.PARTICIPANT) || !moderated(); - } - - public boolean membersOnly() { - return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MEMBERS_ONLY, false); - } - - public List getFeatures() { - return this.serviceDiscoveryResult != null ? this.serviceDiscoveryResult.features : Collections.emptyList(); - } - - public boolean nonanonymous() { - return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_NON_ANONYMOUS, false); - } - - public boolean isPrivateAndNonAnonymous() { - return membersOnly() && nonanonymous(); - } - - public boolean moderated() { - return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MODERATED, false); - } - - public boolean stableId() { - return getFeatures().contains("http://jabber.org/protocol/muc#stable_id"); - } - - public User deleteUser(Jid jid) { - User user = findUserByFullJid(jid); - if (user != null) { - synchronized (users) { - users.remove(user); - boolean realJidInMuc = false; - for (User u : users) { - if (user.realJid != null && user.realJid.equals(u.realJid)) { - realJidInMuc = true; - break; - } - } - boolean self = user.realJid != null && user.realJid.equals(account.getJid().asBareJid()); - if (membersOnly() - && nonanonymous() - && user.affiliation.ranks(Affiliation.MEMBER) - && user.realJid != null - && !realJidInMuc - && !self) { - user.role = Role.NONE; - user.avatar = null; - user.fullJid = null; - users.add(user); - } - } - } - return user; - } - - //returns true if real jid was new; - public boolean updateUser(User user) { - User old; - boolean realJidFound = false; - if (user.fullJid == null && user.realJid != null) { - old = findUserByRealJid(user.realJid); - realJidFound = old != null; - if (old != null) { - if (old.fullJid != null) { - return false; //don't add. user already exists - } else { - synchronized (users) { - users.remove(old); - } - } - } - } else if (user.realJid != null) { - old = findUserByRealJid(user.realJid); - realJidFound = old != null; - synchronized (users) { - if (old != null && (old.fullJid == null || old.role == Role.NONE)) { - users.remove(old); - } - } - } - old = findUserByFullJid(user.getFullJid()); - synchronized (this.users) { - if (old != null) { - users.remove(old); - } - boolean fullJidIsSelf = isOnline && user.getFullJid() != null && user.getFullJid().equals(self.getFullJid()); - if ((!membersOnly() || user.getAffiliation().ranks(Affiliation.MEMBER)) - && user.getAffiliation().outranks(Affiliation.OUTCAST) - && !fullJidIsSelf) { - this.users.add(user); - return !realJidFound && user.realJid != null; - } - } - return false; - } - - public User findUserByFullJid(Jid jid) { - if (jid == null) { - return null; - } - synchronized (users) { - for (User user : users) { - if (jid.equals(user.getFullJid())) { - return user; - } - } - } - return null; - } - - public User findUserByRealJid(Jid jid) { - if (jid == null) { - return null; - } - synchronized (users) { - for (User user : users) { - if (jid.equals(user.realJid)) { - return user; - } - } - } - return null; - } - - public User findOrCreateUserByRealJid(Jid jid, Jid fullJid) { - User user = findUserByRealJid(jid); - if (user == null) { - user = new User(this, fullJid); - user.setRealJid(jid); - } - return user; - } - - public User findUser(ReadByMarker readByMarker) { - if (readByMarker.getRealJid() != null) { - return findOrCreateUserByRealJid(readByMarker.getRealJid().asBareJid(), readByMarker.getFullJid()); - } else if (readByMarker.getFullJid() != null) { - return findUserByFullJid(readByMarker.getFullJid()); - } else { - return null; - } - } - - public boolean isContactInRoom(Contact contact) { - return contact != null && findUserByRealJid(contact.getJid().asBareJid()) != null; - } - - public boolean isUserInRoom(Jid jid) { - return findUserByFullJid(jid) != null; - } - - public boolean setOnline() { - boolean before = this.isOnline; - this.isOnline = true; - return !before; - } - - public ArrayList getUsers() { - return getUsers(true); - } - - public ArrayList getUsers(boolean includeOffline) { - synchronized (users) { - ArrayList users = new ArrayList<>(); - for (User user : this.users) { - if (!user.isDomain() && (includeOffline || user.getRole().ranks(Role.PARTICIPANT))) { - users.add(user); - } - } - return users; - } - } - - public ArrayList getUsersWithChatState(ChatState state, int max) { - synchronized (users) { - ArrayList list = new ArrayList<>(); - for (User user : users) { - if (user.chatState == state) { - list.add(user); - if (list.size() >= max) { - break; - } - } - } - return list; - } - } - - public List getUsers(int max) { - ArrayList subset = new ArrayList<>(); - HashSet jids = new HashSet<>(); - jids.add(account.getJid().asBareJid()); - synchronized (users) { - for (User user : users) { - if (user.getRealJid() == null || (user.getRealJid().getLocal() != null && jids.add(user.getRealJid()))) { - subset.add(user); - } - if (subset.size() >= max) { - break; - } - } - } - return subset; - } - - public static List sub(List users, int max) { - ArrayList subset = new ArrayList<>(); - HashSet jids = new HashSet<>(); - for (User user : users) { - jids.add(user.getAccount().getJid().asBareJid()); - if (user.getRealJid() == null || (user.getRealJid().getLocal() != null && jids.add(user.getRealJid()))) { - subset.add(user); - } - if (subset.size() >= max) { - break; - } - } - return subset; - } - - public int getUserCount() { - synchronized (users) { - return users.size(); - } - } - - public String getProposedNick() { - final Bookmark bookmark = this.conversation.getBookmark(); - final String bookmarkedNick = normalize(account.getJid(), bookmark == null ? null : bookmark.getNick()); - if (bookmarkedNick != null) { - this.tookProposedNickFromBookmark = true; - return bookmarkedNick; - } else if (!conversation.getJid().isBareJid()) { - return conversation.getJid().getResource(); - } else { - return defaultNick(account); - } - } - - public static String defaultNick(final Account account) { - final String displayName = normalize(account.getJid(), account.getDisplayName()); - if (displayName == null) { - return JidHelper.localPartOrFallback(account.getJid()); - } else { - return displayName; - } - } - - private static String normalize(Jid account, String nick) { - if (account == null || TextUtils.isEmpty(nick)) { - return null; - } - try { - return account.withResource(nick).getResource(); - } catch (IllegalArgumentException e) { - return null; - } - - } - - public String getActualNick() { - if (this.self.getName() != null) { - return this.self.getName(); - } else { - return this.getProposedNick(); - } - } - - public boolean online() { - return this.isOnline; - } - - public Error getError() { - return this.error; - } - - public void setError(Error error) { - this.isOnline = isOnline && error == Error.NONE; - this.error = error; - } - - public void setOnRenameListener(OnRenameListener listener) { - this.onRenameListener = listener; - } - - public void setOffline() { - synchronized (users) { - this.users.clear(); - } - this.error = Error.NO_RESPONSE; - this.isOnline = false; - } - - public User getSelf() { - return self; - } - - public boolean setSubject(String subject) { - return this.conversation.setAttribute("subject", subject); - } - - public String getSubject() { - return this.conversation.getAttribute("subject"); - } - - public String getName() { - return this.conversation.getAttribute("muc_name"); - } - - private List getFallbackUsersFromCryptoTargets() { - List users = new ArrayList<>(); - for (Jid jid : conversation.getAcceptedCryptoTargets()) { - User user = new User(this, null); - user.setRealJid(jid); - users.add(user); - } - return users; - } - - public List getUsersRelevantForNameAndAvatar() { - final List users; - if (isOnline) { - users = getUsers(5); - } else { - users = getFallbackUsersFromCryptoTargets(); - } - return users; - } - - String createNameFromParticipants() { - List users = getUsersRelevantForNameAndAvatar(); - if (users.size() >= 2) { - StringBuilder builder = new StringBuilder(); - for (User user : users) { - if (builder.length() != 0) { - builder.append(", "); - } - String name = UIHelper.getDisplayName(user); - if (name != null) { - builder.append(name.split("\\s+")[0]); - } - } - return builder.toString(); - } else { - return null; - } - } - - public long[] getPgpKeyIds() { - List ids = new ArrayList<>(); - for (User user : this.users) { - if (user.getPgpKeyId() != 0) { - ids.add(user.getPgpKeyId()); - } - } - ids.add(account.getPgpId()); - long[] primitiveLongArray = new long[ids.size()]; - for (int i = 0; i < ids.size(); ++i) { - primitiveLongArray[i] = ids.get(i); - } - return primitiveLongArray; - } - - public boolean pgpKeysInUse() { - synchronized (users) { - for (User user : users) { - if (user.getPgpKeyId() != 0) { - return true; - } - } - } - return false; - } - - public boolean everybodyHasKeys() { - synchronized (users) { - for (User user : users) { - if (user.getPgpKeyId() == 0) { - return false; - } - } - } - return true; - } - - public Jid createJoinJid(String nick) { - try { - return conversation.getJid().withResource(nick); - } catch (final IllegalArgumentException e) { - return null; - } - } - - public Jid getTrueCounterpart(Jid jid) { - if (jid.equals(getSelf().getFullJid())) { - return account.getJid().asBareJid(); - } - User user = findUserByFullJid(jid); - return user == null ? null : user.realJid; - } - - public String getPassword() { - this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD); - if (this.password == null && conversation.getBookmark() != null - && conversation.getBookmark().getPassword() != null) { - return conversation.getBookmark().getPassword(); - } else { - return this.password; - } - } - - public void setPassword(String password) { - if (conversation.getBookmark() != null) { - conversation.getBookmark().setPassword(password); - } else { - this.password = password; - } - conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password); - } - - public Conversation getConversation() { - return this.conversation; - } - - public List getMembers(final boolean includeDomains) { - ArrayList members = new ArrayList<>(); - synchronized (users) { - for (User user : users) { - if (user.affiliation.ranks(Affiliation.MEMBER) && user.realJid != null && !user.realJid.asBareJid().equals(conversation.account.getJid().asBareJid()) && (!user.isDomain() || includeDomains)) { - members.add(user.realJid); - } - } - } - return members; - } - - public enum Affiliation { - OWNER(4, R.string.owner), - ADMIN(3, R.string.admin), - MEMBER(2, R.string.member), - OUTCAST(0, R.string.outcast), - NONE(1, R.string.no_affiliation); - - private final int resId; - private final int rank; - - Affiliation(int rank, int resId) { - this.resId = resId; - this.rank = rank; - } - - public static Affiliation of(@Nullable String value) { - if (value == null) { - return NONE; - } - try { - return Affiliation.valueOf(value.toUpperCase(Locale.US)); - } catch (IllegalArgumentException e) { - return NONE; - } - } - - public int getResId() { - return resId; - } - - @Override - public String toString() { - return name().toLowerCase(Locale.US); - } - - public boolean outranks(Affiliation affiliation) { - return rank > affiliation.rank; - } - - public boolean ranks(Affiliation affiliation) { - return rank >= affiliation.rank; - } - } - - public enum Role { - MODERATOR(R.string.moderator, 3), - VISITOR(R.string.visitor, 1), - PARTICIPANT(R.string.participant, 2), - NONE(R.string.no_role, 0); - - private final int resId; - private final int rank; - - Role(int resId, int rank) { - this.resId = resId; - this.rank = rank; - } - - public static Role of(@Nullable String value) { - if (value == null) { - return NONE; - } - try { - return Role.valueOf(value.toUpperCase(Locale.US)); - } catch (IllegalArgumentException e) { - return NONE; - } - } - - public int getResId() { - return resId; - } - - @Override - public String toString() { - return name().toLowerCase(Locale.US); - } - - public boolean ranks(Role role) { - return rank >= role.rank; - } - } - - public enum Error { - NO_RESPONSE, - SERVER_NOT_FOUND, - REMOTE_SERVER_TIMEOUT, - NONE, - NICK_IN_USE, - PASSWORD_REQUIRED, - BANNED, - MEMBERS_ONLY, - RESOURCE_CONSTRAINT, - KICKED, - SHUTDOWN, - DESTROYED, - INVALID_NICK, - TECHNICAL_PROBLEMS, - UNKNOWN, - NON_ANONYMOUS - } - - private interface OnEventListener { - void onSuccess(); - - void onFailure(); - } - - public interface OnRenameListener extends OnEventListener { - - } - - public static class User implements Comparable, AvatarService.Avatarable { - private Role role = Role.NONE; - private Affiliation affiliation = Affiliation.NONE; - private Jid realJid; - private Jid fullJid; - private long pgpKeyId = 0; - private Avatar avatar; - private final MucOptions options; - private ChatState chatState = Config.DEFAULT_CHAT_STATE; - - public User(MucOptions options, Jid fullJid) { - this.options = options; - this.fullJid = fullJid; - } - - public String getName() { - return fullJid == null ? null : fullJid.getResource(); - } - - public Role getRole() { - return this.role; - } - - public void setRole(String role) { - this.role = Role.of(role); - } - - public Affiliation getAffiliation() { - return this.affiliation; - } - - public void setAffiliation(String affiliation) { - this.affiliation = Affiliation.of(affiliation); - } - - public long getPgpKeyId() { - if (this.pgpKeyId != 0) { - return this.pgpKeyId; - } else if (realJid != null) { - return getAccount().getRoster().getContact(realJid).getPgpKeyId(); - } else { - return 0; - } - } - - public void setPgpKeyId(long id) { - this.pgpKeyId = id; - } - - public Contact getContact() { - if (fullJid != null) { - return getAccount().getRoster().getContactFromContactList(realJid); - } else if (realJid != null) { - return getAccount().getRoster().getContact(realJid); - } else { - return null; - } - } - - public boolean setAvatar(Avatar avatar) { - if (this.avatar != null && this.avatar.equals(avatar)) { - return false; - } else { - this.avatar = avatar; - return true; - } - } - - public String getAvatar() { - if (avatar != null) { - return avatar.getFilename(); - } - Avatar avatar = realJid != null ? getAccount().getRoster().getContact(realJid).getAvatar() : null; - return avatar == null ? null : avatar.getFilename(); - } - - public Account getAccount() { - return options.getAccount(); - } - - public Conversation getConversation() { - return options.getConversation(); - } - - public Jid getFullJid() { - return fullJid; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - User user = (User) o; - - if (role != user.role) return false; - if (affiliation != user.affiliation) return false; - if (realJid != null ? !realJid.equals(user.realJid) : user.realJid != null) - return false; - return fullJid != null ? fullJid.equals(user.fullJid) : user.fullJid == null; - - } - - public boolean isDomain() { - return realJid != null && realJid.getLocal() == null && role == Role.NONE; - } - - @Override - public int hashCode() { - int result = role != null ? role.hashCode() : 0; - result = 31 * result + (affiliation != null ? affiliation.hashCode() : 0); - result = 31 * result + (realJid != null ? realJid.hashCode() : 0); - result = 31 * result + (fullJid != null ? fullJid.hashCode() : 0); - return result; - } - - @Override - public String toString() { - return "[fulljid:" + fullJid + ",realjid:" + realJid + ",affiliation" + affiliation.toString() + "]"; - } - - public boolean realJidMatchesAccount() { - return realJid != null && realJid.equals(options.account.getJid().asBareJid()); - } - - @Override - public int compareTo(@NonNull User another) { - if (another.getAffiliation().outranks(getAffiliation())) { - return 1; - } else if (getAffiliation().outranks(another.getAffiliation())) { - return -1; - } else { - return getComparableName().compareToIgnoreCase(another.getComparableName()); - } - } - - public String getComparableName() { - Contact contact = getContact(); - if (contact != null) { - return contact.getDisplayName(); - } else { - String name = getName(); - return name == null ? "" : name; - } - } - - public Jid getRealJid() { - return realJid; - } - - public void setRealJid(Jid jid) { - this.realJid = jid != null ? jid.asBareJid() : null; - } - - public boolean setChatState(ChatState chatState) { - if (this.chatState == chatState) { - return false; - } - this.chatState = chatState; - return true; - } - - @Override - public int getAvatarBackgroundColor() { - final String seed = realJid != null ? realJid.asBareJid().toString() : null; - return UIHelper.getColorForName(seed == null ? getName() : seed); - } - - @Override - public String getAvatarName() { - return getConversation().getName().toString(); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/Presence.java b/src/main/java/eu/siacs/conversations/entities/Presence.java deleted file mode 100644 index d9ef4a46e..000000000 --- a/src/main/java/eu/siacs/conversations/entities/Presence.java +++ /dev/null @@ -1,105 +0,0 @@ -package eu.siacs.conversations.entities; - -import androidx.annotation.NonNull; - -import java.util.Locale; - -import eu.siacs.conversations.xml.Element; - -public class Presence implements Comparable { - - public enum Status { - CHAT, ONLINE, AWAY, XA, DND, OFFLINE; - - public String toShowString() { - switch (this) { - case CHAT: - return "chat"; - case AWAY: - return "away"; - case XA: - return "xa"; - case DND: - return "dnd"; - } - return null; - } - - public static Status fromShowString(String show) { - if (show == null) { - return ONLINE; - } else { - switch (show.toLowerCase(Locale.US)) { - case "away": - return AWAY; - case "xa": - return XA; - case "dnd": - return DND; - case "chat": - return CHAT; - default: - return ONLINE; - } - } - } - } - - private final Status status; - private ServiceDiscoveryResult disco; - private final String ver; - private final String hash; - private final String node; - private final String message; - - private Presence(Status status, String ver, String hash, String node, String message) { - this.status = status; - this.ver = ver; - this.hash = hash; - this.node = node; - this.message = message; - } - - public static Presence parse(String show, Element caps, String message) { - final String hash = caps == null ? null : caps.getAttribute("hash"); - final String ver = caps == null ? null : caps.getAttribute("ver"); - final String node = caps == null ? null : caps.getAttribute("node"); - return new Presence(Status.fromShowString(show), ver, hash, node, message); - } - - public int compareTo(@NonNull Presence other) { - return this.status.compareTo(((Presence) other).status); - } - - public Status getStatus() { - return this.status; - } - - public boolean hasCaps() { - return ver != null && hash != null; - } - - public String getVer() { - return this.ver; - } - - public String getNode() { - return this.node; - } - - public String getHash() { - return this.hash; - } - - public String getMessage() { - return this.message; - } - - public void setServiceDiscoveryResult(ServiceDiscoveryResult disco) { - this.disco = disco; - } - - public ServiceDiscoveryResult getServiceDiscoveryResult() { - return disco; - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/PresenceTemplate.java b/src/main/java/eu/siacs/conversations/entities/PresenceTemplate.java deleted file mode 100644 index 98ea2cf66..000000000 --- a/src/main/java/eu/siacs/conversations/entities/PresenceTemplate.java +++ /dev/null @@ -1,81 +0,0 @@ -package eu.siacs.conversations.entities; - -import android.content.ContentValues; -import android.database.Cursor; - - -public class PresenceTemplate extends AbstractEntity { - - public static final String TABELNAME = "presence_templates"; - public static final String LAST_USED = "last_used"; - public static final String MESSAGE = "message"; - public static final String STATUS = "status"; - - private long lastUsed = 0; - private String statusMessage; - private Presence.Status status = Presence.Status.ONLINE; - - public PresenceTemplate(Presence.Status status, String statusMessage) { - this.status = status; - this.statusMessage = statusMessage; - this.lastUsed = System.currentTimeMillis(); - this.uuid = java.util.UUID.randomUUID().toString(); - } - - private PresenceTemplate() { - - } - - @Override - public ContentValues getContentValues() { - final String show = status.toShowString(); - ContentValues values = new ContentValues(); - values.put(LAST_USED, lastUsed); - values.put(MESSAGE, statusMessage); - values.put(STATUS, show == null ? "" : show); - values.put(UUID, uuid); - return values; - } - - public static PresenceTemplate fromCursor(Cursor cursor) { - PresenceTemplate template = new PresenceTemplate(); - template.uuid = cursor.getString(cursor.getColumnIndex(UUID)); - template.lastUsed = cursor.getLong(cursor.getColumnIndex(LAST_USED)); - template.statusMessage = cursor.getString(cursor.getColumnIndex(MESSAGE)); - template.status = Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS))); - return template; - } - - public Presence.Status getStatus() { - return status; - } - - public String getStatusMessage() { - return statusMessage; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - PresenceTemplate template = (PresenceTemplate) o; - - if (statusMessage != null ? !statusMessage.equals(template.statusMessage) : template.statusMessage != null) - return false; - return status == template.status; - - } - - @Override - public int hashCode() { - int result = statusMessage != null ? statusMessage.hashCode() : 0; - result = 31 * result + status.hashCode(); - return result; - } - - @Override - public String toString() { - return statusMessage; - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/Presences.java b/src/main/java/eu/siacs/conversations/entities/Presences.java deleted file mode 100644 index f3daff13b..000000000 --- a/src/main/java/eu/siacs/conversations/entities/Presences.java +++ /dev/null @@ -1,194 +0,0 @@ -package eu.siacs.conversations.entities; - -import android.util.Pair; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -public class Presences { - private final Hashtable presences = new Hashtable<>(); - - private static String nameWithoutVersion(String name) { - String[] parts = name.split(" "); - if (parts.length > 1 && Character.isDigit(parts[parts.length - 1].charAt(0))) { - StringBuilder output = new StringBuilder(); - for (int i = 0; i < parts.length - 1; ++i) { - if (output.length() != 0) { - output.append(' '); - } - output.append(parts[i]); - } - return output.toString(); - } else { - return name; - } - } - - public List getPresences() { - synchronized (this.presences) { - return new ArrayList<>(this.presences.values()); - } - } - - public Map getPresencesMap() { - synchronized (this.presences) { - return new HashMap<>(this.presences); - } - } - - public Presence get(String resource) { - synchronized (this.presences) { - return this.presences.get(resource); - } - } - - public void updatePresence(String resource, Presence presence) { - synchronized (this.presences) { - this.presences.put(resource, presence); - } - } - - public void removePresence(String resource) { - synchronized (this.presences) { - this.presences.remove(resource); - } - } - - public void clearPresences() { - synchronized (this.presences) { - this.presences.clear(); - } - } - - public Presence.Status getShownStatus() { - Presence.Status status = Presence.Status.OFFLINE; - synchronized (this.presences) { - for (Presence p : presences.values()) { - if (p.getStatus() == Presence.Status.DND) { - return p.getStatus(); - } else if (p.getStatus().compareTo(status) < 0) { - status = p.getStatus(); - } - } - } - return status; - } - - public String getMostAvailableResource() { - synchronized (this.presences) { - if (presences.size() < 1) { - return ""; - } - Presence p = Collections.min(presences.values()); - Iterator> it = presences.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry entry = it.next(); - if (entry.getValue().equals(p)) { - return "(" + entry.getKey() + ")"; - } - } - return ""; - } - } - - public int size() { - synchronized (this.presences) { - return presences.size(); - } - } - - public String[] toResourceArray() { - synchronized (this.presences) { - final String[] presencesArray = new String[presences.size()]; - presences.keySet().toArray(presencesArray); - return presencesArray; - } - } - - public List asTemplates() { - synchronized (this.presences) { - ArrayList templates = new ArrayList<>(presences.size()); - for (Presence p : presences.values()) { - if (p.getMessage() != null && !p.getMessage().trim().isEmpty()) { - templates.add(new PresenceTemplate(p.getStatus(), p.getMessage())); - } - } - return templates; - } - } - - public boolean has(String presence) { - synchronized (this.presences) { - return presences.containsKey(presence); - } - } - - public List getStatusMessages() { - ArrayList messages = new ArrayList<>(); - synchronized (this.presences) { - for (Presence presence : this.presences.values()) { - String message = presence.getMessage() == null ? null : presence.getMessage().trim(); - if (message != null && !message.isEmpty() && !messages.contains(message)) { - messages.add(message); - } - } - } - return messages; - } - - public boolean allOrNonSupport(String namespace) { - synchronized (this.presences) { - for (Presence presence : this.presences.values()) { - ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult(); - if (disco == null || !disco.getFeatures().contains(namespace)) { - return false; - } - } - } - return true; - } - - public boolean anySupport(final String namespace) { - synchronized (this.presences) { - if (this.presences.size() == 0) { - return true; - } - for (Presence presence : this.presences.values()) { - ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult(); - if (disco != null && disco.getFeatures().contains(namespace)) { - return true; - } - } - } - return false; - } - - public Pair, Map> toTypeAndNameMap() { - Map typeMap = new HashMap<>(); - Map nameMap = new HashMap<>(); - synchronized (this.presences) { - for (Map.Entry presenceEntry : this.presences.entrySet()) { - String resource = presenceEntry.getKey(); - Presence presence = presenceEntry.getValue(); - ServiceDiscoveryResult serviceDiscoveryResult = presence == null ? null : presence.getServiceDiscoveryResult(); - if (serviceDiscoveryResult != null && serviceDiscoveryResult.getIdentities().size() > 0) { - ServiceDiscoveryResult.Identity identity = serviceDiscoveryResult.getIdentities().get(0); - String type = identity.getType(); - String name = identity.getName(); - if (type != null) { - typeMap.put(resource, type); - } - if (name != null) { - nameMap.put(resource, nameWithoutVersion(name)); - } - } - } - } - return new Pair<>(typeMap, nameMap); - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/RawBlockable.java b/src/main/java/eu/siacs/conversations/entities/RawBlockable.java deleted file mode 100644 index b76b4067d..000000000 --- a/src/main/java/eu/siacs/conversations/entities/RawBlockable.java +++ /dev/null @@ -1,102 +0,0 @@ -package eu.siacs.conversations.entities; - -import android.content.Context; -import android.text.TextUtils; - -import java.util.Collections; -import java.util.List; -import java.util.Locale; - -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xmpp.Jid; - -public class RawBlockable implements ListItem, Blockable { - - private final Account account; - private final Jid jid; - - public RawBlockable(Account account, Jid jid) { - this.account = account; - this.jid = jid; - } - - @Override - public boolean isBlocked() { - return true; - } - - @Override - public boolean isDomainBlocked() { - throw new AssertionError("not implemented"); - } - - @Override - public Jid getBlockedJid() { - return this.jid; - } - - @Override - public String getDisplayName() { - if (jid.isFullJid()) { - return jid.getResource(); - } else { - return jid.toEscapedString(); - } - } - - @Override - public int getOffline() { - return 0; - } - - @Override - public Jid getJid() { - return this.jid; - } - - @Override - public List getTags(Context context) { - return Collections.emptyList(); - } - - @Override - public boolean getActive() { - return false; - } - - @Override - public boolean match(Context context, String needle) { - if (TextUtils.isEmpty(needle)) { - return true; - } - needle = needle.toLowerCase(Locale.US).trim(); - String[] parts = needle.split("\\s+"); - for (String part : parts) { - if (!jid.toEscapedString().contains(part)) { - return false; - } - } - return true; - } - - @Override - public Account getAccount() { - return account; - } - - @Override - public int getAvatarBackgroundColor() { - return UIHelper.getColorForName(jid.toEscapedString()); - } - - @Override - public String getAvatarName() { - return getDisplayName(); - } - - @Override - public int compareTo(ListItem o) { - return this.getDisplayName().compareToIgnoreCase( - o.getDisplayName()); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/entities/ReadByMarker.java b/src/main/java/eu/siacs/conversations/entities/ReadByMarker.java deleted file mode 100644 index 6740eb55c..000000000 --- a/src/main/java/eu/siacs/conversations/entities/ReadByMarker.java +++ /dev/null @@ -1,177 +0,0 @@ -package eu.siacs.conversations.entities; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.Collection; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; - -import eu.siacs.conversations.xmpp.Jid; - -public class ReadByMarker { - - private ReadByMarker() { - - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - ReadByMarker marker = (ReadByMarker) o; - - if (fullJid != null ? !fullJid.equals(marker.fullJid) : marker.fullJid != null) - return false; - return realJid != null ? realJid.equals(marker.realJid) : marker.realJid == null; - - } - - @Override - public int hashCode() { - int result = fullJid != null ? fullJid.hashCode() : 0; - result = 31 * result + (realJid != null ? realJid.hashCode() : 0); - return result; - } - - private Jid fullJid; - private Jid realJid; - - public Jid getFullJid() { - return fullJid; - } - - public Jid getRealJid() { - return realJid; - } - - public JSONObject toJson() { - JSONObject jsonObject = new JSONObject(); - if (fullJid != null) { - try { - jsonObject.put("fullJid", fullJid.toString()); - } catch (JSONException e) { - //ignore - } - } - if (realJid != null) { - try { - jsonObject.put("realJid", realJid.toString()); - } catch (JSONException e) { - //ignore - } - } - return jsonObject; - } - - public static Set fromJson(final JSONArray jsonArray) { - final Set readByMarkers = new CopyOnWriteArraySet<>(); - for (int i = 0; i < jsonArray.length(); ++i) { - try { - readByMarkers.add(fromJson(jsonArray.getJSONObject(i))); - } catch (JSONException e) { - //ignored - } - } - return readByMarkers; - } - - public static ReadByMarker from(Jid fullJid, Jid realJid) { - final ReadByMarker marker = new ReadByMarker(); - marker.fullJid = fullJid; - marker.realJid = realJid == null ? null : realJid.asBareJid(); - return marker; - } - - public static ReadByMarker from(Message message) { - final ReadByMarker marker = new ReadByMarker(); - marker.fullJid = message.getCounterpart(); - marker.realJid = message.getTrueCounterpart(); - return marker; - } - - public static ReadByMarker from(MucOptions.User user) { - final ReadByMarker marker = new ReadByMarker(); - marker.fullJid = user.getFullJid(); - marker.realJid = user.getRealJid(); - return marker; - } - - public static Set from(Collection users) { - final Set markers = new CopyOnWriteArraySet<>(); - for (MucOptions.User user : users) { - markers.add(from(user)); - } - return markers; - } - - public static ReadByMarker fromJson(JSONObject jsonObject) { - ReadByMarker marker = new ReadByMarker(); - try { - marker.fullJid = Jid.of(jsonObject.getString("fullJid")); - } catch (JSONException e) { - marker.fullJid = null; - } catch (IllegalArgumentException e) { - marker.fullJid = null; - } - try { - marker.realJid = Jid.of(jsonObject.getString("realJid")); - } catch (JSONException e) { - marker.realJid = null; - } catch (IllegalArgumentException e) { - marker.realJid = null; - } - return marker; - } - - public static Set fromJsonString(String json) { - try { - return fromJson(new JSONArray(json)); - } catch (JSONException e) { - return new CopyOnWriteArraySet<>(); - } catch (NullPointerException e) { - return new CopyOnWriteArraySet<>(); - } - } - - public static JSONArray toJson(final Set readByMarkers) { - JSONArray jsonArray = new JSONArray(); - for (final ReadByMarker marker : readByMarkers) { - jsonArray.put(marker.toJson()); - } - return jsonArray; - } - - public static boolean contains(ReadByMarker needle, final Set readByMarkers) { - for(final ReadByMarker marker : readByMarkers) { - if (marker.realJid != null && needle.realJid != null) { - if (marker.realJid.asBareJid().equals(needle.realJid.asBareJid())) { - return true; - } - } else if (marker.fullJid != null && needle.fullJid != null) { - if (marker.fullJid.equals(needle.fullJid)) { - return true; - } - } - } - return false; - } - - public static boolean allUsersRepresented(Collection users, Set markers) { - for(MucOptions.User user : users) { - if (!contains(from(user),markers)) { - return false; - } - } - return true; - } - - public static boolean allUsersRepresented(Collection users, Set markers, ReadByMarker marker) { - final Set markersCopy = new CopyOnWriteArraySet<>(markers); - markersCopy.add(marker); - return allUsersRepresented(users, markersCopy); - } - -} diff --git a/src/main/java/eu/siacs/conversations/entities/ReceiptRequest.java b/src/main/java/eu/siacs/conversations/entities/ReceiptRequest.java deleted file mode 100644 index b101f3914..000000000 --- a/src/main/java/eu/siacs/conversations/entities/ReceiptRequest.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.entities; - -import eu.siacs.conversations.xmpp.Jid; - -public class ReceiptRequest { - - private final Jid jid; - private final String id; - - public ReceiptRequest(Jid jid, String id) { - if (id == null) { - throw new IllegalArgumentException("id must not be null"); - } - if (jid == null) { - throw new IllegalArgumentException("jid must not be null"); - } - this.jid = jid.asBareJid(); - this.id = id; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - ReceiptRequest that = (ReceiptRequest) o; - - if (!jid.equals(that.jid)) return false; - return id.equals(that.id); - } - - @Override - public int hashCode() { - int result = jid.hashCode(); - result = 31 * result + id.hashCode(); - return result; - } - - public String getId() { - return id; - } - - public Jid getJid() { - return jid; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/entities/Room.java b/src/main/java/eu/siacs/conversations/entities/Room.java deleted file mode 100644 index 9e1d61fc3..000000000 --- a/src/main/java/eu/siacs/conversations/entities/Room.java +++ /dev/null @@ -1,93 +0,0 @@ -package eu.siacs.conversations.entities; - -import com.google.common.base.Objects; -import com.google.common.base.Strings; -import com.google.common.collect.ComparisonChain; - -import eu.siacs.conversations.services.AvatarService; -import eu.siacs.conversations.utils.LanguageUtils; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xmpp.Jid; - -public class Room implements AvatarService.Avatarable, Comparable { - - public String address; - public String name; - public String description; - public String language; - public int nusers; - - public Room(String address, String name, String description, String language, int nusers) { - this.address = address; - this.name = name; - this.description = description; - this.language = language; - this.nusers = nusers; - } - - public Room() { - - } - - public String getName() { - return name; - } - - public String getDescription() { - return description; - } - - public Jid getRoom() { - try { - return Jid.of(address); - } catch (IllegalArgumentException e) { - return null; - } - } - - public String getLanguage() { - return LanguageUtils.convert(language); - } - - @Override - public int getAvatarBackgroundColor() { - Jid room = getRoom(); - return UIHelper.getColorForName(room != null ? room.asBareJid().toEscapedString() : name); - } - - @Override - public String getAvatarName() { - return name; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Room room = (Room) o; - return Objects.equal(address, room.address) && - Objects.equal(name, room.name) && - Objects.equal(description, room.description); - } - - @Override - public int hashCode() { - return Objects.hashCode(address, name, description); - } - - - public boolean contains(String needle) { - return Strings.nullToEmpty(name).contains(needle) - || Strings.nullToEmpty(description).contains(needle) - || Strings.nullToEmpty(address).contains(needle); - } - - @Override - public int compareTo(Room o) { - return ComparisonChain.start() - .compare(o.nusers, nusers) - .compare(Strings.nullToEmpty(name), Strings.nullToEmpty(o.name)) - .compare(Strings.nullToEmpty(address), Strings.nullToEmpty(o.address)) - .result(); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/entities/Roster.java b/src/main/java/eu/siacs/conversations/entities/Roster.java deleted file mode 100644 index ef24f827f..000000000 --- a/src/main/java/eu/siacs/conversations/entities/Roster.java +++ /dev/null @@ -1,97 +0,0 @@ -package eu.siacs.conversations.entities; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; - -import eu.siacs.conversations.android.AbstractPhoneContact; -import eu.siacs.conversations.xmpp.Jid; - -public class Roster { - private final Account account; - private final HashMap contacts = new HashMap<>(); - private String version = null; - - public Roster(Account account) { - this.account = account; - } - - public Contact getContactFromContactList(Jid jid) { - if (jid == null) { - return null; - } - synchronized (this.contacts) { - Contact contact = contacts.get(jid.asBareJid()); - if (contact != null && contact.showInContactList()) { - return contact; - } else { - return null; - } - } - } - - public Contact getContact(final Jid jid) { - synchronized (this.contacts) { - if (!contacts.containsKey(jid.asBareJid())) { - Contact contact = new Contact(jid.asBareJid()); - contact.setAccount(account); - contacts.put(contact.getJid().asBareJid(), contact); - return contact; - } - return contacts.get(jid.asBareJid()); - } - } - - public void clearPresences() { - for (Contact contact : getContacts()) { - contact.clearPresences(); - } - } - - public void markAllAsNotInRoster() { - for (Contact contact : getContacts()) { - contact.resetOption(Contact.Options.IN_ROSTER); - } - } - - public List getWithSystemAccounts(Class clazz) { - int option = Contact.getOption(clazz); - List with = getContacts(); - for (Iterator iterator = with.iterator(); iterator.hasNext(); ) { - Contact contact = iterator.next(); - if (!contact.getOption(option)) { - iterator.remove(); - } - } - return with; - } - - public List getContacts() { - synchronized (this.contacts) { - return new ArrayList<>(this.contacts.values()); - } - } - - public void initContact(final Contact contact) { - if (contact == null) { - return; - } - contact.setAccount(account); - synchronized (this.contacts) { - contacts.put(contact.getJid().asBareJid(), contact); - } - } - - public void setVersion(String version) { - this.version = version; - } - - public String getVersion() { - return this.version; - } - - public Account getAccount() { - return this.account; - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/RtpSessionStatus.java b/src/main/java/eu/siacs/conversations/entities/RtpSessionStatus.java deleted file mode 100644 index 87a2e0601..000000000 --- a/src/main/java/eu/siacs/conversations/entities/RtpSessionStatus.java +++ /dev/null @@ -1,59 +0,0 @@ -package eu.siacs.conversations.entities; - -import androidx.annotation.DrawableRes; - -import com.google.common.base.Strings; - -import eu.siacs.conversations.R; - -public class RtpSessionStatus { - - public final boolean successful; - public final long duration; - - - public RtpSessionStatus(boolean successful, long duration) { - this.successful = successful; - this.duration = duration; - } - - @Override - public String toString() { - return successful + ":" + duration; - } - - public static RtpSessionStatus of(final String body) { - final String[] parts = Strings.nullToEmpty(body).split(":", 2); - long duration = 0; - if (parts.length == 2) { - try { - duration = Long.parseLong(parts[1]); - } catch (NumberFormatException e) { - //do nothing - } - } - boolean made; - try { - made = Boolean.parseBoolean(parts[0]); - } catch (Exception e) { - made = false; - } - return new RtpSessionStatus(made, duration); - } - - public static @DrawableRes int getDrawable(final boolean received, final boolean successful, final boolean darkTheme) { - if (received) { - if (successful) { - return darkTheme ? R.drawable.ic_call_received_white_18dp : R.drawable.ic_call_received_black_18dp; - } else { - return darkTheme ? R.drawable.ic_call_missed_white_18dp : R.drawable.ic_call_missed_black_18dp; - } - } else { - if (successful) { - return darkTheme ? R.drawable.ic_call_made_white_18dp : R.drawable.ic_call_made_black_18dp; - } else { - return darkTheme ? R.drawable.ic_call_missed_outgoing_white_18dp : R.drawable.ic_call_missed_outgoing_black_18dp; - } - } - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java deleted file mode 100644 index 4014713c7..000000000 --- a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java +++ /dev/null @@ -1,361 +0,0 @@ -package eu.siacs.conversations.entities; - -import android.content.ContentValues; -import android.database.Cursor; -import android.util.Base64; - -import androidx.annotation.NonNull; - -import com.google.common.base.Strings; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.forms.Data; -import eu.siacs.conversations.xmpp.forms.Field; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; - -public class ServiceDiscoveryResult { - public static final String TABLENAME = "discovery_results"; - public static final String HASH = "hash"; - public static final String VER = "ver"; - public static final String RESULT = "result"; - protected final String hash; - protected final byte[] ver; - protected final List features; - protected final List forms; - private final List identities; - - public ServiceDiscoveryResult(final IqPacket packet) { - this.identities = new ArrayList<>(); - this.features = new ArrayList<>(); - this.forms = new ArrayList<>(); - this.hash = "sha-1"; // We only support sha-1 for now - - final List elements = packet.query().getChildren(); - - for (final Element element : elements) { - if (element.getName().equals("identity")) { - Identity id = new Identity(element); - if (id.getType() != null && id.getCategory() != null) { - identities.add(id); - } - } else if (element.getName().equals("feature")) { - if (element.getAttribute("var") != null) { - features.add(element.getAttribute("var")); - } - } else if (element.getName().equals("x") && element.getAttribute("xmlns").equals(Namespace.DATA)) { - forms.add(Data.parse(element)); - } - } - this.ver = this.mkCapHash(); - } - - private ServiceDiscoveryResult(String hash, byte[] ver, JSONObject o) throws JSONException { - this.identities = new ArrayList<>(); - this.features = new ArrayList<>(); - this.forms = new ArrayList<>(); - this.hash = hash; - this.ver = ver; - - JSONArray identities = o.optJSONArray("identities"); - if (identities != null) { - for (int i = 0; i < identities.length(); i++) { - this.identities.add(new Identity(identities.getJSONObject(i))); - } - } - JSONArray features = o.optJSONArray("features"); - if (features != null) { - for (int i = 0; i < features.length(); i++) { - this.features.add(features.getString(i)); - } - } - JSONArray forms = o.optJSONArray("forms"); - if (forms != null) { - for (int i = 0; i < forms.length(); i++) { - this.forms.add(createFormFromJSONObject(forms.getJSONObject(i))); - } - } - } - - private ServiceDiscoveryResult() { - this.hash = "sha-1"; - this.features = Collections.emptyList(); - this.identities = Collections.emptyList(); - this.ver = null; - this.forms = Collections.emptyList(); - } - - public static ServiceDiscoveryResult empty() { - return new ServiceDiscoveryResult(); - } - - public ServiceDiscoveryResult(Cursor cursor) throws JSONException { - this( - cursor.getString(cursor.getColumnIndex(HASH)), - Base64.decode(cursor.getString(cursor.getColumnIndex(VER)), Base64.DEFAULT), - new JSONObject(cursor.getString(cursor.getColumnIndex(RESULT))) - ); - } - - private static String clean(String s) { - return s.replace("<", "<"); - } - - private static String blankNull(String s) { - return s == null ? "" : clean(s); - } - - private static Data createFormFromJSONObject(JSONObject o) { - Data data = new Data(); - JSONArray names = o.names(); - for (int i = 0; i < names.length(); ++i) { - try { - String name = names.getString(i); - JSONArray jsonValues = o.getJSONArray(name); - ArrayList values = new ArrayList<>(jsonValues.length()); - for (int j = 0; j < jsonValues.length(); ++j) { - values.add(jsonValues.getString(j)); - } - data.put(name, values); - } catch (Exception e) { - e.printStackTrace(); - } - } - return data; - } - - private static JSONObject createJSONFromForm(Data data) { - JSONObject object = new JSONObject(); - for (Field field : data.getFields()) { - try { - JSONArray jsonValues = new JSONArray(); - for (String value : field.getValues()) { - jsonValues.put(value); - } - object.put(field.getFieldName(), jsonValues); - } catch (Exception e) { - e.printStackTrace(); - } - } - try { - JSONArray jsonValues = new JSONArray(); - jsonValues.put(data.getFormType()); - object.put(Data.FORM_TYPE, jsonValues); - } catch (Exception e) { - e.printStackTrace(); - } - return object; - } - - public String getVer() { - return Base64.encodeToString(this.ver, Base64.NO_WRAP); - } - - public List getIdentities() { - return this.identities; - } - - public List getFeatures() { - return this.features; - } - - public boolean hasIdentity(String category, String type) { - for (Identity id : this.getIdentities()) { - if ((category == null || id.getCategory().equals(category)) && - (type == null || id.getType().equals(type))) { - return true; - } - } - - return false; - } - - public String getExtendedDiscoInformation(String formType, String name) { - for (Data form : this.forms) { - if (formType.equals(form.getFormType())) { - for (Field field : form.getFields()) { - if (name.equals(field.getFieldName())) { - return field.getValue(); - } - } - } - } - return null; - } - - private byte[] mkCapHash() { - StringBuilder s = new StringBuilder(); - - List identities = this.getIdentities(); - Collections.sort(identities); - - for (Identity id : identities) { - s.append(blankNull(id.getCategory())) - .append("/") - .append(blankNull(id.getType())) - .append("/") - .append(blankNull(id.getLang())) - .append("/") - .append(blankNull(id.getName())) - .append("<"); - } - - List features = this.getFeatures(); - Collections.sort(features); - - for (String feature : features) { - s.append(clean(feature)).append("<"); - } - - Collections.sort(forms, (lhs, rhs) -> lhs.getFormType().compareTo(rhs.getFormType())); - - for (Data form : forms) { - s.append(clean(form.getFormType())).append("<"); - List fields = form.getFields(); - Collections.sort(fields, (lhs, rhs) -> Strings.nullToEmpty(lhs.getFieldName()).compareTo(Strings.nullToEmpty(rhs.getFieldName()))); - for (Field field : fields) { - s.append(Strings.nullToEmpty(field.getFieldName())).append("<"); - List values = field.getValues(); - Collections.sort(values); - for (String value : values) { - s.append(blankNull(value)).append("<"); - } - } - } - - MessageDigest md; - try { - md = MessageDigest.getInstance("SHA-1"); - } catch (NoSuchAlgorithmException e) { - return null; - } - - try { - return md.digest(s.toString().getBytes("UTF-8")); - } catch (UnsupportedEncodingException e) { - return null; - } - } - - private JSONObject toJSON() { - try { - JSONObject o = new JSONObject(); - - JSONArray ids = new JSONArray(); - for (Identity id : this.getIdentities()) { - ids.put(id.toJSON()); - } - o.put("identities", ids); - - o.put("features", new JSONArray(this.getFeatures())); - - JSONArray forms = new JSONArray(); - for (Data data : this.forms) { - forms.put(createJSONFromForm(data)); - } - o.put("forms", forms); - - return o; - } catch (JSONException e) { - return null; - } - } - - public ContentValues getContentValues() { - final ContentValues values = new ContentValues(); - values.put(HASH, this.hash); - values.put(VER, getVer()); - JSONObject jsonObject = toJSON(); - values.put(RESULT, jsonObject == null ? "" : jsonObject.toString()); - return values; - } - - public static class Identity implements Comparable { - protected final String type; - protected final String lang; - protected final String name; - final String category; - - Identity(final String category, final String type, final String lang, final String name) { - this.category = category; - this.type = type; - this.lang = lang; - this.name = name; - } - - Identity(final Element el) { - this( - el.getAttribute("category"), - el.getAttribute("type"), - el.getAttribute("xml:lang"), - el.getAttribute("name") - ); - } - - Identity(final JSONObject o) { - - this( - o.optString("category", null), - o.optString("type", null), - o.optString("lang", null), - o.optString("name", null) - ); - } - - public String getCategory() { - return this.category; - } - - public String getType() { - return this.type; - } - - public String getLang() { - return this.lang; - } - - public String getName() { - return this.name; - } - - public int compareTo(@NonNull Object other) { - Identity o = (Identity) other; - int r = blankNull(this.getCategory()).compareTo(blankNull(o.getCategory())); - if (r == 0) { - r = blankNull(this.getType()).compareTo(blankNull(o.getType())); - } - if (r == 0) { - r = blankNull(this.getLang()).compareTo(blankNull(o.getLang())); - } - if (r == 0) { - r = blankNull(this.getName()).compareTo(blankNull(o.getName())); - } - - return r; - } - - JSONObject toJSON() { - try { - JSONObject o = new JSONObject(); - o.put("category", this.getCategory()); - o.put("type", this.getType()); - o.put("lang", this.getLang()); - o.put("name", this.getName()); - return o; - } catch (JSONException e) { - return null; - } - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/entities/StubConversation.java b/src/main/java/eu/siacs/conversations/entities/StubConversation.java deleted file mode 100644 index d3111e02e..000000000 --- a/src/main/java/eu/siacs/conversations/entities/StubConversation.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.entities; - -import eu.siacs.conversations.xmpp.Jid; - - -public class StubConversation implements Conversational { - - private final Account account; - private final String uuid; - private final Jid jid; - private final int mode; - - public StubConversation(Account account, String uuid, Jid jid, int mode) { - this.account = account; - this.uuid = uuid; - this.jid = jid; - this.mode = mode; - } - - @Override - public Account getAccount() { - return account; - } - - @Override - public Contact getContact() { - return account.getRoster().getContact(jid); - } - - @Override - public Jid getJid() { - return jid; - } - - @Override - public int getMode() { - return mode; - } - - @Override - public String getUuid() { - return uuid; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/entities/Transferable.java b/src/main/java/eu/siacs/conversations/entities/Transferable.java deleted file mode 100644 index 1d361f422..000000000 --- a/src/main/java/eu/siacs/conversations/entities/Transferable.java +++ /dev/null @@ -1,42 +0,0 @@ -package eu.siacs.conversations.entities; - -import java.util.Arrays; -import java.util.List; - -public interface Transferable { - List VALID_IMAGE_EXTENSIONS = Arrays.asList( - "webp", - "jpeg", - "jpg", - "png", - "jpe", - "gif", - "tif" - ); - List VALID_CRYPTO_EXTENSIONS = Arrays.asList( - "pgp", - "gpg", - "otr" - ); - - int STATUS_WAITING = 0x199; - int STATUS_UNKNOWN = 0x200; - int STATUS_CHECKING = 0x201; - int STATUS_FAILED = 0x202; - int STATUS_OFFER = 0x203; - int STATUS_DOWNLOADING = 0x204; - int STATUS_OFFER_CHECK_FILESIZE = 0x206; - int STATUS_UPLOADING = 0x207; - int STATUS_CANCELLED = 0x208; - - - boolean start(); - - int getStatus(); - - Long getFileSize(); - - int getProgress(); - - void cancel(); -} diff --git a/src/main/java/eu/siacs/conversations/entities/TransferablePlaceholder.java b/src/main/java/eu/siacs/conversations/entities/TransferablePlaceholder.java deleted file mode 100644 index b4f7b3d46..000000000 --- a/src/main/java/eu/siacs/conversations/entities/TransferablePlaceholder.java +++ /dev/null @@ -1,35 +0,0 @@ -package eu.siacs.conversations.entities; - -public class TransferablePlaceholder implements Transferable { - - private int status; - - public TransferablePlaceholder(int status) { - this.status = status; - } - - @Override - public boolean start() { - return false; - } - - @Override - public int getStatus() { - return status; - } - - @Override - public Long getFileSize() { - return null; - } - - @Override - public int getProgress() { - return 0; - } - - @Override - public void cancel() { - - } -} diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java deleted file mode 100644 index dd3b1d4d1..000000000 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ /dev/null @@ -1,159 +0,0 @@ -package eu.siacs.conversations.generator; - -import android.util.Base64; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.TimeZone; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.utils.PhoneHelper; -import eu.siacs.conversations.xmpp.XmppConnection; -import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; - -public abstract class AbstractGenerator { - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); - private final String[] FEATURES = { - Namespace.JINGLE, - - //Jingle File Transfer - FileTransferDescription.Version.FT_3.getNamespace(), - FileTransferDescription.Version.FT_4.getNamespace(), - FileTransferDescription.Version.FT_5.getNamespace(), - Namespace.JINGLE_TRANSPORTS_S5B, - Namespace.JINGLE_TRANSPORTS_IBB, - Namespace.JINGLE_ENCRYPTED_TRANSPORT, - Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO, - "http://jabber.org/protocol/muc", - "jabber:x:conference", - Namespace.OOB, - "http://jabber.org/protocol/caps", - "http://jabber.org/protocol/disco#info", - "urn:xmpp:avatar:metadata+notify", - Namespace.NICK + "+notify", - "urn:xmpp:ping", - "jabber:iq:version", - "http://jabber.org/protocol/chatstates" - }; - private final String[] MESSAGE_CONFIRMATION_FEATURES = { - "urn:xmpp:chat-markers:0", - "urn:xmpp:receipts" - }; - private final String[] MESSAGE_CORRECTION_FEATURES = { - "urn:xmpp:message-correct:0" - }; - private final String[] MESSAGE_RETRACTION_FEATURES = { - "urn:xmpp:message-retract:0" - }; - private final String[] PRIVACY_SENSITIVE = { - "urn:xmpp:time" //XEP-0202: Entity Time leaks time zone - }; - private final String[] OTR = { - "urn:xmpp:otr:0" - }; - private final String[] VOIP_NAMESPACES = { - Namespace.JINGLE_TRANSPORT_ICE_UDP, - Namespace.JINGLE_FEATURE_AUDIO, - Namespace.JINGLE_FEATURE_VIDEO, - Namespace.JINGLE_APPS_RTP, - Namespace.JINGLE_APPS_DTLS, - Namespace.JINGLE_MESSAGE - }; - - protected XmppConnectionService mXmppConnectionService; - private String mVersion = null; - - AbstractGenerator(XmppConnectionService service) { - this.mXmppConnectionService = service; - } - - public static String getTimestamp(long time) { - DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); - return DATE_FORMAT.format(time); - } - - String getIdentityVersion() { - if (mVersion == null) { - this.mVersion = PhoneHelper.getVersionName(mXmppConnectionService); - } - return this.mVersion; - } - - public String getIdentityName() { - return mXmppConnectionService.getString(R.string.app_name) + ' ' + getIdentityVersion(); - } - - public String getUserAgent() { - return System.getProperty("http.agent"); - } - - String getIdentityType() { - if ("chromium".equals(android.os.Build.BRAND)) { - return "pc"; - } else { - return mXmppConnectionService.getString(R.string.default_resource).toLowerCase(); - } - } - - String getCapHash(final Account account) { - StringBuilder s = new StringBuilder(); - s.append("client/").append(getIdentityType()).append("//").append(getIdentityName()).append('<'); - MessageDigest md; - try { - md = MessageDigest.getInstance("SHA-1"); - } catch (NoSuchAlgorithmException e) { - return null; - } - - for (String feature : getFeatures(account)) { - s.append(feature).append('<'); - } - final byte[] sha1 = md.digest(s.toString().getBytes()); - return Base64.encodeToString(sha1, Base64.NO_WRAP); - } - - public List getFeatures(Account account) { - final XmppConnection connection = account.getXmppConnection(); - final ArrayList features = new ArrayList<>(Arrays.asList(FEATURES)); - if (mXmppConnectionService.confirmMessages()) { - features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES)); - } - if (mXmppConnectionService.allowMessageCorrection()) { - features.addAll(Arrays.asList(MESSAGE_CORRECTION_FEATURES)); - } - if (mXmppConnectionService.allowMessageRetraction()) { - features.addAll(Arrays.asList(MESSAGE_RETRACTION_FEATURES)); - } - if (Config.supportOmemo()) { - features.add(AxolotlService.PEP_DEVICE_LIST_NOTIFY); - } - if (!mXmppConnectionService.useTorToConnect() && !account.isOnion() && !mXmppConnectionService.useI2PToConnect() && !account.isI2P()) { - features.addAll(Arrays.asList(PRIVACY_SENSITIVE)); - features.addAll(Arrays.asList(VOIP_NAMESPACES)); - } - if (Config.supportOtr()) { - features.addAll(Arrays.asList(OTR)); - } - if (mXmppConnectionService.broadcastLastActivity()) { - features.add(Namespace.IDLE); - } - if (connection != null && connection.getFeatures().bookmarks2()) { - features.add(Namespace.BOOKMARKS2 + "+notify"); - } else { - features.add(Namespace.BOOKMARKS + "+notify"); - } - Collections.sort(features); - return features; - } -} diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java deleted file mode 100644 index c5da1f1c2..000000000 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ /dev/null @@ -1,584 +0,0 @@ -package eu.siacs.conversations.generator; - -import android.os.Bundle; -import android.util.Base64; -import android.util.Log; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Bookmark; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.services.MessageArchiveService; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.forms.Data; -import eu.siacs.conversations.xmpp.pep.Avatar; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import java.nio.ByteBuffer; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.TimeZone; -import java.util.UUID; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; - -public class IqGenerator extends AbstractGenerator { - - public IqGenerator(final XmppConnectionService service) { - super(service); - } - - public IqPacket discoResponse(final Account account, final IqPacket request) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.RESULT); - packet.setId(request.getId()); - packet.setTo(request.getFrom()); - final Element query = packet.addChild("query", "http://jabber.org/protocol/disco#info"); - query.setAttribute("node", request.query().getAttribute("node")); - final Element identity = query.addChild("identity"); - identity.setAttribute("category", "client"); - identity.setAttribute("type", getIdentityType()); - identity.setAttribute("name", getIdentityName()); - for (final String feature : getFeatures(account)) { - query.addChild("feature").setAttribute("var", feature); - } - return packet; - } - - public IqPacket versionResponse(final IqPacket request) { - final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT); - Element query = packet.query("jabber:iq:version"); - query.addChild("name").setContent(mXmppConnectionService.getString(R.string.app_name)); - query.addChild("version").setContent(getIdentityVersion()); - if ("chromium".equals(android.os.Build.BRAND)) { - query.addChild("os").setContent("Chrome OS"); - } else { - query.addChild("os").setContent("Android"); - } - return packet; - } - - public IqPacket entityTimeResponse(IqPacket request) { - final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT); - Element time = packet.addChild("time", "urn:xmpp:time"); - final long now = System.currentTimeMillis(); - time.addChild("utc").setContent(getTimestamp(now)); - TimeZone ourTimezone = TimeZone.getDefault(); - long offsetSeconds = ourTimezone.getOffset(now) / 1000; - long offsetMinutes = Math.abs((offsetSeconds % 3600) / 60); - long offsetHours = offsetSeconds / 3600; - String hours; - if (offsetHours < 0) { - hours = String.format(Locale.US, "%03d", offsetHours); - } else { - hours = String.format(Locale.US, "%02d", offsetHours); - } - String minutes = String.format(Locale.US, "%02d", offsetMinutes); - time.addChild("tzo").setContent(hours + ":" + minutes); - return packet; - } - - public IqPacket purgeOfflineMessages() { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - packet.addChild("offline", Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge"); - return packet; - } - - protected IqPacket publish(final String node, final Element item, final Bundle options) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - final Element pubsub = packet.addChild("pubsub", Namespace.PUB_SUB); - final Element publish = pubsub.addChild("publish"); - publish.setAttribute("node", node); - publish.addChild(item); - if (options != null) { - final Element publishOptions = pubsub.addChild("publish-options"); - publishOptions.addChild(Data.create(Namespace.PUB_SUB_PUBLISH_OPTIONS, options)); - } - return packet; - } - - protected IqPacket publish(final String node, final Element item) { - return publish(node, item, null); - } - - private IqPacket retrieve(String node, Element item) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); - final Element pubsub = packet.addChild("pubsub", Namespace.PUB_SUB); - final Element items = pubsub.addChild("items"); - items.setAttribute("node", node); - if (item != null) { - items.addChild(item); - } - return packet; - } - - public IqPacket retrieveBookmarks() { - return retrieve(Namespace.BOOKMARKS2, null); - } - - public IqPacket publishNick(String nick) { - final Element item = new Element("item"); - item.setAttribute("id", "current"); - item.addChild("nick", Namespace.NICK).setContent(nick); - return publish(Namespace.NICK, item); - } - - public IqPacket deleteNode(final String node) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - final Element pubsub = packet.addChild("pubsub", Namespace.PUB_SUB_OWNER); - pubsub.addChild("delete").setAttribute("node", node); - return packet; - } - - public IqPacket deleteItem(final String node, final String id) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - final Element pubsub = packet.addChild("pubsub", Namespace.PUB_SUB); - final Element retract = pubsub.addChild("retract"); - retract.setAttribute("node", node); - retract.setAttribute("notify", "true"); - retract.addChild("item").setAttribute("id", id); - return packet; - } - - public IqPacket publishAvatar(Avatar avatar, Bundle options) { - final Element item = new Element("item"); - item.setAttribute("id", avatar.sha1sum); - final Element data = item.addChild("data", Namespace.AVATAR_DATA); - data.setContent(avatar.image); - return publish(Namespace.AVATAR_DATA, item, options); - } - - public IqPacket publishElement( - final String namespace, final Element element, String id, final Bundle options) { - final Element item = new Element("item"); - item.setAttribute("id", id); - item.addChild(element); - return publish(namespace, item, options); - } - - public IqPacket publishAvatarMetadata(final Avatar avatar, final Bundle options) { - final Element item = new Element("item"); - item.setAttribute("id", avatar.sha1sum); - final Element metadata = item.addChild("metadata", Namespace.AVATAR_METADATA); - final Element info = metadata.addChild("info"); - info.setAttribute("bytes", avatar.size); - info.setAttribute("id", avatar.sha1sum); - info.setAttribute("height", avatar.height); - info.setAttribute("width", avatar.height); - info.setAttribute("type", avatar.type); - return publish(Namespace.AVATAR_METADATA, item, options); - } - - public IqPacket retrievePepAvatar(final Avatar avatar) { - final Element item = new Element("item"); - item.setAttribute("id", avatar.sha1sum); - final IqPacket packet = retrieve(Namespace.AVATAR_DATA, item); - packet.setTo(avatar.owner); - return packet; - } - - public IqPacket retrieveVcardAvatar(final Avatar avatar) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); - packet.setTo(avatar.owner); - packet.addChild("vCard", "vcard-temp"); - return packet; - } - - public IqPacket retrieveVcardAvatar(final Jid to) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); - packet.setTo(to); - packet.addChild("vCard", "vcard-temp"); - return packet; - } - - public IqPacket retrieveAvatarMetaData(final Jid to) { - final IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null); - if (to != null) { - packet.setTo(to); - } - return packet; - } - - public IqPacket retrieveDeviceIds(final Jid to) { - final IqPacket packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null); - if (to != null) { - packet.setTo(to); - } - return packet; - } - - public IqPacket retrieveBundlesForDevice(final Jid to, final int deviceid) { - final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLES + ":" + deviceid, null); - packet.setTo(to); - return packet; - } - - public IqPacket retrieveVerificationForDevice(final Jid to, final int deviceid) { - final IqPacket packet = retrieve(AxolotlService.PEP_VERIFICATION + ":" + deviceid, null); - packet.setTo(to); - return packet; - } - - public IqPacket publishDeviceIds(final Set ids, final Bundle publishOptions) { - final Element item = new Element("item"); - item.setAttribute("id", "current"); - final Element list = item.addChild("list", AxolotlService.PEP_PREFIX); - for (Integer id : ids) { - final Element device = new Element("device"); - device.setAttribute("id", id); - list.addChild(device); - } - return publish(AxolotlService.PEP_DEVICE_LIST, item, publishOptions); - } - - public Element publishBookmarkItem(final Bookmark bookmark) { - final String name = bookmark.getBookmarkName(); - final String nick = bookmark.getNick(); - final boolean autojoin = bookmark.autojoin(); - final Element conference = new Element("conference", Namespace.BOOKMARKS2); - if (name != null) { - conference.setAttribute("name", name); - } - if (nick != null) { - conference.addChild("nick").setContent(nick); - } - conference.setAttribute("autojoin", String.valueOf(autojoin)); - return conference; - } - - public IqPacket publishBundles( - final SignedPreKeyRecord signedPreKeyRecord, - final IdentityKey identityKey, - final Set preKeyRecords, - final int deviceId, - Bundle publishOptions) { - final Element item = new Element("item"); - item.setAttribute("id", "current"); - final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX); - final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic"); - signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId()); - ECPublicKey publicKey = signedPreKeyRecord.getKeyPair().getPublicKey(); - signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(), Base64.NO_WRAP)); - final Element signedPreKeySignature = bundle.addChild("signedPreKeySignature"); - signedPreKeySignature.setContent( - Base64.encodeToString(signedPreKeyRecord.getSignature(), Base64.NO_WRAP)); - final Element identityKeyElement = bundle.addChild("identityKey"); - identityKeyElement.setContent( - Base64.encodeToString(identityKey.serialize(), Base64.NO_WRAP)); - - final Element prekeys = bundle.addChild("prekeys", AxolotlService.PEP_PREFIX); - for (PreKeyRecord preKeyRecord : preKeyRecords) { - final Element prekey = prekeys.addChild("preKeyPublic"); - prekey.setAttribute("preKeyId", preKeyRecord.getId()); - prekey.setContent( - Base64.encodeToString( - preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.NO_WRAP)); - } - - return publish(AxolotlService.PEP_BUNDLES + ":" + deviceId, item, publishOptions); - } - - public IqPacket publishVerification( - byte[] signature, X509Certificate[] certificates, final int deviceId) { - final Element item = new Element("item"); - item.setAttribute("id", "current"); - final Element verification = item.addChild("verification", AxolotlService.PEP_PREFIX); - final Element chain = verification.addChild("chain"); - for (int i = 0; i < certificates.length; ++i) { - try { - Element certificate = chain.addChild("certificate"); - certificate.setContent( - Base64.encodeToString(certificates[i].getEncoded(), Base64.NO_WRAP)); - certificate.setAttribute("index", i); - } catch (CertificateEncodingException e) { - Log.d(Config.LOGTAG, "could not encode certificate"); - } - } - verification - .addChild("signature") - .setContent(Base64.encodeToString(signature, Base64.NO_WRAP)); - return publish(AxolotlService.PEP_VERIFICATION + ":" + deviceId, item); - } - - public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - final Element query = packet.query(mam.version.namespace); - query.setAttribute("queryid", mam.getQueryId()); - final Data data = new Data(); - data.setFormType(mam.version.namespace); - if (mam.muc()) { - packet.setTo(mam.getWith()); - } else if (mam.getWith() != null) { - data.put("with", mam.getWith().toEscapedString()); - } - final long start = mam.getStart(); - final long end = mam.getEnd(); - if (start != 0) { - data.put("start", getTimestamp(start)); - } - if (end != 0) { - data.put("end", getTimestamp(end)); - } - data.submit(); - query.addChild(data); - Element set = query.addChild("set", "http://jabber.org/protocol/rsm"); - if (mam.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) { - set.addChild("before").setContent(mam.getReference()); - } else if (mam.getReference() != null) { - set.addChild("after").setContent(mam.getReference()); - } - set.addChild("max").setContent(String.valueOf(Config.PAGE_SIZE)); - return packet; - } - - public IqPacket generateGetBlockList() { - final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); - iq.addChild("blocklist", Namespace.BLOCKING); - - return iq; - } - - public IqPacket generateSetBlockRequest(final Jid jid, boolean reportSpam) { - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - final Element block = iq.addChild("block", Namespace.BLOCKING); - final Element item = block.addChild("item").setAttribute("jid", jid); - if (reportSpam) { - item.addChild("report", "urn:xmpp:reporting:0").addChild("spam"); - } - Log.d(Config.LOGTAG, iq.toString()); - return iq; - } - - public IqPacket generateSetUnblockRequest(final Jid jid) { - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - final Element block = iq.addChild("unblock", Namespace.BLOCKING); - block.addChild("item").setAttribute("jid", jid); - return iq; - } - - public IqPacket generateSetPassword(final Account account, final String newPassword) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - packet.setTo(account.getDomain()); - final Element query = packet.addChild("query", Namespace.REGISTER); - final Jid jid = account.getJid(); - query.addChild("username").setContent(jid.getLocal()); - query.addChild("password").setContent(newPassword); - return packet; - } - - public IqPacket changeAffiliation(Conversation conference, Jid jid, String affiliation) { - List jids = new ArrayList<>(); - jids.add(jid); - return changeAffiliation(conference, jids, affiliation); - } - - public IqPacket changeAffiliation(Conversation conference, List jids, String affiliation) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - packet.setTo(conference.getJid().asBareJid()); - packet.setFrom(conference.getAccount().getJid()); - Element query = packet.query("http://jabber.org/protocol/muc#admin"); - for (Jid jid : jids) { - Element item = query.addChild("item"); - item.setAttribute("jid", jid); - item.setAttribute("affiliation", affiliation); - } - return packet; - } - - public IqPacket changeRole(Conversation conference, String nick, String role) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - packet.setTo(conference.getJid().asBareJid()); - packet.setFrom(conference.getAccount().getJid()); - Element item = packet.query("http://jabber.org/protocol/muc#admin").addChild("item"); - item.setAttribute("nick", nick); - item.setAttribute("role", role); - return packet; - } - - public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); - packet.setTo(host); - Element request = packet.addChild("request", Namespace.HTTP_UPLOAD); - request.setAttribute("filename", convertFilename(file.getName())); - request.setAttribute("size", file.getExpectedSize()); - request.setAttribute("content-type", mime); - return packet; - } - - public IqPacket requestHttpUploadLegacySlot(Jid host, DownloadableFile file, String mime) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); - packet.setTo(host); - Element request = packet.addChild("request", Namespace.HTTP_UPLOAD_LEGACY); - request.addChild("filename").setContent(convertFilename(file.getName())); - request.addChild("size").setContent(String.valueOf(file.getExpectedSize())); - request.addChild("content-type").setContent(mime); - return packet; - } - - private static String convertFilename(String name) { - int pos = name.indexOf('.'); - if (pos != -1) { - try { - UUID uuid = UUID.fromString(name.substring(0, pos)); - ByteBuffer bb = ByteBuffer.wrap(new byte[16]); - bb.putLong(uuid.getMostSignificantBits()); - bb.putLong(uuid.getLeastSignificantBits()); - return Base64.encodeToString( - bb.array(), Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP) - + name.substring(pos); - } catch (Exception e) { - return name; - } - } else { - return name; - } - } - - public IqPacket generateCreateAccountWithCaptcha(Account account, String id, Data data) { - final IqPacket register = new IqPacket(IqPacket.TYPE.SET); - register.setFrom(account.getJid().asBareJid()); - register.setTo(account.getDomain()); - register.setId(id); - Element query = register.query(Namespace.REGISTER); - if (data != null) { - query.addChild(data); - } - return register; - } - - public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId) { - return pushTokenToAppServer(appServer, token, deviceId, null); - } - - public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId, Jid muc) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - packet.setTo(appServer); - final Element command = packet.addChild("command", Namespace.COMMANDS); - command.setAttribute("node", "register-push-fcm"); - command.setAttribute("action", "execute"); - final Data data = new Data(); - data.put("token", token); - data.put("android-id", deviceId); - if (muc != null) { - data.put("muc", muc.toEscapedString()); - } - data.submit(); - command.addChild(data); - return packet; - } - - public IqPacket unregisterChannelOnAppServer(Jid appServer, String deviceId, String channel) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - packet.setTo(appServer); - final Element command = packet.addChild("command", Namespace.COMMANDS); - command.setAttribute("node", "unregister-push-fcm"); - command.setAttribute("action", "execute"); - final Data data = new Data(); - data.put("channel", channel); - data.put("android-id", deviceId); - data.submit(); - command.addChild(data); - return packet; - } - - public IqPacket enablePush(final Jid jid, final String node, final String secret) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - Element enable = packet.addChild("enable", Namespace.PUSH); - enable.setAttribute("jid", jid); - enable.setAttribute("node", node); - if (secret != null) { - Data data = new Data(); - data.setFormType(Namespace.PUB_SUB_PUBLISH_OPTIONS); - data.put("secret", secret); - data.submit(); - enable.addChild(data); - } - return packet; - } - - public IqPacket disablePush(final Jid jid, final String node) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); - Element disable = packet.addChild("disable", Namespace.PUSH); - disable.setAttribute("jid", jid); - disable.setAttribute("node", node); - return packet; - } - - public IqPacket queryAffiliation(Conversation conversation, String affiliation) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); - packet.setTo(conversation.getJid().asBareJid()); - packet.query("http://jabber.org/protocol/muc#admin") - .addChild("item") - .setAttribute("affiliation", affiliation); - return packet; - } - - public static Bundle defaultGroupChatConfiguration() { - Bundle options = new Bundle(); - options.putString("muc#roomconfig_persistentroom", "1"); - options.putString("muc#roomconfig_membersonly", "1"); - options.putString("muc#roomconfig_publicroom", "0"); - options.putString("muc#roomconfig_whois", "anyone"); - options.putString("muc#roomconfig_changesubject", "0"); - options.putString("muc#roomconfig_allowinvites", "0"); - options.putString("muc#roomconfig_enablearchiving", "1"); // prosody - options.putString("mam", "1"); // ejabberd community - options.putString("muc#roomconfig_mam", "1"); // ejabberd saas - return options; - } - - public static Bundle defaultChannelConfiguration() { - Bundle options = new Bundle(); - options.putString("muc#roomconfig_persistentroom", "1"); - options.putString("muc#roomconfig_membersonly", "0"); - options.putString("muc#roomconfig_publicroom", "1"); - options.putString("muc#roomconfig_whois", "moderators"); - options.putString("muc#roomconfig_changesubject", "0"); - options.putString("muc#roomconfig_enablearchiving", "1"); // prosody - options.putString("mam", "1"); // ejabberd community - options.putString("muc#roomconfig_mam", "1"); // ejabberd saas - return options; - } - - public IqPacket requestPubsubConfiguration(Jid jid, String node) { - return pubsubConfiguration(jid, node, null); - } - - public IqPacket publishPubsubConfiguration(Jid jid, String node, Data data) { - return pubsubConfiguration(jid, node, data); - } - - private IqPacket pubsubConfiguration(Jid jid, String node, Data data) { - IqPacket packet = new IqPacket(data == null ? IqPacket.TYPE.GET : IqPacket.TYPE.SET); - packet.setTo(jid); - Element pubsub = packet.addChild("pubsub", "http://jabber.org/protocol/pubsub#owner"); - Element configure = pubsub.addChild("configure").setAttribute("node", node); - if (data != null) { - configure.addChild(data); - } - return packet; - } - - public IqPacket queryDiscoItems(Jid jid) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); - packet.setTo(jid); - packet.addChild("query", Namespace.DISCO_ITEMS); - return packet; - } - - public IqPacket queryDiscoInfo(Jid jid) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); - packet.setTo(jid); - packet.addChild("query", Namespace.DISCO_INFO); - return packet; - } -} diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java deleted file mode 100644 index 487e28864..000000000 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ /dev/null @@ -1,327 +0,0 @@ -package eu.siacs.conversations.generator; - -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; - -import net.java.otr4j.OtrException; -import net.java.otr4j.session.Session; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Conversational; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.chatstate.ChatState; -import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; -import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; -import eu.siacs.conversations.xmpp.jingle.Media; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; - -public class MessageGenerator extends AbstractGenerator { - public static final String OTR_FALLBACK_MESSAGE = "I would like to start a private (OTR encrypted) conversation but your client doesn’t seem to support that"; - private static final String OMEMO_FALLBACK_MESSAGE = "I sent you an OMEMO encrypted message but your client doesn’t seem to support that. Find more information on https://conversations.im/omemo"; - private static final String PGP_FALLBACK_MESSAGE = "I sent you a PGP encrypted message but your client doesn’t seem to support that."; - - public MessageGenerator(XmppConnectionService service) { - super(service); - } - - private MessagePacket preparePacket(Message message) { - Conversation conversation = (Conversation) message.getConversation(); - Account account = conversation.getAccount(); - MessagePacket packet = new MessagePacket(); - final boolean isWithSelf = conversation.getContact().isSelf(); - if (conversation.getMode() == Conversation.MODE_SINGLE) { - packet.setTo(message.getCounterpart()); - packet.setType(MessagePacket.TYPE_CHAT); - if (this.mXmppConnectionService.indicateReceived() && !isWithSelf) { - packet.addChild("request", "urn:xmpp:receipts"); - } - } else if (message.isPrivateMessage()) { - packet.setTo(message.getCounterpart()); - packet.setType(MessagePacket.TYPE_CHAT); - packet.addChild("x", "http://jabber.org/protocol/muc#user"); - if (this.mXmppConnectionService.indicateReceived()) { - packet.addChild("request", "urn:xmpp:receipts"); - } - } else { - packet.setTo(message.getCounterpart().asBareJid()); - packet.setType(MessagePacket.TYPE_GROUPCHAT); - } - if (conversation.isSingleOrPrivateAndNonAnonymous() && !message.isPrivateMessage()) { - packet.addChild("markable", "urn:xmpp:chat-markers:0"); - } - packet.setFrom(account.getJid()); - packet.setId(message.getUuid()); - if (conversation.getMode() == Conversational.MODE_SINGLE || message.isPrivateMessage() || !conversation.getMucOptions().stableId()) { - packet.addChild("origin-id", Namespace.STANZA_IDS).setAttribute("id", message.getUuid()); - } - if (message.edited() && !message.isMessageDeleted()) { - packet.addChild("replace", "urn:xmpp:message-correct:0").setAttribute("id", message.getEditedIdWireFormat()); - } else if (message.isMessageDeleted()) { - Element apply = packet.addChild("apply-to", "urn:xmpp:fasten:0").setAttribute("id", (message.getRetractId() != null ? message.getRetractId() : (message.getRemoteMsgId() != null ? message.getRemoteMsgId() : (message.getEditedIdWireFormat() != null ? message.getEditedIdWireFormat() : message.getUuid())))); - apply.addChild("retract", "urn:xmpp:message-retract:0"); - packet.addChild("fallback", "urn:xmpp:fallback:0"); - packet.addChild("store", "urn:xmpp:hints"); - packet.setBody("This person attempted to retract a previous message, but it's unsupported by your client."); - } - return packet; - } - - public void addDelay(MessagePacket packet, long timestamp) { - final SimpleDateFormat mDateFormat = new SimpleDateFormat( - "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); - mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - Element delay = packet.addChild("delay", "urn:xmpp:delay"); - Date date = new Date(timestamp); - delay.setAttribute("stamp", mDateFormat.format(date)); - } - - public MessagePacket generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) { - MessagePacket packet = preparePacket(message); - if (axolotlMessage == null) { - return null; - } - packet.setAxolotlMessage(axolotlMessage.toElement()); - packet.setBody(OMEMO_FALLBACK_MESSAGE); - packet.addChild("store", "urn:xmpp:hints"); - packet.addChild("encryption", "urn:xmpp:eme:0") - .setAttribute("name", "OMEMO") - .setAttribute("namespace", AxolotlService.PEP_PREFIX); - return packet; - } - - public MessagePacket generateKeyTransportMessage(Jid to, XmppAxolotlMessage axolotlMessage) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); - packet.setTo(to); - packet.setAxolotlMessage(axolotlMessage.toElement()); - packet.addChild("store", "urn:xmpp:hints"); - return packet; - } - - - public static void addMessageHints(MessagePacket packet) { - packet.addChild("private", "urn:xmpp:carbons:2"); - packet.addChild("no-copy", "urn:xmpp:hints"); - packet.addChild("no-permanent-store", "urn:xmpp:hints"); - packet.addChild("no-permanent-storage", "urn:xmpp:hints"); //do not copy this. this is wrong. it is *store* - } - - - public MessagePacket generateOtrChat(Message message) { - Conversation conversation = (Conversation) message.getConversation(); - Session otrSession = conversation.getOtrSession(); - if (otrSession == null) { - return null; - } - MessagePacket packet = preparePacket(message); - addMessageHints(packet); - try { - String content; - if (message.hasFileOnRemoteHost()) { - content = message.getFileParams().url.toString(); - } else { - content = message.getBody(); - } - packet.setBody(otrSession.transformSending(content)[0]); - packet.addChild("encryption", "urn:xmpp:eme:0").setAttribute("namespace", "urn:xmpp:otr:0"); - return packet; - } catch (OtrException e) { - return null; - } - } - - - public MessagePacket generateChat(Message message) { - MessagePacket packet = preparePacket(message); - String content; - if (message.hasFileOnRemoteHost()) { - final Message.FileParams fileParams = message.getFileParams(); - content = fileParams.url; - packet.addChild("x", Namespace.OOB).addChild("url").setContent(content); - } else { - content = message.getBody(); - } - if (!message.isMessageDeleted()) - packet.setBody(content); - return packet; - } - - public MessagePacket generatePgpChat(Message message) { - MessagePacket packet = preparePacket(message); - if (message.hasFileOnRemoteHost()) { - Message.FileParams fileParams = message.getFileParams(); - final String url = fileParams.url; - packet.setBody(url); - packet.addChild("x", Namespace.OOB).addChild("url").setContent(url); - } else { - if (Config.supportUnencrypted()) { - packet.setBody(PGP_FALLBACK_MESSAGE); - } - if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - packet.addChild("x", "jabber:x:encrypted").setContent(message.getEncryptedBody()); - } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { - packet.addChild("x", "jabber:x:encrypted").setContent(message.getBody()); - } - packet.addChild("encryption", "urn:xmpp:eme:0") - .setAttribute("namespace", "jabber:x:encrypted"); - } - return packet; - } - - public MessagePacket generateChatState(Conversation conversation) { - final Account account = conversation.getAccount(); - MessagePacket packet = new MessagePacket(); - packet.setType(conversation.getMode() == Conversation.MODE_MULTI ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT); - packet.setTo(conversation.getJid().asBareJid()); - packet.setFrom(account.getJid()); - packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); - packet.addChild("no-store", "urn:xmpp:hints"); - packet.addChild("no-storage", "urn:xmpp:hints"); //wrong! don't copy this. Its *store* - return packet; - } - - public MessagePacket confirm(final Message message) { - final boolean groupChat = message.getConversation().getMode() == Conversational.MODE_MULTI; - final Jid to = message.getCounterpart(); - final MessagePacket packet = new MessagePacket(); - packet.setType(groupChat ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT); - packet.setTo(groupChat ? to.asBareJid() : to); - final Element displayed = packet.addChild("displayed", "urn:xmpp:chat-markers:0"); - if (groupChat) { - final String stanzaId = message.getServerMsgId(); - if (stanzaId != null) { - displayed.setAttribute("id", stanzaId); - } else { - displayed.setAttribute("sender", to.toString()); - displayed.setAttribute("id", message.getRemoteMsgId()); - } - } else { - displayed.setAttribute("id", message.getRemoteMsgId()); - } - packet.addChild("store", "urn:xmpp:hints"); - return packet; - } - - public MessagePacket conferenceSubject(Conversation conversation, String subject) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_GROUPCHAT); - packet.setTo(conversation.getJid().asBareJid()); - packet.addChild("subject").setContent(subject); - packet.setFrom(conversation.getAccount().getJid().asBareJid()); - return packet; - } - - public MessagePacket directInvite(final Conversation conversation, final Jid contact) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_NORMAL); - packet.setTo(contact); - packet.setFrom(conversation.getAccount().getJid()); - Element x = packet.addChild("x", "jabber:x:conference"); - x.setAttribute("jid", conversation.getJid().asBareJid()); - String password = conversation.getMucOptions().getPassword(); - if (password != null) { - x.setAttribute("password", password); - } - if (contact.isFullJid()) { - packet.addChild("no-store", "urn:xmpp:hints"); - packet.addChild("no-copy", "urn:xmpp:hints"); - } - return packet; - } - - public MessagePacket invite(Conversation conversation, Jid contact) { - MessagePacket packet = new MessagePacket(); - packet.setTo(conversation.getJid().asBareJid()); - packet.setFrom(conversation.getAccount().getJid()); - Element x = new Element("x"); - x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user"); - Element invite = new Element("invite"); - invite.setAttribute("to", contact.asBareJid()); - x.addChild(invite); - packet.addChild(x); - return packet; - } - - public MessagePacket received(Account account, final Jid from, final String id, ArrayList namespaces, int type) { - final MessagePacket receivedPacket = new MessagePacket(); - receivedPacket.setType(type); - receivedPacket.setTo(from); - receivedPacket.setFrom(account.getJid()); - for (final String namespace : namespaces) { - receivedPacket.addChild("received", namespace).setAttribute("id", id); - } - receivedPacket.addChild("store", "urn:xmpp:hints"); - return receivedPacket; - } - - public MessagePacket received(Account account, Jid to, String id) { - MessagePacket packet = new MessagePacket(); - packet.setFrom(account.getJid()); - packet.setTo(to); - packet.addChild("received", "urn:xmpp:receipts").setAttribute("id", id); - packet.addChild("store", "urn:xmpp:hints"); - return packet; - } - - public MessagePacket generateOtrError(Jid to, String id, String errorText) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_ERROR); - packet.setAttribute("id", id); - packet.setTo(to); - Element error = packet.addChild("error"); - error.setAttribute("code", "406"); - error.setAttribute("type", "modify"); - error.addChild("not-acceptable", "urn:ietf:params:xml:ns:xmpp-stanzas"); - error.addChild("text").setContent("?OTR Error:" + errorText); - return packet; - } - - public MessagePacket sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) { - final MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those - packet.setTo(proposal.with); - packet.setId(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX + proposal.sessionId); - final Element propose = packet.addChild("propose", Namespace.JINGLE_MESSAGE); - propose.setAttribute("id", proposal.sessionId); - for (final Media media : proposal.media) { - propose.addChild("description", Namespace.JINGLE_APPS_RTP).setAttribute("media", media.toString()); - } - - packet.addChild("request", "urn:xmpp:receipts"); - packet.addChild("store", "urn:xmpp:hints"); - return packet; - } - - public MessagePacket sessionRetract(final JingleConnectionManager.RtpSessionProposal proposal) { - final MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those - packet.setTo(proposal.with); - final Element propose = packet.addChild("retract", Namespace.JINGLE_MESSAGE); - propose.setAttribute("id", proposal.sessionId); - propose.addChild("description", Namespace.JINGLE_APPS_RTP); - packet.addChild("store", "urn:xmpp:hints"); - return packet; - } - - public MessagePacket sessionReject(final Jid with, final String sessionId) { - final MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those - packet.setTo(with); - final Element propose = packet.addChild("reject", Namespace.JINGLE_MESSAGE); - propose.setAttribute("id", sessionId); - propose.addChild("description", Namespace.JINGLE_APPS_RTP); - packet.addChild("store", "urn:xmpp:hints"); - return packet; - } -} diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java deleted file mode 100644 index b270af7cb..000000000 --- a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java +++ /dev/null @@ -1,100 +0,0 @@ -package eu.siacs.conversations.generator; - -import android.text.TextUtils; - -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.entities.Presence; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.stanzas.PresencePacket; - -public class PresenceGenerator extends AbstractGenerator { - - public PresenceGenerator(XmppConnectionService service) { - super(service); - } - - private PresencePacket subscription(String type, Contact contact) { - PresencePacket packet = new PresencePacket(); - packet.setAttribute("type", type); - packet.setTo(contact.getJid()); - packet.setFrom(contact.getAccount().getJid().asBareJid()); - return packet; - } - - public PresencePacket requestPresenceUpdatesFrom(final Contact contact) { - return requestPresenceUpdatesFrom(contact, null); - } - - public PresencePacket requestPresenceUpdatesFrom(final Contact contact, final String preAuth) { - PresencePacket packet = subscription("subscribe", contact); - String displayName = contact.getAccount().getDisplayName(); - if (!TextUtils.isEmpty(displayName)) { - packet.addChild("nick", Namespace.NICK).setContent(displayName); - } - if (preAuth != null) { - packet.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth); - } - return packet; - } - - public PresencePacket stopPresenceUpdatesFrom(Contact contact) { - return subscription("unsubscribe", contact); - } - - public PresencePacket stopPresenceUpdatesTo(Contact contact) { - return subscription("unsubscribed", contact); - } - - public PresencePacket sendPresenceUpdatesTo(Contact contact) { - return subscription("subscribed", contact); - } - - public PresencePacket selfPresence(Account account, Presence.Status status) { - return selfPresence(account, status, true); - } - - public PresencePacket selfPresence(final Account account, final Presence.Status status, final boolean personal) { - final PresencePacket packet = new PresencePacket(); - if (personal) { - final String sig = account.getPgpSignature(); - final String message = account.getPresenceStatusMessage(); - if (status.toShowString() != null) { - packet.addChild("show").setContent(status.toShowString()); - } - if (!TextUtils.isEmpty(message)) { - packet.addChild(new Element("status").setContent(message)); - } - if (sig != null && mXmppConnectionService.getPgpEngine() != null) { - packet.addChild("x", "jabber:x:signed").setContent(sig); - } - } - final String capHash = getCapHash(account); - if (capHash != null) { - Element cap = packet.addChild("c", - "http://jabber.org/protocol/caps"); - cap.setAttribute("hash", "sha-1"); - cap.setAttribute("node", "http://monocles.de"); - cap.setAttribute("ver", capHash); - } - return packet; - } - - public PresencePacket leave(final MucOptions mucOptions) { - PresencePacket presencePacket = new PresencePacket(); - presencePacket.setTo(mucOptions.getSelf().getFullJid()); - presencePacket.setFrom(mucOptions.getAccount().getJid()); - presencePacket.setAttribute("type", "unavailable"); - return presencePacket; - } - - public PresencePacket sendOfflinePresence(Account account) { - PresencePacket packet = new PresencePacket(); - packet.setFrom(account.getJid()); - packet.setAttribute("type", "unavailable"); - return packet; - } -} diff --git a/src/main/java/eu/siacs/conversations/http/AesGcmURL.java b/src/main/java/eu/siacs/conversations/http/AesGcmURL.java deleted file mode 100644 index 6cacf64d7..000000000 --- a/src/main/java/eu/siacs/conversations/http/AesGcmURL.java +++ /dev/null @@ -1,41 +0,0 @@ -package eu.siacs.conversations.http; - -import java.util.regex.Pattern; - -import okhttp3.HttpUrl; - -public final class AesGcmURL { - - /** - * This matches a 48 or 44 byte IV + KEY hex combo, like used in http/aesgcm upload anchors - */ - public static final Pattern IV_KEY = Pattern.compile("([A-Fa-f0-9]{2}){48}|([A-Fa-f0-9]{2}){44}"); - - public static final String PROTOCOL_NAME = "aesgcm"; - - private AesGcmURL() { - - } - - public static String toAesGcmUrl(HttpUrl url) { - if (url.isHttps()) { - return PROTOCOL_NAME + url.toString().substring(5); - } else { - return url.toString(); - } - } - - public static HttpUrl of(final String url) { - final int end = url.indexOf("://"); - if (end < 0) { - throw new IllegalArgumentException("Scheme not found"); - } - final String protocol = url.substring(0, end); - if (PROTOCOL_NAME.equals(protocol)) { - return HttpUrl.get("https" + url.substring(PROTOCOL_NAME.length())); - } else { - return HttpUrl.get(url); - } - } - -} diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java deleted file mode 100644 index 4c27ddbdf..000000000 --- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +++ /dev/null @@ -1,180 +0,0 @@ -package eu.siacs.conversations.http; - -import android.os.Build; -import android.util.Log; - -import org.apache.http.conn.ssl.StrictHostnameVerifier; -import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; - -import java.io.IOException; -import java.io.InputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Proxy; -import java.net.UnknownHostException; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.X509TrustManager; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.services.AbstractConnectionManager; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.TLSSocketFactory; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.ResponseBody; - -public class HttpConnectionManager extends AbstractConnectionManager { - - private final List downloadConnections = new ArrayList<>(); - private final List uploadConnections = new ArrayList<>(); - - public static final Executor FileTransferExecutor = Executors.newFixedThreadPool(4); - - public static final OkHttpClient OK_HTTP_CLIENT; - - static { - OK_HTTP_CLIENT = new OkHttpClient.Builder() - .addInterceptor(chain -> { - final Request original = chain.request(); - final Request modified = original.newBuilder() - .header("User-Agent", getUserAgent()) - .build(); - return chain.proceed(modified); - }) - .build(); - } - - public static String getUserAgent() { - return System.getProperty("http.agent"); - } - - public HttpConnectionManager(XmppConnectionService service) { - super(service); - } - - public static Proxy getProxy(boolean isI2P) { - final InetAddress localhost; - try { - localhost = InetAddress.getByAddress(new byte[]{127, 0, 0, 1}); - } catch (final UnknownHostException e) { - throw new IllegalStateException(e); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(localhost, isI2P ? 4447 : 9050)); - } else { - return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(localhost, isI2P ? 4444 : 8118)); - } - } - - public static InputStream open(final String url, final boolean tor) throws IOException { - return open(String.valueOf(HttpUrl.get(url)), tor); - } - - - public void createNewDownloadConnection(Message message) { - this.createNewDownloadConnection(message, false); - } - - public void createNewDownloadConnection(final Message message, boolean interactive) { - synchronized (this.downloadConnections) { - for (HttpDownloadConnection connection : this.downloadConnections) { - if (connection.getMessage() == message) { - Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": download already in progress"); - return; - } - } - final HttpDownloadConnection connection = new HttpDownloadConnection(message, this); - connection.init(interactive); - this.downloadConnections.add(connection); - } - } - - public void createNewUploadConnection(final Message message, boolean delay) { - synchronized (this.uploadConnections) { - for (HttpUploadConnection connection : this.uploadConnections) { - if (connection.getMessage() == message) { - Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": upload already in progress"); - return; - } - } - HttpUploadConnection connection = new HttpUploadConnection(message, Method.determine(message.getConversation().getAccount()), this); - connection.init(delay); - this.uploadConnections.add(connection); - } - } - - void finishConnection(HttpDownloadConnection connection) { - synchronized (this.downloadConnections) { - this.downloadConnections.remove(connection); - } - } - - void finishUploadConnection(HttpUploadConnection httpUploadConnection) { - synchronized (this.uploadConnections) { - this.uploadConnections.remove(httpUploadConnection); - } - } - - OkHttpClient buildHttpClient(final HttpUrl url, final Account account, boolean interactive) { - return buildHttpClient(url, account, 30, interactive); - } - - OkHttpClient buildHttpClient(final HttpUrl url, final Account account, int readTimeout, boolean interactive) { - final String slotHostname = url.host(); - final boolean onionSlot = slotHostname.endsWith(".onion"); - final boolean I2PSlot = slotHostname.endsWith(".i2p"); - final OkHttpClient.Builder builder = OK_HTTP_CLIENT.newBuilder(); - builder.writeTimeout(30, TimeUnit.SECONDS); - builder.readTimeout(readTimeout, TimeUnit.SECONDS); - setupTrustManager(builder, interactive); - if (mXmppConnectionService.useTorToConnect() || account.isOnion() || onionSlot || mXmppConnectionService.useI2PToConnect() || account.isI2P() || I2PSlot) { - builder.proxy(HttpConnectionManager.getProxy(I2PSlot)).build(); - } - return builder.build(); - } - - private void setupTrustManager(final OkHttpClient.Builder builder, final boolean interactive) { - final X509TrustManager trustManager; - if (interactive) { - trustManager = mXmppConnectionService.getMemorizingTrustManager().getInteractive(); - } else { - trustManager = mXmppConnectionService.getMemorizingTrustManager().getNonInteractive(); - } - try { - final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, SECURE_RANDOM); - builder.sslSocketFactory(sf, trustManager); - builder.hostnameVerifier(new StrictHostnameVerifier()); - } catch (final KeyManagementException ignored) { - } catch (final NoSuchAlgorithmException ignored) { - } - } - - public static InputStream open(final String url, final boolean tor, final boolean i2p) throws IOException { - return open(HttpUrl.get(url), tor, i2p); - } - - public static InputStream open(final HttpUrl httpUrl, final boolean tor, final boolean i2p) throws IOException { - final OkHttpClient.Builder builder = OK_HTTP_CLIENT.newBuilder(); - if (tor || i2p) { - builder.proxy(HttpConnectionManager.getProxy(i2p)).build(); - } - final OkHttpClient client = builder.build(); - final Request request = new Request.Builder().get().url(httpUrl).build(); - final ResponseBody body = client.newCall(request).execute().body(); - if (body == null) { - throw new IOException("No response body found"); - } - return body.byteStream(); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java deleted file mode 100644 index 49386dc01..000000000 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ /dev/null @@ -1,504 +0,0 @@ -package eu.siacs.conversations.http; - -import static eu.siacs.conversations.http.HttpConnectionManager.FileTransferExecutor; - -import android.util.Log; - -import androidx.annotation.Nullable; - -import com.google.common.base.Strings; -import com.google.common.io.ByteStreams; -import com.google.common.primitives.Longs; - -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -import javax.net.ssl.SSLHandshakeException; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.Transferable; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.services.AbstractConnectionManager; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.FileWriterException; -import eu.siacs.conversations.utils.MimeUtils; -import okhttp3.Call; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - -public class HttpDownloadConnection implements Transferable { - - private final Message message; - private final HttpConnectionManager mHttpConnectionManager; - private final XmppConnectionService mXmppConnectionService; - private HttpUrl mUrl; - private DownloadableFile file; - private int mStatus = Transferable.STATUS_UNKNOWN; - private boolean acceptedAutomatically = false; - private int mProgress = 0; - private Call mostRecentCall; - - private final SimpleDateFormat fileDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); - - HttpDownloadConnection(Message message, HttpConnectionManager manager) { - this.message = message; - this.mHttpConnectionManager = manager; - this.mXmppConnectionService = manager.getXmppConnectionService(); - } - - @Override - public boolean start() { - if (mXmppConnectionService.hasInternetConnection()) { - if (this.mStatus == STATUS_OFFER_CHECK_FILESIZE) { - checkFileSize(true); - } else { - download(true); - } - return true; - } else { - return false; - } - } - - public void init(boolean interactive) { - if (message.isFileDeleted()) { - if (message.getType() == Message.TYPE_PRIVATE_FILE) { - message.setType(Message.TYPE_PRIVATE); - } else if (message.isFileOrImage()) { - message.setType(Message.TYPE_TEXT); - } - message.setOob(true); - message.setFileDeleted(false); - mXmppConnectionService.updateMessage(message); - } - this.message.setTransferable(this); - try { - final Message.FileParams fileParams = message.getFileParams(); - if (message.hasFileOnRemoteHost()) { - mUrl = AesGcmURL.of(fileParams.url); - } else if (message.isOOb() && fileParams.url != null && fileParams.size != null) { - mUrl = AesGcmURL.of(fileParams.url); - } else { - mUrl = AesGcmURL.of(message.getBody().split("\n")[0]); - } - final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath()); - if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) { - this.message.setEncryption(Message.ENCRYPTION_PGP); - } else if (message.getEncryption() != Message.ENCRYPTION_OTR - && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) { - this.message.setEncryption(Message.ENCRYPTION_NONE); - } - final String ext = extension.getExtension(); - if (ext != null) { - if (message.getStatus() == Message.STATUS_RECEIVED) { - message.setRelativeFilePath(String.format("%s.%s", fileDateFormat.format(new Date(message.getTimeSent())) + "_" + message.getUuid().substring(0, 4), ext)); - } else { - message.setRelativeFilePath("Sent/" + String.format("%s.%s", fileDateFormat.format(new Date(message.getTimeSent())) + "_" + message.getUuid().substring(0, 4), ext)); - } - } else if (Strings.isNullOrEmpty(message.getRelativeFilePath())) { - if (message.getStatus() == Message.STATUS_RECEIVED) { - message.setRelativeFilePath(fileDateFormat.format(new Date(message.getTimeSent())) + "_" + message.getUuid().substring(0, 4)); - } else { - message.setRelativeFilePath("Sent/" + fileDateFormat.format(new Date(message.getTimeSent())) + "_" + message.getUuid().substring(0, 4)); - } - } - setupFile(); - if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) { - this.message.setEncryption(Message.ENCRYPTION_NONE); - } - final Long knownFileSize; - if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - knownFileSize = null; - } else { - knownFileSize = message.getFileParams().size; - } - Log.d(Config.LOGTAG, "knownFileSize: " + knownFileSize + ", body=" + message.getBody()); - if (knownFileSize != null && interactive) { - if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL - && this.file.getKey() != null) { - this.file.setExpectedSize(knownFileSize + 16); - } else { - this.file.setExpectedSize(knownFileSize); - } - download(true); - } else { - checkFileSize(interactive); - } - } catch (final IllegalArgumentException e) { - this.cancel(); - } - } - - private void setupFile() { - final String reference = mUrl.fragment(); - if (reference != null && AesGcmURL.IV_KEY.matcher(reference).matches()) { - this.file = new DownloadableFile(mXmppConnectionService.getCacheDir(), message.getUuid()); - this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference)); - Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")"); - } else { - this.file = mXmppConnectionService.getFileBackend().getFile(message, false); - } - } - - private void download(final boolean interactive) { - changeStatus(STATUS_WAITING); - Log.d(Config.LOGTAG, "download()", new Exception()); - FileTransferExecutor.execute(new FileDownloader(interactive)); - } - - private void checkFileSize(final boolean interactive) { - changeStatus(STATUS_WAITING); - FileTransferExecutor.execute(new FileSizeChecker(interactive)); - } - - @Override - public void cancel() { - final Call call = this.mostRecentCall; - if (call != null && !call.isCanceled()) { - call.cancel(); - } - mHttpConnectionManager.finishConnection(this); - message.setTransferable(null); - if (message.isFileOrImage()) { - message.setFileDeleted(true); - } - mHttpConnectionManager.updateConversationUi(true); - } - - private void decryptFile() throws IOException { - final DownloadableFile outputFile = mXmppConnectionService.getFileBackend().getFile(message, true); - - if (outputFile.getParentFile().mkdirs()) { - Log.d(Config.LOGTAG, "created parent directories for " + outputFile.getAbsolutePath()); - } - - if (!outputFile.createNewFile()) { - Log.w(Config.LOGTAG, "unable to create output file " + outputFile.getAbsolutePath()); - } - - final InputStream is = new FileInputStream(this.file); - - outputFile.setKey(this.file.getKey()); - outputFile.setIv(this.file.getIv()); - final OutputStream os = AbstractConnectionManager.createOutputStream(outputFile, false, true); - - ByteStreams.copy(is, os); - - FileBackend.close(is); - FileBackend.close(os); - - if (!file.delete()) { - Log.w(Config.LOGTAG, "unable to delete temporary OMEMO encrypted file " + file.getAbsolutePath()); - } - } - - private void finish() { - message.setTransferable(null); - mHttpConnectionManager.finishConnection(this); - boolean notify = acceptedAutomatically && !message.isRead(); - if (message.getEncryption() == Message.ENCRYPTION_PGP) { - notify = message.getConversation().getAccount().getPgpDecryptionService().decrypt(message, notify); - } - mHttpConnectionManager.updateConversationUi(true); - final boolean notifyAfterScan = notify; - final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message, true); - FileBackend.updateMediaScanner(mXmppConnectionService, file, () -> { - if (notifyAfterScan) { - mXmppConnectionService.getNotificationService().push(message); - } - }); - } - - private void decryptIfNeeded() throws IOException { - if (file.getKey() != null && file.getIv() != null) { - decryptFile(); - } - } - - private void changeStatus(int status) { - this.mStatus = status; - mHttpConnectionManager.updateConversationUi(true); - } - - private void showToastForException(final Exception e) { - e.printStackTrace(); - final Call call = mostRecentCall; - final boolean cancelled = call != null && call.isCanceled(); - if (e == null || cancelled) { - return; - } - if (e instanceof java.net.UnknownHostException) { - mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found); - } else if (e instanceof java.net.ConnectException) { - mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect); - } else if (e instanceof FileWriterException) { - mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file); - } else if (e instanceof InvalidFileException) { - mXmppConnectionService.showErrorToastInUi(R.string.download_failed_invalid_file); - } else { - mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found); - } - } - - private void updateProgress(long i) { - this.mProgress = (int) i; - mHttpConnectionManager.updateConversationUi(false); - } - - @Override - public int getStatus() { - return this.mStatus; - } - - @Override - public Long getFileSize() { - if (this.file != null) { - return this.file.getExpectedSize(); - } else { - return null; - } - } - - @Override - public int getProgress() { - return this.mProgress; - } - - public Message getMessage() { - return message; - } - - private class FileSizeChecker implements Runnable { - - private final boolean interactive; - - FileSizeChecker(boolean interactive) { - this.interactive = interactive; - } - - - @Override - public void run() { - check(); - } - - private void retrieveFailed(@Nullable final Exception e) { - changeStatus(STATUS_OFFER_CHECK_FILESIZE); - if (interactive) { - showToastForException(e); - } else { - HttpDownloadConnection.this.acceptedAutomatically = false; - HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message); - } - cancel(); - } - - private void check() { - long size; - try { - size = retrieveFileSize(); - } catch (final Exception e) { - Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage()); - retrieveFailed(e); - return; - } - final Message.FileParams fileParams = message.getFileParams(); - FileBackend.updateFileParams(message, fileParams.url, size); - message.setOob(true); - mXmppConnectionService.databaseBackend.updateMessage(message, true); - file.setExpectedSize(size); - message.resetFileParams(); - if (mHttpConnectionManager.hasStoragePermission() - && size <= mHttpConnectionManager.getAutoAcceptFileSize() - && mXmppConnectionService.isDataSaverDisabled()) { - HttpDownloadConnection.this.acceptedAutomatically = true; - download(interactive); - } else { - changeStatus(STATUS_OFFER); - HttpDownloadConnection.this.acceptedAutomatically = false; - HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message); - } - } - - private long retrieveFileSize() throws IOException { - Log.d(Config.LOGTAG, "retrieve file size. interactive:" + interactive); - changeStatus(STATUS_CHECKING); - final OkHttpClient client = mHttpConnectionManager.buildHttpClient( - mUrl, - message.getConversation().getAccount(), - interactive - ); - final Request request = new Request.Builder() - .url(URL.stripFragment(mUrl)) - .addHeader("Accept-Encoding", "identity") - .head() - .build(); - mostRecentCall = client.newCall(request); - try { - final Response response = mostRecentCall.execute(); - throwOnInvalidCode(response); - final String contentLength = response.header("Content-Length"); - final String contentType = response.header("Content-Type"); - final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.encodedPath()); - if (Strings.isNullOrEmpty(extension.getExtension()) && contentType != null) { - final String fileExtension = MimeUtils.guessExtensionFromMimeType(contentType); - if (fileExtension != null) { - mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, String.format("%s.%s", fileDateFormat.format(new Date(message.getTimeSent())) + "_" + message.getUuid().substring(0, 4), fileExtension), contentType); - Log.d(Config.LOGTAG, "rewriting name after not finding extension in url but in content type"); - setupFile(); - } - } - if (Strings.isNullOrEmpty(contentLength)) { - throw new IOException("no content-length found in HEAD response"); - } - final long size = Long.parseLong(contentLength, 10); - if (size < 0) { - throw new IOException("Server reported negative file size"); - } - return size; - } catch (final IOException e) { - Log.d(Config.LOGTAG, "io exception during HEAD " + e.getMessage()); - throw e; - } catch (final NumberFormatException e) { - throw new IOException(e); - } - } - - } - - private class FileDownloader implements Runnable { - - private final boolean interactive; - - public FileDownloader(boolean interactive) { - this.interactive = interactive; - } - - @Override - public void run() { - try { - changeStatus(STATUS_DOWNLOADING); - download(); - decryptIfNeeded(); - updateImageBounds(); - finish(); - } catch (final SSLHandshakeException e) { - changeStatus(STATUS_OFFER); - } catch (final Exception e) { - Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": unable to download file", e); - if (interactive) { - showToastForException(e); - } else { - HttpDownloadConnection.this.acceptedAutomatically = false; - HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message); - } - changeStatus(STATUS_OFFER); - cancel(); - } - } - - private void download() throws Exception { - final OkHttpClient client = mHttpConnectionManager.buildHttpClient( - mUrl, - message.getConversation().getAccount(), - interactive - ); - - final Request.Builder requestBuilder = new Request.Builder().url(URL.stripFragment(mUrl)); - - final long expected = file.getExpectedSize(); - final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected; - final long resumeSize; - if (tryResume) { - resumeSize = file.getSize(); - Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected); - requestBuilder.addHeader("Range", String.format(Locale.ENGLISH, "bytes=%d-", resumeSize)); - } else { - resumeSize = 0; - } - final Request request = requestBuilder.build(); - mostRecentCall = client.newCall(request); - final Response response = mostRecentCall.execute(); - throwOnInvalidCode(response); - final String contentRange = response.header("Content-Range"); - final boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-"); - final InputStream inputStream = response.body().byteStream(); - final OutputStream outputStream; - long transmitted = 0; - if (tryResume && serverResumed) { - Log.d(Config.LOGTAG, "server resumed"); - transmitted = file.getSize(); - updateProgress(Math.round(((double) transmitted / expected) * 100)); - outputStream = AbstractConnectionManager.createOutputStream(file, true, false); - } else { - final String contentLength = response.header("Content-Length"); - final long size = Strings.isNullOrEmpty(contentLength) ? 0 : Longs.tryParse(contentLength); - if (expected != size) { - Log.d(Config.LOGTAG, "content-length reported on GET (" + size + ") did not match Content-Length reported on HEAD (" + expected + ")"); - } - file.getParentFile().mkdirs(); - Log.d(Config.LOGTAG,"creating file: "+file.getAbsolutePath()); - if (!file.exists() && !file.createNewFile()) { - throw new FileWriterException(file); - } - outputStream = AbstractConnectionManager.createOutputStream(file, false, false); - } - int count; - final byte[] buffer = new byte[4096]; - while ((count = inputStream.read(buffer)) != -1) { - transmitted += count; - try { - outputStream.write(buffer, 0, count); - } catch (final IOException e) { - throw new FileWriterException(file); - } - if (transmitted > expected) { - throw new InvalidFileException(String.format("File exceeds expected size of %d", expected)); - } - updateProgress(Math.round(((double) transmitted / expected) * 100)); - } - outputStream.flush(); - } - - private void updateImageBounds() { - final boolean privateMessage = message.isPrivateMessage(); - message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE); - final String url; - final String ref = mUrl.fragment(); - if (ref != null && AesGcmURL.IV_KEY.matcher(ref).matches()) { - url = AesGcmURL.toAesGcmUrl(mUrl); - } else { - url = mUrl.toString(); - } - mXmppConnectionService.getFileBackend().updateFileParams(message, url); - mXmppConnectionService.updateMessage(message); - } - - } - - private static void throwOnInvalidCode(final Response response) throws IOException { - final int code = response.code(); - if (code < 200 || code >= 300) { - throw new IOException(String.format(Locale.ENGLISH, "HTTP Status code was %d", code)); - } - } - - private static class InvalidFileException extends IOException { - - private InvalidFileException(final String message) { - super(message); - } - - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java deleted file mode 100644 index c3165bfea..000000000 --- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java +++ /dev/null @@ -1,229 +0,0 @@ -package eu.siacs.conversations.http; - -import static eu.siacs.conversations.http.HttpConnectionManager.FileTransferExecutor; -import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; - -import android.util.Log; - -import androidx.annotation.Nullable; - -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; - -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.Future; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.Transferable; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.services.AbstractConnectionManager; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.CryptoHelper; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; - -public class HttpUploadConnection implements Transferable, AbstractConnectionManager.ProgressListener { - - static final List WHITE_LISTED_HEADERS = Arrays.asList( - "Authorization", - "Cookie", - "Expires" - ); - - private final HttpConnectionManager mHttpConnectionManager; - private final XmppConnectionService mXmppConnectionService; - private final Method method; - private boolean delayed = false; - private DownloadableFile file; - private final Message message; - private SlotRequester.Slot slot; - private byte[] key = null; - private int mStatus = Transferable.STATUS_UNKNOWN; - - private long transmitted = 0; - private Call mostRecentCall; - private ListenableFuture slotFuture; - - public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager) { - this.message = message; - this.method = method; - this.mHttpConnectionManager = httpConnectionManager; - this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService(); - } - - @Override - public boolean start() { - return false; - } - - @Override - public int getStatus() { - return this.mStatus; - } - - @Override - public Long getFileSize() { - return file == null ? null : file.getExpectedSize(); - } - - @Override - public int getProgress() { - if (file == null) { - return 0; - } - return (int) ((((double) transmitted) / file.getExpectedSize()) * 100); - } - - @Override - public void cancel() { - final ListenableFuture slotFuture = this.slotFuture; - if (slotFuture != null && !slotFuture.isDone()) { - if (slotFuture.cancel(true)) { - Log.d(Config.LOGTAG,"cancelled slot requester"); - } - } - final Call call = this.mostRecentCall; - if (call != null && !call.isCanceled()) { - call.cancel(); - Log.d(Config.LOGTAG,"cancelled HTTP request"); - } - } - - private void fail(String errorMessage) { - finish(); - final Call call = this.mostRecentCall; - final Future slotFuture = this.slotFuture; - final boolean cancelled = (call != null && call.isCanceled()) || (slotFuture != null && slotFuture.isCancelled()); - mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage); - } - - private void finish() { - mHttpConnectionManager.finishUploadConnection(this); - message.setTransferable(null); - } - - public void init(boolean delay) { - final Account account = message.getConversation().getAccount(); - this.file = mXmppConnectionService.getFileBackend().getFile(message, false); - final String mime; - if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - mime = "application/pgp-encrypted"; - } else { - mime = this.file.getMimeType(); - } - final long originalFileSize = file.getSize(); - this.delayed = delay; - if (Config.ENCRYPT_ON_HTTP_UPLOADED - || message.getEncryption() == Message.ENCRYPTION_AXOLOTL - || message.getEncryption() == Message.ENCRYPTION_OTR) { - this.key = new byte[44]; - SECURE_RANDOM.nextBytes(this.key); - this.file.setKeyAndIv(this.key); - } - this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0)); - message.resetFileParams(); - this.slotFuture = new SlotRequester(mXmppConnectionService).request(method, account, file, mime); - Futures.addCallback(this.slotFuture, new FutureCallback() { - @Override - public void onSuccess(@Nullable SlotRequester.Slot result) { - changeStatus(STATUS_WAITING); - FileTransferExecutor.execute(() -> { - changeStatus(STATUS_UPLOADING); - HttpUploadConnection.this.slot = result; - try { - HttpUploadConnection.this.upload(); - } catch (final Exception e) { - changeStatus(STATUS_FAILED); - fail(e.getMessage()); - } - }); - } - - @Override - public void onFailure(@NotNull final Throwable throwable) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to request slot", throwable); - // TODO consider fall back to jingle in 1-on-1 chats with exactly one online presence - fail(throwable.getMessage()); - } - }, MoreExecutors.directExecutor()); - message.setTransferable(this); - mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); - } - - - private void upload() { - final OkHttpClient client = mHttpConnectionManager.buildHttpClient( - slot.put, - message.getConversation().getAccount(), - 0, - true - ); - final RequestBody requestBody = AbstractConnectionManager.requestBody(file, this); - final Request request = new Request.Builder() - .url(slot.put) - .put(requestBody) - .headers(slot.headers) - .build(); - Log.d(Config.LOGTAG, "uploading file to " + slot.put); - this.mostRecentCall = client.newCall(request); - this.mostRecentCall.enqueue(new Callback() { - @Override - public void onFailure(@NotNull Call call, IOException e) { - Log.d(Config.LOGTAG, "http upload failed", e); - fail(e.getMessage()); - } - - @Override - public void onResponse(@NotNull Call call, @NotNull Response response) { - final int code = response.code(); - if (code == 200 || code == 201) { - Log.d(Config.LOGTAG, "finished uploading file"); - final String get; - if (key != null) { - get = AesGcmURL.toAesGcmUrl(slot.get.newBuilder().fragment(CryptoHelper.bytesToHex(key)).build()); - } else { - get = slot.get.toString(); - } - mXmppConnectionService.getFileBackend().updateFileParams(message, get); - FileBackend.updateMediaScanner(mXmppConnectionService, file); - finish(); - if (!message.isPrivateMessage()) { - message.setCounterpart(message.getConversation().getJid().asBareJid()); - } - mXmppConnectionService.resendMessage(message, delayed); - } else { - Log.d(Config.LOGTAG, "http upload failed because response code was " + code); - fail("http upload failed because response code was " + code); - } - } - }); - } - - public Message getMessage() { - return message; - } - - private void changeStatus(int status) { - this.mStatus = status; - mHttpConnectionManager.updateConversationUi(true); - } - - @Override - public void onProgress(final long progress) { - this.transmitted = progress; - mHttpConnectionManager.updateConversationUi(false); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/http/Method.java b/src/main/java/eu/siacs/conversations/http/Method.java deleted file mode 100644 index 4ddb8df74..000000000 --- a/src/main/java/eu/siacs/conversations/http/Method.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.http; - -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xmpp.XmppConnection; - -public enum Method { - HTTP_UPLOAD, HTTP_UPLOAD_LEGACY; - - public static Method determine(Account account) { - XmppConnection.Features features = account.getXmppConnection() == null ? null : account.getXmppConnection().getFeatures(); - if (features == null) { - return HTTP_UPLOAD; - } - if (features.useLegacyHttpUpload()) { - return HTTP_UPLOAD_LEGACY; - } else if (features.httpUpload(0)) { - return HTTP_UPLOAD; - } else { - return HTTP_UPLOAD; - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/http/NoSSLv3SocketFactory.java b/src/main/java/eu/siacs/conversations/http/NoSSLv3SocketFactory.java deleted file mode 100644 index b0f748b00..000000000 --- a/src/main/java/eu/siacs/conversations/http/NoSSLv3SocketFactory.java +++ /dev/null @@ -1,419 +0,0 @@ -package eu.siacs.conversations.http; - -/*Copyright 2015 Bhavit Singh Sengar -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.*/ - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.Socket; -import java.net.SocketAddress; -import java.net.SocketException; -import java.nio.channels.SocketChannel; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.net.ssl.HandshakeCompletedListener; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; - - -public class NoSSLv3SocketFactory extends SSLSocketFactory { - private final SSLSocketFactory delegate; - - public NoSSLv3SocketFactory() { - this.delegate = HttpsURLConnection.getDefaultSSLSocketFactory(); - } - - public NoSSLv3SocketFactory(SSLSocketFactory delegate) { - this.delegate = delegate; - } - - @Override - public String[] getDefaultCipherSuites() { - return delegate.getDefaultCipherSuites(); - } - - @Override - public String[] getSupportedCipherSuites() { - return delegate.getSupportedCipherSuites(); - } - - private Socket makeSocketSafe(Socket socket) { - if (socket instanceof SSLSocket) { - socket = new NoSSLv3SSLSocket((SSLSocket) socket); - } - return socket; - } - - @Override - public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { - return makeSocketSafe(delegate.createSocket(s, host, port, autoClose)); - } - - @Override - public Socket createSocket(String host, int port) throws IOException { - return makeSocketSafe(delegate.createSocket(host, port)); - } - - @Override - public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { - return makeSocketSafe(delegate.createSocket(host, port, localHost, localPort)); - } - - @Override - public Socket createSocket(InetAddress host, int port) throws IOException { - return makeSocketSafe(delegate.createSocket(host, port)); - } - - @Override - public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { - return makeSocketSafe(delegate.createSocket(address, port, localAddress, localPort)); - } - - private class NoSSLv3SSLSocket extends DelegateSSLSocket { - - private NoSSLv3SSLSocket(SSLSocket delegate) { - super(delegate); - - } - - @Override - public void setEnabledProtocols(String[] protocols) { - if (protocols != null && protocols.length == 1 && "SSLv3".equals(protocols[0])) { - - List enabledProtocols = new ArrayList(Arrays.asList(delegate.getEnabledProtocols())); - if (enabledProtocols.size() > 1) { - enabledProtocols.remove("SSLv3"); - System.out.println("Removed SSLv3 from enabled protocols"); - } else { - System.out.println("SSL stuck with protocol available for " + String.valueOf(enabledProtocols)); - } - protocols = enabledProtocols.toArray(new String[enabledProtocols.size()]); - } - - super.setEnabledProtocols(protocols); - } - } - - public class DelegateSSLSocket extends SSLSocket { - - protected final SSLSocket delegate; - - DelegateSSLSocket(SSLSocket delegate) { - this.delegate = delegate; - } - - @Override - public String[] getSupportedCipherSuites() { - return delegate.getSupportedCipherSuites(); - } - - @Override - public String[] getEnabledCipherSuites() { - return delegate.getEnabledCipherSuites(); - } - - @Override - public void setEnabledCipherSuites(String[] suites) { - delegate.setEnabledCipherSuites(suites); - } - - @Override - public String[] getSupportedProtocols() { - return delegate.getSupportedProtocols(); - } - - @Override - public String[] getEnabledProtocols() { - return delegate.getEnabledProtocols(); - } - - @Override - public void setEnabledProtocols(String[] protocols) { - delegate.setEnabledProtocols(protocols); - } - - @Override - public SSLSession getSession() { - return delegate.getSession(); - } - - @Override - public void addHandshakeCompletedListener(HandshakeCompletedListener listener) { - delegate.addHandshakeCompletedListener(listener); - } - - @Override - public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) { - delegate.removeHandshakeCompletedListener(listener); - } - - @Override - public void startHandshake() throws IOException { - delegate.startHandshake(); - } - - @Override - public void setUseClientMode(boolean mode) { - delegate.setUseClientMode(mode); - } - - @Override - public boolean getUseClientMode() { - return delegate.getUseClientMode(); - } - - @Override - public void setNeedClientAuth(boolean need) { - delegate.setNeedClientAuth(need); - } - - @Override - public void setWantClientAuth(boolean want) { - delegate.setWantClientAuth(want); - } - - @Override - public boolean getNeedClientAuth() { - return delegate.getNeedClientAuth(); - } - - @Override - public boolean getWantClientAuth() { - return delegate.getWantClientAuth(); - } - - @Override - public void setEnableSessionCreation(boolean flag) { - delegate.setEnableSessionCreation(flag); - } - - @Override - public boolean getEnableSessionCreation() { - return delegate.getEnableSessionCreation(); - } - - @Override - public void bind(SocketAddress localAddr) throws IOException { - delegate.bind(localAddr); - } - - @Override - public synchronized void close() throws IOException { - delegate.close(); - } - - @Override - public void connect(SocketAddress remoteAddr) throws IOException { - delegate.connect(remoteAddr); - } - - @Override - public void connect(SocketAddress remoteAddr, int timeout) throws IOException { - delegate.connect(remoteAddr, timeout); - } - - @Override - public SocketChannel getChannel() { - return delegate.getChannel(); - } - - @Override - public InetAddress getInetAddress() { - return delegate.getInetAddress(); - } - - @Override - public InputStream getInputStream() throws IOException { - return delegate.getInputStream(); - } - - @Override - public boolean getKeepAlive() throws SocketException { - return delegate.getKeepAlive(); - } - - @Override - public InetAddress getLocalAddress() { - return delegate.getLocalAddress(); - } - - @Override - public int getLocalPort() { - return delegate.getLocalPort(); - } - - @Override - public SocketAddress getLocalSocketAddress() { - return delegate.getLocalSocketAddress(); - } - - @Override - public boolean getOOBInline() throws SocketException { - return delegate.getOOBInline(); - } - - @Override - public OutputStream getOutputStream() throws IOException { - return delegate.getOutputStream(); - } - - @Override - public int getPort() { - return delegate.getPort(); - } - - @Override - public synchronized int getReceiveBufferSize() throws SocketException { - return delegate.getReceiveBufferSize(); - } - - @Override - public SocketAddress getRemoteSocketAddress() { - return delegate.getRemoteSocketAddress(); - } - - @Override - public boolean getReuseAddress() throws SocketException { - return delegate.getReuseAddress(); - } - - @Override - public synchronized int getSendBufferSize() throws SocketException { - return delegate.getSendBufferSize(); - } - - @Override - public int getSoLinger() throws SocketException { - return delegate.getSoLinger(); - } - - @Override - public synchronized int getSoTimeout() throws SocketException { - return delegate.getSoTimeout(); - } - - @Override - public boolean getTcpNoDelay() throws SocketException { - return delegate.getTcpNoDelay(); - } - - @Override - public int getTrafficClass() throws SocketException { - return delegate.getTrafficClass(); - } - - @Override - public boolean isBound() { - return delegate.isBound(); - } - - @Override - public boolean isClosed() { - return delegate.isClosed(); - } - - @Override - public boolean isConnected() { - return delegate.isConnected(); - } - - @Override - public boolean isInputShutdown() { - return delegate.isInputShutdown(); - } - - @Override - public boolean isOutputShutdown() { - return delegate.isOutputShutdown(); - } - - @Override - public void sendUrgentData(int value) throws IOException { - delegate.sendUrgentData(value); - } - - @Override - public void setKeepAlive(boolean keepAlive) throws SocketException { - delegate.setKeepAlive(keepAlive); - } - - @Override - public void setOOBInline(boolean oobinline) throws SocketException { - delegate.setOOBInline(oobinline); - } - - @Override - public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) { - delegate.setPerformancePreferences(connectionTime, latency, bandwidth); - } - - @Override - public synchronized void setReceiveBufferSize(int size) throws SocketException { - delegate.setReceiveBufferSize(size); - } - - @Override - public void setReuseAddress(boolean reuse) throws SocketException { - delegate.setReuseAddress(reuse); - } - - @Override - public synchronized void setSendBufferSize(int size) throws SocketException { - delegate.setSendBufferSize(size); - } - - @Override - public void setSoLinger(boolean on, int timeout) throws SocketException { - delegate.setSoLinger(on, timeout); - } - - @Override - public synchronized void setSoTimeout(int timeout) throws SocketException { - delegate.setSoTimeout(timeout); - } - - @Override - public void setTcpNoDelay(boolean on) throws SocketException { - delegate.setTcpNoDelay(on); - } - - @Override - public void setTrafficClass(int value) throws SocketException { - delegate.setTrafficClass(value); - } - - @Override - public void shutdownInput() throws IOException { - delegate.shutdownInput(); - } - - @Override - public void shutdownOutput() throws IOException { - delegate.shutdownOutput(); - } - - @Override - public String toString() { - return delegate.toString(); - } - - @Override - public boolean equals(Object o) { - return delegate.equals(o); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/http/PicassoHelper.java b/src/main/java/eu/siacs/conversations/http/PicassoHelper.java deleted file mode 100644 index 0545a7d4a..000000000 --- a/src/main/java/eu/siacs/conversations/http/PicassoHelper.java +++ /dev/null @@ -1,4 +0,0 @@ -package eu.siacs.conversations.http; - -public class PicassoHelper { -} diff --git a/src/main/java/eu/siacs/conversations/http/SlotRequester.java b/src/main/java/eu/siacs/conversations/http/SlotRequester.java deleted file mode 100644 index 30cc8f07f..000000000 --- a/src/main/java/eu/siacs/conversations/http/SlotRequester.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.http; - -import com.google.common.collect.ImmutableMap; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.SettableFuture; - -import java.util.Map; - -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.parser.IqParser; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.IqResponseException; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import okhttp3.Headers; -import okhttp3.HttpUrl; - -public class SlotRequester { - - private XmppConnectionService service; - - public SlotRequester(XmppConnectionService service) { - this.service = service; - } - - public ListenableFuture request(Method method, Account account, DownloadableFile file, String mime) { - if (method == Method.HTTP_UPLOAD_LEGACY) { - final Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY); - return requestHttpUploadLegacy(account, host, file, mime); - } else { - final Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD); - return requestHttpUpload(account, host, file, mime); - } - } - - private ListenableFuture requestHttpUploadLegacy(Account account, Jid host, DownloadableFile file, String mime) { - final SettableFuture future = SettableFuture.create(); - final IqPacket request = service.getIqGenerator().requestHttpUploadLegacySlot(host, file, mime); - service.sendIqPacket(account, request, (a, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD_LEGACY); - if (slotElement != null) { - try { - final String putUrl = slotElement.findChildContent("put"); - final String getUrl = slotElement.findChildContent("get"); - if (getUrl != null && putUrl != null) { - final Slot slot = new Slot( - HttpUrl.get(putUrl), - HttpUrl.get(getUrl), - Headers.of("Content-Type", mime == null ? "application/octet-stream" : mime) - ); - future.set(slot); - return; - } - } catch (final IllegalArgumentException e) { - future.setException(e); - return; - } - } - } - future.setException(new IqResponseException(IqParser.extractErrorMessage(packet))); - }); - return future; - } - - private ListenableFuture requestHttpUpload(Account account, Jid host, DownloadableFile file, String mime) { - final SettableFuture future = SettableFuture.create(); - final IqPacket request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime); - service.sendIqPacket(account, request, (a, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD); - if (slotElement != null) { - try { - final Element put = slotElement.findChild("put"); - final Element get = slotElement.findChild("get"); - final String putUrl = put == null ? null : put.getAttribute("url"); - final String getUrl = get == null ? null : get.getAttribute("url"); - if (getUrl != null && putUrl != null) { - final ImmutableMap.Builder headers = new ImmutableMap.Builder<>(); - for (final Element child : put.getChildren()) { - if ("header".equals(child.getName())) { - final String name = child.getAttribute("name"); - final String value = child.getContent(); - if (HttpUploadConnection.WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) { - headers.put(name, value.trim()); - } - } - } - headers.put("Content-Type", mime == null ? "application/octet-stream" : mime); - final Slot slot = new Slot(HttpUrl.get(putUrl), HttpUrl.get(getUrl), headers.build()); - future.set(slot); - return; - } - } catch (final IllegalArgumentException e) { - future.setException(e); - return; - } - } - } - future.setException(new IqResponseException(IqParser.extractErrorMessage(packet))); - }); - return future; - } - - public static class Slot { - public final HttpUrl put; - public final HttpUrl get; - public final Headers headers; - - private Slot(HttpUrl put, HttpUrl get, Headers headers) { - this.put = put; - this.get = get; - this.headers = headers; - } - - private Slot(HttpUrl put, HttpUrl getUrl, Map headers) { - this.put = put; - this.get = getUrl; - this.headers = Headers.of(headers); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/http/URL.java b/src/main/java/eu/siacs/conversations/http/URL.java deleted file mode 100644 index e294ed8a0..000000000 --- a/src/main/java/eu/siacs/conversations/http/URL.java +++ /dev/null @@ -1,32 +0,0 @@ -package eu.siacs.conversations.http; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Arrays; -import java.util.List; - -import okhttp3.HttpUrl; - -public class URL { - - public static final List WELL_KNOWN_SCHEMES = Arrays.asList("http", "https", AesGcmURL.PROTOCOL_NAME); - - public static String tryParse(String url) { - final URI uri; - try { - uri = new URI(url); - } catch (URISyntaxException e) { - return null; - } - if (WELL_KNOWN_SCHEMES.contains(uri.getScheme())) { - return uri.toString(); - } else { - return null; - } - } - - public static HttpUrl stripFragment(final HttpUrl url) { - return url.newBuilder().fragment(null).build(); - } - -} diff --git a/src/main/java/eu/siacs/conversations/http/services/MuclumbusService.java b/src/main/java/eu/siacs/conversations/http/services/MuclumbusService.java deleted file mode 100644 index f8d8465e9..000000000 --- a/src/main/java/eu/siacs/conversations/http/services/MuclumbusService.java +++ /dev/null @@ -1,44 +0,0 @@ -package eu.siacs.conversations.http.services; - -import java.util.Collections; -import java.util.List; -import java.util.Set; - -import eu.siacs.conversations.entities.Room; -import retrofit2.Call; -import retrofit2.http.Body; -import retrofit2.http.GET; -import retrofit2.http.POST; -import retrofit2.http.Query; - -public interface MuclumbusService { - - @GET("/api/1.0/rooms/unsafe") - Call getRooms(@Query("p") int page); - - @POST("/api/1.0/search") - Call search(@Body SearchRequest searchRequest); - - class Rooms { - int page; - int total; - int pages; - public List items; - } - - class SearchRequest { - public final Set keywords; - - public SearchRequest(String keyword) { - this.keywords = Collections.singleton(keyword); - } - } - - class SearchResult { - public Result result; - } - - class Result { - public List items; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java deleted file mode 100644 index 7c43fe540..000000000 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ /dev/null @@ -1,208 +0,0 @@ -package eu.siacs.conversations.parser; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Date; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.InvalidJid; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; - -public abstract class AbstractParser { - - protected XmppConnectionService mXmppConnectionService; - - protected AbstractParser(XmppConnectionService service) { - this.mXmppConnectionService = service; - } - - public static Long parseTimestamp(Element element, Long d) { - return parseTimestamp(element, d, false); - } - - public static Long parseTimestamp(Element element, Long d, boolean ignoreCsiAndSm) { - long min = Long.MAX_VALUE; - boolean returnDefault = true; - final Jid to; - if (ignoreCsiAndSm && element instanceof AbstractStanza) { - to = ((AbstractStanza) element).getTo(); - } else { - to = null; - } - for (Element child : element.getChildren()) { - if ("delay".equals(child.getName()) && "urn:xmpp:delay".equals(child.getNamespace())) { - final Jid f = to == null ? null : InvalidJid.getNullForInvalid(child.getAttributeAsJid("from")); - if (f != null && (to.asBareJid().equals(f) || to.getDomain().equals(f))) { - continue; - } - final String stamp = child.getAttribute("stamp"); - if (stamp != null) { - try { - min = Math.min(min, AbstractParser.parseTimestamp(stamp)); - returnDefault = false; - } catch (Throwable t) { - //ignore - } - } - } - } - if (returnDefault) { - return d; - } else { - return min; - } - } - - public static long parseTimestamp(Element element) { - return parseTimestamp(element, System.currentTimeMillis()); - } - - public static long parseTimestamp(String timestamp) throws ParseException { - timestamp = timestamp.replace("Z", "+0000"); - SimpleDateFormat dateFormat; - long ms; - if (timestamp.length() >= 25 && timestamp.charAt(19) == '.') { - String millis = timestamp.substring(19, timestamp.length() - 5); - try { - double fractions = Double.parseDouble("0" + millis); - ms = Math.round(1000 * fractions); - } catch (NumberFormatException e) { - ms = 0; - } - } else { - ms = 0; - } - timestamp = timestamp.substring(0, 19) + timestamp.substring(timestamp.length() - 5, timestamp.length()); - dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US); - return Math.min(dateFormat.parse(timestamp).getTime() + ms, System.currentTimeMillis()); - } - public static long getTimestamp(final String input) throws ParseException { - if (input == null) { - throw new IllegalArgumentException("timestamp should not be null"); - } - final String timestamp = input.replace("Z", "+0000"); - final SimpleDateFormat simpleDateFormat = - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US); - final long milliseconds = getMilliseconds(timestamp); - final String formatted = - timestamp.substring(0, 19) + timestamp.substring(timestamp.length() - 5); - final Date date = simpleDateFormat.parse(formatted); - if (date == null) { - throw new IllegalArgumentException("Date was null"); - } - return date.getTime() + milliseconds; - } - - private static long getMilliseconds(final String timestamp) { - if (timestamp.length() >= 25 && timestamp.charAt(19) == '.') { - final String millis = timestamp.substring(19, timestamp.length() - 5); - try { - double fractions = Double.parseDouble("0" + millis); - return Math.round(1000 * fractions); - } catch (NumberFormatException e) { - return 0; - } - } else { - return 0; - } - } - - protected void updateLastseen(final Account account, final Jid from) { - final Contact contact = account.getRoster().getContact(from); - contact.setLastResource(from.isBareJid() ? "" : from.getResource()); - } - - protected String avatarData(Element items) { - Element item = items.findChild("item"); - if (item == null) { - return null; - } - return item.findChildContent("data", Namespace.AVATAR_DATA); - } - - public static MucOptions.User parseItem(Conversation conference, Element item) { - return parseItem(conference, item, null); - } - - public static MucOptions.User parseItem(Conversation conference, Element item, Jid fullJid) { - final String local = conference.getJid().getLocal(); - final String domain = conference.getJid().getDomain().toEscapedString(); - String affiliation = item.getAttribute("affiliation"); - String role = item.getAttribute("role"); - String nick = item.getAttribute("nick"); - if (nick != null && fullJid == null) { - try { - fullJid = Jid.of(local, domain, nick); - } catch (IllegalArgumentException e) { - fullJid = null; - } - } - Jid realJid = item.getAttributeAsJid("jid"); - MucOptions.User user = new MucOptions.User(conference.getMucOptions(), fullJid); - if (InvalidJid.isValid(realJid)) { - user.setRealJid(realJid); - } - user.setAffiliation(affiliation); - user.setRole(role); - return user; - } - - public static String extractErrorMessage(Element packet) { - final Element error = packet.findChild("error"); - if (error != null && error.getChildren().size() > 0) { - final List errorNames = orderedElementNames(error.getChildren()); - final String text = error.findChildContent("text"); - if (text != null && !text.trim().isEmpty()) { - return prefixError(errorNames) + text; - } else if (errorNames.size() > 0) { - return prefixError(errorNames) + errorNames.get(0).replace("-", " "); - } - } - return null; - } - - public static String errorMessage(Element packet) { - final Element error = packet.findChild("error"); - if (error != null && error.getChildren().size() > 0) { - final List errorNames = orderedElementNames(error.getChildren()); - final String text = error.findChildContent("text"); - if (text != null && !text.trim().isEmpty()) { - return text; - } else if (errorNames.size() > 0){ - return errorNames.get(0).replace("-"," "); - } - } - return null; - } - - private static String prefixError(List errorNames) { - if (errorNames.size() > 0) { - return errorNames.get(0) + '\u001f'; - } - return ""; - } - - private static List orderedElementNames(List children) { - List names = new ArrayList<>(); - for (Element child : children) { - final String name = child.getName(); - if (name != null && !name.equals("text")) { - if ("urn:ietf:params:xml:ns:xmpp-stanzas".equals(child.getNamespace())) { - names.add(name); - } else { - names.add(0, name); - } - } - } - return names; - } -} diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java deleted file mode 100644 index 32abce4d5..000000000 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ /dev/null @@ -1,552 +0,0 @@ -package eu.siacs.conversations.parser; - -import android.text.TextUtils; -import android.util.Log; -import android.util.Pair; -import androidx.annotation.NonNull; -import com.google.common.base.CharMatcher; -import com.google.common.io.BaseEncoding; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Room; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.InvalidJid; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; -import eu.siacs.conversations.xmpp.OnUpdateBlocklist; -import eu.siacs.conversations.xmpp.forms.Data; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import java.io.ByteArrayInputStream; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.ecc.Curve; -import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.state.PreKeyBundle; - -public class IqParser extends AbstractParser implements OnIqPacketReceived { - - public IqParser(final XmppConnectionService service) { - super(service); - } - - public static List items(IqPacket packet) { - ArrayList items = new ArrayList<>(); - final Element query = packet.findChild("query", Namespace.DISCO_ITEMS); - if (query == null) { - return items; - } - for (Element child : query.getChildren()) { - if ("item".equals(child.getName())) { - Jid jid = child.getAttributeAsJid("jid"); - if (jid != null) { - items.add(jid); - } - } - } - return items; - } - - public static Room parseRoom(IqPacket packet) { - final Element query = packet.findChild("query", Namespace.DISCO_INFO); - if (query == null) { - return null; - } - final Element x = query.findChild("x"); - if (x == null) { - return null; - } - final Element identity = query.findChild("identity"); - Data data = Data.parse(x); - String address = packet.getFrom().toString(); - String name = identity == null ? null : identity.getAttribute("name"); - String roomName = data.getValue("muc#roomconfig_roomname"); - String description = data.getValue("muc#roominfo_description"); - String language = data.getValue("muc#roominfo_lang"); - String occupants = data.getValue("muc#roominfo_occupants"); - int nusers; - try { - nusers = occupants == null ? 0 : Integer.parseInt(occupants); - } catch (NumberFormatException e) { - nusers = 0; - } - - return new Room( - address, - TextUtils.isEmpty(roomName) ? name : roomName, - description, - language, - nusers); - } - - private void rosterItems(final Account account, final Element query) { - final String version = query.getAttribute("ver"); - if (version != null) { - account.getRoster().setVersion(version); - } - for (final Element item : query.getChildren()) { - if (item.getName().equals("item")) { - final Jid jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid")); - if (jid == null) { - continue; - } - final String name = item.getAttribute("name"); - final String subscription = item.getAttribute("subscription"); - final Contact contact = account.getRoster().getContact(jid); - boolean bothPre = - contact.getOption(Contact.Options.TO) - && contact.getOption(Contact.Options.FROM); - if (!contact.getOption(Contact.Options.DIRTY_PUSH)) { - contact.setServerName(name); - contact.parseGroupsFromElement(item); - } - if ("remove".equals(subscription)) { - contact.resetOption(Contact.Options.IN_ROSTER); - contact.resetOption(Contact.Options.DIRTY_DELETE); - contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); - } else { - contact.setOption(Contact.Options.IN_ROSTER); - contact.resetOption(Contact.Options.DIRTY_PUSH); - contact.parseSubscriptionFromElement(item); - } - boolean both = - contact.getOption(Contact.Options.TO) - && contact.getOption(Contact.Options.FROM); - if ((both != bothPre) && both) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": gained mutual presence subscription with " - + contact.getJid()); - AxolotlService axolotlService = account.getAxolotlService(); - if (axolotlService != null) { - axolotlService.clearErrorsInFetchStatusMap(contact.getJid()); - } - } - mXmppConnectionService.getAvatarService().clear(contact); - } - } - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.updateRosterUi(); - mXmppConnectionService.getShortcutService().refresh(); - mXmppConnectionService.syncRoster(account); - } - - public String avatarData(final IqPacket packet) { - final Element pubsub = packet.findChild("pubsub", Namespace.PUB_SUB); - if (pubsub == null) { - return null; - } - final Element items = pubsub.findChild("items"); - if (items == null) { - return null; - } - return super.avatarData(items); - } - - public Element getItem(final IqPacket packet) { - final Element pubsub = packet.findChild("pubsub", Namespace.PUB_SUB); - if (pubsub == null) { - return null; - } - final Element items = pubsub.findChild("items"); - if (items == null) { - return null; - } - return items.findChild("item"); - } - - @NonNull - public Set deviceIds(final Element item) { - Set deviceIds = new HashSet<>(); - if (item != null) { - final Element list = item.findChild("list"); - if (list != null) { - for (Element device : list.getChildren()) { - if (!device.getName().equals("device")) { - continue; - } - try { - Integer id = Integer.valueOf(device.getAttribute("id")); - deviceIds.add(id); - } catch (NumberFormatException e) { - Log.e( - Config.LOGTAG, - AxolotlService.LOGPREFIX - + " : " - + "Encountered invalid node in PEP (" - + e.getMessage() - + "):" - + device.toString() - + ", skipping..."); - } - } - } - } - return deviceIds; - } - - private Integer signedPreKeyId(final Element bundle) { - final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic"); - if (signedPreKeyPublic == null) { - return null; - } - try { - return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId")); - } catch (NumberFormatException e) { - return null; - } - } - - private ECPublicKey signedPreKeyPublic(final Element bundle) { - ECPublicKey publicKey = null; - final String signedPreKeyPublic = bundle.findChildContent("signedPreKeyPublic"); - if (signedPreKeyPublic == null) { - return null; - } - try { - publicKey = Curve.decodePoint(base64decode(signedPreKeyPublic), 0); - } catch (final IllegalArgumentException | InvalidKeyException e) { - Log.e( - Config.LOGTAG, - AxolotlService.LOGPREFIX - + " : " - + "Invalid signedPreKeyPublic in PEP: " - + e.getMessage()); - } - return publicKey; - } - - private byte[] signedPreKeySignature(final Element bundle) { - final String signedPreKeySignature = bundle.findChildContent("signedPreKeySignature"); - if (signedPreKeySignature == null) { - return null; - } - try { - return base64decode(signedPreKeySignature); - } catch (final IllegalArgumentException e) { - Log.e( - Config.LOGTAG, - AxolotlService.LOGPREFIX + " : Invalid base64 in signedPreKeySignature"); - return null; - } - } - - private IdentityKey identityKey(final Element bundle) { - final String identityKey = bundle.findChildContent("identityKey"); - if (identityKey == null) { - return null; - } - try { - return new IdentityKey(base64decode(identityKey), 0); - } catch (final IllegalArgumentException | InvalidKeyException e) { - Log.e( - Config.LOGTAG, - AxolotlService.LOGPREFIX - + " : " - + "Invalid identityKey in PEP: " - + e.getMessage()); - return null; - } - } - - public Map preKeyPublics(final IqPacket packet) { - Map preKeyRecords = new HashMap<>(); - Element item = getItem(packet); - if (item == null) { - Log.d( - Config.LOGTAG, - AxolotlService.LOGPREFIX - + " : " - + "Couldn't find in bundle IQ packet: " - + packet); - return null; - } - final Element bundleElement = item.findChild("bundle"); - if (bundleElement == null) { - return null; - } - final Element prekeysElement = bundleElement.findChild("prekeys"); - if (prekeysElement == null) { - Log.d( - Config.LOGTAG, - AxolotlService.LOGPREFIX - + " : " - + "Couldn't find in bundle IQ packet: " - + packet); - return null; - } - for (Element preKeyPublicElement : prekeysElement.getChildren()) { - if (!preKeyPublicElement.getName().equals("preKeyPublic")) { - Log.d( - Config.LOGTAG, - AxolotlService.LOGPREFIX - + " : " - + "Encountered unexpected tag in prekeys list: " - + preKeyPublicElement); - continue; - } - final String preKey = preKeyPublicElement.getContent(); - if (preKey == null) { - continue; - } - Integer preKeyId = null; - try { - preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId")); - final ECPublicKey preKeyPublic = Curve.decodePoint(base64decode(preKey), 0); - preKeyRecords.put(preKeyId, preKeyPublic); - } catch (NumberFormatException e) { - Log.e( - Config.LOGTAG, - AxolotlService.LOGPREFIX - + " : " - + "could not parse preKeyId from preKey " - + preKeyPublicElement.toString()); - } catch (Throwable e) { - Log.e( - Config.LOGTAG, - AxolotlService.LOGPREFIX - + " : " - + "Invalid preKeyPublic (ID=" - + preKeyId - + ") in PEP: " - + e.getMessage() - + ", skipping..."); - } - } - return preKeyRecords; - } - - private static byte[] base64decode(String input) { - return BaseEncoding.base64().decode(CharMatcher.whitespace().removeFrom(input)); - } - - public Pair verification(final IqPacket packet) { - Element item = getItem(packet); - Element verification = - item != null ? item.findChild("verification", AxolotlService.PEP_PREFIX) : null; - Element chain = verification != null ? verification.findChild("chain") : null; - String signature = verification != null ? verification.findChildContent("signature") : null; - if (chain != null && signature != null) { - List certElements = chain.getChildren(); - X509Certificate[] certificates = new X509Certificate[certElements.size()]; - try { - CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - int i = 0; - for (final Element certElement : certElements) { - final String cert = certElement.getContent(); - if (cert == null) { - continue; - } - certificates[i] = - (X509Certificate) - certificateFactory.generateCertificate( - new ByteArrayInputStream( - BaseEncoding.base64().decode(cert))); - ++i; - } - return new Pair<>(certificates, BaseEncoding.base64().decode(signature)); - } catch (CertificateException e) { - return null; - } - } else { - return null; - } - } - - public PreKeyBundle bundle(final IqPacket bundle) { - final Element bundleItem = getItem(bundle); - if (bundleItem == null) { - return null; - } - final Element bundleElement = bundleItem.findChild("bundle"); - if (bundleElement == null) { - return null; - } - final ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement); - final Integer signedPreKeyId = signedPreKeyId(bundleElement); - final byte[] signedPreKeySignature = signedPreKeySignature(bundleElement); - final IdentityKey identityKey = identityKey(bundleElement); - if (signedPreKeyId == null - || signedPreKeyPublic == null - || identityKey == null - || signedPreKeySignature == null - || signedPreKeySignature.length == 0) { - return null; - } - return new PreKeyBundle( - 0, - 0, - 0, - null, - signedPreKeyId, - signedPreKeyPublic, - signedPreKeySignature, - identityKey); - } - - public List preKeys(final IqPacket preKeys) { - List bundles = new ArrayList<>(); - Map preKeyPublics = preKeyPublics(preKeys); - if (preKeyPublics != null) { - for (Integer preKeyId : preKeyPublics.keySet()) { - ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId); - bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic, 0, null, null, null)); - } - } - - return bundles; - } - - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - final boolean isGet = packet.getType() == IqPacket.TYPE.GET; - if (packet.getType() == IqPacket.TYPE.ERROR || packet.getType() == IqPacket.TYPE.TIMEOUT) { - return; - } - if (packet.hasChild("query", Namespace.ROSTER) && packet.fromServer(account)) { - final Element query = packet.findChild("query"); - // If this is in response to a query for the whole roster: - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.getRoster().markAllAsNotInRoster(); - } - this.rosterItems(account, query); - } else if ((packet.hasChild("block", Namespace.BLOCKING) - || packet.hasChild("blocklist", Namespace.BLOCKING)) - && packet.fromServer(account)) { - // Block list or block push. - Log.d(Config.LOGTAG, "Received blocklist update from server"); - final Element blocklist = packet.findChild("blocklist", Namespace.BLOCKING); - final Element block = packet.findChild("block", Namespace.BLOCKING); - final Collection items = - blocklist != null - ? blocklist.getChildren() - : (block != null ? block.getChildren() : null); - // If this is a response to a blocklist query, clear the block list and replace with the - // new one. - // Otherwise, just update the existing blocklist. - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.clearBlocklist(); - account.getXmppConnection().getFeatures().setBlockListRequested(true); - } - if (items != null) { - final Collection jids = new ArrayList<>(items.size()); - // Create a collection of Jids from the packet - for (final Element item : items) { - if (item.getName().equals("item")) { - final Jid jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid")); - if (jid != null) { - jids.add(jid); - } - } - } - account.getBlocklist().addAll(jids); - if (packet.getType() == IqPacket.TYPE.SET) { - boolean removed = false; - for (Jid jid : jids) { - removed |= mXmppConnectionService.removeBlockedConversations(account, jid); - } - if (removed) { - mXmppConnectionService.updateConversationUi(); - } - } - } - // Update the UI - mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); - if (packet.getType() == IqPacket.TYPE.SET) { - final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); - mXmppConnectionService.sendIqPacket(account, response, null); - } - } else if (packet.hasChild("unblock", Namespace.BLOCKING) - && packet.fromServer(account) - && packet.getType() == IqPacket.TYPE.SET) { - Log.d(Config.LOGTAG, "Received unblock update from server"); - final Collection items = - packet.findChild("unblock", Namespace.BLOCKING).getChildren(); - if (items.size() == 0) { - // No children to unblock == unblock all - account.getBlocklist().clear(); - } else { - final Collection jids = new ArrayList<>(items.size()); - for (final Element item : items) { - if (item.getName().equals("item")) { - final Jid jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid")); - if (jid != null) { - jids.add(jid); - } - } - } - account.getBlocklist().removeAll(jids); - } - mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); - final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); - mXmppConnectionService.sendIqPacket(account, response, null); - } else if (packet.hasChild("open", "http://jabber.org/protocol/ibb") - || packet.hasChild("data", "http://jabber.org/protocol/ibb") - || packet.hasChild("close", "http://jabber.org/protocol/ibb")) { - mXmppConnectionService.getJingleConnectionManager().deliverIbbPacket(account, packet); - } else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) { - final IqPacket response = - mXmppConnectionService.getIqGenerator().discoResponse(account, packet); - mXmppConnectionService.sendIqPacket(account, response, null); - } else if (packet.hasChild("query", "jabber:iq:version") && isGet) { - final IqPacket response = - mXmppConnectionService.getIqGenerator().versionResponse(packet); - mXmppConnectionService.sendIqPacket(account, response, null); - } else if (packet.hasChild("ping", "urn:xmpp:ping") && isGet) { - final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); - mXmppConnectionService.sendIqPacket(account, response, null); - } else if (packet.hasChild("time", "urn:xmpp:time") && isGet) { - final IqPacket response; - if (mXmppConnectionService.useTorToConnect() || account.isOnion()) { - response = packet.generateResponse(IqPacket.TYPE.ERROR); - final Element error = response.addChild("error"); - error.setAttribute("type", "cancel"); - error.addChild("not-allowed", "urn:ietf:params:xml:ns:xmpp-stanzas"); - } else { - response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet); - } - mXmppConnectionService.sendIqPacket(account, response, null); - } else if (packet.hasChild("push", Namespace.UNIFIED_PUSH) - && packet.getType() == IqPacket.TYPE.SET) { - final Jid transport = packet.getFrom(); - final Element push = packet.findChild("push", Namespace.UNIFIED_PUSH); - final boolean success = - push != null - && mXmppConnectionService.processUnifiedPushMessage( - account, transport, push); - final IqPacket response; - if (success) { - response = packet.generateResponse(IqPacket.TYPE.RESULT); - } else { - response = packet.generateResponse(IqPacket.TYPE.ERROR); - final Element error = response.addChild("error"); - error.setAttribute("type", "cancel"); - error.setAttribute("code", "404"); - error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas"); - } - mXmppConnectionService.sendIqPacket(account, response, null); - } else { - if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) { - final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); - final Element error = response.addChild("error"); - error.setAttribute("type", "cancel"); - error.addChild("feature-not-implemented", "urn:ietf:params:xml:ns:xmpp-stanzas"); - account.getXmppConnection().sendIqPacket(response, null); - } - } - } -} diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java deleted file mode 100644 index 2fa4beaef..000000000 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ /dev/null @@ -1,1332 +0,0 @@ -package eu.siacs.conversations.parser; - -import android.util.Log; -import android.util.Pair; - -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -import android.os.Build; -import android.text.Html; - -import net.java.otr4j.session.Session; -import net.java.otr4j.session.SessionStatus; -import eu.siacs.conversations.crypto.OtrService; -import eu.siacs.conversations.entities.Presence; -import eu.siacs.conversations.entities.ServiceDiscoveryResult; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.crypto.axolotl.BrokenSessionException; -import eu.siacs.conversations.crypto.axolotl.NotEncryptedForThisDeviceException; -import eu.siacs.conversations.crypto.axolotl.OutdatedSenderException; -import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Bookmark; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Conversational; -import eu.siacs.conversations.entities.Edit; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.entities.ReadByMarker; -import eu.siacs.conversations.entities.ReceiptRequest; -import eu.siacs.conversations.entities.RtpSessionStatus; -import eu.siacs.conversations.http.HttpConnectionManager; -import eu.siacs.conversations.services.MessageArchiveService; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xml.LocalizedContent; -import eu.siacs.conversations.xmpp.InvalidJid; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnMessagePacketReceived; -import eu.siacs.conversations.xmpp.chatstate.ChatState; -import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; -import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; - - - -public class MessageParser extends AbstractParser implements OnMessagePacketReceived { - - private static final List CLIENTS_SENDING_HTML_IN_OTR = Arrays.asList("Pidgin", "Adium", "Trillian"); - - private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH); - - private static final List JINGLE_MESSAGE_ELEMENT_NAMES = Arrays.asList("accept", "propose", "proceed", "reject", "retract"); - - public MessageParser(XmppConnectionService service) { - super(service); - } - - private static String extractStanzaId(Element packet, boolean isTypeGroupChat, Conversation conversation) { - final Jid by; - final boolean safeToExtract; - if (isTypeGroupChat) { - by = conversation.getJid().asBareJid(); - safeToExtract = conversation.getMucOptions().hasFeature(Namespace.STANZA_IDS); - } else { - Account account = conversation.getAccount(); - by = account.getJid().asBareJid(); - safeToExtract = account.getXmppConnection().getFeatures().stanzaIds(); - } - return safeToExtract ? extractStanzaId(packet, by) : null; - } - - private static String extractStanzaId(Account account, Element packet) { - final boolean safeToExtract = account.getXmppConnection().getFeatures().stanzaIds(); - return safeToExtract ? extractStanzaId(packet, account.getJid().asBareJid()) : null; - } - - private static String extractStanzaId(Element packet, Jid by) { - for (Element child : packet.getChildren()) { - if (child.getName().equals("stanza-id") - && Namespace.STANZA_IDS.equals(child.getNamespace()) - && by.equals(InvalidJid.getNullForInvalid(child.getAttributeAsJid("by")))) { - return child.getAttribute("id"); - } - } - return null; - } - - private static Jid getTrueCounterpart(Element mucUserElement, Jid fallback) { - final Element item = mucUserElement == null ? null : mucUserElement.findChild("item"); - Jid result = item == null ? null : InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid")); - return result != null ? result : fallback; - } - - private static boolean clientMightSendHtml(Account account, Jid from) { - String resource = from.getResource(); - if (resource == null) { - return false; - } - Presence presence = account.getRoster().getContact(from).getPresences().getPresencesMap().get(resource); - ServiceDiscoveryResult disco = presence == null ? null : presence.getServiceDiscoveryResult(); - if (disco == null) { - return false; - } - return hasIdentityKnowForSendingHtml(disco.getIdentities()); - } - - private static boolean hasIdentityKnowForSendingHtml(List identities) { - for (ServiceDiscoveryResult.Identity identity : identities) { - if (identity.getName() != null) { - if (CLIENTS_SENDING_HTML_IN_OTR.contains(identity.getName())) { - return true; - } - } - } - return false; - } - - - private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final MessagePacket packet) { - ChatState state = ChatState.parse(packet); - if (state != null && c != null) { - final Account account = c.getAccount(); - final Jid from = packet.getFrom(); - if (from.asBareJid().equals(account.getJid().asBareJid())) { - c.setOutgoingChatState(state); - if (state == ChatState.ACTIVE || state == ChatState.COMPOSING) { - if (c.getContact().isSelf()) { - return false; - } - mXmppConnectionService.markRead(c); - activateGracePeriod(account); - } - return false; - } else { - if (isTypeGroupChat) { - MucOptions.User user = c.getMucOptions().findUserByFullJid(from); - if (user != null) { - return user.setChatState(state); - } else { - return false; - } - } else { - return c.setIncomingChatState(state); - } - } - } - return false; - } - - private Message parseOtrChat(String body, Jid from, String id, Conversation conversation) { - String presence; - if (from.isBareJid()) { - presence = ""; - } else { - presence = from.getResource(); - } - if (body.matches("^\\?OTRv\\d{1,2}\\?.*")) { - conversation.endOtrIfNeeded(); - } - if (!conversation.hasValidOtrSession()) { - conversation.startOtrSession(presence, false); - } else { - String foreignPresence = conversation.getOtrSession().getSessionID().getUserID(); - if (!foreignPresence.equals(presence)) { - conversation.endOtrIfNeeded(); - conversation.startOtrSession(presence, false); - } - } - try { - conversation.setLastReceivedOtrMessageId(id); - Session otrSession = conversation.getOtrSession(); - body = otrSession.transformReceiving(body); - SessionStatus status = otrSession.getSessionStatus(); - if (body == null && status == SessionStatus.ENCRYPTED) { - mXmppConnectionService.onOtrSessionEstablished(conversation); - return null; - } else if (body == null && status == SessionStatus.FINISHED) { - conversation.resetOtrSession(); - mXmppConnectionService.updateConversationUi(); - return null; - } else if (body == null || (body.isEmpty())) { - return null; - } - if (body.startsWith(CryptoHelper.FILETRANSFER)) { - String key = body.substring(CryptoHelper.FILETRANSFER.length()); - conversation.setSymmetricKey(CryptoHelper.hexToBytes(key)); - return null; - } - if (clientMightSendHtml(conversation.getAccount(), from)) { - Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": received OTR message from bad behaving client. escaping HTML…"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - body = Html.fromHtml(body, Html.FROM_HTML_MODE_LEGACY).toString(); - } else { - body = Html.fromHtml(body).toString(); - } - } - - final OtrService otrService = conversation.getAccount().getOtrService(); - Message finishedMessage = new Message(conversation, body, Message.ENCRYPTION_OTR, Message.STATUS_RECEIVED); - finishedMessage.setFingerprint(otrService.getFingerprint(otrSession.getRemotePublicKey())); - conversation.setLastReceivedOtrMessageId(null); - - return finishedMessage; - } catch (Exception e) { - conversation.resetOtrSession(); - return null; - } - } - - - private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status, final boolean checkedForDuplicates, boolean postpone) { - final AxolotlService service = conversation.getAccount().getAxolotlService(); - final XmppAxolotlMessage xmppAxolotlMessage; - try { - xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.asBareJid()); - } catch (Exception e) { - Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": invalid omemo message received " + e.getMessage()); - return null; - } - if (xmppAxolotlMessage.hasPayload()) { - final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage; - try { - plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage, postpone); - } catch (BrokenSessionException e) { - if (checkedForDuplicates) { - if (service.trustedOrPreviouslyResponded(from.asBareJid())) { - service.reportBrokenSessionException(e, postpone); - return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status); - } else { - Log.d(Config.LOGTAG, "ignoring broken session exception because contact was not trusted"); - return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status); - } - } else { - Log.d(Config.LOGTAG, "ignoring broken session exception because checkForDuplicates failed"); - return null; - } - } catch (NotEncryptedForThisDeviceException e) { - return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE, status); - } catch (OutdatedSenderException e) { - return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status); - } - if (plaintextMessage != null) { - Message finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status); - finishedMessage.setFingerprint(plaintextMessage.getFingerprint()); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount()) + " Received Message with session fingerprint: " + plaintextMessage.getFingerprint()); - return finishedMessage; - } - } else { - Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": received OMEMO key transport message"); - service.processReceivingKeyTransportMessage(xmppAxolotlMessage, postpone); - } - return null; - } - - private Invite extractInvite(Element message) { - final Element mucUser = message.findChild("x", Namespace.MUC_USER); - if (mucUser != null) { - Element invite = mucUser.findChild("invite"); - if (invite != null) { - String password = mucUser.findChildContent("password"); - Jid from = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("from")); - Jid room = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from")); - if (room == null) { - return null; - } - return new Invite(room, password, false, from); - } - } - final Element conference = message.findChild("x", "jabber:x:conference"); - if (conference != null) { - Jid from = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from")); - Jid room = InvalidJid.getNullForInvalid(conference.getAttributeAsJid("jid")); - if (room == null) { - return null; - } - return new Invite(room, conference.getAttribute("password"), true, from); - } - return null; - } - - private void parseEvent(final Element event, final Jid from, final Account account) { - final Element items = event.findChild("items"); - final String node = items == null ? null : items.getAttribute("node"); - if (Namespace.NICK.equals(node)) { - final Element i = items.findChild("item"); - final String nick = i == null ? null : i.findChildContent("nick", Namespace.NICK); - if (nick != null) { - setNick(account, from, nick); - } - } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) { - Element item = items.findChild("item"); - Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received PEP device list " + deviceIds + " update from " + from + ", processing... "); - final AxolotlService axolotlService = account.getAxolotlService(); - axolotlService.registerDevices(from, deviceIds); - } else if (Namespace.BOOKMARKS.equals(node) && account.getJid().asBareJid().equals(from)) { - if (account.getXmppConnection().getFeatures().bookmarksConversion()) { - final Element i = items.findChild("item"); - final Element storage = i == null ? null : i.findChild("storage", Namespace.BOOKMARKS); - Map bookmarks = Bookmark.parseFromStorage(storage, account); - mXmppConnectionService.processBookmarksInitial(account, bookmarks, true); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": processing bookmark PEP event"); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring bookmark PEP event because bookmark conversion was not detected"); - } - } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) { - final Element item = items.findChild("item"); - final Element retract = items.findChild("retract"); - if (item != null) { - final Bookmark bookmark = Bookmark.parseFromItem(item, account); - if (bookmark != null) { - account.putBookmark(bookmark); - mXmppConnectionService.processModifiedBookmark(bookmark); - mXmppConnectionService.updateConversationUi(); - } - } - if (retract != null) { - final Jid id = InvalidJid.getNullForInvalid(retract.getAttributeAsJid("id")); - if (id != null) { - account.removeBookmark(id); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmark for " + id); - mXmppConnectionService.processDeletedBookmark(account, id); - mXmppConnectionService.updateConversationUi(); - } - } - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + " received pubsub notification for node=" + node); - } - } - - private void parseDeleteEvent(final Element event, final Jid from, final Account account) { - final Element delete = event.findChild("delete"); - final String node = delete == null ? null : delete.getAttribute("node"); - if (Namespace.NICK.equals(node)) { - Log.d(Config.LOGTAG, "parsing nick delete event from " + from); - setNick(account, from, null); - } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) { - account.setBookmarks(Collections.emptyMap()); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmarks node"); - } else if (Namespace.AVATAR_METADATA.equals(node) && account.getJid().asBareJid().equals(from)) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": deleted avatar metadata node"); - } - } - - private void parsePurgeEvent(final Element event, final Jid from, final Account account) { - final Element purge = event.findChild("purge"); - final String node = purge == null ? null : purge.getAttribute("node"); - if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) { - account.setBookmarks(Collections.emptyMap()); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": purged bookmarks"); - } - } - - private void setNick(Account account, Jid user, String nick) { - if (user.asBareJid().equals(account.getJid().asBareJid())) { - account.setDisplayName(nick); - } else { - Contact contact = account.getRoster().getContact(user); - if (contact.setPresenceName(nick)) { - mXmppConnectionService.syncRoster(account); - mXmppConnectionService.getAvatarService().clear(contact); - } - } - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.updateAccountUi(); - } - - private boolean handleErrorMessage(final Account account, final MessagePacket packet) { - if (packet.getType() == MessagePacket.TYPE_ERROR) { - if (packet.fromServer(account)) { - final Pair forwarded = packet.getForwardedMessagePacket("received", Namespace.CARBONS); - if (forwarded != null) { - return handleErrorMessage(account, forwarded.first); - } - } - final Jid from = packet.getFrom(); - final String id = packet.getId(); - if (from != null && id != null) { - final Message message = mXmppConnectionService.markMessage(account, - from.asBareJid(), - packet.getId(), - Message.STATUS_SEND_FAILED, - extractErrorMessage(packet)); - if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) { - final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length()); - mXmppConnectionService.getJingleConnectionManager() - .updateProposedSessionDiscovered(account, from, sessionId, JingleConnectionManager.DeviceDiscoveryState.FAILED); - return true; - } - if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX)) { - final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROCEED_ID_PREFIX.length()); - mXmppConnectionService.getJingleConnectionManager().failProceed(account, from, sessionId); - return true; - } - mXmppConnectionService.markMessage(account, - from.asBareJid(), - id, - Message.STATUS_SEND_FAILED, - extractErrorMessage(packet)); - final Element error = packet.findChild("error"); - final boolean pingWorthyError = error != null && (error.hasChild("not-acceptable") || error.hasChild("remote-server-timeout") || error.hasChild("remote-server-not-found")); - if (pingWorthyError) { - Conversation conversation = mXmppConnectionService.find(account, from); - if (conversation != null && conversation.getMode() == Conversational.MODE_MULTI) { - if (conversation.getMucOptions().online()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ping worthy error for seemingly online muc at " + from); - mXmppConnectionService.mucSelfPingAndRejoin(conversation); - } - } - } - - if (message != null) { - if (message.getEncryption() == Message.ENCRYPTION_OTR) { - Conversation conversation = (Conversation) message.getConversation(); - conversation.endOtrIfNeeded(); - } - } - - - } - return true; - } - return false; - } - - @Override - public void onMessagePacketReceived(Account account, MessagePacket original) { - if (handleErrorMessage(account, original)) { - return; - } - final MessagePacket packet; - Long timestamp = null; - final boolean isForwarded; - boolean isCarbon = false; - String serverMsgId = null; - final Element fin = original.findChild("fin", MessageArchiveService.Version.MAM_0.namespace); - if (fin != null) { - mXmppConnectionService.getMessageArchiveService().processFinLegacy(fin, original.getFrom()); - return; - } - final Element result = MessageArchiveService.Version.findResult(original); - final String queryId = result == null ? null : result.getAttribute("queryid"); - final MessageArchiveService.Query query = queryId == null ? null : mXmppConnectionService.getMessageArchiveService().findQuery(queryId); - if (query != null && query.validFrom(original.getFrom())) { - final Pair f = original.getForwardedMessagePacket("result", query.version.namespace); - if (f == null) { - return; - } - timestamp = f.second; - packet = f.first; - isForwarded = true; - serverMsgId = result.getAttribute("id"); - query.incrementMessageCount(); - if (handleErrorMessage(account, packet)) { - return; - } - } else if (query != null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received mam result with invalid from (" + original.getFrom() + ") or queryId (" + queryId + ")"); - return; - } else if (original.fromServer(account)) { - Pair f; - f = original.getForwardedMessagePacket("received", Namespace.CARBONS); - f = f == null ? original.getForwardedMessagePacket("sent", Namespace.CARBONS) : f; - packet = f != null ? f.first : original; - if (handleErrorMessage(account, packet)) { - return; - } - timestamp = f != null ? f.second : null; - isCarbon = f != null; - isForwarded = isCarbon; - } else { - packet = original; - isForwarded = false; - } - - if (timestamp == null) { - timestamp = AbstractParser.parseTimestamp(original, AbstractParser.parseTimestamp(packet)); - } - final Element mucUserElement = packet.findChild("x", Namespace.MUC_USER); - final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted"); - final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0"); - final Element oob = packet.findChild("x", Namespace.OOB); - final String oobUrl = oob != null ? oob.findChildContent("url") : null; - final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id"); - - final Element applyToElement = packet.findChild("apply-to", "urn:xmpp:fasten:0"); - final String retractId = applyToElement != null && applyToElement.findChild("retract", "urn:xmpp:message-retract:0") != null ? applyToElement.getAttribute("id") : null; - - if (packet.getBody() == null && retractId != null) { //It's RECOMMENDED that you include a Fallback Indication (XEP-0428) [6] tag with fallback text in the , so that older clients can still indicate the intent to retract and so that older servers will archive the retraction. - //Otherwhise the following code will not execute the retraction, because it searchs for body content! - packet.setBody("This person attempted to retract a previous message, but it's unsupported by your client."); - } - - final LocalizedContent body = packet.getBody(); - - final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); - int status; - final Jid counterpart; - final Jid to = packet.getTo(); - final Jid from = packet.getFrom(); - final Element originId = packet.findChild("origin-id", Namespace.STANZA_IDS); - final String remoteMsgId; - if (originId != null && originId.getAttribute("id") != null) { - remoteMsgId = originId.getAttribute("id"); - } else { - remoteMsgId = packet.getId(); - } - boolean notify = false; - - if (from == null || !InvalidJid.isValid(from) || !InvalidJid.isValid(to)) { - Log.e(Config.LOGTAG, "encountered invalid message from='" + from + "' to='" + to + "'"); - return; - } - - boolean isTypeGroupChat = packet.getType() == MessagePacket.TYPE_GROUPCHAT; - if (query != null && !query.muc() && isTypeGroupChat) { - Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": received groupchat (" + from + ") message on regular MAM request. skipping"); - return; - } - boolean isProperlyAddressed = (to != null) && (!to.isBareJid() || account.countPresences() == 0); - boolean isMucStatusMessage = InvalidJid.hasValidFrom(packet) && from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status"); - boolean selfAddressed; - if (packet.fromAccount(account)) { - status = Message.STATUS_SEND; - selfAddressed = to == null || account.getJid().asBareJid().equals(to.asBareJid()); - if (selfAddressed) { - counterpart = from; - } else { - counterpart = to != null ? to : account.getJid(); - } - } else { - status = Message.STATUS_RECEIVED; - counterpart = from; - selfAddressed = false; - } - - final Invite invite = extractInvite(packet); - if (invite != null) { - if (isTypeGroupChat) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring invite to " + invite.jid + " because type=groupchat"); - } else if (invite.direct && (mucUserElement != null || invite.inviter == null || mXmppConnectionService.isMuc(account, invite.inviter))) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring direct invite to " + invite.jid + " because it was received in MUC"); - } else { - invite.execute(account, packet.getBody()); - return; - } - } - - if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null) && !isMucStatusMessage) { - final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain().toEscapedString()); - final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false); - final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI; - - if (serverMsgId == null) { - serverMsgId = extractStanzaId(packet, isTypeGroupChat, conversation); - } - - if (selfAddressed) { - if (mXmppConnectionService.markMessage(conversation, remoteMsgId, Message.STATUS_SEND_RECEIVED, serverMsgId)) { - return; - } - status = Message.STATUS_RECEIVED; - if (remoteMsgId != null && conversation.findMessageWithRemoteId(remoteMsgId, counterpart) != null) { - return; - } - } - - if (isTypeGroupChat) { - if (conversation.getMucOptions().isSelf(counterpart)) { - status = Message.STATUS_SEND_RECEIVED; - isCarbon = true; //not really carbon but received from another resource - if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status, serverMsgId, body)) { - return; - } else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) { - if (body != null) { - Message message = conversation.findSentMessageWithBody(body.content); - if (message != null) { - mXmppConnectionService.markMessage(message, status); - return; - } - } - } - } else { - status = Message.STATUS_RECEIVED; - } - } - final Message message; - if (body != null && body.content.startsWith("?OTR") && Config.supportOtr()) { - if (!isForwarded && !isTypeGroupChat && isProperlyAddressed && !conversationMultiMode) { - message = parseOtrChat(body.content, from, remoteMsgId, conversation); - if (message == null) { - return; - } - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring OTR message from " + from + " isForwarded=" + Boolean.toString(isForwarded) + ", isProperlyAddressed=" + Boolean.valueOf(isProperlyAddressed)); - message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status); - if (body.count > 1) { - message.setBodyLanguage(body.language); - } - } - } else if (pgpEncrypted != null && Config.supportOpenPgp()) { - message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status); - } else if (axolotlEncrypted != null && Config.supportOmemo()) { - Jid origin; - Set fallbacksBySourceId = Collections.emptySet(); - if (conversationMultiMode) { - final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart); - origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback); - if (origin == null) { - try { - fallbacksBySourceId = account.getAxolotlService().findCounterpartsBySourceId(XmppAxolotlMessage.parseSourceId(axolotlEncrypted)); - } catch (IllegalArgumentException e) { - //ignoring - } - } - if (origin == null && fallbacksBySourceId.size() == 0) { - Log.d(Config.LOGTAG, "axolotl message in anonymous conference received and no possible fallbacks"); - return; - } - } else { - fallbacksBySourceId = Collections.emptySet(); - origin = from; - } - - final boolean liveMessage = query == null && !isTypeGroupChat && mucUserElement == null; - final boolean checkedForDuplicates = liveMessage || (serverMsgId != null && remoteMsgId != null && !conversation.possibleDuplicate(serverMsgId, remoteMsgId)); - - if (origin != null) { - message = parseAxolotlChat(axolotlEncrypted, origin, conversation, status, checkedForDuplicates, query != null); - } else { - Message trial = null; - for (Jid fallback : fallbacksBySourceId) { - trial = parseAxolotlChat(axolotlEncrypted, fallback, conversation, status, checkedForDuplicates && fallbacksBySourceId.size() == 1, query != null); - if (trial != null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": decoded muc message using fallback"); - origin = fallback; - break; - } - } - message = trial; - } - if (message == null) { - if (query == null && extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet)) { - mXmppConnectionService.updateConversationUi(); - } - if (query != null && status == Message.STATUS_SEND && remoteMsgId != null) { - Message previouslySent = conversation.findSentMessageWithUuid(remoteMsgId); - if (previouslySent != null && previouslySent.getServerMsgId() == null && serverMsgId != null) { - previouslySent.setServerMsgId(serverMsgId); - mXmppConnectionService.databaseBackend.updateMessage(previouslySent, false); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": encountered previously sent OMEMO message without serverId. updating..."); - } - } - return; - } - if (conversationMultiMode) { - message.setTrueCounterpart(origin); - } - } else if (body == null && oobUrl != null) { - message = new Message(conversation, oobUrl, Message.ENCRYPTION_NONE, status); - message.setOob(true); - if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) { - message.setEncryption(Message.ENCRYPTION_DECRYPTED); - } - } else { - message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status); - if (body.count > 1) { - message.setBodyLanguage(body.language); - } - } - - message.setCounterpart(counterpart); - message.setRemoteMsgId(remoteMsgId); - message.setServerMsgId(serverMsgId); - message.setCarbon(isCarbon); - message.setTime(timestamp); - if (body != null && body.content != null && body.content.equals(oobUrl)) { - message.setOob(true); - if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) { - message.setEncryption(Message.ENCRYPTION_DECRYPTED); - } - } - message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0"); - if (conversationMultiMode) { - message.setMucUser(conversation.getMucOptions().findUserByFullJid(counterpart)); - final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart); - Jid trueCounterpart; - if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { - trueCounterpart = message.getTrueCounterpart(); - } else if (query != null && query.safeToExtractTrueCounterpart()) { - trueCounterpart = getTrueCounterpart(mucUserElement, fallback); - } else { - trueCounterpart = fallback; - } - if (trueCounterpart != null && isTypeGroupChat) { - if (trueCounterpart.asBareJid().equals(account.getJid().asBareJid())) { - status = isTypeGroupChat ? Message.STATUS_SEND_RECEIVED : Message.STATUS_SEND; - } else { - status = Message.STATUS_RECEIVED; - message.setCarbon(false); - } - } - message.setStatus(status); - message.setTrueCounterpart(trueCounterpart); - if (!isTypeGroupChat) { - message.setType(Message.TYPE_PRIVATE); - } - } else { - updateLastseen(account, from); - } - - if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) { - Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId, - counterpart, - message.getStatus() == Message.STATUS_RECEIVED, - message.isCarbon()); - - if (replacedMessage == null) { - replacedMessage = conversation.findSentMessageWithUuidOrRemoteId(replacementId, true, true); - } - if (replacedMessage != null) { - final boolean fingerprintsMatch = replacedMessage.getFingerprint() == null - || replacedMessage.getFingerprint().equals(message.getFingerprint()); - final boolean trueCountersMatch = replacedMessage.getTrueCounterpart() != null - && message.getTrueCounterpart() != null - && replacedMessage.getTrueCounterpart().asBareJid().equals(message.getTrueCounterpart().asBareJid()); - final boolean mucUserMatches = query == null && replacedMessage.sameMucUser(message); //can not be checked when using mam - final boolean duplicate = conversation.hasDuplicateMessage(message); - if (fingerprintsMatch && (trueCountersMatch || !conversationMultiMode || mucUserMatches) && !duplicate) { - Log.d(Config.LOGTAG, "replaced message '" + replacedMessage.getBody() + "' with '" + message.getBody() + "'"); - synchronized (replacedMessage) { - final String uuid = replacedMessage.getUuid(); - - replacedMessage.putEdited(replacedMessage.getRemoteMsgId() != null ? replacedMessage.getRemoteMsgId() : replacedMessage.getUuid(), replacedMessage.getServerMsgId(), replacedMessage.getBody(), replacedMessage.getTimeSent()); - - replacedMessage.setUuid(UUID.randomUUID().toString()); - replacedMessage.setBody(message.getBody()); - replacedMessage.setRemoteMsgId(remoteMsgId); - if (replacedMessage.getServerMsgId() == null || message.getServerMsgId() != null) { - replacedMessage.setServerMsgId(message.getServerMsgId()); - } - replacedMessage.setEncryption(message.getEncryption()); - if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) { - replacedMessage.markUnread(); - } - extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet); - mXmppConnectionService.updateMessage(replacedMessage, uuid); - if (mXmppConnectionService.confirmMessages() - && replacedMessage.getStatus() == Message.STATUS_RECEIVED - && (replacedMessage.trusted() || replacedMessage.isPrivateMessage()) //TODO do we really want to send receipts for all PMs? - && remoteMsgId != null - && !selfAddressed - && !isTypeGroupChat) { - processMessageReceipts(account, packet, remoteMsgId, query); - } - if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) { - conversation.getAccount().getPgpDecryptionService().discard(replacedMessage); - conversation.getAccount().getPgpDecryptionService().decrypt(replacedMessage, false); - } - } - mXmppConnectionService.getNotificationService().updateNotification(); - return; - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received message correction but verification didn't check out"); - } - } - } else if (replacementId != null && !mXmppConnectionService.allowMessageCorrection() && (message.hasDeletedBody())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received deleted message but LMC is deactivated"); - return; - } - - if (retractId != null && mXmppConnectionService.allowMessageRetraction()) { - final Message retractedMessage = conversation.findSentMessageWithUuidOrRemoteId(retractId, true, true); - if (retractedMessage != null) { - final boolean fingerprintsMatch = retractedMessage.getFingerprint() == null - || message.getFingerprint() == null - || retractedMessage.getFingerprint().equals(message.getFingerprint()); - final boolean trueCountersMatch = retractedMessage.getTrueCounterpart() != null - && message.getTrueCounterpart() != null - && retractedMessage.getTrueCounterpart().asBareJid().equals(message.getTrueCounterpart().asBareJid()); - final boolean mucUserMatches = query == null && retractedMessage.sameMucUser(message); //can not be checked when using mam - final boolean duplicate = conversation.hasDuplicateMessage(message); - List lAcc = mXmppConnectionService.getAccounts(); - boolean activeSelf = false; - if (message.getTrueCounterpart() != null) { - for (Account a : lAcc) { - if (a.getJid() != null && a.isOnlineAndConnected() && a.getJid().asBareJid().equals(message.getTrueCounterpart().asBareJid())) { - activeSelf = true; - break; - } - } - } - if (fingerprintsMatch && ((trueCountersMatch || !conversationMultiMode || mucUserMatches || (isCarbon && activeSelf) && !duplicate) || conversationMultiMode)) { - Log.d(Config.LOGTAG, "retracted message '" + retractedMessage.getBody() + "' with '" + message.getBody() + "'"); - synchronized (retractedMessage) { - - retractedMessage.setBody(mXmppConnectionService.getString(R.string.message_deleted)); - retractedMessage.setRetractId(retractId); - - extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet); - - message.setMessageDeleted(true); - message.setRetractId(retractId); - - if (message.getStatus() > Message.STATUS_RECEIVED) { - retractedMessage.setMessageDeleted(true); - } - - for (Edit itm : retractedMessage.getEditedList()) { - Message tmpRetractedMessage = conversation.findMessageWithUuidOrRemoteId(itm.getEditedId()); - if (tmpRetractedMessage != null) { - tmpRetractedMessage.setRetractId(retractId); - mXmppConnectionService.updateMessage(tmpRetractedMessage, tmpRetractedMessage.getUuid()); - } - } - mXmppConnectionService.updateMessage(retractedMessage, retractedMessage.getUuid()); - mXmppConnectionService.databaseBackend.createMessage(message); - if (mXmppConnectionService.confirmMessages() - && retractedMessage.getStatus() == Message.STATUS_RECEIVED - && (retractedMessage.trusted() || retractedMessage.isPrivateMessage()) //TODO do we really want to send receipts for all PMs? - && remoteMsgId != null - && !selfAddressed - && !isTypeGroupChat) { - processMessageReceipts(account, packet, remoteMsgId, query); - } - } - mXmppConnectionService.getNotificationService().updateNotification(); - return; - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received message retraction but checks are not valid"); - } - } else { - //we deleted a carbon from ourself and the dialog allready removed it from ui - message.setMessageDeleted(true); - message.setRetractId(retractId); - mXmppConnectionService.databaseBackend.createMessage(message); - return; - } - } - - long deletionDate = mXmppConnectionService.getAutomaticMessageDeletionDate(); - if (deletionDate != 0 && message.getTimeSent() < deletionDate) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping message from " + message.getCounterpart().toString() + " because it was sent prior to our deletion date"); - return; - } - - boolean checkForDuplicates = (isTypeGroupChat && packet.hasChild("delay", "urn:xmpp:delay")) - || message.isPrivateMessage() - || message.getServerMsgId() != null - || (query == null && mXmppConnectionService.getMessageArchiveService().isCatchupInProgress(conversation)); - if (checkForDuplicates) { - final Message duplicate = conversation.findDuplicateMessage(message); - if (duplicate != null) { - final boolean serverMsgIdUpdated; - if (duplicate.getStatus() != Message.STATUS_RECEIVED - && duplicate.getUuid().equals(message.getRemoteMsgId()) - && duplicate.getServerMsgId() == null - && message.getServerMsgId() != null) { - duplicate.setServerMsgId(message.getServerMsgId()); - if (mXmppConnectionService.databaseBackend.updateMessage(duplicate, false)) { - serverMsgIdUpdated = true; - } else { - serverMsgIdUpdated = false; - Log.e(Config.LOGTAG, "failed to update message"); - } - } else { - serverMsgIdUpdated = false; - } - Log.d(Config.LOGTAG, "skipping duplicate message with " + message.getCounterpart() + ". serverMsgIdUpdated=" + serverMsgIdUpdated); - return; - } - } - - if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) { - conversation.prepend(query.getActualInThisQuery(), message); - } else { - conversation.add(message); - } - if (query != null) { - query.incrementActualMessageCount(); - } - - if (query == null || query.isCatchup()) { //either no mam or catchup - if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) { - mXmppConnectionService.markRead(conversation); - if (query == null) { - activateGracePeriod(account); - } - } else { - message.markUnread(); - notify = true; - } - } - - if (message.getEncryption() == Message.ENCRYPTION_PGP) { - notify = conversation.getAccount().getPgpDecryptionService().decrypt(message, notify); - } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) { - notify = false; - } - - if (query == null) { - extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet); - mXmppConnectionService.updateConversationUi(); - } - - if (mXmppConnectionService.confirmMessages() - && message.getStatus() == Message.STATUS_RECEIVED - && (message.trusted() || message.isPrivateMessage()) - && remoteMsgId != null - && !selfAddressed - && !isTypeGroupChat) { - processMessageReceipts(account, packet, remoteMsgId, query); - } - - if (message.getStatus() == Message.STATUS_RECEIVED - && conversation.getOtrSession() != null - && !conversation.getOtrSession().getSessionID().getUserID() - .equals(message.getCounterpart().getResource())) { - conversation.endOtrIfNeeded(); - } - - mXmppConnectionService.databaseBackend.createMessage(message); - final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager(); - if ((mXmppConnectionService.easyDownloader() || message.trusted()) && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) { - manager.createNewDownloadConnection(message); - } else if (notify) { - if (query != null && query.isCatchup()) { - mXmppConnectionService.getNotificationService().pushFromBacklog(message); - } else { - mXmppConnectionService.getNotificationService().push(message); - } - } - } else if (!packet.hasChild("body")) { //no body - final Conversation conversation = mXmppConnectionService.find(account, from.asBareJid()); - if (axolotlEncrypted != null) { - Jid origin; - if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { - final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart); - origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback); - if (origin == null) { - Log.d(Config.LOGTAG, "omemo key transport message in anonymous conference received"); - return; - } - } else if (isTypeGroupChat) { - return; - } else { - origin = from; - } - try { - final XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlEncrypted, origin.asBareJid()); - account.getAxolotlService().processReceivingKeyTransportMessage(xmppAxolotlMessage, query != null); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": omemo key transport message received from " + origin); - } catch (Exception e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": invalid omemo key transport message received " + e.getMessage()); - return; - } - } - - if (query == null && extractChatState(mXmppConnectionService.find(account, counterpart.asBareJid()), isTypeGroupChat, packet)) { - mXmppConnectionService.updateConversationUi(); - } - - if (isTypeGroupChat) { - if (packet.hasChild("subject")) { //TODO usually we would want to check for lack of body; however some servers do set a body :( - if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { - conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0); - final LocalizedContent subject = packet.findInternationalizedChildContentInDefaultNamespace("subject"); - if (subject != null && conversation.getMucOptions().setSubject(subject.content)) { - mXmppConnectionService.updateConversation(conversation); - } - mXmppConnectionService.updateConversationUi(); - return; - } - } - } - if (conversation != null && mucUserElement != null && InvalidJid.hasValidFrom(packet) && from.isBareJid()) { - for (Element child : mucUserElement.getChildren()) { - if ("status".equals(child.getName())) { - try { - int code = Integer.parseInt(child.getAttribute("code")); - if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) { - mXmppConnectionService.fetchConferenceConfiguration(conversation); - break; - } - } catch (Exception e) { - //ignored - } - } else if ("item".equals(child.getName())) { - MucOptions.User user = AbstractParser.parseItem(conversation, child); - Log.d(Config.LOGTAG, account.getJid() + ": changing affiliation for " - + user.getRealJid() + " to " + user.getAffiliation() + " in " - + conversation.getJid().asBareJid()); - if (!user.realJidMatchesAccount()) { - boolean isNew = conversation.getMucOptions().updateUser(user); - mXmppConnectionService.getAvatarService().clear(conversation); - mXmppConnectionService.updateMucRosterUi(); - mXmppConnectionService.updateConversationUi(); - Contact contact = user.getContact(); - if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) { - Jid jid = user.getRealJid(); - List cryptoTargets = conversation.getAcceptedCryptoTargets(); - if (cryptoTargets.remove(user.getRealJid())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": removed " + jid + " from crypto targets of " + conversation.getName()); - conversation.setAcceptedCryptoTargets(cryptoTargets); - mXmppConnectionService.updateConversation(conversation); - } - } else if (isNew - && user.getRealJid() != null - && conversation.getMucOptions().isPrivateAndNonAnonymous() - && (contact == null || !contact.mutualPresenceSubscription()) - && account.getAxolotlService().hasEmptyDeviceList(user.getRealJid())) { - account.getAxolotlService().fetchDeviceIds(user.getRealJid()); - } - } - } - } - } - if (!isTypeGroupChat) { - for (Element child : packet.getChildren()) { - if (Namespace.JINGLE_MESSAGE.equals(child.getNamespace()) && JINGLE_MESSAGE_ELEMENT_NAMES.contains(child.getName())) { - final String action = child.getName(); - final String sessionId = child.getAttribute("id"); - if (sessionId == null) { - break; - } - if (query == null) { - if (serverMsgId == null) { - serverMsgId = extractStanzaId(account, packet); - } - mXmppConnectionService.getJingleConnectionManager().deliverMessage(account, packet.getTo(), packet.getFrom(), child, remoteMsgId, serverMsgId, timestamp); - if (!account.getJid().asBareJid().equals(from.asBareJid()) && remoteMsgId != null) { - processMessageReceipts(account, packet, remoteMsgId, query); - } - } else if (query.isCatchup()) { - if ("propose".equals(action)) { - final Element description = child.findChild("description"); - final String namespace = description == null ? null : description.getNamespace(); - if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { - final Conversation c = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), false, false); - final Message preExistingMessage = c.findRtpSession(sessionId, status); - if (preExistingMessage != null) { - preExistingMessage.setServerMsgId(serverMsgId); - mXmppConnectionService.updateMessage(preExistingMessage); - break; - } - final Message message = new Message( - c, - status, - Message.TYPE_RTP_SESSION, - sessionId - ); - message.setServerMsgId(serverMsgId); - message.setTime(timestamp); - message.setBody(new RtpSessionStatus(false, 0).toString()); - c.add(message); - mXmppConnectionService.databaseBackend.createMessage(message); - } - } else if ("proceed".equals(action)) { - //status needs to be flipped to find the original propose - final Conversation c = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), false, false); - final int s = packet.fromAccount(account) ? Message.STATUS_RECEIVED : Message.STATUS_SEND; - final Message message = c.findRtpSession(sessionId, s); - if (message != null) { - message.setBody(new RtpSessionStatus(true, 0).toString()); - if (serverMsgId != null) { - message.setServerMsgId(serverMsgId); - } - message.setTime(timestamp); - mXmppConnectionService.updateMessage(message, true); - } else { - Log.d(Config.LOGTAG, "unable to find original rtp session message for received propose"); - } - - } - } else { - //MAM reloads (non catchups - if ("propose".equals(action)) { - final Element description = child.findChild("description"); - final String namespace = description == null ? null : description.getNamespace(); - if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { - final Conversation c = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), false, false); - final Message preExistingMessage = c.findRtpSession(sessionId, status); - if (preExistingMessage != null) { - preExistingMessage.setServerMsgId(serverMsgId); - mXmppConnectionService.updateMessage(preExistingMessage); - break; - } - final Message message = new Message( - c, - status, - Message.TYPE_RTP_SESSION, - sessionId - ); - message.setServerMsgId(serverMsgId); - message.setTime(timestamp); - message.setBody(new RtpSessionStatus(true, 0).toString()); - if (query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) { - c.prepend(query.getActualInThisQuery(), message); - } else { - c.add(message); - } - query.incrementActualMessageCount(); - mXmppConnectionService.databaseBackend.createMessage(message); - } - } - } - break; - } - } - } - } - - Element received = packet.findChild("received", "urn:xmpp:chat-markers:0"); - if (received == null) { - received = packet.findChild("received", "urn:xmpp:receipts"); - } - if (received != null) { - String id = received.getAttribute("id"); - if (packet.fromAccount(account)) { - if (query != null && id != null && packet.getTo() != null) { - query.removePendingReceiptRequest(new ReceiptRequest(packet.getTo(), id)); - } - } else if (id != null) { - if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) { - final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length()); - mXmppConnectionService.getJingleConnectionManager() - .updateProposedSessionDiscovered(account, from, sessionId, JingleConnectionManager.DeviceDiscoveryState.DISCOVERED); - } else { - mXmppConnectionService.markMessage(account, from.asBareJid(), id, Message.STATUS_SEND_RECEIVED); - } - } - } - Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0"); - if (displayed != null) { - final String id = displayed.getAttribute("id"); - final Jid sender = InvalidJid.getNullForInvalid(displayed.getAttributeAsJid("sender")); - if (packet.fromAccount(account) && !selfAddressed) { - dismissNotification(account, counterpart, query, id); - if (query == null) { - activateGracePeriod(account); - } - } else if (isTypeGroupChat) { - final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid()); - Message message; - if (conversation != null && id != null) { - if (sender != null) { - message = conversation.findMessageWithRemoteId(id, sender); - } else { - message = conversation.findMessageWithServerMsgId(id); - } - } else { - message = null; - } - if (message != null) { - final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart); - final Jid trueJid = getTrueCounterpart((query != null && query.safeToExtractTrueCounterpart()) ? mucUserElement : null, fallback); - final boolean trueJidMatchesAccount = account.getJid().asBareJid().equals(trueJid == null ? null : trueJid.asBareJid()); - if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) { - if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections - mXmppConnectionService.markRead(conversation); - } - } else if (!counterpart.isBareJid() && trueJid != null) { - final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid); - if (message.addReadByMarker(readByMarker)) { - final Message displayedMessage = message; - if (message.getStatus() >= Message.STATUS_SEND_RECEIVED) { - mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED); - } - mXmppConnectionService.updateMessage(message, false); - message = displayedMessage == null ? null : displayedMessage.prev(); - while (message != null - && message.getStatus() >= Message.STATUS_SEND_RECEIVED - && message.getTimeSent() < displayedMessage.getTimeSent()) { - mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED); - mXmppConnectionService.updateMessage(message, false); - message = message.prev(); - } - } - } - } - } else { - final Message displayedMessage = mXmppConnectionService.markMessage(account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED); - Message message = displayedMessage == null ? null : displayedMessage.prev(); - while (message != null - && message.getStatus() == Message.STATUS_SEND_RECEIVED - && message.getTimeSent() < displayedMessage.getTimeSent()) { - mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED); - message = message.prev(); - } - if (displayedMessage != null && selfAddressed) { - dismissNotification(account, counterpart, query, id); - } - } - } - - final Element event = original.findChild("event", "http://jabber.org/protocol/pubsub#event"); - if (event != null && InvalidJid.hasValidFrom(original) && original.getFrom().isBareJid()) { - if (event.hasChild("items")) { - parseEvent(event, original.getFrom(), account); - } else if (event.hasChild("delete")) { - parseDeleteEvent(event, original.getFrom(), account); - } else if (event.hasChild("purge")) { - parsePurgeEvent(event, original.getFrom(), account); - } - } - - final String nick = packet.findChildContent("nick", Namespace.NICK); - if (nick != null && InvalidJid.hasValidFrom(original)) { - if (mXmppConnectionService.isMuc(account, from)) { - return; - } - final Contact contact = account.getRoster().getContact(from); - if (contact.setPresenceName(nick)) { - mXmppConnectionService.syncRoster(account); - mXmppConnectionService.getAvatarService().clear(contact); - } - } - } - - private void updateReadMarker(Account account, Jid from, String id, boolean selfAddressed, Jid counterpart, MessageArchiveService.Query query) { - final Message displayedMessage = mXmppConnectionService.markMessage(account, from.asBareJid(), id, Message.STATUS_SEND_DISPLAYED); - Message m = displayedMessage == null ? null : displayedMessage.prev(); - while (m != null - && m.getStatus() == Message.STATUS_SEND_RECEIVED - && m.getTimeSent() < displayedMessage.getTimeSent()) { - mXmppConnectionService.markMessage(m, Message.STATUS_SEND_DISPLAYED); - m = m.prev(); - } - if (displayedMessage != null && selfAddressed) { - dismissNotification(account, counterpart, query, id); - } - } - - private void dismissNotification(Account account, Jid counterpart, MessageArchiveService.Query query, final String id) { - final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid()); - if (conversation != null && (query == null || query.isCatchup())) { - final String displayableId = conversation.findMostRecentRemoteDisplayableId(); - if (displayableId != null && displayableId.equals(id)) { - mXmppConnectionService.markRead(conversation); - } else { - Log.w(Config.LOGTAG, account.getJid().asBareJid() + ": received dismissing display marker that did not match our last id in that conversation"); - } - } - } - - private void processMessageReceipts(final Account account, final MessagePacket packet, final String remoteMsgId, MessageArchiveService.Query query) { - final boolean markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0"); - final boolean request = packet.hasChild("request", "urn:xmpp:receipts"); - if (query == null) { - final ArrayList receiptsNamespaces = new ArrayList<>(); - if (markable) { - receiptsNamespaces.add("urn:xmpp:chat-markers:0"); - } - if (request) { - receiptsNamespaces.add("urn:xmpp:receipts"); - } - if (receiptsNamespaces.size() > 0) { - final MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account, - packet.getFrom(), - remoteMsgId, - receiptsNamespaces, - packet.getType()); - mXmppConnectionService.sendMessagePacket(account, receipt); - } - } else if (query.isCatchup()) { - if (request) { - query.addPendingReceiptRequest(new ReceiptRequest(packet.getFrom(), remoteMsgId)); - } - } - } - - private void activateGracePeriod(Account account) { - long duration = mXmppConnectionService.getLongPreference("grace_period_length", R.integer.grace_period) * 1000; - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": activating grace period till " + TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration))); - account.activateGracePeriod(duration); - } - - private class Invite { - final Jid jid; - final String password; - final boolean direct; - final Jid inviter; - - Invite(Jid jid, String password, boolean direct, Jid inviter) { - this.jid = jid; - this.password = password; - this.direct = direct; - this.inviter = inviter; - } - - public boolean execute(Account account) { - return execute(account, null); - } - - public boolean execute(Account account, LocalizedContent body) { - if (jid != null) { - Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid, true, false); - if (conversation.getMucOptions().online()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received invite to " + jid + ", but muc is considered to be online"); - mXmppConnectionService.mucSelfPingAndRejoin(conversation); - } else { - conversation.getMucOptions().setPassword(password); - mXmppConnectionService.databaseBackend.updateConversation(conversation); - final Contact contact = inviter != null ? account.getRoster().getContactFromContactList(inviter) : null; - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received invite to " + jid + " from " + (contact != null ? contact.getJid().asBareJid() : null)); - mXmppConnectionService.joinMuc(conversation, contact != null && contact.mutualPresenceSubscription()); - mXmppConnectionService.updateConversationUi(); - if (body != null) { - mXmppConnectionService.showInvitationNotification(conversation, contact, body); - } - } - return true; - } - return false; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java deleted file mode 100644 index 6934276d3..000000000 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ /dev/null @@ -1,393 +0,0 @@ -package eu.siacs.conversations.parser; - -import android.util.Log; - -import org.openintents.openpgp.util.OpenPgpUtils; - -import java.util.ArrayList; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.crypto.PgpEngine; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.entities.Presence; -import eu.siacs.conversations.generator.IqGenerator; -import eu.siacs.conversations.generator.PresenceGenerator; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.InvalidJid; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnPresencePacketReceived; -import eu.siacs.conversations.xmpp.pep.Avatar; -import eu.siacs.conversations.xmpp.stanzas.PresencePacket; - -public class PresenceParser extends AbstractParser implements - OnPresencePacketReceived { - - public PresenceParser(XmppConnectionService service) { - super(service); - } - - public void parseConferencePresence(PresencePacket packet, Account account) { - final Conversation conversation = packet.getFrom() == null ? null : mXmppConnectionService.find(account, packet.getFrom().asBareJid()); - if (conversation != null) { - final MucOptions mucOptions = conversation.getMucOptions(); - boolean before = mucOptions.online(); - int count = mucOptions.getUserCount(); - final List tileUserBefore = mucOptions.getUsers(5); - processConferencePresence(packet, conversation); - final List tileUserAfter = mucOptions.getUsers(5); - if (!tileUserAfter.equals(tileUserBefore)) { - mXmppConnectionService.getAvatarService().clear(mucOptions); - } - if (before != mucOptions.online() || (mucOptions.online() && count != mucOptions.getUserCount())) { - mXmppConnectionService.updateConversationUi(); - } else if (mucOptions.online()) { - mXmppConnectionService.updateMucRosterUi(); - } - } - } - - private void processConferencePresence(PresencePacket packet, Conversation conversation) { - final Account account = conversation.getAccount(); - final MucOptions mucOptions = conversation.getMucOptions(); - final Jid jid = conversation.getAccount().getJid(); - final Jid from = packet.getFrom(); - if (!from.isBareJid()) { - final String type = packet.getAttribute("type"); - final Element x = packet.findChild("x", Namespace.MUC_USER); - Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update")); - final List codes = getStatusCodes(x); - if (type == null) { - if (x != null) { - Element item = x.findChild("item"); - if (item != null && !from.isBareJid()) { - mucOptions.setError(MucOptions.Error.NONE); - MucOptions.User user = parseItem(conversation, item, from); - if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE) || (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) && jid.equals(InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"))))) { - if (mucOptions.setOnline()) { - mXmppConnectionService.getAvatarService().clear(mucOptions); - } - if (mucOptions.setSelf(user)) { - Log.d(Config.LOGTAG, "role or affiliation changed"); - mXmppConnectionService.databaseBackend.updateConversation(conversation); - } - mXmppConnectionService.persistSelfNick(user); - invokeRenameListener(mucOptions, true); - } - boolean isNew = mucOptions.updateUser(user); - final AxolotlService axolotlService = conversation.getAccount().getAxolotlService(); - Contact contact = user.getContact(); - if (isNew - && user.getRealJid() != null - && mucOptions.isPrivateAndNonAnonymous() - && (contact == null || !contact.mutualPresenceSubscription()) - && axolotlService.hasEmptyDeviceList(user.getRealJid())) { - axolotlService.fetchDeviceIds(user.getRealJid()); - } - if (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) && mucOptions.autoPushConfiguration()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() - + ": room '" - + mucOptions.getConversation().getJid().asBareJid() - + "' created. pushing default configuration"); - mXmppConnectionService.pushConferenceConfiguration(mucOptions.getConversation(), - IqGenerator.defaultChannelConfiguration(), - null); - } - if (mXmppConnectionService.getPgpEngine() != null) { - Element signed = packet.findChild("x", "jabber:x:signed"); - if (signed != null) { - Element status = packet.findChild("status"); - String msg = status == null ? "" : status.getContent(); - long keyId = mXmppConnectionService.getPgpEngine().fetchKeyId(mucOptions.getAccount(), msg, signed.getContent()); - if (keyId != 0) { - user.setPgpKeyId(keyId); - } - } - } - if (avatar != null) { - avatar.owner = from; - if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { - if (user.setAvatar(avatar)) { - mXmppConnectionService.getAvatarService().clear(user); - } - if (user.getRealJid() != null) { - final Contact c = conversation.getAccount().getRoster().getContact(user.getRealJid()); - c.setAvatar(avatar); - mXmppConnectionService.syncRoster(conversation.getAccount()); - mXmppConnectionService.getAvatarService().clear(c); - mXmppConnectionService.updateRosterUi(); - } - } else if (mXmppConnectionService.isDataSaverDisabled()) { - mXmppConnectionService.fetchAvatar(mucOptions.getAccount(), avatar); - } - } - } - } - } else if (type.equals("unavailable")) { - final boolean fullJidMatches = from.equals(mucOptions.getSelf().getFullJid()); - if (x.hasChild("destroy") && fullJidMatches) { - Element destroy = x.findChild("destroy"); - final Jid alternate = destroy == null ? null : InvalidJid.getNullForInvalid(destroy.getAttributeAsJid("jid")); - mucOptions.setError(MucOptions.Error.DESTROYED); - if (alternate != null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc destroyed. alternate location " + alternate); - } - } else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN) && fullJidMatches) { - mucOptions.setError(MucOptions.Error.SHUTDOWN); - } else if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE)) { - if (codes.contains(MucOptions.STATUS_CODE_TECHNICAL_REASONS)) { - final boolean wasOnline = mucOptions.online(); - mucOptions.setError(MucOptions.Error.TECHNICAL_PROBLEMS); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": received status code 333 in room " - + mucOptions.getConversation().getJid().asBareJid() - + " online=" - + wasOnline); - if (wasOnline) { - mXmppConnectionService.mucSelfPingAndRejoin(conversation); - } - } else if (codes.contains(MucOptions.STATUS_CODE_KICKED)) { - mucOptions.setError(MucOptions.Error.KICKED); - } else if (codes.contains(MucOptions.STATUS_CODE_BANNED)) { - mucOptions.setError(MucOptions.Error.BANNED); - } else if (codes.contains(MucOptions.STATUS_CODE_LOST_MEMBERSHIP)) { - mucOptions.setError(MucOptions.Error.MEMBERS_ONLY); - } else if (codes.contains(MucOptions.STATUS_CODE_AFFILIATION_CHANGE)) { - mucOptions.setError(MucOptions.Error.MEMBERS_ONLY); - } else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN)) { - mucOptions.setError(MucOptions.Error.SHUTDOWN); - } else if (!codes.contains(MucOptions.STATUS_CODE_CHANGED_NICK)) { - mucOptions.setError(MucOptions.Error.UNKNOWN); - Log.d(Config.LOGTAG, "unknown error in conference: " + packet); - } - } else if (!from.isBareJid()) { - Element item = x.findChild("item"); - if (item != null) { - mucOptions.updateUser(parseItem(conversation, item, from)); - } - MucOptions.User user = mucOptions.deleteUser(from); - if (user != null) { - mXmppConnectionService.getAvatarService().clear(user); - } - } - } else if (type.equals("error")) { - final Element error = packet.findChild("error"); - if (error == null) { - return; - } - if (error.hasChild("conflict")) { - if (mucOptions.online()) { - invokeRenameListener(mucOptions, false); - } else { - mucOptions.setError(MucOptions.Error.NICK_IN_USE); - } - } else if (error.hasChild("not-authorized")) { - mucOptions.setError(MucOptions.Error.PASSWORD_REQUIRED); - } else if (error.hasChild("forbidden")) { - mucOptions.setError(MucOptions.Error.BANNED); - } else if (error.hasChild("registration-required")) { - mucOptions.setError(MucOptions.Error.MEMBERS_ONLY); - } else if (error.hasChild("resource-constraint")) { - mucOptions.setError(MucOptions.Error.RESOURCE_CONSTRAINT); - } else if (error.hasChild("remote-server-timeout")) { - mucOptions.setError(MucOptions.Error.REMOTE_SERVER_TIMEOUT); - } else if (error.hasChild("gone")) { - final String gone = error.findChildContent("gone"); - final Jid alternate; - if (gone != null) { - final XmppUri xmppUri = new XmppUri(gone); - if (xmppUri.isValidJid()) { - alternate = xmppUri.getJid(); - } else { - alternate = null; - } - } else { - alternate = null; - } - mucOptions.setError(MucOptions.Error.DESTROYED); - if (alternate != null) { - Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": muc destroyed. alternate location " + alternate); - } - } else { - final String text = error.findChildContent("text"); - if (text != null && text.contains("attribute 'to'")) { - if (mucOptions.online()) { - invokeRenameListener(mucOptions, false); - } else { - mucOptions.setError(MucOptions.Error.INVALID_NICK); - } - } else { - mucOptions.setError(MucOptions.Error.UNKNOWN); - Log.d(Config.LOGTAG, "unknown error in conference: " + packet); - } - } - } - } - } - - private static void invokeRenameListener(final MucOptions options, boolean success) { - if (options.onRenameListener != null) { - if (success) { - options.onRenameListener.onSuccess(); - } else { - options.onRenameListener.onFailure(); - } - options.onRenameListener = null; - } - } - - private static List getStatusCodes(Element x) { - List codes = new ArrayList<>(); - if (x != null) { - for (Element child : x.getChildren()) { - if (child.getName().equals("status")) { - String code = child.getAttribute("code"); - if (code != null) { - codes.add(code); - } - } - } - } - return codes; - } - - private void parseContactPresence(final PresencePacket packet, final Account account) { - final PresenceGenerator mPresenceGenerator = mXmppConnectionService.getPresenceGenerator(); - final Jid from = packet.getFrom(); - if (from == null || from.equals(account.getJid())) { - return; - } - final String type = packet.getAttribute("type"); - final Contact contact = account.getRoster().getContact(from); - if (type == null) { - final String resource = from.isBareJid() ? "" : from.getResource(); - Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update")); - if (avatar != null && (!contact.isSelf() || account.getAvatar() == null)) { - avatar.owner = from.asBareJid(); - if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { - if (avatar.owner.equals(account.getJid().asBareJid())) { - account.setAvatar(avatar.getFilename()); - mXmppConnectionService.databaseBackend.updateAccount(account); - mXmppConnectionService.getAvatarService().clear(account); - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.updateAccountUi(); - } else { - contact.setAvatar(avatar); - mXmppConnectionService.syncRoster(account); - mXmppConnectionService.getAvatarService().clear(contact); - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.updateRosterUi(); - } - } else if (mXmppConnectionService.isDataSaverDisabled()) { - mXmppConnectionService.fetchAvatar(account, avatar); - } - } - - if (mXmppConnectionService.isMuc(account, from)) { - return; - } - int sizeBefore = contact.getPresences().size(); - - final String show = packet.findChildContent("show"); - final Element caps = packet.findChild("c", "http://jabber.org/protocol/caps"); - final String message = packet.findChildContent("status"); - final Presence presence = Presence.parse(show, caps, message); - contact.updatePresence(resource, presence); - if (presence.hasCaps()) { - mXmppConnectionService.fetchCaps(account, from, presence); - } - - final Element idle = packet.findChild("idle", Namespace.IDLE); - if (idle != null) { - try { - final String since = idle.getAttribute("since"); - contact.setLastseen(AbstractParser.parseTimestamp(since)); - contact.flagInactive(); - } catch (Throwable throwable) { - if (contact.setLastseen(AbstractParser.parseTimestamp(packet))) { - contact.flagActive(); - } - } - } else { - if (contact.setLastseen(AbstractParser.parseTimestamp(packet))) { - contact.flagActive(); - } - } - - PgpEngine pgp = mXmppConnectionService.getPgpEngine(); - Element x = packet.findChild("x", "jabber:x:signed"); - if (pgp != null && x != null) { - final String status = packet.findChildContent("status"); - final long keyId = pgp.fetchKeyId(account, status, x.getContent()); - if (keyId != 0 && contact.setPgpKeyId(keyId)) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": found OpenPGP key id for "+contact.getJid()+" "+OpenPgpUtils.convertKeyIdToHex(keyId)); - mXmppConnectionService.syncRoster(account); - } - } - boolean online = sizeBefore < contact.getPresences().size(); - mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, online); - } else if (type.equals("unavailable")) { - if (contact.setLastseen(AbstractParser.parseTimestamp(packet, 0L, true))) { - contact.flagInactive(); - } - if (from.isBareJid()) { - contact.clearPresences(); - } else { - contact.removePresence(from.getResource()); - } - if (contact.getShownStatus() == Presence.Status.OFFLINE) { - contact.flagInactive(); - } - mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, false); - } else if (type.equals("subscribe")) { - if (contact.setPresenceName(packet.findChildContent("nick", Namespace.NICK))) { - mXmppConnectionService.syncRoster(account); - mXmppConnectionService.getAvatarService().clear(contact); - } - if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) { - mXmppConnectionService.sendPresencePacket(account, - mPresenceGenerator.sendPresenceUpdatesTo(contact)); - } else { - contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); - final Conversation conversation = mXmppConnectionService.findOrCreateConversation( - account, contact.getJid().asBareJid(), false, false); - final String statusMessage = packet.findChildContent("status"); - if (statusMessage != null - && !statusMessage.isEmpty() - && conversation.countMessages() == 0) { - conversation.add(new Message( - conversation, - statusMessage, - Message.ENCRYPTION_NONE, - Message.STATUS_RECEIVED - )); - } - } - } - mXmppConnectionService.updateRosterUi(); - } - - @Override - public void onPresencePacketReceived(Account account, PresencePacket packet) { - if (packet.hasChild("x", Namespace.MUC_USER)) { - this.parseConferencePresence(packet, account); - } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { - this.parseConferencePresence(packet, account); - } else if ("error".equals(packet.getAttribute("type")) && mXmppConnectionService.isMuc(account, packet.getFrom())) { - this.parseConferencePresence(packet, account); - } else { - this.parseContactPresence(packet, account); - } - } - -} diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java deleted file mode 100644 index 66e18476a..000000000 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ /dev/null @@ -1,1874 +0,0 @@ -package eu.siacs.conversations.persistance; - -import static eu.siacs.conversations.ui.util.UpdateHelper.moveData_PAM_monocles; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.DatabaseUtils; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.os.Environment; -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; -import org.whispersystems.libsignal.IdentityKeyPair; -import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.SignalProtocolAddress; -import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.SessionRecord; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.IOException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.CopyOnWriteArrayList; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; -import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.PresenceTemplate; -import eu.siacs.conversations.entities.Roster; -import eu.siacs.conversations.entities.ServiceDiscoveryResult; -import eu.siacs.conversations.services.ShortcutService; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.CursorUtils; -import eu.siacs.conversations.utils.FtsUtils; -import eu.siacs.conversations.utils.Resolver; -import eu.siacs.conversations.xmpp.InvalidJid; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.mam.MamReference; - -public class DatabaseBackend extends SQLiteOpenHelper { - - public static final String DATABASE_NAME = "history"; - public static final int DATABASE_VERSION = 59; // = Conversations DATABASE_VERSION + 7 - private static boolean requiresMessageIndexRebuild = false; - private static DatabaseBackend instance = null; - private static final List DB_PRAGMAS = Collections.unmodifiableList(Arrays.asList( - "synchronous", "journal_mode", - "wal_checkpoint", "wal_autocheckpoint", "journal_size_limit", - "page_count", "page_size", "max_page_count", "freelist_count", - "cache_size", "cache_spill", - "soft_heap_limit", "hard_heap_limit", "mmap_size", - "foreign_keys", "auto_vacuum" - )); - - private static final String CREATE_CONTATCS_STATEMENT = "create table " - + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " - + Contact.SERVERNAME + " TEXT, " + Contact.SYSTEMNAME + " TEXT," - + Contact.PRESENCE_NAME + " TEXT," - + Contact.JID + " TEXT," + Contact.KEYS + " TEXT," - + Contact.PHOTOURI + " TEXT," + Contact.OPTIONS + " NUMBER," - + Contact.SYSTEMACCOUNT + " NUMBER, " + Contact.AVATAR + " TEXT, " - + Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, " - + Contact.RTP_CAPABILITY + " TEXT," - + Contact.GROUPS + " TEXT, FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES " - + Account.TABLENAME + "(" + Account.UUID - + ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", " - + Contact.JID + ") ON CONFLICT REPLACE);"; - - private static final String CREATE_DISCOVERY_RESULTS_STATEMENT = "create table " - + ServiceDiscoveryResult.TABLENAME + "(" - + ServiceDiscoveryResult.HASH + " TEXT, " - + ServiceDiscoveryResult.VER + " TEXT, " - + ServiceDiscoveryResult.RESULT + " TEXT, " - + "UNIQUE(" + ServiceDiscoveryResult.HASH + ", " - + ServiceDiscoveryResult.VER + ") ON CONFLICT REPLACE);"; - - private static final String CREATE_PRESENCE_TEMPLATES_STATEMENT = "CREATE TABLE " - + PresenceTemplate.TABELNAME + "(" - + PresenceTemplate.UUID + " TEXT, " - + PresenceTemplate.LAST_USED + " NUMBER," - + PresenceTemplate.MESSAGE + " TEXT," - + PresenceTemplate.STATUS + " TEXT," - + "UNIQUE(" + PresenceTemplate.MESSAGE + "," + PresenceTemplate.STATUS + ") ON CONFLICT REPLACE);"; - - private static final String CREATE_PREKEYS_STATEMENT = "CREATE TABLE " - + SQLiteAxolotlStore.PREKEY_TABLENAME + "(" - + SQLiteAxolotlStore.ACCOUNT + " TEXT, " - + SQLiteAxolotlStore.ID + " INTEGER, " - + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" - + SQLiteAxolotlStore.ACCOUNT - + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " - + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " - + SQLiteAxolotlStore.ID - + ") ON CONFLICT REPLACE" - + ");"; - - private static String CREATE_SIGNED_PREKEYS_STATEMENT = "CREATE TABLE " - + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME + "(" - + SQLiteAxolotlStore.ACCOUNT + " TEXT, " - + SQLiteAxolotlStore.ID + " INTEGER, " - + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" - + SQLiteAxolotlStore.ACCOUNT - + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " - + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " - + SQLiteAxolotlStore.ID - + ") ON CONFLICT REPLACE" + - ");"; - - private static String CREATE_SESSIONS_STATEMENT = "CREATE TABLE " - + SQLiteAxolotlStore.SESSION_TABLENAME + "(" - + SQLiteAxolotlStore.ACCOUNT + " TEXT, " - + SQLiteAxolotlStore.NAME + " TEXT, " - + SQLiteAxolotlStore.DEVICE_ID + " INTEGER, " - + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" - + SQLiteAxolotlStore.ACCOUNT - + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " - + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " - + SQLiteAxolotlStore.NAME + ", " - + SQLiteAxolotlStore.DEVICE_ID - + ") ON CONFLICT REPLACE" - + ");"; - - private static String CREATE_IDENTITIES_STATEMENT = "CREATE TABLE " - + SQLiteAxolotlStore.IDENTITIES_TABLENAME + "(" - + SQLiteAxolotlStore.ACCOUNT + " TEXT, " - + SQLiteAxolotlStore.NAME + " TEXT, " - + SQLiteAxolotlStore.OWN + " INTEGER, " - + SQLiteAxolotlStore.FINGERPRINT + " TEXT, " - + SQLiteAxolotlStore.CERTIFICATE + " BLOB, " - + SQLiteAxolotlStore.TRUST + " TEXT, " - + SQLiteAxolotlStore.ACTIVE + " NUMBER, " - + SQLiteAxolotlStore.LAST_ACTIVATION + " NUMBER," - + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" - + SQLiteAxolotlStore.ACCOUNT - + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " - + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " - + SQLiteAxolotlStore.NAME + ", " - + SQLiteAxolotlStore.FINGERPRINT - + ") ON CONFLICT IGNORE" - + ");"; - - private static String RESOLVER_RESULTS_TABLENAME = "resolver_results"; - - private static String CREATE_RESOLVER_RESULTS_TABLE = "create table " + RESOLVER_RESULTS_TABLENAME + "(" - + Resolver.Result.DOMAIN + " TEXT," - + Resolver.Result.HOSTNAME + " TEXT," - + Resolver.Result.IP + " BLOB," - + Resolver.Result.PRIORITY + " NUMBER," - + Resolver.Result.DIRECT_TLS + " NUMBER," - + Resolver.Result.AUTHENTICATED + " NUMBER," - + Resolver.Result.PORT + " NUMBER," - + Resolver.Result.TIME_REQUESTED + " NUMBER," - + "UNIQUE(" + Resolver.Result.DOMAIN + ") ON CONFLICT REPLACE" - + ");"; - - private static String CREATE_MESSAGE_TIME_INDEX = "CREATE INDEX message_time_index ON " + Message.TABLENAME + "(" + Message.TIME_SENT + ")"; - private static String CREATE_MESSAGE_CONVERSATION_INDEX = "CREATE INDEX message_conversation_index ON " + Message.TABLENAME + "(" + Message.CONVERSATION + ")"; - private static String CREATE_MESSAGE_DELETED_INDEX = "CREATE INDEX message_deleted_index ON " + Message.TABLENAME + "(" + Message.DELETED + ")"; - private static String CREATE_MESSAGE_FILE_DELETED_INDEX = "create index message_file_deleted_index ON " + Message.TABLENAME + "(" + Message.FILE_DELETED + ")"; - private static String CREATE_MESSAGE_RELATIVE_FILE_PATH_INDEX = "CREATE INDEX message_file_path_index ON " + Message.TABLENAME + "(" + Message.RELATIVE_FILE_PATH + ")"; - private static String CREATE_MESSAGE_TYPE_INDEX = "CREATE INDEX message_type_index ON " + Message.TABLENAME + "(" + Message.TYPE + ")"; - - private static String CREATE_MESSAGE_INDEX_TABLE = "CREATE VIRTUAL TABLE messages_index USING fts4 (uuid,body,notindexed=\"uuid\",content=\"" + Message.TABLENAME + "\",tokenize='unicode61')"; - private static 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 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 String COPY_PREEXISTING_ENTRIES = "INSERT INTO messages_index(messages_index) VALUES('rebuild');"; - - private DatabaseBackend(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - } - - private static ContentValues createFingerprintStatusContentValues(FingerprintStatus.Trust trust, boolean active) { - ContentValues values = new ContentValues(); - values.put(SQLiteAxolotlStore.TRUST, trust.toString()); - values.put(SQLiteAxolotlStore.ACTIVE, active ? 1 : 0); - 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); - } - return instance; - } - - @Override - public void onConfigure(SQLiteDatabase db) { - final long start = SystemClock.elapsedRealtime(); - db.execSQL("PRAGMA foreign_keys=ON"); - - // https://www.sqlite.org/pragma.html#pragma_auto_vacuum - // https://android.googlesource.com/platform/external/sqlite.git/+/6ab557bdc070f11db30ede0696888efd19800475%5E!/ - boolean sqlite_auto_vacuum = false; - String mode = (sqlite_auto_vacuum ? "FULL" : "INCREMENTAL"); - Log.d(Config.LOGTAG, "Set PRAGMA auto_vacuum = " + mode); - try (Cursor cursor = db.rawQuery("PRAGMA auto_vacuum = " + mode + ";", null)) { - cursor.moveToNext(); // required - } - - // https://sqlite.org/pragma.html#pragma_synchronous - boolean sqlite_sync_extra = true; - String sync = (sqlite_sync_extra ? "EXTRA" : "NORMAL"); - Log.d(Config.LOGTAG, "Set PRAGMA synchronous = " + sync); - try (Cursor cursor = db.rawQuery("PRAGMA synchronous = " + sync + ";", null)) { - cursor.moveToNext(); // required - } - - // Prevent long running operations from getting an exclusive lock - // https://www.sqlite.org/pragma.html#pragma_cache_spill - Log.d(Config.LOGTAG, "Set PRAGMA cache_spill=0"); - try (Cursor cursor = db.rawQuery("PRAGMA cache_spill=0;", null)) { - cursor.moveToNext(); // required - } - - // https://www.sqlite.org/pragma.html - for (String pragma : DB_PRAGMAS) - try (Cursor cursor = db.rawQuery("PRAGMA " + pragma + ";", null)) { - Log.d(Config.LOGTAG, "Get PRAGMA " + pragma + "=" + (cursor.moveToNext() ? cursor.getString(0) : "?")); - } - - db.rawQuery("PRAGMA secure_delete=ON", null).close(); - Log.d(Config.LOGTAG, "configure the DB in " + (SystemClock.elapsedRealtime() - start) + "ms"); - } - - @Override - public void onCreate(SQLiteDatabase db) { - db.execSQL("create table " + Account.TABLENAME + "(" + Account.UUID + " TEXT PRIMARY KEY," - + Account.USERNAME + " TEXT," - + Account.SERVER + " TEXT," - + Account.PASSWORD + " TEXT," - + Account.DISPLAY_NAME + " TEXT, " - + Account.STATUS + " TEXT," - + Account.STATUS_MESSAGE + " TEXT," - + Account.ROSTERVERSION + " TEXT," - + Account.OPTIONS + " NUMBER, " - + Account.AVATAR + " TEXT, " - + Account.KEYS + " TEXT, " - + Account.HOSTNAME + " TEXT, " - + Account.RESOURCE + " TEXT," - + Account.PINNED_MECHANISM + " TEXT," - + Account.PINNED_CHANNEL_BINDING + " TEXT," - + Account.FAST_MECHANISM + " TEXT," - + Account.FAST_TOKEN + " TEXT," - + Account.PORT + " NUMBER DEFAULT 5222)"); - db.execSQL("create table " + Conversation.TABLENAME + " (" - + Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME - + " TEXT, " + Conversation.CONTACT + " TEXT, " - + Conversation.ACCOUNT + " TEXT, " + Conversation.CONTACTJID - + " TEXT, " + Conversation.CREATED + " NUMBER, " - + Conversation.STATUS + " NUMBER, " + Conversation.MODE - + " NUMBER, " + Conversation.ATTRIBUTES + " TEXT, FOREIGN KEY(" - + Conversation.ACCOUNT + ") REFERENCES " + Account.TABLENAME - + "(" + Account.UUID + ") ON DELETE CASCADE);"); - db.execSQL("create table " + Message.TABLENAME + "( " + Message.UUID - + " TEXT PRIMARY KEY, " + Message.CONVERSATION + " TEXT, " - + Message.TIME_SENT + " NUMBER, " + Message.COUNTERPART - + " TEXT, " + Message.TRUE_COUNTERPART + " TEXT," - + Message.BODY + " TEXT, " + Message.ENCRYPTION + " NUMBER, " - + Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, " - + Message.RELATIVE_FILE_PATH + " TEXT, " - + Message.SERVER_MSG_ID + " TEXT, " - + Message.FINGERPRINT + " TEXT, " - + Message.CARBON + " INTEGER, " - + Message.EDITED + " TEXT, " - + Message.READ + " NUMBER DEFAULT 1, " - + Message.DELETED + " NUMBER DEFAULT 0, " - + Message.OOB + " INTEGER, " - + Message.ERROR_MESSAGE + " TEXT," - + Message.READ_BY_MARKERS + " TEXT," - + Message.MARKABLE + " NUMBER DEFAULT 0," - + Message.FILE_DELETED + " NUMBER DEFAULT 0," - + Message.BODY_LANGUAGE + " TEXT," - + Message.RETRACT_ID + " TEXT," - + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY(" - + Message.CONVERSATION + ") REFERENCES " - + Conversation.TABLENAME + "(" + Conversation.UUID - + ") ON DELETE CASCADE);"); - - db.execSQL(CREATE_MESSAGE_TIME_INDEX); - db.execSQL(CREATE_MESSAGE_CONVERSATION_INDEX); - db.execSQL(CREATE_MESSAGE_DELETED_INDEX); - db.execSQL(CREATE_MESSAGE_FILE_DELETED_INDEX); - db.execSQL(CREATE_MESSAGE_RELATIVE_FILE_PATH_INDEX); - db.execSQL(CREATE_MESSAGE_TYPE_INDEX); - db.execSQL(CREATE_CONTATCS_STATEMENT); - db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT); - db.execSQL(CREATE_SESSIONS_STATEMENT); - db.execSQL(CREATE_PREKEYS_STATEMENT); - db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); - db.execSQL(CREATE_IDENTITIES_STATEMENT); - db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT); - db.execSQL(CREATE_RESOLVER_RESULTS_TABLE); - 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 - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - if (oldVersion < 2 && newVersion >= 2) { - db.execSQL("update " + Account.TABLENAME + " set " - + Account.OPTIONS + " = " + Account.OPTIONS + " | 8"); - } - if (oldVersion < 3 && newVersion >= 3) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.TYPE + " NUMBER"); - } - if (oldVersion < 5 && newVersion >= 5) { - db.execSQL("DROP TABLE " + Contact.TABLENAME); - db.execSQL(CREATE_CONTATCS_STATEMENT); - db.execSQL("UPDATE " + Account.TABLENAME + " SET " - + Account.ROSTERVERSION + " = NULL"); - } - if (oldVersion < 6 && newVersion >= 6) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.TRUE_COUNTERPART + " TEXT"); - } - if (oldVersion < 7 && newVersion >= 7) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.REMOTE_MSG_ID + " TEXT"); - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.AVATAR + " TEXT"); - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " - + Account.AVATAR + " TEXT"); - } - if (oldVersion < 8 && newVersion >= 8) { - db.execSQL("ALTER TABLE " + Conversation.TABLENAME + " ADD COLUMN " - + Conversation.ATTRIBUTES + " TEXT"); - } - if (oldVersion < 9 && newVersion >= 9) { - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.LAST_TIME + " NUMBER"); - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.LAST_PRESENCE + " TEXT"); - } - if (oldVersion < 10 && newVersion >= 10) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.RELATIVE_FILE_PATH + " TEXT"); - } - if (oldVersion < 11 && newVersion >= 11) { - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.GROUPS + " TEXT"); - db.execSQL("delete from " + Contact.TABLENAME); - db.execSQL("update " + Account.TABLENAME + " set " + Account.ROSTERVERSION + " = NULL"); - } - if (oldVersion < 12 && newVersion >= 12) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.SERVER_MSG_ID + " TEXT"); - } - if (oldVersion < 13 && newVersion >= 13) { - db.execSQL("delete from " + Contact.TABLENAME); - db.execSQL("update " + Account.TABLENAME + " set " + Account.ROSTERVERSION + " = NULL"); - } - if (oldVersion < 14 && newVersion >= 14) { - canonicalizeJids(db); - } - if (oldVersion < 15 && newVersion >= 15) { - recreateAxolotlDb(db); - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.FINGERPRINT + " TEXT"); - } - if (oldVersion < 16 && newVersion >= 16) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.CARBON + " INTEGER"); - } - if (oldVersion < 19 && newVersion >= 19) { - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.DISPLAY_NAME + " TEXT"); - } - if (oldVersion < 20 && newVersion >= 20) { - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.HOSTNAME + " TEXT"); - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PORT + " NUMBER DEFAULT 5222"); - } - if (oldVersion < 26 && newVersion >= 26) { - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS + " TEXT"); - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS_MESSAGE + " TEXT"); - } - if (oldVersion < 41 && newVersion >= 41) { - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.RESOURCE + " TEXT"); - } - /* Any migrations that alter the Account table need to happen BEFORE this migration, as it - * depends on account de-serialization. - */ - if (oldVersion < 17 && newVersion >= 17 && newVersion < 31) { - List accounts = getAccounts(db); - for (Account account : accounts) { - String ownDeviceIdString = account.getKey(SQLiteAxolotlStore.JSONKEY_REGISTRATION_ID); - if (ownDeviceIdString == null) { - continue; - } - int ownDeviceId = Integer.valueOf(ownDeviceIdString); - SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().asBareJid().toString(), ownDeviceId); - deleteSession(db, account, ownAddress); - IdentityKeyPair identityKeyPair = loadOwnIdentityKeyPair(db, account); - if (identityKeyPair != null) { - String[] selectionArgs = { - account.getUuid(), - CryptoHelper.bytesToHex(identityKeyPair.getPublicKey().serialize()) - }; - ContentValues values = new ContentValues(); - values.put(SQLiteAxolotlStore.TRUSTED, 2); - db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, - SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + SQLiteAxolotlStore.FINGERPRINT + " = ? ", - selectionArgs); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not load own identity key pair"); - } - } - } - if (oldVersion < 18 && newVersion >= 18) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.READ + " NUMBER DEFAULT 1"); - } - - if (oldVersion < 21 && newVersion >= 21) { - List accounts = getAccounts(db); - for (Account account : accounts) { - account.unsetPgpSignature(); - db.update(Account.TABLENAME, account.getContentValues(), Account.UUID - + "=?", new String[]{account.getUuid()}); - } - } - - if (oldVersion >= 15 && oldVersion < 22 && newVersion >= 22) { - db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.CERTIFICATE); - } - - if (oldVersion < 23 && newVersion >= 23) { - db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT); - } - - if (oldVersion < 24 && newVersion >= 24) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.EDITED + " TEXT"); - } - - if (oldVersion < 25 && newVersion >= 25) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.OOB + " INTEGER"); - } - - if (oldVersion < 26 && newVersion >= 26) { - db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT); - } - - if (oldVersion < 27 && newVersion >= 27) { - db.execSQL("DELETE FROM " + ServiceDiscoveryResult.TABLENAME); - } - - if (oldVersion < 28 && newVersion >= 28) { - canonicalizeJids(db); - } - - if (oldVersion < 29 && newVersion >= 29) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.ERROR_MESSAGE + " TEXT"); - } - - if (oldVersion >= 15 && oldVersion < 31 && newVersion >= 31) { - db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.TRUST + " TEXT"); - db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.ACTIVE + " NUMBER"); - HashMap migration = new HashMap<>(); - migration.put(0, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true)); - migration.put(1, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true)); - migration.put(2, createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, true)); - migration.put(3, createFingerprintStatusContentValues(FingerprintStatus.Trust.COMPROMISED, false)); - migration.put(4, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false)); - migration.put(5, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false)); - migration.put(6, createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, false)); - migration.put(7, createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, true)); - migration.put(8, createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, false)); - for (Map.Entry entry : migration.entrySet()) { - String whereClause = SQLiteAxolotlStore.TRUSTED + "=?"; - String[] where = {String.valueOf(entry.getKey())}; - db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, entry.getValue(), whereClause, where); - } - } - if (oldVersion >= 15 && oldVersion < 32 && newVersion >= 32) { - db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.LAST_ACTIVATION + " NUMBER"); - ContentValues defaults = new ContentValues(); - defaults.put(SQLiteAxolotlStore.LAST_ACTIVATION, System.currentTimeMillis()); - db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, defaults, null, null); - } - if (oldVersion >= 15 && oldVersion < 33 && newVersion >= 33) { - String whereClause = SQLiteAxolotlStore.OWN + "=1"; - db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED, true), whereClause, null); - } - if (oldVersion < 34 && newVersion >= 34) { - db.execSQL(CREATE_MESSAGE_TIME_INDEX); - // do nothing else at this point because we have seperated videos, images, audios and other files in different directories - } - if (oldVersion < 35 && newVersion >= 35) { - db.execSQL(CREATE_MESSAGE_CONVERSATION_INDEX); - } - if (oldVersion < 36 && newVersion >= 36) { - // only rename videos, images, audios and other files directories - final File oldPicturesDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Images/"); - final File oldFilesDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Files/"); - final File oldAudiosDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Audios/"); - final File oldVideosDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Videos/"); - - if (oldPicturesDirectory.exists() && oldPicturesDirectory.isDirectory()) { - final File newPicturesDirectory = new File(Environment.getExternalStorageDirectory() + "/Monocles Messenger/Media/Monocles Messenger Images/"); - newPicturesDirectory.getParentFile().mkdirs(); - final File[] files = oldPicturesDirectory.listFiles(); - if (files == null) { - return; - } - if (oldPicturesDirectory.renameTo(newPicturesDirectory)) { - Log.d(Config.LOGTAG, "moved " + oldPicturesDirectory.getAbsolutePath() + " to " + newPicturesDirectory.getAbsolutePath()); - } - } - if (oldFilesDirectory.exists() && oldFilesDirectory.isDirectory()) { - final File newFilesDirectory = new File(Environment.getExternalStorageDirectory() + "/Monocles Messenger/Media/Monocles Messenger Files/"); - newFilesDirectory.mkdirs(); - final File[] files = oldFilesDirectory.listFiles(); - if (files == null) { - return; - } - if (oldFilesDirectory.renameTo(newFilesDirectory)) { - Log.d(Config.LOGTAG, "moved " + oldFilesDirectory.getAbsolutePath() + " to " + newFilesDirectory.getAbsolutePath()); - } - } - if (oldAudiosDirectory.exists() && oldAudiosDirectory.isDirectory()) { - final File newAudiosDirectory = new File(Environment.getExternalStorageDirectory() + "/Monocles Messenger/Media/Monocles Messenger Audios/"); - newAudiosDirectory.mkdirs(); - final File[] files = oldAudiosDirectory.listFiles(); - if (files == null) { - return; - } - if (oldAudiosDirectory.renameTo(newAudiosDirectory)) { - Log.d(Config.LOGTAG, "moved " + oldAudiosDirectory.getAbsolutePath() + " to " + newAudiosDirectory.getAbsolutePath()); - } - } - if (oldVideosDirectory.exists() && oldVideosDirectory.isDirectory()) { - final File newVideosDirectory = new File(Environment.getExternalStorageDirectory() + "/Monocles Messenger/Media/Monocles Messenger Videos/"); - newVideosDirectory.mkdirs(); - final File[] files = oldVideosDirectory.listFiles(); - if (files == null) { - return; - } - if (oldVideosDirectory.renameTo(newVideosDirectory)) { - Log.d(Config.LOGTAG, "moved " + oldVideosDirectory.getAbsolutePath() + " to " + newVideosDirectory.getAbsolutePath()); - } - } - } - - if (oldVersion < 37 && newVersion >= 37) { - List accounts = getAccounts(db); - for (Account account : accounts) { - account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, true); - account.setOption(Account.OPTION_LOGGED_IN_SUCCESSFULLY, false); - db.update(Account.TABLENAME, account.getContentValues(), Account.UUID - + "=?", new String[]{account.getUuid()}); - } - } - - if (oldVersion < 38 && newVersion >= 38) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.READ_BY_MARKERS + " TEXT"); - } - - if (oldVersion < 39 && newVersion >= 39) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.MARKABLE + " NUMBER DEFAULT 0"); - } - - if (oldVersion < 43 && newVersion >= 43) { - db.execSQL("DROP TRIGGER IF EXISTS after_message_delete"); - } - - if (oldVersion < 44 && newVersion >= 44) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.DELETED + " NUMBER DEFAULT 0"); - } - - if (oldVersion < 45 && newVersion >= 45) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.FILE_DELETED + " NUMBER DEFAULT 0"); - db.execSQL(CREATE_MESSAGE_DELETED_INDEX); - db.execSQL(CREATE_MESSAGE_FILE_DELETED_INDEX); - db.execSQL(CREATE_MESSAGE_RELATIVE_FILE_PATH_INDEX); - db.execSQL(CREATE_MESSAGE_TYPE_INDEX); - } - - if (oldVersion < 46 && newVersion == 46) { // only available for old database version 46 - if (!isColumnExisting(db, SQLiteAxolotlStore.IDENTITIES_TABLENAME, SQLiteAxolotlStore.TRUSTED)) { - db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.TRUSTED); // TODO - just to make old databases importable, column isn't needed at all - } - } - - if (oldVersion < 49 && newVersion >= 49) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.BODY_LANGUAGE); - } - - if (oldVersion < 50 && newVersion >= 50) { - final long start = SystemClock.elapsedRealtime(); - db.rawQuery("PRAGMA secure_delete = FALSE", null).close(); - 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"); - } - - if (oldVersion < 51 && newVersion >= 51) { - // values in resolver_result are cache and not worth to store - db.execSQL("DROP TABLE IF EXISTS " + RESOLVER_RESULTS_TABLENAME); - db.execSQL(CREATE_RESOLVER_RESULTS_TABLE); - } - - if (oldVersion < 52 && newVersion >= 52) { - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.PRESENCE_NAME + " TEXT"); - } - - if (oldVersion < 53 && newVersion >= 53) { - moveData_PAM_monocles(); - } - - if (oldVersion < 54 && newVersion >= 54) { - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.RTP_CAPABILITY + " TEXT"); - } - if (oldVersion < 55 && newVersion >= 55) { - 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; - } - if (oldVersion < 56 && newVersion >= 56) { - db.beginTransaction(); - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.RETRACT_ID + " TEXT;"); - db.setTransactionSuccessful(); - db.endTransaction(); - requiresMessageIndexRebuild = true; - } - if (oldVersion < 57 && newVersion >= 57) { - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PINNED_MECHANISM + " TEXT"); - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PINNED_CHANNEL_BINDING + " TEXT"); - } - if (oldVersion < 59 && newVersion >= 59) { - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.FAST_MECHANISM + " TEXT"); - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.FAST_TOKEN + " TEXT"); - } - } - - private boolean isColumnExisting(SQLiteDatabase db, String TableName, String ColumnName) { - boolean isExist = false; - Cursor cursor = db.rawQuery("PRAGMA table_info(" + TableName + ")", null); - cursor.moveToFirst(); - do { - String currentColumn = cursor.getString(1); - if (currentColumn.equals(ColumnName)) { - isExist = true; - } - } while (cursor.moveToNext()); - cursor.close(); - return isExist; - } - - private void canonicalizeJids(SQLiteDatabase db) { - // migrate db to new, canonicalized JID domainpart representation - - // Conversation table - Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME, new String[0]); - while (cursor.moveToNext()) { - String newJid; - try { - newJid = Jid.of(cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))).toString(); - } catch (IllegalArgumentException ignored) { - Log.e(Config.LOGTAG, "Failed to migrate Conversation CONTACTJID " - + cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID)) - + ": " + ignored + ". Skipping..."); - continue; - } - - final String[] updateArgs = { - newJid, - cursor.getString(cursor.getColumnIndex(Conversation.UUID)), - }; - db.execSQL("update " + Conversation.TABLENAME - + " set " + Conversation.CONTACTJID + " = ? " - + " where " + Conversation.UUID + " = ?", updateArgs); - } - cursor.close(); - - // Contact table - cursor = db.rawQuery("select * from " + Contact.TABLENAME, new String[0]); - while (cursor.moveToNext()) { - String newJid; - try { - newJid = Jid.of(cursor.getString(cursor.getColumnIndex(Contact.JID))).toString(); - } catch (final IllegalArgumentException e) { - Log.e(Config.LOGTAG, "Failed to migrate Contact JID " - + cursor.getString(cursor.getColumnIndex(Contact.JID)) - + ": Skipping...", e); - continue; - } - - final String[] updateArgs = { - newJid, - cursor.getString(cursor.getColumnIndex(Contact.ACCOUNT)), - cursor.getString(cursor.getColumnIndex(Contact.JID)), - }; - db.execSQL("update " + Contact.TABLENAME - + " set " + Contact.JID + " = ? " - + " where " + Contact.ACCOUNT + " = ? " - + " AND " + Contact.JID + " = ?", updateArgs); - } - cursor.close(); - - // Account table - cursor = db.rawQuery("select * from " + Account.TABLENAME, new String[0]); - while (cursor.moveToNext()) { - String newServer; - try { - newServer = Jid.of( - cursor.getString(cursor.getColumnIndex(Account.USERNAME)), - cursor.getString(cursor.getColumnIndex(Account.SERVER)), - null - ).getDomain().toEscapedString(); - } catch (IllegalArgumentException ignored) { - Log.e(Config.LOGTAG, "Failed to migrate Account SERVER " - + cursor.getString(cursor.getColumnIndex(Account.SERVER)) - + ": " + ignored + ". Skipping..."); - continue; - } - - String[] updateArgs = { - newServer, - cursor.getString(cursor.getColumnIndex(Account.UUID)), - }; - db.execSQL("update " + Account.TABLENAME - + " set " + Account.SERVER + " = ? " - + " where " + Account.UUID + " = ?", updateArgs); - } - cursor.close(); - } - - public void createConversation(Conversation conversation) { - SQLiteDatabase db = this.getWritableDatabase(); - db.insert(Conversation.TABLENAME, null, conversation.getContentValues()); - } - - public void createMessage(Message message) { - SQLiteDatabase db = this.getWritableDatabase(); - db.insert(Message.TABLENAME, null, message.getContentValues()); - } - - public void createAccount(Account account) { - SQLiteDatabase db = this.getWritableDatabase(); - db.insert(Account.TABLENAME, null, account.getContentValues()); - } - - public void insertDiscoveryResult(ServiceDiscoveryResult result) { - SQLiteDatabase db = this.getWritableDatabase(); - db.insert(ServiceDiscoveryResult.TABLENAME, null, result.getContentValues()); - } - - public ServiceDiscoveryResult findDiscoveryResult(final String hash, final String ver) { - SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = {hash, ver}; - Cursor cursor = db.query(ServiceDiscoveryResult.TABLENAME, null, - ServiceDiscoveryResult.HASH + "=? AND " + ServiceDiscoveryResult.VER + "=?", - selectionArgs, null, null, null); - if (cursor.getCount() == 0) { - cursor.close(); - return null; - } - cursor.moveToFirst(); - - ServiceDiscoveryResult result = null; - try { - result = new ServiceDiscoveryResult(cursor); - } catch (JSONException e) { /* result is still null */ } - - cursor.close(); - return result; - } - - public void saveResolverResult(String domain, Resolver.Result result) { - SQLiteDatabase db = this.getWritableDatabase(); - ContentValues contentValues = result.toContentValues(); - contentValues.put(Resolver.Result.DOMAIN, domain); - db.insert(RESOLVER_RESULTS_TABLENAME, null, contentValues); - } - - public synchronized Resolver.Result findResolverResult(String domain) { - SQLiteDatabase db = this.getReadableDatabase(); - String where = Resolver.Result.DOMAIN + "=?"; - String[] whereArgs = {domain}; - final Cursor cursor = db.query(RESOLVER_RESULTS_TABLENAME, null, where, whereArgs, null, null, null); - Resolver.Result result = null; - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - result = Resolver.Result.fromCursor(cursor); - } - } catch (Exception e) { - Log.d(Config.LOGTAG, "unable to find cached resolver result in database " + e.getMessage()); - return null; - } finally { - cursor.close(); - } - } - return result; - } - - public void insertPresenceTemplate(PresenceTemplate template) { - SQLiteDatabase db = this.getWritableDatabase(); - String whereToDelete = PresenceTemplate.MESSAGE + "=?"; - String[] whereToDeleteArgs = {template.getStatusMessage()}; - db.delete(PresenceTemplate.TABELNAME, whereToDelete, whereToDeleteArgs); - db.delete(PresenceTemplate.TABELNAME, PresenceTemplate.UUID + " not in (select " + PresenceTemplate.UUID + " from " + PresenceTemplate.TABELNAME + " order by " + PresenceTemplate.LAST_USED + " desc limit 9)", null); - db.insert(PresenceTemplate.TABELNAME, null, template.getContentValues()); - } - - public List getPresenceTemplates() { - ArrayList templates = new ArrayList<>(); - SQLiteDatabase db = this.getReadableDatabase(); - Cursor cursor = db.query(PresenceTemplate.TABELNAME, null, null, null, null, null, PresenceTemplate.LAST_USED + " desc"); - while (cursor.moveToNext()) { - templates.add(PresenceTemplate.fromCursor(cursor)); - } - cursor.close(); - return templates; - } - - public CopyOnWriteArrayList getConversations(int status) { - CopyOnWriteArrayList list = new CopyOnWriteArrayList<>(); - SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = {Integer.toString(status)}; - Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME - + " where " + Conversation.STATUS + " = ? and " + Conversation.CONTACTJID + " is not null order by " - + Conversation.CREATED + " desc", selectionArgs); - while (cursor.moveToNext()) { - final Conversation conversation = Conversation.fromCursor(cursor); - if (conversation.getJid() instanceof InvalidJid) { - continue; - } - list.add(conversation); - } - cursor.close(); - return list; - } - - public ArrayList getMessages(Conversation conversations, int limit) { - return getMessages(conversations, limit, -1); - } - - public ArrayList getMessages(Conversation conversation, int limit, long timestamp) { - ArrayList list = new ArrayList<>(); - SQLiteDatabase db = this.getReadableDatabase(); - Cursor cursor; - if (timestamp == -1) { - String[] selectionArgs = {conversation.getUuid(), "1"}; - cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION - + "=? and " + Message.DELETED + " term, final String uuid) { - 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.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(" ORDER BY " + Message.TIME_SENT + " DESC limit " + Config.MAX_SEARCH_RESULTS); - Log.d(Config.LOGTAG, "search term: " + FtsUtils.toMatchString(term)); - return db.rawQuery(SQL.toString(), selectionArgs); - } - - public Iterable getMessagesIterable(final Conversation conversation) { - return () -> { - class MessageIterator implements Iterator { - SQLiteDatabase db = getReadableDatabase(); - String[] selectionArgs = {conversation.getUuid(), "1"}; - Cursor cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION - + "=? and " + Message.DELETED + " markFileAsDeleted(final File file, final boolean internal) { - SQLiteDatabase db = this.getReadableDatabase(); - String selection; - String[] selectionArgs; - if (internal) { - final String name = file.getName(); - if (name.endsWith(".pgp")) { - selection = "(" + Message.RELATIVE_FILE_PATH + " IN(?,?) OR (" + Message.RELATIVE_FILE_PATH + "=? and encryption in(1,4))) and type in (1,2,5)"; - selectionArgs = new String[]{file.getAbsolutePath(), name, name.substring(0, name.length() - 4)}; - } else { - selection = Message.RELATIVE_FILE_PATH + " IN(?,?) and type in (1,2,5)"; - selectionArgs = new String[]{file.getAbsolutePath(), name}; - } - } else { - selection = Message.RELATIVE_FILE_PATH + "=? and type in (1,2,5)"; - selectionArgs = new String[]{file.getAbsolutePath()}; - } - final List uuids = new ArrayList<>(); - Cursor cursor = db.query(Message.TABLENAME, new String[]{Message.UUID}, selection, selectionArgs, null, null, null); - while (cursor != null && cursor.moveToNext()) { - uuids.add(cursor.getString(0)); - } - if (cursor != null) { - cursor.close(); - } - markFileAsDeleted(uuids); - return uuids; - } - - public void markFileAsDeleted(List uuids) { - SQLiteDatabase db = this.getReadableDatabase(); - final ContentValues contentValues = new ContentValues(); - final String where = Message.UUID + "=?"; - contentValues.put(Message.FILE_DELETED, 1); - db.beginTransaction(); - for (String uuid : uuids) { - db.update(Message.TABLENAME, contentValues, where, new String[]{uuid}); - } - db.setTransactionSuccessful(); - db.endTransaction(); - } - - public void markFilesAsChanged(List files) { - SQLiteDatabase db = this.getReadableDatabase(); - final String where = Message.UUID + "=?"; - db.beginTransaction(); - for (FilePathInfo info : files) { - final ContentValues contentValues = new ContentValues(); - contentValues.put(Message.FILE_DELETED, info.FileDeleted ? 1 : 0); - db.update(Message.TABLENAME, contentValues, where, new String[]{info.uuid.toString()}); - } - db.setTransactionSuccessful(); - db.endTransaction(); - } - - 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 List list = new ArrayList<>(); - while (cursor != null && cursor.moveToNext()) { - list.add(new FilePathInfo(cursor.getString(0), cursor.getString(1), cursor.getInt(2) > 0)); - } - if (cursor != null) { - cursor.close(); - } - return list; - } - - 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[] args = {account, jid.toString(), jid.toString() + "/%"}; - Cursor cursor = db.rawQuery(SQL + (limit > 0 ? " limit " + String.valueOf(limit) : ""), args); - List filesPaths = new ArrayList<>(); - while (cursor.moveToNext()) { - filesPaths.add(new FilePath(cursor.getString(0), cursor.getString(1))); - } - cursor.close(); - return filesPaths; - } - - public static class FilePath { - public final UUID uuid; - public final String path; - - private FilePath(String uuid, String path) { - this.uuid = UUID.fromString(uuid); - this.path = path; - } - } - - public static class FilePathInfo extends FilePath { - public boolean FileDeleted; - - private FilePathInfo(String uuid, String path, boolean deleted) { - super(uuid, path); - this.FileDeleted = deleted; - } - - public boolean setFileDeleted(boolean deleted) { - final boolean changed = deleted != this.FileDeleted; - this.FileDeleted = deleted; - return changed; - } - } - - public Conversation findConversation(final Account account, final Jid contactJid) { - SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = {account.getUuid(), - contactJid.asBareJid().toString() + "/%", - contactJid.asBareJid().toString() - }; - try(final Cursor cursor = db.query(Conversation.TABLENAME, null, - Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID - + " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null)) { - if (cursor.getCount() == 0) { - return null; - } - cursor.moveToFirst(); - final Conversation conversation = Conversation.fromCursor(cursor); - if (conversation.getJid() instanceof InvalidJid) { - return null; - } - return conversation; - } - } - - public void updateConversation(final Conversation conversation) { - final SQLiteDatabase db = this.getWritableDatabase(); - final String[] args = {conversation.getUuid()}; - db.update(Conversation.TABLENAME, conversation.getContentValues(), - Conversation.UUID + "=?", args); - } - - public List getAccounts() { - SQLiteDatabase db = this.getReadableDatabase(); - return getAccounts(db); - } - - public List getAccountJids(final boolean enabledOnly) { - final SQLiteDatabase db = this.getReadableDatabase(); - final List jids = new ArrayList<>(); - final String[] columns = new String[]{Account.USERNAME, Account.SERVER}; - final String where = enabledOnly ? "not options & (1 <<1)" : null; - try (final Cursor cursor = db.query(Account.TABLENAME, columns, where, null, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - jids.add(Jid.of(cursor.getString(0), cursor.getString(1), null)); - } - } catch (final Exception e) { - return jids; - } - return jids; - } - - - private List getAccounts(SQLiteDatabase db) { - final List list = new ArrayList<>(); - try (final Cursor cursor = db.query(Account.TABLENAME, null, null, null, null, null, null)) { - while (cursor != null && cursor.moveToNext()) { - list.add(Account.fromCursor(cursor)); - } - } catch (final Exception e) { - e.printStackTrace(); - } - return list; - } - - public boolean updateAccount(Account account) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = {account.getUuid()}; - final int rows = db.update(Account.TABLENAME, account.getContentValues(), Account.UUID + "=?", args); - return rows == 1; - } - - public boolean deleteAccount(Account account) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = {account.getUuid()}; - final int rows = db.delete(Account.TABLENAME, Account.UUID + "=?", args); - return rows == 1; - } - - public boolean updateMessage(Message message, boolean includeBody) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = {message.getUuid()}; - ContentValues contentValues = message.getContentValues(); - contentValues.remove(Message.UUID); - if (!includeBody) { - contentValues.remove(Message.BODY); - } - return db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + "=?", args) == 1; - } - - public boolean updateMessage(Message message, String uuid) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = {uuid}; - return db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + "=?", args) == 1; - } - - public void readRoster(Roster roster) { - final SQLiteDatabase db = this.getReadableDatabase(); - final String[] args = {roster.getAccount().getUuid()}; - try (final Cursor cursor = - db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", args, null, null, null)) { - while (cursor.moveToNext()) { - roster.initContact(Contact.fromCursor(cursor)); - } - } - } - - public void writeRoster(final Roster roster) { - long start = SystemClock.elapsedRealtime(); - final Account account = roster.getAccount(); - final SQLiteDatabase db = this.getWritableDatabase(); - db.beginTransaction(); - for (Contact contact : roster.getContacts()) { - if (contact.getOption(Contact.Options.IN_ROSTER) || contact.hasAvatarOrPresenceName() || contact.getOption(Contact.Options.SYNCED_VIA_OTHER)) { - db.insert(Contact.TABLENAME, null, contact.getContentValues()); - } else { - String where = Contact.ACCOUNT + "=? AND " + Contact.JID + "=?"; - String[] whereArgs = {account.getUuid(), contact.getJid().toString()}; - db.delete(Contact.TABLENAME, where, whereArgs); - } - } - db.setTransactionSuccessful(); - db.endTransaction(); - account.setRosterVersion(roster.getVersion()); - updateAccount(account); - long duration = SystemClock.elapsedRealtime() - start; - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": persisted roster in " + duration + "ms"); - } - - public void deleteMessageInConversation(Message message) { - long start = SystemClock.elapsedRealtime(); - final String uuid = message.getUuid(); - final SQLiteDatabase db = this.getWritableDatabase(); - db.beginTransaction(); - ContentValues values = new ContentValues(); - values.put(Message.DELETED, "1"); - String[] args = {uuid}; - int rows = db.update("messages", values, "uuid =?", args); - db.setTransactionSuccessful(); - db.endTransaction(); - Log.d(Config.LOGTAG, "deleted " + rows + " message (" + uuid + ") in " + (SystemClock.elapsedRealtime() - start) + "ms"); - } - - public void deleteMessagesInConversation(Conversation conversation) { - long start = SystemClock.elapsedRealtime(); - final SQLiteDatabase db = this.getWritableDatabase(); - db.beginTransaction(); - final String[] args = {conversation.getUuid()}; - int num = db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args); - db.setTransactionSuccessful(); - db.endTransaction(); - Log.d(Config.LOGTAG, "deleted " + num + " messages for " + conversation.getJid().asBareJid() + " in " + (SystemClock.elapsedRealtime() - start) + "ms"); - } - - public long countExpireOldMessages(long timestamp) { - long start = SystemClock.elapsedRealtime(); - final String[] args = {String.valueOf(timestamp)}; - SQLiteDatabase db = this.getReadableDatabase(); - db.beginTransaction(); - long num = DatabaseUtils.queryNumEntries(db, Message.TABLENAME, "timeSent= 1) { - final String[] args = {String.valueOf(timestamp)}; - SQLiteDatabase db = this.getReadableDatabase(); - db.beginTransaction(); - db.delete(Message.TABLENAME, "timeSent getSubDeviceSessions(Account account, SignalProtocolAddress contact) { - final SQLiteDatabase db = this.getReadableDatabase(); - return getSubDeviceSessions(db, account, contact); - } - - private List getSubDeviceSessions(SQLiteDatabase db, Account account, SignalProtocolAddress contact) { - List devices = new ArrayList<>(); - String[] columns = {SQLiteAxolotlStore.DEVICE_ID}; - String[] selectionArgs = {account.getUuid(), - contact.getName()}; - Cursor cursor = db.query(SQLiteAxolotlStore.SESSION_TABLENAME, - columns, - SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + SQLiteAxolotlStore.NAME + " = ?", - selectionArgs, - null, null, null); - - while (cursor.moveToNext()) { - devices.add(cursor.getInt( - cursor.getColumnIndex(SQLiteAxolotlStore.DEVICE_ID))); - } - - cursor.close(); - return devices; - } - - public List getKnownSignalAddresses(Account account) { - List addresses = new ArrayList<>(); - String[] colums = {"DISTINCT " + SQLiteAxolotlStore.NAME}; - String[] selectionArgs = {account.getUuid()}; - Cursor cursor = getReadableDatabase().query(SQLiteAxolotlStore.SESSION_TABLENAME, - colums, - SQLiteAxolotlStore.ACCOUNT + " = ?", - selectionArgs, - null, null, null - ); - while (cursor.moveToNext()) { - addresses.add(cursor.getString(0)); - } - cursor.close(); - return addresses; - } - - public boolean containsSession(Account account, SignalProtocolAddress contact) { - Cursor cursor = getCursorForSession(account, contact); - int count = cursor.getCount(); - cursor.close(); - return count != 0; - } - - public void storeSession(Account account, SignalProtocolAddress contact, SessionRecord session) { - SQLiteDatabase db = this.getWritableDatabase(); - ContentValues values = new ContentValues(); - values.put(SQLiteAxolotlStore.NAME, contact.getName()); - values.put(SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId()); - values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(), Base64.DEFAULT)); - values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); - db.insert(SQLiteAxolotlStore.SESSION_TABLENAME, null, values); - } - - public void deleteSession(Account account, SignalProtocolAddress contact) { - SQLiteDatabase db = this.getWritableDatabase(); - deleteSession(db, account, contact); - } - - private void deleteSession(SQLiteDatabase db, Account account, SignalProtocolAddress contact) { - String[] args = {account.getUuid(), - contact.getName(), - Integer.toString(contact.getDeviceId())}; - db.delete(SQLiteAxolotlStore.SESSION_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + SQLiteAxolotlStore.NAME + " = ? AND " - + SQLiteAxolotlStore.DEVICE_ID + " = ? ", - args); - } - - public void deleteAllSessions(Account account, SignalProtocolAddress contact) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = {account.getUuid(), contact.getName()}; - db.delete(SQLiteAxolotlStore.SESSION_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + "=? AND " - + SQLiteAxolotlStore.NAME + " = ?", - args); - } - - private Cursor getCursorForPreKey(Account account, int preKeyId) { - SQLiteDatabase db = this.getReadableDatabase(); - String[] columns = {SQLiteAxolotlStore.KEY}; - String[] selectionArgs = {account.getUuid(), Integer.toString(preKeyId)}; - Cursor cursor = db.query(SQLiteAxolotlStore.PREKEY_TABLENAME, - columns, - SQLiteAxolotlStore.ACCOUNT + "=? AND " - + SQLiteAxolotlStore.ID + "=?", - selectionArgs, - null, null, null); - - return cursor; - } - - public PreKeyRecord loadPreKey(Account account, int preKeyId) { - PreKeyRecord record = null; - Cursor cursor = getCursorForPreKey(account, preKeyId); - if (cursor.getCount() != 0) { - cursor.moveToFirst(); - try { - record = new PreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)); - } catch (IOException e) { - throw new AssertionError(e); - } - } - cursor.close(); - return record; - } - - public boolean containsPreKey(Account account, int preKeyId) { - Cursor cursor = getCursorForPreKey(account, preKeyId); - int count = cursor.getCount(); - cursor.close(); - return count != 0; - } - - public void storePreKey(Account account, PreKeyRecord record) { - SQLiteDatabase db = this.getWritableDatabase(); - ContentValues values = new ContentValues(); - values.put(SQLiteAxolotlStore.ID, record.getId()); - values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(), Base64.DEFAULT)); - values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); - db.insert(SQLiteAxolotlStore.PREKEY_TABLENAME, null, values); - } - - public int deletePreKey(Account account, int preKeyId) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = {account.getUuid(), Integer.toString(preKeyId)}; - return db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + "=? AND " - + SQLiteAxolotlStore.ID + "=?", - args); - } - - private Cursor getCursorForSignedPreKey(Account account, int signedPreKeyId) { - SQLiteDatabase db = this.getReadableDatabase(); - String[] columns = {SQLiteAxolotlStore.KEY}; - String[] selectionArgs = {account.getUuid(), Integer.toString(signedPreKeyId)}; - Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, - columns, - SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?", - selectionArgs, - null, null, null); - - return cursor; - } - - public SignedPreKeyRecord loadSignedPreKey(Account account, int signedPreKeyId) { - SignedPreKeyRecord record = null; - Cursor cursor = getCursorForSignedPreKey(account, signedPreKeyId); - if (cursor.getCount() != 0) { - cursor.moveToFirst(); - try { - record = new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)); - } catch (IOException e) { - throw new AssertionError(e); - } - } - cursor.close(); - return record; - } - - public List loadSignedPreKeys(Account account) { - List prekeys = new ArrayList<>(); - SQLiteDatabase db = this.getReadableDatabase(); - String[] columns = {SQLiteAxolotlStore.KEY}; - String[] selectionArgs = {account.getUuid()}; - Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, - columns, - SQLiteAxolotlStore.ACCOUNT + "=?", - selectionArgs, - null, null, null); - - while (cursor.moveToNext()) { - try { - prekeys.add(new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT))); - } catch (IOException ignored) { - } - } - cursor.close(); - return prekeys; - } - - public int getSignedPreKeysCount(Account account) { - String[] columns = {"count(" + SQLiteAxolotlStore.KEY + ")"}; - String[] selectionArgs = {account.getUuid()}; - SQLiteDatabase db = this.getReadableDatabase(); - Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, - columns, - SQLiteAxolotlStore.ACCOUNT + "=?", - selectionArgs, - null, null, null); - final int count; - if (cursor.moveToFirst()) { - count = cursor.getInt(0); - } else { - count = 0; - } - cursor.close(); - return count; - } - - public boolean containsSignedPreKey(Account account, int signedPreKeyId) { - Cursor cursor = getCursorForPreKey(account, signedPreKeyId); - int count = cursor.getCount(); - cursor.close(); - return count != 0; - } - - public void storeSignedPreKey(Account account, SignedPreKeyRecord record) { - SQLiteDatabase db = this.getWritableDatabase(); - ContentValues values = new ContentValues(); - values.put(SQLiteAxolotlStore.ID, record.getId()); - values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(), Base64.DEFAULT)); - values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); - db.insert(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values); - } - - public void deleteSignedPreKey(Account account, int signedPreKeyId) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = {account.getUuid(), Integer.toString(signedPreKeyId)}; - db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + "=? AND " - + SQLiteAxolotlStore.ID + "=?", - args); - } - - private Cursor getIdentityKeyCursor(Account account, String name, boolean own) { - final SQLiteDatabase db = this.getReadableDatabase(); - return getIdentityKeyCursor(db, account, name, own); - } - - private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String name, boolean own) { - return getIdentityKeyCursor(db, account, name, own, null); - } - - private Cursor getIdentityKeyCursor(Account account, String fingerprint) { - final SQLiteDatabase db = this.getReadableDatabase(); - return getIdentityKeyCursor(db, account, fingerprint); - } - - private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String fingerprint) { - return getIdentityKeyCursor(db, account, null, null, fingerprint); - } - - private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String name, Boolean own, String fingerprint) { - String[] columns = {SQLiteAxolotlStore.TRUST, - SQLiteAxolotlStore.ACTIVE, - SQLiteAxolotlStore.LAST_ACTIVATION, - SQLiteAxolotlStore.KEY}; - ArrayList selectionArgs = new ArrayList<>(4); - selectionArgs.add(account.getUuid()); - String selectionString = SQLiteAxolotlStore.ACCOUNT + " = ?"; - if (name != null) { - selectionArgs.add(name); - selectionString += " AND " + SQLiteAxolotlStore.NAME + " = ?"; - } - if (fingerprint != null) { - selectionArgs.add(fingerprint); - selectionString += " AND " + SQLiteAxolotlStore.FINGERPRINT + " = ?"; - } - if (own != null) { - selectionArgs.add(own ? "1" : "0"); - selectionString += " AND " + SQLiteAxolotlStore.OWN + " = ?"; - } - Cursor cursor = db.query(SQLiteAxolotlStore.IDENTITIES_TABLENAME, - columns, - selectionString, - selectionArgs.toArray(new String[selectionArgs.size()]), - null, null, null); - - return cursor; - } - - public IdentityKeyPair loadOwnIdentityKeyPair(Account account) { - SQLiteDatabase db = getReadableDatabase(); - return loadOwnIdentityKeyPair(db, account); - } - - private IdentityKeyPair loadOwnIdentityKeyPair(SQLiteDatabase db, Account account) { - String name = account.getJid().asBareJid().toString(); - IdentityKeyPair identityKeyPair = null; - Cursor cursor = getIdentityKeyCursor(db, account, name, true); - if (cursor.getCount() != 0) { - cursor.moveToFirst(); - try { - identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)); - } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().asBareJid() + ", address: " + name); - } - } - cursor.close(); - - return identityKeyPair; - } - - public Set loadIdentityKeys(Account account, String name) { - return loadIdentityKeys(account, name, null); - } - - public Set loadIdentityKeys(Account account, String name, FingerprintStatus status) { - Set identityKeys = new HashSet<>(); - Cursor cursor = getIdentityKeyCursor(account, name, false); - - while (cursor.moveToNext()) { - if (status != null && !FingerprintStatus.fromCursor(cursor).equals(status)) { - continue; - } - try { - String key = cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)); - if (key != null) { - identityKeys.add(new IdentityKey(Base64.decode(key, Base64.DEFAULT), 0)); - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Missing key (possibly preverified) in database for account" + account.getJid().asBareJid() + ", address: " + name); - } - } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().asBareJid() + ", address: " + name); - } - } - cursor.close(); - - return identityKeys; - } - - public long numTrustedKeys(Account account, String name) { - SQLiteDatabase db = getReadableDatabase(); - String[] args = { - account.getUuid(), - name, - FingerprintStatus.Trust.TRUSTED.toString(), - FingerprintStatus.Trust.VERIFIED.toString(), - FingerprintStatus.Trust.VERIFIED_X509.toString() - }; - return DatabaseUtils.queryNumEntries(db, SQLiteAxolotlStore.IDENTITIES_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + " = ?" - + " AND " + SQLiteAxolotlStore.NAME + " = ?" - + " AND (" + SQLiteAxolotlStore.TRUST + " = ? OR " + SQLiteAxolotlStore.TRUST + " = ? OR " + SQLiteAxolotlStore.TRUST + " = ?)" - + " AND " + SQLiteAxolotlStore.ACTIVE + " > 0", - args - ); - } - - private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, FingerprintStatus status) { - SQLiteDatabase db = this.getWritableDatabase(); - ContentValues values = new ContentValues(); - values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); - values.put(SQLiteAxolotlStore.NAME, name); - values.put(SQLiteAxolotlStore.OWN, own ? 1 : 0); - values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint); - values.put(SQLiteAxolotlStore.KEY, base64Serialized); - values.putAll(status.toContentValues()); - String where = SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.NAME + "=? AND " + SQLiteAxolotlStore.FINGERPRINT + " =?"; - String[] whereArgs = {account.getUuid(), name, fingerprint}; - int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, where, whereArgs); - if (rows == 0) { - db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values); - } - } - - public void storePreVerification(Account account, String name, String fingerprint, FingerprintStatus status) { - SQLiteDatabase db = this.getWritableDatabase(); - ContentValues values = new ContentValues(); - values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); - values.put(SQLiteAxolotlStore.NAME, name); - values.put(SQLiteAxolotlStore.OWN, 0); - values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint); - values.putAll(status.toContentValues()); - db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values); - } - - public FingerprintStatus getFingerprintStatus(Account account, String fingerprint) { - Cursor cursor = getIdentityKeyCursor(account, fingerprint); - final FingerprintStatus status; - if (cursor.getCount() > 0) { - cursor.moveToFirst(); - status = FingerprintStatus.fromCursor(cursor); - } else { - status = null; - } - cursor.close(); - return status; - } - - public boolean setIdentityKeyTrust(Account account, String fingerprint, FingerprintStatus fingerprintStatus) { - SQLiteDatabase db = this.getWritableDatabase(); - return setIdentityKeyTrust(db, account, fingerprint, fingerprintStatus); - } - - private boolean setIdentityKeyTrust(SQLiteDatabase db, Account account, String fingerprint, FingerprintStatus status) { - String[] selectionArgs = { - account.getUuid(), - fingerprint - }; - int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, status.toContentValues(), - SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + SQLiteAxolotlStore.FINGERPRINT + " = ? ", - selectionArgs); - return rows == 1; - } - - public boolean setIdentityKeyCertificate(Account account, String fingerprint, X509Certificate x509Certificate) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] selectionArgs = { - account.getUuid(), - fingerprint - }; - try { - ContentValues values = new ContentValues(); - values.put(SQLiteAxolotlStore.CERTIFICATE, x509Certificate.getEncoded()); - return db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, - SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + SQLiteAxolotlStore.FINGERPRINT + " = ? ", - selectionArgs) == 1; - } catch (CertificateEncodingException e) { - Log.d(Config.LOGTAG, "could not encode certificate"); - return false; - } - } - - public X509Certificate getIdentityKeyCertifcate(Account account, String fingerprint) { - SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = { - account.getUuid(), - fingerprint - }; - String[] colums = {SQLiteAxolotlStore.CERTIFICATE}; - String selection = SQLiteAxolotlStore.ACCOUNT + " = ? AND " + SQLiteAxolotlStore.FINGERPRINT + " = ? "; - Cursor cursor = db.query(SQLiteAxolotlStore.IDENTITIES_TABLENAME, colums, selection, selectionArgs, null, null, null); - if (cursor.getCount() < 1) { - return null; - } else { - cursor.moveToFirst(); - byte[] certificate = cursor.getBlob(cursor.getColumnIndex(SQLiteAxolotlStore.CERTIFICATE)); - cursor.close(); - if (certificate == null || certificate.length == 0) { - return null; - } - try { - CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(certificate)); - } catch (CertificateException e) { - Log.d(Config.LOGTAG, "certificate exception " + e.getMessage()); - return null; - } - } - } - - public void storeIdentityKey(Account account, String name, IdentityKey identityKey, FingerprintStatus status) { - storeIdentityKey(account, name, false, CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT), status); - } - - public void storeOwnIdentityKeyPair(Account account, IdentityKeyPair identityKeyPair) { - storeIdentityKey(account, account.getJid().asBareJid().toString(), true, CryptoHelper.bytesToHex(identityKeyPair.getPublicKey().serialize()), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), FingerprintStatus.createActiveVerified(false)); - } - - - private void recreateAxolotlDb(SQLiteDatabase db) { - Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + ">>> (RE)CREATING AXOLOTL DATABASE <<<"); - db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SESSION_TABLENAME); - db.execSQL(CREATE_SESSIONS_STATEMENT); - db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.PREKEY_TABLENAME); - db.execSQL(CREATE_PREKEYS_STATEMENT); - db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME); - db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); - db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.IDENTITIES_TABLENAME); - db.execSQL(CREATE_IDENTITIES_STATEMENT); - } - - public void wipeAxolotlDb(Account account) { - String accountName = account.getUuid(); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + accountName + " <<<"); - SQLiteDatabase db = this.getWritableDatabase(); - String[] deleteArgs = { - accountName - }; - db.delete(SQLiteAxolotlStore.SESSION_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + " = ?", - deleteArgs); - db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + " = ?", - deleteArgs); - db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + " = ?", - deleteArgs); - db.delete(SQLiteAxolotlStore.IDENTITIES_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + " = ?", - deleteArgs); - } - - public List getFrequentContacts(int days) { - SQLiteDatabase db = this.getReadableDatabase(); - final String SQL = "select " + Conversation.TABLENAME + "." + Conversation.ACCOUNT + "," + Conversation.TABLENAME + "." + Conversation.CONTACTJID + " from " + Conversation.TABLENAME + " join " + Message.TABLENAME + " on conversations.uuid=messages.conversationUuid where messages.status>0 and carbon==0 and conversations.mode=0 and messages.timeSent>=? group by conversations.uuid order by count(body) desc limit 4;"; - String[] whereArgs = new String[]{String.valueOf(System.currentTimeMillis() - (Config.MILLISECONDS_IN_DAY * days))}; - Cursor cursor = db.rawQuery(SQL, whereArgs); - ArrayList contacts = new ArrayList<>(); - while (cursor.moveToNext()) { - try { - contacts.add(new ShortcutService.FrequentContact(cursor.getString(0), Jid.of(cursor.getString(1)))); - } catch (Exception e) { - Log.d(Config.LOGTAG, e.getMessage()); - } - } - cursor.close(); - return contacts; - } -} diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java deleted file mode 100644 index 071fd9ca4..000000000 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ /dev/null @@ -1,2204 +0,0 @@ -package eu.siacs.conversations.persistance; - -import static eu.siacs.conversations.utils.StorageHelper.getConversationsDirectory; -import static eu.siacs.conversations.utils.StorageHelper.getGlobalAudiosPath; -import static eu.siacs.conversations.utils.StorageHelper.getGlobalDocumentsPath; -import static eu.siacs.conversations.utils.StorageHelper.getGlobalPicturesPath; -import static eu.siacs.conversations.utils.StorageHelper.getGlobalVideosPath; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Matrix; -import android.graphics.Movie; -import android.graphics.Paint; -import android.graphics.RectF; -import android.graphics.drawable.Drawable; -import android.graphics.pdf.PdfRenderer; -import android.media.MediaMetadataRetriever; -import android.media.MediaScannerConnection; -import android.net.Uri; -import android.os.Build; -import android.os.Environment; -import android.os.ParcelFileDescriptor; -import android.os.StatFs; -import android.os.SystemClock; -import android.preference.PreferenceManager; -import android.provider.MediaStore; -import android.provider.OpenableColumns; -import android.system.Os; -import android.system.StructStat; -import android.util.Base64; -import android.util.Base64OutputStream; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.LruCache; - -import androidx.annotation.StringRes; -import androidx.core.content.FileProvider; -import androidx.exifinterface.media.ExifInterface; - -import com.google.common.base.Strings; -import com.google.common.io.ByteStreams; - -import java.io.ByteArrayOutputStream; -import java.io.Closeable; -import java.io.File; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.ref.WeakReference; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.URL; -import java.nio.channels.FileChannel; -import java.nio.charset.StandardCharsets; -import java.security.DigestOutputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.Date; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Stack; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.services.AttachFileToConversationRunnable; -import eu.siacs.conversations.ui.adapter.MediaAdapter; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.SettingsActivity; -import eu.siacs.conversations.ui.util.Attachment; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.FileUtils; -import eu.siacs.conversations.utils.FileWriterException; -import eu.siacs.conversations.utils.MimeUtils; -import eu.siacs.conversations.xmpp.pep.Avatar; -import ezvcard.Ezvcard; -import ezvcard.VCard; -import me.drakeet.support.toast.ToastCompat; - -public class FileBackend { - - private static final Object THUMBNAIL_LOCK = new Object(); - - private static final SimpleDateFormat fileDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); - - private static final String FILE_PROVIDER = ".files"; - public static final String APP_DIRECTORY = "monocles chat"; - public static final String FILES = "Files"; - public static final String SENT_FILES = "Files" + File.separator + "Sent"; - public static final String AUDIOS = "Audios"; - public static final String SENT_AUDIOS = "Audios" + File.separator + "Sent"; - public static final String IMAGES = "Images"; - public static final String SENT_IMAGES = "Images" + File.separator + "Sent"; - public static final String VIDEOS = "Videos"; - public static final String SENT_VIDEOS = "Videos" + File.separator + "Sent"; - - public static final AtomicInteger STORAGE_INDEX = new AtomicInteger(0); - - private final XmppConnectionService mXmppConnectionService; - - public FileBackend(XmppConnectionService service) { - this.mXmppConnectionService = service; - } - - public static void switchStorage(boolean checked) { - STORAGE_INDEX.set(checked ? 1 : 0); - } - - private static void createNoMedia(Context context) { - final File nomedia_files = new File(getConversationsDirectory(context, FILES) + File.separator + ".nomedia"); - final File nomedia_audios = new File(getConversationsDirectory(context, AUDIOS) + File.separator + ".nomedia"); - final File nomedia_videos_sent = new File(getConversationsDirectory(context, SENT_VIDEOS) + File.separator + ".nomedia"); - final File nomedia_files_sent = new File(getConversationsDirectory(context, SENT_FILES) + File.separator + ".nomedia"); - final File nomedia_audios_sent = new File(getConversationsDirectory(context, SENT_AUDIOS) + File.separator + ".nomedia"); - final File nomedia_images_sent = new File(getConversationsDirectory(context, SENT_IMAGES) + File.separator + ".nomedia"); - if (!nomedia_files.exists()) { - try { - nomedia_files.createNewFile(); - } catch (Exception e) { - Log.d(Config.LOGTAG, "could not create nomedia file for files directory"); - } - } - if (!nomedia_audios.exists()) { - try { - nomedia_audios.createNewFile(); - } catch (Exception e) { - Log.d(Config.LOGTAG, "could not create nomedia file for audio directory"); - } - } - if (!nomedia_videos_sent.exists()) { - try { - nomedia_videos_sent.createNewFile(); - } catch (Exception e) { - Log.d(Config.LOGTAG, "could not create nomedia file for videos sent directory"); - } - } - if (!nomedia_files_sent.exists()) { - try { - nomedia_files_sent.createNewFile(); - } catch (Exception e) { - Log.d(Config.LOGTAG, "could not create nomedia file for files sent directory"); - } - } - if (!nomedia_audios_sent.exists()) { - try { - nomedia_audios_sent.createNewFile(); - } catch (Exception e) { - Log.d(Config.LOGTAG, "could not create nomedia file for audios sent directory"); - } - } - if (!nomedia_images_sent.exists()) { - try { - nomedia_images_sent.createNewFile(); - } catch (Exception e) { - Log.d(Config.LOGTAG, "could not create nomedia file for images sent directory"); - } - } - } - - public static Uri getMediaUri(Context context, File file) { - final String filePath = file.getAbsolutePath(); - try (final Cursor - cursor = context.getContentResolver().query( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - new String[]{MediaStore.Images.Media._ID}, - MediaStore.Images.Media.DATA + "=? ", - new String[]{filePath}, null)) { - if (cursor != null && cursor.moveToFirst()) { - final int id = - cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)); - return Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id)); - } else { - return null; - } - } catch (final Exception e) { - return null; - } - } - - public static void updateFileParams(Message message, String url, long size) { - final StringBuilder body = new StringBuilder(); - body.append(url).append('|').append(size); - message.setBody(body.toString()); - } - - private void createNoMedia(File diretory) { - final File noMedia = new File(diretory, ".nomedia"); - if (!noMedia.exists()) { - try { - if (!noMedia.createNewFile()) { - Log.d(Config.LOGTAG, "created nomedia file " + noMedia.getAbsolutePath()); - } - } catch (Exception e) { - Log.d(Config.LOGTAG, "could not create nomedia file"); - } - } - } - - public static void updateMediaScanner(XmppConnectionService mXmppConnectionService, File file) { - updateMediaScanner(mXmppConnectionService, file, null); - } - - public static void updateMediaScanner(XmppConnectionService mXmppConnectionService, File file, final Runnable callback) { - if ((file.getAbsolutePath().startsWith(getConversationsDirectory(mXmppConnectionService, IMAGES).getAbsolutePath()) - || file.getAbsolutePath().startsWith(getConversationsDirectory(mXmppConnectionService, VIDEOS).getAbsolutePath())) - && !file.getAbsolutePath().toLowerCase(Locale.US).contains("sent")) { - MediaScannerConnection.scanFile(mXmppConnectionService, new String[]{file.getAbsolutePath()}, null, new MediaScannerConnection.MediaScannerConnectionClient() { - @Override - public void onMediaScannerConnected() { - - } - - @Override - public void onScanCompleted(String path, Uri uri) { - if (callback != null && file.getAbsolutePath().equals(path)) { - callback.run(); - } else { - Log.d(Config.LOGTAG, "media scanner scanned wrong file"); - if (callback != null) { - callback.run(); - } - } - } - }); - Log.d(Config.LOGTAG, "media scanner broadcasts file scan: " + file.getAbsolutePath()); - Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); - intent.setData(Uri.fromFile(new File(file.getAbsolutePath()))); - mXmppConnectionService.sendBroadcast(intent); - } else { - createNoMedia(mXmppConnectionService); - } - if (callback != null) { - callback.run(); - } - } - - public boolean deleteFile(File file) { - return file.delete(); - } - - public boolean deleteFile(Message message) { - File file = getFile(message); - return deleteFile(file); - } - - public void expireOldFiles(File dir, long timestamp) { - try { - long start = SystemClock.elapsedRealtime(); - int num = 0; - if (dir == null) { - return; - } - Stack dirlist = new Stack(); - dirlist.clear(); - dirlist.push(dir); - while (!dirlist.isEmpty()) { - File dirCurrent = dirlist.pop(); - File[] fileList = dirCurrent.listFiles(); - if (fileList != null) { - for (File file : fileList) { - if (file.isDirectory()) { - dirlist.push(file); - } else { - if (file.exists() && !file.getName().equalsIgnoreCase(".nomedia")) { - long lastModified = file.lastModified(); - if (lastModified < timestamp) { - num++; - deleteFile(file); - } - } - } - } - } - } - Log.d(Config.LOGTAG, "deleted " + num + " expired files in " + (SystemClock.elapsedRealtime() - start) + "ms"); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public static void deleteOldBackups(File dir, List mAccounts) { - try { - long start = SystemClock.elapsedRealtime(); - int num = 0; - if (dir == null) { - return; - } - Stack dirlist = new Stack(); - dirlist.clear(); - dirlist.push(dir); - File dirCurrent = dirlist.pop(); - File[] fileList = dirCurrent.listFiles(); - while (!dirlist.isEmpty()) { - if (fileList != null) { - for (File file : fileList) { - if (file.isDirectory()) { - dirlist.push(file); - } - } - } - } - if (fileList != null) { - ArrayList fileListByAccount = new ArrayList(); - ArrayList simpleFileList = new ArrayList(Arrays.asList(fileList)); - for (Account account : mAccounts) { - String jid = account.getJid().asBareJid().toString(); - for (int i = 0; i < simpleFileList.size(); i++) { - File currentFile = simpleFileList.get(i); - String fileName = currentFile.getName(); - if (fileName.startsWith(jid) && fileName.endsWith(".ceb")) { - fileListByAccount.add(currentFile); - simpleFileList.remove(currentFile); - i--; - } - } - if (fileListByAccount.size() > 2) { - num += expireOldBackups(fileListByAccount); - } - fileListByAccount.clear(); - } - } else { - return; - } - Log.d(Config.LOGTAG, "deleted " + num + " old backup files in " + (SystemClock.elapsedRealtime() - start) + "ms"); - } catch (Exception e) { - e.printStackTrace(); - } - } - - private static int expireOldBackups(ArrayList fileListByAccount) { - int num = 0; - try { - Collections.sort(fileListByAccount, new Comparator() { - @Override - public int compare(File f1, File f2) { - return Long.compare(f2.lastModified(), f1.lastModified()); - } - }); - fileListByAccount.subList(0, 2).clear(); - for (File currentFile : fileListByAccount) { - if (currentFile.delete()) { - num++; - } - } - - } catch (Exception e) { - e.printStackTrace(); - } - return num; - } - - public void deleteFilesInDir(File dir) { - long start = SystemClock.elapsedRealtime(); - int num = 0; - if (dir == null) { - return; - } - Stack dirlist = new Stack<>(); - dirlist.push(dir); - while (!dirlist.isEmpty()) { - File dirCurrent = dirlist.pop(); - File[] fileList = dirCurrent.listFiles(); - if (fileList != null && fileList.length > 0) { - for (File file : fileList) { - if (file.isDirectory()) { - dirlist.push(file); - } else { - if (file.exists() && !file.getName().equalsIgnoreCase(".nomedia")) { - num++; - deleteFile(file); - } - } - } - } - } - Log.d(Config.LOGTAG, "deleted " + num + " files in " + dir + " in " + (SystemClock.elapsedRealtime() - start) + "ms"); - } - - public DownloadableFile getFile(Message message) { - return getFile(message, true); - } - - public DownloadableFile getFileForPath(String path) { - return getFileForPath(path, MimeUtils.guessMimeTypeFromExtension(MimeUtils.extractRelevantExtension(path))); - } - - private DownloadableFile getFileForPath(String path, String mime) { - DownloadableFile file = null; - if (path.startsWith(File.separator)) { - file = new DownloadableFile(path); - } else { - if (mime != null && mime.startsWith("image")) { - file = new DownloadableFile(getConversationsDirectory(mXmppConnectionService, IMAGES) + File.separator + path); - } else if (mime != null && mime.startsWith("video")) { - file = new DownloadableFile(getConversationsDirectory(mXmppConnectionService, VIDEOS) + File.separator + path); - } else if (mime != null && mime.startsWith("audio")) { - file = new DownloadableFile(getConversationsDirectory(mXmppConnectionService, AUDIOS) + File.separator + path); - } else { - file = new DownloadableFile(getConversationsDirectory(mXmppConnectionService, FILES) + File.separator + path); - } - } - return file; - } - - public boolean isInternalFile(final File file) { - final File internalFile = getFileForPath(file.getName()); - return file.getAbsolutePath().equals(internalFile.getAbsolutePath()); - } - - public DownloadableFile getFile(Message message, boolean decrypted) { - final boolean encrypted = - !decrypted - && (message.getEncryption() == Message.ENCRYPTION_PGP - || message.getEncryption() == Message.ENCRYPTION_DECRYPTED); - String path = message.getRelativeFilePath(); - if (path == null) { - path = fileDateFormat.format(new Date(message.getTimeSent())) + "_" + message.getUuid().substring(0, 4); - } - final DownloadableFile file = getFileForPath(path, message.getMimeType()); - if (encrypted) { - return new DownloadableFile( - mXmppConnectionService.getCacheDir(), - String.format("%s.%s", file.getName(), "pgp")); - } else { - return file; - } - } - - public static long getFileSize(Context context, Uri uri) { - try { - final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); - if (cursor != null && cursor.moveToFirst()) { - @SuppressLint("Range") long size = cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE)); - cursor.close(); - return size; - } else { - return -1; - } - } catch (Exception e) { - return -1; - } - } - - public static long getDirectorySize(final File file) { - try { - if (file == null || !file.exists() || !file.isDirectory()) - return 0; - final List dirs = new LinkedList<>(); - dirs.add(file); - long result = 0; - while (!dirs.isEmpty()) { - final File dir = dirs.remove(0); - if (!dir.exists()) { - continue; - } - final File[] listFiles = dir.listFiles(); - if (listFiles == null || listFiles.length == 0) { - continue; - } - for (final File child : listFiles) { - result += child.length(); - if (child.isDirectory()) { - dirs.add(child); - } - } - } - return result; - } catch (Exception e) { - e.printStackTrace(); - return 0; - } - } - - public static long getDiskSize() { - try { - StatFs external = new StatFs(Environment.getExternalStorageDirectory().getAbsolutePath()); - return (long) external.getBlockCount() * (long) external.getBlockSize(); - } catch (Exception e) { - e.printStackTrace(); - return 0; - } - } - - public static boolean allFilesUnderSize(Context context, List attachments, long max) { - final boolean compressVideo = !AttachFileToConversationRunnable.getVideoCompression(context).equals("uncompressed"); - if (max <= 0) { - Log.d(Config.LOGTAG, "server did not report max file size for http upload"); - return true; //exception to be compatible with HTTP Upload < v0.2 - } - for (Attachment attachment : attachments) { - if (attachment.getType() != Attachment.Type.FILE) { - continue; - } - String mime = attachment.getMime(); - if (mime != null && mime.startsWith("video/") && compressVideo) { - try { - Dimensions dimensions = FileBackend.getVideoDimensions(context, attachment.getUri()); - if (dimensions.getMin() >= 720) { - Log.d(Config.LOGTAG, - "do not consider video file with min width larger than 720 for" - + " size check"); - continue; - } - } catch (NotAVideoFile notAVideoFile) { - //ignore and fall through - } - } - if (FileBackend.getFileSize(context, attachment.getUri()) > max) { - Log.d(Config.LOGTAG, "not all files are under " + max + " bytes. suggesting falling back to jingle"); - return false; - } - } - return true; - } - - public List convertToAttachments(final List relativeFilePaths) { - final List attachments = new ArrayList<>(); - for (final DatabaseBackend.FilePath relativeFilePath : relativeFilePaths) { - final String mime = MimeUtils.guessMimeTypeFromExtension(MimeUtils.extractRelevantExtension(relativeFilePath.path)); - final File file = getFileForPath(relativeFilePath.path, mime); - if (file.exists()) { - attachments.add(Attachment.of(relativeFilePath.uuid, file, mime)); - } - } - return attachments; - } - - public static String getFileType(File file) { - String extension = MimeUtils.extractRelevantExtension(file.getAbsolutePath(), true); - String mime = MimeUtils.guessMimeTypeFromExtension(extension); - if (mime.toLowerCase(Locale.US).contains("image")) { - return IMAGES; - } else if (mime.toLowerCase(Locale.US).contains("video")) { - return VIDEOS; - } else if (mime.toLowerCase(Locale.US).contains("audio")) { - return AUDIOS; - } else { - return FILES; - } - } - - private Bitmap resize(final Bitmap originalBitmap, int size) throws IOException { - int w = originalBitmap.getWidth(); - int h = originalBitmap.getHeight(); - if (w <= 0 || h <= 0) { - throw new IOException("Decoded bitmap reported bounds smaller 0"); - } else if (Math.max(w, h) > size) { - int scalledW; - int scalledH; - if (w <= h) { - scalledW = Math.max((int) (w / ((double) h / size)), 1); - scalledH = size; - } else { - scalledW = size; - scalledH = Math.max((int) (h / ((double) w / size)), 1); - } - final Bitmap result = Bitmap.createScaledBitmap(originalBitmap, scalledW, scalledH, true); - if (!originalBitmap.isRecycled()) { - originalBitmap.recycle(); - } - return result; - } else { - return originalBitmap; - } - } - - private static Bitmap rotate(final Bitmap bitmap, final int degree) { - if (degree == 0) { - return bitmap; - } - final int w = bitmap.getWidth(); - final int h = bitmap.getHeight(); - final Matrix matrix = new Matrix(); - matrix.postRotate(degree); - final Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, w, h, matrix, true); - if (!bitmap.isRecycled()) { - bitmap.recycle(); - } - return result; - } - - public boolean useImageAsIs(final Uri uri) { - final String path = getOriginalPath(uri); - if (path == null || isPathBlacklisted(path)) { - return false; - } - final File file = new File(path); - long size = file.length(); - if ((size == 0 || size >= mXmppConnectionService.getCompressImageSizePreference()) && mXmppConnectionService.getCompressImageSizePreference() != 0) { - return false; - } - if (mXmppConnectionService.getCompressImageResolutionPreference() == 0 && mXmppConnectionService.getCompressImageSizePreference() == 0) { - return true; - } - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - try { - final InputStream inputStream = mXmppConnectionService.getContentResolver().openInputStream(uri); - BitmapFactory.decodeStream(inputStream, null, options); - close(inputStream); - if (options.outMimeType == null || options.outHeight <= 0 || options.outWidth <= 0) { - return false; - } - return (options.outWidth <= mXmppConnectionService.getCompressImageResolutionPreference() && options.outHeight <= mXmppConnectionService.getCompressImageResolutionPreference() && options.outMimeType.contains(Config.IMAGE_FORMAT.name().toLowerCase())); - } catch (FileNotFoundException e) { - Log.d(Config.LOGTAG, "unable to get image dimensions", e); - return false; - } - } - - public boolean useFileAsIs(Uri uri) { - String path = getOriginalPath(uri); - if (path == null) { - Log.d(Config.LOGTAG, "File path = null"); - return false; - } - if (path.contains(getConversationsDirectory(mXmppConnectionService, "null").getAbsolutePath())) { - Log.d(Config.LOGTAG, "File " + path + " is in our directory"); - return true; - } - Log.d(Config.LOGTAG, "File " + path + " is not in our directory"); - return false; - } - - public static boolean isPathBlacklisted(String path) { - Environment.getDataDirectory(); - final String androidDataPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "Android" + File.separator + "data" + File.separator + ""; - return path.startsWith(androidDataPath); - } - - public String getOriginalPath(Uri uri) { - return FileUtils.getPath(mXmppConnectionService, uri); - } - - private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException { - Log.d(Config.LOGTAG, "copy file (" + uri.toString() + ") to private storage " + file.getAbsolutePath()); - file.getParentFile().mkdirs(); - try { - file.createNewFile(); - } 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 (Exception e) { - throw new FileWriterException(file); - } - try { - os.flush(); - } catch (IOException e) { - throw new FileWriterException(file); - } - } 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 | IllegalStateException 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); - } - } - - public void copyFileToPrivateStorage(Message message, Uri uri, String type) throws FileCopyException { - String mime = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type); - Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage (mime=" + mime + ")"); - String extension = MimeUtils.guessExtensionFromMimeType(mime); - if (extension == null) { - Log.d(Config.LOGTAG, "extension from mime type was null"); - extension = getExtensionFromUri(uri); - } - if ("ogg".equals(extension) && type != null && type.startsWith("audio/")) { - extension = "oga"; - } - String filename = "Sent" + File.separator + fileDateFormat.format(new Date(message.getTimeSent())) + "_" + message.getUuid().substring(0, 4); - setupRelativeFilePath(message, String.format("%s.%s", filename, extension)); - copyFileToPrivateStorage(mXmppConnectionService.getFileBackend().getFile(message), uri); - } - - public static void moveDirectory(XmppConnectionService mXmppConnectionService, File sourceLocation, File targetLocation) throws Exception { - if (sourceLocation.isDirectory()) { - Log.d(Config.LOGTAG, "Migration: copy from " + sourceLocation.getAbsolutePath() + " to " + targetLocation.getAbsolutePath()); - if (!targetLocation.exists()) { - targetLocation.mkdir(); - Log.d(Config.LOGTAG, "Migration: creating target dir " + targetLocation.getAbsolutePath()); - } - String[] children = sourceLocation.list(); - for (String child : children) { - try { - Log.d(Config.LOGTAG, "Migration: iterating in dir " + child); - moveDirectory(mXmppConnectionService, new File(sourceLocation, child), new File(targetLocation, child)); - } finally { - sourceLocation.delete(); - } - } - } else { - try { - Log.d(Config.LOGTAG, "Migration: copy " + sourceLocation.getName() + " to target dir " + targetLocation.getAbsolutePath()); - InputStream in = new FileInputStream(sourceLocation); - OutputStream out = new FileOutputStream(targetLocation); - // Copy the bits from instream to outstream - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - } - in.close(); - out.close(); - } finally { - updateMediaScanner(mXmppConnectionService, targetLocation); - sourceLocation.delete(); - } - } - } - - private String getExtensionFromUri(final Uri uri) { - final String[] projection = {MediaStore.MediaColumns.DATA}; - String filename = null; - try (final Cursor cursor = mXmppConnectionService.getContentResolver().query(uri, projection, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - filename = cursor.getString(0); - } - } catch (final Exception e) { - filename = null; - } - if (filename == null) { - final List segments = uri.getPathSegments(); - if (segments.size() > 0) { - filename = segments.get(segments.size() - 1); - } - } - final int pos = filename == null ? -1 : filename.lastIndexOf('.'); - return pos > 0 ? filename.substring(pos + 1) : null; - } - - private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException, ImageCompressionException { - final File parent = file.getParentFile(); - if (parent != null && parent.mkdirs()) { - Log.d(Config.LOGTAG, "created parent directory"); - } - InputStream is = null; - OutputStream os = null; - try { - if (!file.exists() && !file.createNewFile()) { - throw new FileCopyException(R.string.error_unable_to_create_temporary_file); - } - is = mXmppConnectionService.getContentResolver().openInputStream(image); - if (is == null) { - throw new FileCopyException(R.string.error_not_an_image_file); - } - final Bitmap originalBitmap; - final BitmapFactory.Options options = new BitmapFactory.Options(); - final int inSampleSize = (int) Math.pow(2, sampleSize); - Log.d(Config.LOGTAG, "reading bitmap with sample size " + inSampleSize); - options.inSampleSize = inSampleSize; - originalBitmap = BitmapFactory.decodeStream(is, null, options); - is.close(); - if (originalBitmap == null) { - throw new ImageCompressionException("Source file was not an image"); - } - if (!"image/jpeg".equals(options.outMimeType) && hasAlpha(originalBitmap)) { - originalBitmap.recycle(); - throw new ImageCompressionException("Source file had alpha channel"); - } - int size; - if (mXmppConnectionService.getCompressImageResolutionPreference() == 0) { - int height = originalBitmap.getHeight(); - int width = originalBitmap.getWidth(); - size = height > width ? height : width; - } else { - size = mXmppConnectionService.getCompressImageResolutionPreference(); - } - Bitmap scaledBitmap = resize(originalBitmap, size); - final int rotation = getRotation(image); - scaledBitmap = rotate(scaledBitmap, rotation); - boolean targetSizeReached = false; - int quality = Config.IMAGE_QUALITY; - while (!targetSizeReached) { - os = new FileOutputStream(file); - boolean success = scaledBitmap.compress(Config.IMAGE_FORMAT, quality, os); - if (!success) { - throw new FileCopyException(R.string.error_compressing_image); - } - os.flush(); - targetSizeReached = (file.length() <= mXmppConnectionService.getCompressImageSizePreference() && mXmppConnectionService.getCompressImageSizePreference() != 0) || quality <= 50; - quality -= 5; - } - scaledBitmap.recycle(); - } catch (final FileNotFoundException e) { - cleanup(file); - throw new FileCopyException(R.string.error_file_not_found); - } 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 (final OutOfMemoryError e) { - ++sampleSize; - if (sampleSize <= 3) { - copyImageToPrivateStorage(file, image, sampleSize); - } else { - throw new FileCopyException(R.string.error_out_of_memory); - } - } finally { - close(os); - close(is); - } - } - - 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); - } - - public void copyImageToPrivateStorage(Message message, Uri image) throws FileCopyException, ImageCompressionException { - String filename; - String file = "Sent" + File.separator + fileDateFormat.format(new Date(message.getTimeSent())) + "_" + message.getUuid().substring(0, 4); - switch (Config.IMAGE_FORMAT) { - case JPEG: - filename = String.format("%s.%s", file, ".jpg"); - break; - case PNG: - filename = String.format("%s.%s", file, ".png"); - break; - case WEBP: - filename = String.format("%s.%s", file, ".webp"); - break; - default: - throw new IllegalStateException("Unknown image format"); - } - setupRelativeFilePath(message, filename); - copyImageToPrivateStorage(getFile(message), image); - updateFileParams(message); - } - - public void setupRelativeFilePath(final Message message, final String filename) { - final String extension = MimeUtils.extractRelevantExtension(filename); - final String mime = MimeUtils.guessMimeTypeFromExtension(extension); - setupRelativeFilePath(message, filename, mime); - } - - public File getStorageLocation(final String filename, final String mime) { - final File parentDirectory; - if (Strings.isNullOrEmpty(mime)) { - parentDirectory = getConversationsDirectory(mXmppConnectionService, FILES); - } else if (mime.startsWith("image/")) { - parentDirectory = getConversationsDirectory(mXmppConnectionService, IMAGES); - } else if (mime.startsWith("video/")) { - parentDirectory = getConversationsDirectory(mXmppConnectionService, VIDEOS); - } else if (mime.startsWith("audio/")) { - parentDirectory = getConversationsDirectory(mXmppConnectionService, AUDIOS); - } else { - parentDirectory = getConversationsDirectory(mXmppConnectionService, FILES); - } - return new File(parentDirectory, filename); - } - - public void setupRelativeFilePath(final Message message, final String filename, final String mime) { - final File file = getStorageLocation(filename, mime); - message.setRelativeFilePath(file.getAbsolutePath()); - } - - public void copyFile(File sourceFile, File destFile) throws IOException { - if (!destFile.getParentFile().exists()) { - destFile.getParentFile().mkdirs(); - } - if (!destFile.exists()) { - destFile.createNewFile(); - } - Log.d(Config.LOGTAG, "Copy " + sourceFile.getAbsolutePath() + " to " + destFile.getAbsolutePath()); - FileChannel source = null; - FileChannel destination = null; - try { - source = new FileInputStream(sourceFile).getChannel(); - destination = new FileOutputStream(destFile).getChannel(); - destination.transferFrom(source, 0, source.size()); - } catch (Exception e) { - e.printStackTrace(); - } finally { - if (source != null) { - source.close(); - } - if (destination != null) { - destination.close(); - } - updateMediaScanner(mXmppConnectionService, destFile); - } - } - - public static boolean copyStream(InputStream sourceFile, OutputStream destFile) throws IOException { - byte[] buf = new byte[4096]; - int len; - while ((len = sourceFile.read(buf)) > 0) { - Thread.yield(); - destFile.write(buf, 0, len); - } - destFile.close(); - Log.d(Config.LOGTAG, "Copy stream from " + sourceFile + " to " + destFile); - return true; - } - - public boolean unusualBounds(final Uri image) { - try { - final BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - final InputStream inputStream = mXmppConnectionService.getContentResolver().openInputStream(image); - BitmapFactory.decodeStream(inputStream, null, options); - close(inputStream); - float ratio = (float) options.outHeight / options.outWidth; - return ratio > (21.0f / 9.0f) || ratio < (9.0f / 21.0f); - } catch (final Exception e) { - Log.w(Config.LOGTAG, "unable to detect image bounds", e); - return false; - } - } - - private int getRotation(final File file) { - try (final InputStream inputStream = new FileInputStream(file)) { - return getRotation(inputStream); - } catch (Exception e) { - return 0; - } - } - - 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; - } - } - - public 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; - } - } - - public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) throws IOException { - // The key for getting a cached thumbnail contains the UUID and the size - // since this method is used for thumbnails of (bigger) normal image messages and (smaller) image message references. - // If only the UUID were used, the first loaded thumbnail would be cached and the next loading - // would get that thumbnail which would have the size of the first cached thumbnail - // possibly leading to undesirable appearance of the displayed thumbnail. - final String key = message.getUuid() + size; - final String uuid = message.getUuid(); - final LruCache cache = mXmppConnectionService.getBitmapCache(); - Bitmap thumbnail = cache.get(key); - if ((thumbnail == null) && (!cacheOnly)) { - synchronized (THUMBNAIL_LOCK) { - thumbnail = cache.get(key); - if (thumbnail != null) { - return thumbnail; - } - DownloadableFile file = getFile(message); - final String mime = file.getMimeType(); - if ("application/pdf".equals(mime)) { - thumbnail = getPDFPreview(file, size); - } else if (mime.startsWith("video/")) { - thumbnail = getVideoPreview(file, size); - } else if (mime.startsWith("image/")) { - final Bitmap fullSize = getFullsizeImagePreview(file, size); - if (fullSize == null) { - throw new FileNotFoundException(); - } - thumbnail = resize(fullSize, size); - thumbnail = rotate(thumbnail, getRotation(file)); - if (mime.equals("image/gif")) { - Bitmap withGifOverlay = thumbnail.copy(Bitmap.Config.ARGB_8888, true); - drawOverlay(withGifOverlay, R.drawable.play_gif, 1.0f); - thumbnail.recycle(); - thumbnail = withGifOverlay; - } - } - cache.put(key, thumbnail); - } - } - return thumbnail; - } - - private Bitmap getPDFPreview(final File file, int size) { - try { - final ParcelFileDescriptor mFileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); - if (mFileDescriptor == null) { - return null; - } - final PdfRenderer renderer = new PdfRenderer(mFileDescriptor); - final PdfRenderer.Page page = renderer.openPage(0); - final Dimensions dimensions = scalePdfDimensions(new Dimensions(page.getHeight(), page.getWidth())); - final Bitmap bitmap = Bitmap.createBitmap(dimensions.width, dimensions.height, Bitmap.Config.ARGB_8888); - bitmap.eraseColor(Color.WHITE); - page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); - drawOverlay(bitmap, R.drawable.show_pdf, 0.75f); - page.close(); - renderer.close(); - return bitmap; - } catch (Exception e) { - e.printStackTrace(); - final Bitmap placeholder = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); - placeholder.eraseColor(Color.WHITE); - drawOverlay(placeholder, R.drawable.show_pdf, 0.75f); - return placeholder; - } - } - - private Dimensions scalePdfDimensions(final Dimensions dimensions) { - final DisplayMetrics displayMetrics = mXmppConnectionService.getResources().getDisplayMetrics(); - final int target = (int) (displayMetrics.density * 288); - final int w, h; - if (dimensions.width <= dimensions.height) { - w = Math.max((int) (dimensions.width / ((double) dimensions.height / target)), 1); - h = target; - } else { - w = target; - h = Math.max((int) (dimensions.height / ((double) dimensions.width / target)), 1); - } - return new Dimensions(h, w); - } - - private Bitmap getFullsizeImagePreview(File file, int size) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inSampleSize = calcSampleSize(file, size); - try { - return BitmapFactory.decodeFile(file.getAbsolutePath(), options); - } catch (OutOfMemoryError e) { - options.inSampleSize *= 2; - return BitmapFactory.decodeFile(file.getAbsolutePath(), options); - } - } - - public void drawOverlay(final Bitmap bitmap, final int resource, final float factor) { - drawOverlay(bitmap, resource, factor, false); - } - - public void drawOverlay(final Bitmap bitmap, final int resource, final float factor, final boolean corner) { - Bitmap overlay = BitmapFactory.decodeResource(mXmppConnectionService.getResources(), resource); - Canvas canvas = new Canvas(bitmap); - float targetSize = Math.min(canvas.getWidth(), canvas.getHeight()) * factor; - Log.d(Config.LOGTAG, "target size overlay: " + targetSize + " overlay bitmap size was " + overlay.getHeight()); - float left; - float top; - if (corner) { - left = canvas.getWidth() - targetSize; - top = canvas.getHeight() - targetSize; - } else { - left = (canvas.getWidth() - targetSize) / 2.0f; - top = (canvas.getHeight() - targetSize) / 2.0f; - } - RectF dst = new RectF(left, top, left + targetSize - 1, top + targetSize - 1); - canvas.drawBitmap(overlay, null, dst, createAntiAliasingPaint()); - } - - public void drawOverlayFromDrawable(final Drawable drawable, final int resource, final float factor) { - Bitmap overlay = BitmapFactory.decodeResource(mXmppConnectionService.getResources(), resource); - Bitmap original = drawableToBitmap(drawable); - Canvas canvas = new Canvas(original); - float targetSize = Math.min(canvas.getWidth(), canvas.getHeight()) * factor; - Log.d(Config.LOGTAG, "target size overlay: " + targetSize + " overlay bitmap size was " + overlay.getHeight()); - float left = (canvas.getWidth() - targetSize) / 2.0f; - float top = (canvas.getHeight() - targetSize) / 2.0f; - RectF dst = new RectF(left, top, left + targetSize - 1, top + targetSize - 1); - canvas.drawBitmap(overlay, null, dst, createAntiAliasingPaint()); - } - - private static Bitmap drawableToBitmap(Drawable drawable) { - Bitmap bitmap = null; - if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { - bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel - } else { - bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - } - Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - drawable.draw(canvas); - return bitmap; - } - - private static Paint createAntiAliasingPaint() { - Paint paint = new Paint(); - paint.setAntiAlias(true); - paint.setFilterBitmap(true); - paint.setDither(true); - return paint; - } - - private Bitmap cropCenterSquareVideo(Uri uri, int size) { - MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever(); - Bitmap frame; - try { - metadataRetriever.setDataSource(mXmppConnectionService, uri); - frame = metadataRetriever.getFrameAtTime(0); - metadataRetriever.release(); - return cropCenterSquare(frame, size); - } catch (Exception e) { - frame = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); - frame.eraseColor(0xff000000); - return frame; - } - } - - private Bitmap getVideoPreview(final File file, final int size) { - final MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever(); - Bitmap frame; - try { - metadataRetriever.setDataSource(file.getAbsolutePath()); - frame = metadataRetriever.getFrameAtTime(0); - metadataRetriever.release(); - frame = resize(frame, size); - } catch (IOException e) { - frame = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); - frame.eraseColor(0xff000000); - } catch (RuntimeException e) { - frame = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); - frame.eraseColor(0xff000000); - } - drawOverlay(frame, R.drawable.play_video, 0.75f); - return frame; - } - - public static int safeLongToInt(long l) { - if (l < Integer.MIN_VALUE || l > Integer.MAX_VALUE) { - throw new IllegalArgumentException - (l + " cannot be cast to int without changing its value."); - } - return (int) l; - } - - public static String formatTime(int ms) { - return String.format(Locale.ENGLISH, "%d:%02d", ms / 60000, Math.min(Math.round((ms % 60000) / 1000f), 59)); - } - - private static String getTakeFromCameraPath() { - return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + File.separator + "Camera" + File.separator; - } - - public Uri getTakePhotoUri() { - final String filename = String.format("IMG_%s.%s", fileDateFormat.format(new Date()), "jpg"); - final File directory; - if (STORAGE_INDEX.get() == 1) { - directory = new File(mXmppConnectionService.getCacheDir(), "Camera"); - } else { - directory = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "Camera"); - } - final File file = new File(directory, filename); - file.getParentFile().mkdirs(); - return getUriForFile(mXmppConnectionService, file); - } - - public static Uri getUriForUri(Context context, Uri uri) { - if ("file".equals(uri.getScheme())) { - return getUriForFile(context, new File(uri.getPath())); - } else { - return uri; - } - } - - public static Uri getUriForFile(Context context, File file) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N || PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SettingsActivity.USE_INNER_STORAGE, context.getResources().getBoolean(R.bool.use_inner_storage))) { - try { - return FileProvider.getUriForFile(context, getAuthority(context), file); - } catch (IllegalArgumentException e) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - throw new SecurityException(e); - } else { - return Uri.fromFile(file); - } - } - } else { - return Uri.fromFile(file); - } - } - - public static String getAuthority(Context context) { - return context.getPackageName() + FILE_PROVIDER; - } - - public Uri getTakeVideoUri() { - File file = new File(getTakeFromCameraPath() + "VID_" + fileDateFormat.format(new Date()) + ".mp4"); - file.getParentFile().mkdirs(); - return getUriForFile(mXmppConnectionService, file); - } - - private static boolean hasAlpha(final Bitmap bitmap) { - final int w = bitmap.getWidth(); - final int h = bitmap.getHeight(); - final int yStep = Math.max(1, w / 100); - final int xStep = Math.max(1, h / 100); - for (int x = 0; x < w; x += xStep) { - for (int y = 0; y < h; y += yStep) { - if (Color.alpha(bitmap.getPixel(x, y)) < 255) { - return true; - } - } - } - return false; - } - - public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) { - final Avatar uncompressAvatar = getUncompressedAvatar(image); - if (uncompressAvatar != null && uncompressAvatar.image.length() <= Config.AVATAR_CHAR_LIMIT) { - return uncompressAvatar; - } - if (uncompressAvatar != null) { - Log.d(Config.LOGTAG, "uncompressed avatar exceeded char limit by " + (uncompressAvatar.image.length() - Config.AVATAR_CHAR_LIMIT)); - } - - Bitmap bm = cropCenterSquare(image, size); - if (bm == null) { - return null; - } - if (hasAlpha(bm)) { - Log.d(Config.LOGTAG, "alpha in avatar detected; uploading as PNG"); - bm.recycle(); - bm = cropCenterSquare(image, 96); - return getPepAvatar(bm, Bitmap.CompressFormat.PNG, 100); - } - return getPepAvatar(bm, format, 100); - } - - private Avatar getUncompressedAvatar(Uri uri) { - Bitmap bitmap = null; - try { - bitmap = BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri)); - return getPepAvatar(bitmap, Bitmap.CompressFormat.PNG, 100); - } catch (Exception e) { - return null; - } finally { - if (bitmap != null) { - bitmap.recycle(); - } - } - } - - private Avatar getPepAvatar(Bitmap bitmap, Bitmap.CompressFormat format, int quality) { - try { - ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream(); - Base64OutputStream mBase64OutputStream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT); - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - DigestOutputStream mDigestOutputStream = new DigestOutputStream(mBase64OutputStream, digest); - if (!bitmap.compress(format, quality, mDigestOutputStream)) { - return null; - } - mDigestOutputStream.flush(); - mDigestOutputStream.close(); - long chars = mByteArrayOutputStream.size(); - if (format != Bitmap.CompressFormat.PNG && quality >= 50 && chars >= Config.AVATAR_CHAR_LIMIT) { - int q = quality - 2; - Log.d(Config.LOGTAG, "avatar char length was " + chars + " reducing quality to " + q); - return getPepAvatar(bitmap, format, q); - } - Log.d(Config.LOGTAG, "settled on char length " + chars + " with quality=" + quality); - final Avatar avatar = new Avatar(); - avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest()); - avatar.image = mByteArrayOutputStream.toString(); - if (format.equals(Bitmap.CompressFormat.WEBP)) { - avatar.type = "image/webp"; - } else if (format.equals(Bitmap.CompressFormat.JPEG)) { - avatar.type = "image/jpeg"; - } else if (format.equals(Bitmap.CompressFormat.PNG)) { - avatar.type = "image/png"; - } - avatar.width = bitmap.getWidth(); - avatar.height = bitmap.getHeight(); - return avatar; - } catch (OutOfMemoryError e) { - Log.d(Config.LOGTAG, "unable to convert avatar to base64 due to low memory"); - return null; - } catch (Exception e) { - return null; - } - } - - public Avatar getStoredPepAvatar(String hash) { - if (hash == null) { - return null; - } - Avatar avatar = new Avatar(); - final File file = getAvatarFile(hash); - FileInputStream is = null; - try { - avatar.size = file.length(); - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(file.getAbsolutePath(), options); - is = new FileInputStream(file); - ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream(); - Base64OutputStream mBase64OutputStream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT); - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - DigestOutputStream os = new DigestOutputStream(mBase64OutputStream, digest); - byte[] buffer = new byte[4096]; - int length; - while ((length = is.read(buffer)) > 0) { - os.write(buffer, 0, length); - } - os.flush(); - os.close(); - avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest()); - avatar.image = mByteArrayOutputStream.toString(); - avatar.height = options.outHeight; - avatar.width = options.outWidth; - avatar.type = options.outMimeType; - return avatar; - } catch (NoSuchAlgorithmException e) { - return null; - } catch (IOException e) { - return null; - } finally { - close(is); - } - } - - public boolean isAvatarCached(Avatar avatar) { - final File file = getAvatarFile(avatar.getFilename()); - return file.exists(); - } - - public boolean deleteAvatar(final String avatarFilename) { - final File file = getAvatarFile(avatarFilename); - return deleteAvatar(file); - } - - public boolean deleteAvatar(final Avatar avatar) { - final File file = getAvatarFile(avatar.getFilename()); - return deleteAvatar(file); - } - - public boolean deleteAvatar(final File avatar) { - if (avatar.exists()) { - return avatar.delete(); - } - return false; - } - - public boolean save(final Avatar avatar) { - File file; - if (isAvatarCached(avatar)) { - file = getAvatarFile(avatar.getFilename()); - avatar.size = file.length(); - } else { - file = new File(mXmppConnectionService.getCacheDir().getAbsolutePath() + File.separator + UUID.randomUUID().toString()); - if (file.getParentFile().mkdirs()) { - Log.d(Config.LOGTAG, "created cache directory"); - } - OutputStream os = null; - try { - if (!file.createNewFile()) { - Log.d(Config.LOGTAG, "unable to create temporary file " + file.getAbsolutePath()); - } - os = new FileOutputStream(file); - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - digest.reset(); - DigestOutputStream mDigestOutputStream = new DigestOutputStream(os, digest); - final byte[] bytes = avatar.getImageAsBytes(); - mDigestOutputStream.write(bytes); - mDigestOutputStream.flush(); - mDigestOutputStream.close(); - String sha1sum = CryptoHelper.bytesToHex(digest.digest()); - if (sha1sum.equals(avatar.sha1sum)) { - final File outputFile = getAvatarFile(avatar.getFilename()); - if (outputFile.getParentFile().mkdirs()) { - Log.d(Config.LOGTAG, "created avatar directory"); - } - final File avatarFile = getAvatarFile(avatar.getFilename()); - if (!file.renameTo(avatarFile)) { - Log.d(Config.LOGTAG, "unable to rename " + file.getAbsolutePath() + " to " + outputFile); - return false; - } - } else { - Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner); - if (!file.delete()) { - Log.d(Config.LOGTAG, "unable to delete temporary file"); - } - return false; - } - avatar.size = bytes.length; - } catch (IllegalArgumentException e) { - return false; - } catch (IOException e) { - return false; - } catch (NoSuchAlgorithmException e) { - return false; - } finally { - close(os); - } - } - return true; - } - - public void deleteHistoricAvatarPath() { - delete(getHistoricAvatarPath()); - } - - private void delete(final File file) { - if (file.isDirectory()) { - final File[] files = file.listFiles(); - if (files != null) { - for (final File f : files) { - delete(f); - } - } - } - if (file.delete()) { - Log.d(Config.LOGTAG, "deleted " + file.getAbsolutePath()); - } - } - - private File getHistoricAvatarPath() { - return new File(mXmppConnectionService.getFilesDir(), File.separator + "avatars" + File.separator); - } - - public File getAvatarFile(String avatar) { - return new File(mXmppConnectionService.getCacheDir(), File.separator + "avatars" + File.separator + avatar); - } - - public Uri getAvatarUri(String avatar) { - return Uri.fromFile(getAvatarFile(avatar)); - } - - public Bitmap cropCenterSquare(Uri image, int size) { - if (image == null) { - return null; - } - InputStream is = null; - try { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inSampleSize = calcSampleSize(image, size); - is = mXmppConnectionService.getContentResolver().openInputStream(image); - if (is == null) { - return null; - } - Bitmap input = BitmapFactory.decodeStream(is, null, options); - if (input == null) { - return null; - } else { - input = rotate(input, getRotation(image)); - return cropCenterSquare(input, size); - } - } catch (FileNotFoundException e) { - Log.d(Config.LOGTAG, "unable to open file " + image, e); - return null; - } catch (SecurityException e) { - Log.d(Config.LOGTAG, "unable to open file " + image, e); - return null; - } finally { - close(is); - } - } - - public Bitmap cropCenter(Uri image, int newHeight, int newWidth) { - if (image == null) { - return null; - } - InputStream is = null; - try { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inSampleSize = calcSampleSize(image, Math.max(newHeight, newWidth)); - is = mXmppConnectionService.getContentResolver().openInputStream(image); - if (is == null) { - return null; - } - Bitmap source = BitmapFactory.decodeStream(is, null, options); - if (source == null) { - return null; - } - int sourceWidth = source.getWidth(); - int sourceHeight = source.getHeight(); - float xScale = (float) newWidth / sourceWidth; - float yScale = (float) newHeight / sourceHeight; - float scale = Math.max(xScale, yScale); - float scaledWidth = scale * sourceWidth; - float scaledHeight = scale * sourceHeight; - float left = (newWidth - scaledWidth) / 2; - float top = (newHeight - scaledHeight) / 2; - - RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight); - Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(dest); - canvas.drawBitmap(source, null, targetRect, createAntiAliasingPaint()); - if (source.isRecycled()) { - source.recycle(); - } - return dest; - } catch (SecurityException | FileNotFoundException e) { - return null; //android 6.0 with revoked permissions for example - } finally { - close(is); - } - } - - public Bitmap cropCenterSquare(Bitmap input, int size) { - int w = input.getWidth(); - int h = input.getHeight(); - - float scale = Math.max((float) size / h, (float) size / w); - - float outWidth = scale * w; - float outHeight = scale * h; - float left = (size - outWidth) / 2; - float top = (size - outHeight) / 2; - RectF target = new RectF(left, top, left + outWidth, top + outHeight); - - Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(output); - canvas.drawBitmap(input, null, target, createAntiAliasingPaint()); - if (!input.isRecycled()) { - input.recycle(); - } - return output; - } - - private int calcSampleSize(Uri image, int size) throws FileNotFoundException, SecurityException { - final BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - final InputStream inputStream = mXmppConnectionService.getContentResolver().openInputStream(image); - BitmapFactory.decodeStream(inputStream, null, options); - close(inputStream); - return calcSampleSize(options, size); - } - - private static int calcSampleSize(File image, int size) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(image.getAbsolutePath(), options); - return calcSampleSize(options, size); - } - - private static int calcSampleSize(BitmapFactory.Options options, int size) { - int height = options.outHeight; - int width = options.outWidth; - int inSampleSize = 1; - - if (height > size || width > size) { - int halfHeight = height / 2; - int halfWidth = width / 2; - - while ((halfHeight / inSampleSize) > size - && (halfWidth / inSampleSize) > size) { - inSampleSize *= 2; - } - } - return inSampleSize; - } - - public void updateFileParams(Message message) { - updateFileParams(message, null); - } - - public void updateFileParams(final Message message, final String url) { - final boolean encrypted = - message.getEncryption() == Message.ENCRYPTION_PGP - || message.getEncryption() == Message.ENCRYPTION_DECRYPTED; - final DownloadableFile file = getFile(message); - final String mime = file.getMimeType(); - final boolean image = - message.getType() == Message.TYPE_IMAGE - || (mime != null && mime.startsWith("image/")); - final boolean isGif = image & (mime != null && mime.equalsIgnoreCase("image/gif")); - final boolean privateMessage = message.isPrivateMessage(); - /* file params: - 1 | 2 | 3 | 4 | 5 | 6 | - | image/video/pdf | a/v/gif | vcard/apk/audio | - url | filesize | width | height | runtime | name | - */ - final StringBuilder body = new StringBuilder(); - if (url != null) { - body.append(url); // 1 - } - if (encrypted && !file.exists()) { - Log.d(Config.LOGTAG, "skipping updateFileParams because file is encrypted"); - final DownloadableFile encryptedFile = getFile(message, false); - body.append('|').append(encryptedFile.getSize()); // 2 - } else { - Log.d(Config.LOGTAG, "running updateFileParams"); - final boolean ambiguous = MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime); - final boolean video = mime != null && mime.startsWith("video/"); - final boolean audio = mime != null && mime.startsWith("audio/"); - final boolean vcard = mime != null && mime.contains("vcard"); - final boolean apk = mime != null && mime.equals("application/vnd.android.package-archive"); - final boolean pdf = "application/pdf".equals(mime); - body.append('|').append(file.getSize()); // 2 - if (ambiguous) { - try { - final Dimensions dimensions = getVideoDimensions(file); - if (dimensions.valid()) { - Log.d(Config.LOGTAG, "ambiguous file " + mime + " is video"); - body.append('|') - .append(dimensions.width).append('|') // 3 - .append(dimensions.height); // 4 - } else { - Log.d(Config.LOGTAG, "ambiguous file " + mime + " is audio"); - body.append("|0|0|").append(getMediaRuntime(file, false)) // 5 - .append('|').append(getAudioTitleArtist(file)); // 6 - } - } catch (final NotAVideoFile e) { - Log.d(Config.LOGTAG, "ambiguous file " + mime + " is audio"); - body.append("|0|0|").append(getMediaRuntime(file, false)) // 5 - .append('|').append(getAudioTitleArtist(file)); // 6 - } - } else if (image || video || pdf) { - try { - final Dimensions dimensions; - if (video) { - dimensions = getVideoDimensions(file); - } else if (pdf) { - dimensions = getPDFDimensions(file); - } else { - dimensions = getImageDimensions(file); - } - if (dimensions.valid()) { - body.append('|') - .append(dimensions.width) // 3 - .append('|') - .append(dimensions.height); // 4 - if (isGif || video) { - body.append("|").append(getMediaRuntime(file, isGif)); // 5 - } - } - } catch (Exception notAVideoFile) { - Log.d(Config.LOGTAG, "file with mime type " + file.getMimeType() + " was not a video file, trying to handle it as audio file"); - try { - body.append("|0|0|") // 3, 4 - .append(getMediaRuntime(file, false)) // 5 - .append('|') - .append(getAudioTitleArtist(file)); // 6 - } catch (Exception e) { - Log.d(Config.LOGTAG, "file with mime type " + file.getMimeType() + " was neither a video file nor an audio file"); - //fall threw - } - } - } else if (audio) { - body.append("|0|0|") // 3, 4 - .append(getMediaRuntime(file, false)) // 5 - .append('|') - .append(getAudioTitleArtist(file)); // 6 - } else if (vcard) { - body.append("|0|0|0|") // 3, 4, 5 - .append(getVCard(file)); // 6 - } else if (apk) { - body.append("|0|0|0|") // 3, 4, 5 - .append(getAPK(file, mXmppConnectionService.getApplicationContext())); // 6 - } - } - message.setBody(body.toString()); - message.setFileDeleted(false); - message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : (image ? Message.TYPE_IMAGE : Message.TYPE_FILE)); - } - - private Dimensions getPDFDimensions(final File file) { - final ParcelFileDescriptor fileDescriptor; - try { - fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); - if (fileDescriptor == null) { - return new Dimensions(0, 0); - } - } catch (FileNotFoundException e) { - return new Dimensions(0, 0); - } - try { - final PdfRenderer pdfRenderer = new PdfRenderer(fileDescriptor); - final PdfRenderer.Page page = pdfRenderer.openPage(0); - final int height = page.getHeight(); - final int width = page.getWidth(); - page.close(); - pdfRenderer.close(); - return scalePdfDimensions(new Dimensions(height, width)); - } catch (Exception e) { - Log.d(Config.LOGTAG, "unable to get dimensions for pdf document", e); - return new Dimensions(0, 0); - } - } - - public static void updateFileParams(Message message, URL url, long size) { - final StringBuilder body = new StringBuilder(); - body.append(url.toString()).append('|').append(size); - message.setBody(body.toString()); - } - - public int getMediaRuntime(final File file, final boolean isGif) { - if (isGif) { - try { - final InputStream inputStream = mXmppConnectionService.getContentResolver().openInputStream(getUriForFile(mXmppConnectionService, file)); - final Movie movie = Movie.decodeStream(inputStream); - int duration = movie.duration(); - close(inputStream); - return duration; - } catch (FileNotFoundException e) { - Log.d(Config.LOGTAG, "unable to get image dimensions", e); - return 0; - } - } else { - try { - final MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); - mediaMetadataRetriever.setDataSource(file.toString()); - final String value = - mediaMetadataRetriever.extractMetadata( - MediaMetadataRetriever.METADATA_KEY_DURATION); - if (Strings.isNullOrEmpty(value)) { - return 0; - } - return Integer.parseInt(value); - } catch (final Exception e) { - return 0; - } - } - } - - private String getAudioTitleArtist(final File file) { - String artist; - String title; - StringBuilder builder = new StringBuilder(); - try { - MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); - mediaMetadataRetriever.setDataSource(file.toString()); - artist = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST); - if (artist == null) { - artist = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST); - } - if (artist == null) { - artist = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COMPOSER); - } - title = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE); - mediaMetadataRetriever.release(); - boolean separator = false; - if (artist != null && artist.length() > 0) { - builder.append(artist); - separator = true; - } - if (title != null && title.length() > 0) { - if (separator) { - builder.append(" - "); - } - builder.append(title); - } - final String s = builder.substring(0, Math.min(128, builder.length())); - final byte[] data = s.trim().getBytes(StandardCharsets.UTF_8); - return Base64.encodeToString(data, Base64.DEFAULT); - } catch (Exception e) { - e.printStackTrace(); - return ""; - } - } - - private String getAPK(File file, Context context) { - String APKName; - final PackageManager pm = context.getPackageManager(); - final PackageInfo pi = pm.getPackageArchiveInfo(file.toString(), 0); - String AppName; - String AppVersion; - try { - pi.applicationInfo.sourceDir = file.toString(); - pi.applicationInfo.publicSourceDir = file.toString(); - AppName = (String) pi.applicationInfo.loadLabel(pm); - AppVersion = pi.versionName; - Log.d(Config.LOGTAG, "APK name: " + AppName); - APKName = " (" + AppName + " " + AppVersion + ")"; - } catch (Exception e) { - e.printStackTrace(); - Log.d(Config.LOGTAG, "no APK name detected"); - APKName = ""; - } - - byte[] data = APKName.getBytes(StandardCharsets.UTF_8); - APKName = Base64.encodeToString(data, Base64.DEFAULT); - return APKName; - } - - private String getVCard(File file) { - VCard VCard = new VCard(); - String VCardName = ""; - try { - VCard = Ezvcard.parse(file).first(); - if (VCard != null) { - final String version = VCard.getVersion().toString(); - Log.d(Config.LOGTAG, "VCard version: " + version); - final String name = VCard.getFormattedName().getValue(); - VCardName = " (" + name + ")"; - } - } catch (IOException e) { - e.printStackTrace(); - } - byte[] data = VCardName.getBytes(StandardCharsets.UTF_8); - VCardName = Base64.encodeToString(data, Base64.DEFAULT); - - return VCardName; - } - - private Dimensions getImageDimensions(File file) { - final BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(file.getAbsolutePath(), options); - final int rotation = getRotation(file); - final boolean rotated = rotation == 90 || rotation == 270; - final int imageHeight = rotated ? options.outWidth : options.outHeight; - final int imageWidth = rotated ? options.outHeight : options.outWidth; - return new Dimensions(imageHeight, imageWidth); - } - - private Dimensions getVideoDimensions(File file) throws NotAVideoFile { - try (final MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever()) { - metadataRetriever.setDataSource(file.getAbsolutePath()); - return getVideoDimensions(metadataRetriever); - } catch (IOException | RuntimeException e) { - throw new NotAVideoFile(e); - } - } - - public Bitmap getPreviewForUri(Attachment attachment, int size, boolean cacheOnly) { - final String key = "attachment_" + attachment.getUuid().toString() + "_" + size; - final LruCache cache = mXmppConnectionService.getBitmapCache(); - Bitmap bitmap = cache.get(key); - if (bitmap != null || cacheOnly) { - return bitmap; - } - DownloadableFile file = new DownloadableFile(attachment.getUri().getPath()); - if ("application/pdf".equals(attachment.getMime())) { - bitmap = cropCenterSquare(getPDFPreview(file, size), size); - } else if (attachment.getMime() != null && attachment.getMime().startsWith("video/")) { - bitmap = cropCenterSquareVideo(attachment.getUri(), size); - drawOverlay(bitmap, R.drawable.play_video, 0.75f); - } else { - bitmap = cropCenterSquare(attachment.getUri(), size); - if (bitmap != null && "image/gif".equals(attachment.getMime())) { - Bitmap withGifOverlay = bitmap.copy(Bitmap.Config.ARGB_8888, true); - drawOverlay(withGifOverlay, R.drawable.play_gif, 1.0f); - bitmap.recycle(); - bitmap = withGifOverlay; - } - } - if (bitmap != null) { - cache.put(key, bitmap); - } - return bitmap; - } - - private static Dimensions getVideoDimensions(Context context, Uri uri) throws NotAVideoFile { - try (final MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever()) { - try { - mediaMetadataRetriever.setDataSource(context, uri); - return getVideoDimensions(mediaMetadataRetriever); - } catch (IOException | RuntimeException e) { - throw new NotAVideoFile(e); - } - } catch (Exception e) { - throw new NotAVideoFile(); - } - } - - private static Dimensions getVideoDimensionsOfFrame(MediaMetadataRetriever mediaMetadataRetriever) { - Bitmap bitmap = null; - try { - bitmap = mediaMetadataRetriever.getFrameAtTime(); - return new Dimensions(bitmap.getHeight(), bitmap.getWidth()); - } catch (Exception e) { - return null; - } finally { - if (bitmap != null) { - bitmap.recycle(); - } - } - } - - private static Dimensions getVideoDimensions(MediaMetadataRetriever metadataRetriever) throws NotAVideoFile, IOException { - String hasVideo = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO); - if (hasVideo == null) { - throw new NotAVideoFile(); - } - Dimensions dimensions = getVideoDimensionsOfFrame(metadataRetriever); - if (dimensions != null) { - return dimensions; - } - final int rotation; - rotation = extractRotationFromMediaRetriever(metadataRetriever); - boolean rotated = rotation == 90 || rotation == 270; - int height; - try { - String h = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); - height = Integer.parseInt(h); - } catch (Exception e) { - height = -1; - } - int width; - try { - String w = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); - width = Integer.parseInt(w); - } catch (Exception e) { - width = -1; - } - // metadataRetriever.release(); - Log.d(Config.LOGTAG, "extracted video dims " + width + "x" + height); - return rotated ? new Dimensions(width, height) : new Dimensions(height, width); - } - - private static int extractRotationFromMediaRetriever(MediaMetadataRetriever metadataRetriever) { - String r = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); - try { - return Integer.parseInt(r); - } catch (Exception e) { - return 0; - } - } - - private static class Dimensions { - public final int width; - public final int height; - - Dimensions(int height, int width) { - this.width = width; - this.height = height; - } - - public int getMin() { - return Math.min(width, height); - } - - public boolean valid() { - return width > 0 && height > 0; - } - } - - private static class NotAVideoFile extends Exception { - public NotAVideoFile(Throwable t) { - super(t); - } - - public NotAVideoFile() { - super(); - } - } - - public static class ImageCompressionException extends Exception { - - ImageCompressionException(String message) { - super(message); - } - } - - - public static class FileCopyException extends Exception { - private final int resId; - - private FileCopyException(@StringRes int resId) { - this.resId = resId; - } - - public @StringRes - int getResId() { - return resId; - } - } - - public Bitmap getAvatar(String avatar, int size) { - if (avatar == null) { - return null; - } - Bitmap bm = cropCenter(getAvatarUri(avatar), size, size); - return bm; - } - - public boolean isFileAvailable(Message message) { - return getFile(message).exists(); - } - - public static void close(final Closeable stream) { - if (stream != null) { - try { - stream.close(); - } catch (Exception e) { - Log.d(Config.LOGTAG, "unable to close stream", e); - } - } - } - - public static void close(final Socket socket) { - if (socket != null) { - try { - socket.close(); - } catch (IOException e) { - Log.d(Config.LOGTAG, "unable to close socket", e); - } - } - } - - public static void close(final ServerSocket socket) { - if (socket != null) { - try { - socket.close(); - } catch (IOException e) { - Log.d(Config.LOGTAG, "unable to close socket", e); - } - } - } - - public static boolean weOwnFile(final Uri uri) { - if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { - return false; - } else { - return weOwnFileLollipop(uri); - } - } - - private static boolean weOwnFileLollipop(Uri uri) { - try { - File file = new File(uri.getPath()); - FileDescriptor fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).getFileDescriptor(); - StructStat st = Os.fstat(fd); - return st.st_uid == android.os.Process.myUid(); - } catch (FileNotFoundException e) { - return false; - } catch (Exception e) { - return true; - } - } - - public static Bitmap rotateBitmap(File file, Bitmap bitmap, int orientation) { - - if (orientation == 1) { - return bitmap; - } - - Matrix matrix = new Matrix(); - switch (orientation) { - case 2: - matrix.setScale(-1, 1); - break; - case 3: - matrix.setRotate(180); - break; - case 4: - matrix.setRotate(180); - matrix.postScale(-1, 1); - break; - case 5: - matrix.setRotate(90); - matrix.postScale(-1, 1); - break; - case 6: - matrix.setRotate(90); - break; - case 7: - matrix.setRotate(-90); - matrix.postScale(-1, 1); - break; - case 8: - matrix.setRotate(-90); - break; - default: - return bitmap; - } - - try { - Bitmap oriented = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); - bitmap.recycle(); - return oriented; - } catch (OutOfMemoryError e) { - e.printStackTrace(); - return bitmap; - } - } - - public void saveFile(final Message message, final Activity activity) { - new Thread(new SaveFileFinisher(getFile(message), new File(getDestinationToSaveFile(message)), activity, this)).start(); - } - - private static class SaveFileFinisher implements Runnable { - - private final DownloadableFile source; - private final File destination; - private final WeakReference activityReference; - private final FileBackend fileBackend; - - private SaveFileFinisher(DownloadableFile source, File destination, Activity activity, FileBackend fileBackend) { - this.source = source; - this.destination = destination; - this.activityReference = new WeakReference<>(activity); - this.fileBackend = fileBackend; - } - - @Override - public void run() { - final Activity activity = activityReference.get(); - if (activity == null) { - return; - } - activity.runOnUiThread( - () -> { - try { - activity.runOnUiThread(() -> { - ToastCompat.makeText(activity, activity.getString(R.string.copy_file_to, destination), ToastCompat.LENGTH_SHORT).show(); - }); - fileBackend.copyFile(source, destination); - activity.runOnUiThread(() -> { - ToastCompat.makeText(activity, activity.getString(R.string.file_copied_to, destination), ToastCompat.LENGTH_SHORT).show(); - }); - } catch (IOException e) { - e.printStackTrace(); - } - }); - } - } - - public static void moveFile(String inputPath, String inputFile, String outputPath) { - Log.d(Config.LOGTAG, "Move " + inputPath + File.separator + inputFile + " to " + outputPath); - InputStream in = null; - OutputStream out = null; - try { - //create output directory if it doesn't exist - File dir = new File(outputPath); - if (!dir.exists()) { - dir.mkdirs(); - } - in = new FileInputStream(inputPath + File.separator + inputFile); - out = new FileOutputStream(outputPath + File.separator + inputFile); - byte[] buffer = new byte[4096]; - int read; - while ((read = in.read(buffer)) != -1) { - out.write(buffer, 0, read); - } - in.close(); - in = null; - // write the output file - out.flush(); - out.close(); - out = null; - // delete the original file - new File(inputPath + File.separator + inputFile).delete(); - } catch (Exception e) { - Log.e(Config.LOGTAG, e.getMessage()); - } - } - - public String getDestinationToSaveFile(Message message) { - final DownloadableFile file = getFile(message); - final String mime = file.getMimeType(); - String extension = MimeUtils.guessExtensionFromMimeType(mime); - if (extension == null) { - Log.d(Config.LOGTAG, "extension from mime type was null"); - extension = "null"; - } - if ("ogg".equals(extension) && mime.startsWith("audio/")) { - extension = "oga"; - } - String filename = fileDateFormat.format(new Date(message.getTimeSent())) + "_" + message.getUuid().substring(0, 4) + "." + extension; - if (mime != null && mime.startsWith("image")) { - return getGlobalPicturesPath() + File.separator + filename; - } else if (mime != null && mime.startsWith("video")) { - return getGlobalVideosPath() + File.separator + filename; - } else if (mime != null && mime.startsWith("audio")) { - return getGlobalAudiosPath() + File.separator + filename; - } else { - return getGlobalDocumentsPath() + File.separator + filename; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java b/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java deleted file mode 100644 index 6106c0eee..000000000 --- a/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java +++ /dev/null @@ -1,5 +0,0 @@ -package eu.siacs.conversations.persistance; - -public interface OnPhoneContactsMerged { - void phoneContactsMerged(); -} diff --git a/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java b/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java deleted file mode 100644 index f36506bd1..000000000 --- a/src/main/java/eu/siacs/conversations/persistance/UnifiedPushDatabase.java +++ /dev/null @@ -1,262 +0,0 @@ -package eu.siacs.conversations.persistance; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.util.Log; - -import androidx.annotation.Nullable; - -import com.google.common.base.MoreObjects; -import com.google.common.base.Objects; -import com.google.common.base.Optional; -import com.google.common.collect.ImmutableList; - -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.services.UnifiedPushBroker; - -public class UnifiedPushDatabase extends SQLiteOpenHelper { - private static final String DATABASE_NAME = "unified-push-distributor"; - private static final int DATABASE_VERSION = 1; - - private static UnifiedPushDatabase instance; - - public static UnifiedPushDatabase getInstance(final Context context) { - synchronized (UnifiedPushDatabase.class) { - if (instance == null) { - instance = new UnifiedPushDatabase(context.getApplicationContext()); - } - return instance; - } - } - - private UnifiedPushDatabase(@Nullable Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - } - - @Override - public void onCreate(final SQLiteDatabase sqLiteDatabase) { - sqLiteDatabase.execSQL( - "CREATE TABLE push (account TEXT, transport TEXT, application TEXT NOT NULL, instance TEXT NOT NULL UNIQUE, endpoint TEXT, expiration NUMBER DEFAULT 0)"); - } - - public boolean register(final String application, final String instance) { - final SQLiteDatabase sqLiteDatabase = getWritableDatabase(); - sqLiteDatabase.beginTransaction(); - final Optional existingApplication; - try (final Cursor cursor = - sqLiteDatabase.query( - "push", - new String[] {"application"}, - "instance=?", - new String[] {instance}, - null, - null, - null)) { - if (cursor != null && cursor.moveToFirst()) { - existingApplication = Optional.of(cursor.getString(0)); - } else { - existingApplication = Optional.absent(); - } - } - if (existingApplication.isPresent()) { - sqLiteDatabase.setTransactionSuccessful(); - sqLiteDatabase.endTransaction(); - return application.equals(existingApplication.get()); - } - final ContentValues contentValues = new ContentValues(); - contentValues.put("application", application); - contentValues.put("instance", instance); - contentValues.put("expiration", 0); - final long inserted = sqLiteDatabase.insert("push", null, contentValues); - if (inserted > 0) { - Log.d(Config.LOGTAG, "inserted new application/instance tuple into unified push db"); - } - sqLiteDatabase.setTransactionSuccessful(); - sqLiteDatabase.endTransaction(); - return true; - } - - public List getRenewals(final String account, final String transport) { - final ImmutableList.Builder renewalBuilder = ImmutableList.builder(); - final long expiration = System.currentTimeMillis() + UnifiedPushBroker.TIME_TO_RENEW; - final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); - try (final Cursor cursor = - sqLiteDatabase.query( - "push", - new String[] {"application", "instance"}, - "account <> ? OR transport <> ? OR expiration < " + expiration, - new String[] {account, transport}, - null, - null, - null)) { - while (cursor != null && cursor.moveToNext()) { - renewalBuilder.add( - new PushTarget( - cursor.getString(cursor.getColumnIndexOrThrow("application")), - cursor.getString(cursor.getColumnIndexOrThrow("instance")))); - } - } - return renewalBuilder.build(); - } - - public ApplicationEndpoint getEndpoint( - final String account, final String transport, final String instance) { - final long expiration = System.currentTimeMillis() + UnifiedPushBroker.TIME_TO_RENEW; - final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); - try (final Cursor cursor = - sqLiteDatabase.query( - "push", - new String[] {"application", "endpoint"}, - "account = ? AND transport = ? AND instance = ? AND endpoint IS NOT NULL AND expiration >= " - + expiration, - new String[] {account, transport, instance}, - null, - null, - null)) { - if (cursor != null && cursor.moveToFirst()) { - return new ApplicationEndpoint( - cursor.getString(cursor.getColumnIndexOrThrow("application")), - cursor.getString(cursor.getColumnIndexOrThrow("endpoint"))); - } - } - return null; - } - - public boolean hasEndpoints(final UnifiedPushBroker.Transport transport) { - final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); - try (final Cursor cursor = - sqLiteDatabase.rawQuery( - "SELECT EXISTS(SELECT endpoint FROM push WHERE account = ? AND transport = ?)", - new String[] { - transport.account.getUuid(), transport.transport.toEscapedString() - })) { - if (cursor != null && cursor.moveToFirst()) { - return cursor.getInt(0) > 0; - } - } - return false; - } - - @Override - public void onUpgrade( - final SQLiteDatabase sqLiteDatabase, final int oldVersion, final int newVersion) {} - - public boolean updateEndpoint( - final String instance, - final String account, - final String transport, - final String endpoint, - final long expiration) { - final SQLiteDatabase sqLiteDatabase = getWritableDatabase(); - sqLiteDatabase.beginTransaction(); - final String existingEndpoint; - try (final Cursor cursor = - sqLiteDatabase.query( - "push", - new String[] {"endpoint"}, - "instance=?", - new String[] {instance}, - null, - null, - null)) { - if (cursor != null && cursor.moveToFirst()) { - existingEndpoint = cursor.getString(0); - } else { - existingEndpoint = null; - } - } - final ContentValues contentValues = new ContentValues(); - contentValues.put("account", account); - contentValues.put("transport", transport); - contentValues.put("endpoint", endpoint); - contentValues.put("expiration", expiration); - sqLiteDatabase.update("push", contentValues, "instance=?", new String[] {instance}); - sqLiteDatabase.setTransactionSuccessful(); - sqLiteDatabase.endTransaction(); - return !endpoint.equals(existingEndpoint); - } - - public List getPushTargets(final String account, final String transport) { - final ImmutableList.Builder renewalBuilder = ImmutableList.builder(); - final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); - try (final Cursor cursor = - sqLiteDatabase.query( - "push", - new String[] {"application", "instance"}, - "account = ?", - new String[] {account}, - null, - null, - null)) { - while (cursor != null && cursor.moveToNext()) { - renewalBuilder.add( - new PushTarget( - cursor.getString(cursor.getColumnIndexOrThrow("application")), - cursor.getString(cursor.getColumnIndexOrThrow("instance")))); - } - } - return renewalBuilder.build(); - } - - public boolean deleteInstance(final String instance) { - final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); - final int rows = sqLiteDatabase.delete("push", "instance=?", new String[] {instance}); - return rows >= 1; - } - - public boolean deleteApplication(final String application) { - final SQLiteDatabase sqLiteDatabase = getReadableDatabase(); - final int rows = sqLiteDatabase.delete("push", "application=?", new String[] {application}); - return rows >= 1; - } - - public static class ApplicationEndpoint { - public final String application; - public final String endpoint; - - public ApplicationEndpoint(String application, String endpoint) { - this.application = application; - this.endpoint = endpoint; - } - } - - public static class PushTarget { - public final String application; - public final String instance; - - public PushTarget(final String application, final String instance) { - this.application = application; - this.instance = instance; - } - - @NotNull - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("application", application) - .add("instance", instance) - .toString(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PushTarget that = (PushTarget) o; - return Objects.equal(application, that.application) - && Objects.equal(instance, that.instance); - } - - @Override - public int hashCode() { - return Objects.hashCode(application, instance); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java deleted file mode 100644 index 96d5dca01..000000000 --- a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java +++ /dev/null @@ -1,188 +0,0 @@ -package eu.siacs.conversations.services; - -import static eu.siacs.conversations.entities.Transferable.VALID_CRYPTO_EXTENSIONS; - -import android.os.PowerManager; -import android.os.SystemClock; -import android.util.Log; - -import androidx.core.content.ContextCompat; - -import org.bouncycastle.crypto.engines.AESEngine; -import org.bouncycastle.crypto.io.CipherInputStream; -import org.bouncycastle.crypto.io.CipherOutputStream; -import org.bouncycastle.crypto.modes.AEADBlockCipher; -import org.bouncycastle.crypto.modes.GCMBlockCipher; -import org.bouncycastle.crypto.params.AEADParameters; -import org.bouncycastle.crypto.params.KeyParameter; - -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.concurrent.atomic.AtomicLong; - -import javax.annotation.Nullable; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.utils.Compatibility; -import okhttp3.MediaType; -import okhttp3.RequestBody; -import okio.BufferedSink; -import okio.Okio; -import okio.Source; - -public class AbstractConnectionManager { - private static final int UI_REFRESH_THRESHOLD = Config.REFRESH_UI_INTERVAL; - private static final AtomicLong LAST_UI_UPDATE_CALL = new AtomicLong(0); - protected XmppConnectionService mXmppConnectionService; - - public AbstractConnectionManager(XmppConnectionService service) { - this.mXmppConnectionService = service; - } - - public static InputStream upgrade(DownloadableFile file, InputStream is) { - if (file.getKey() != null && file.getIv() != null) { - AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); - cipher.init(true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv())); - return new CipherInputStream(is, cipher); - } else { - return is; - } - } - - - //For progress tracking see: - //https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java - - public static RequestBody requestBody(final DownloadableFile file, final ProgressListener progressListener) { - return new RequestBody() { - - @Override - public long contentLength() { - return file.getSize() + (file.getKey() != null ? 16 : 0); - } - - @Nullable - @Override - public MediaType contentType() { - return MediaType.parse(file.getMimeType()); - } - - @Override - public void writeTo(final BufferedSink sink) throws IOException { - long transmitted = 0; - try (final Source source = Okio.source(upgrade(file, new FileInputStream(file)))) { - long read; - while ((read = source.read(sink.buffer(), 8196)) != -1) { - transmitted += read; - sink.flush(); - progressListener.onProgress(transmitted); - } - } - } - }; - } - - public interface ProgressListener { - void onProgress(long progress); - } - - public static OutputStream createOutputStream(DownloadableFile file, boolean append, boolean decrypt) { - FileOutputStream os; - try { - os = new FileOutputStream(file, append); - if (file.getKey() == null || !decrypt) { - return os; - } - } catch (FileNotFoundException e) { - Log.d(Config.LOGTAG, "unable to create output stream", e); - return null; - } - try { - AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); - cipher.init(false, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv())); - return new CipherOutputStream(os, cipher); - } catch (Exception e) { - Log.d(Config.LOGTAG, "unable to create cipher output stream", e); - return null; - } - } - - public XmppConnectionService getXmppConnectionService() { - return this.mXmppConnectionService; - } - - public long getAutoAcceptFileSize() { - final long defaultValue_wifi = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_wifi); - final long defaultValue_mobile = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_mobile); - final long defaultValue_roaming = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_roaming); - - String config = "0"; - if (mXmppConnectionService.isWIFI()) { - config = this.mXmppConnectionService.getPreferences().getString( - "auto_accept_file_size_wifi", String.valueOf(defaultValue_wifi)); - } else if (mXmppConnectionService.isMobile()) { - config = this.mXmppConnectionService.getPreferences().getString( - "auto_accept_file_size_mobile", String.valueOf(defaultValue_mobile)); - } else if (mXmppConnectionService.isMobileRoaming()) { - config = this.mXmppConnectionService.getPreferences().getString( - "auto_accept_file_size_roaming", String.valueOf(defaultValue_roaming)); - } - try { - return Long.parseLong(config) <= 0 ? -1 : Long.parseLong(config); - } catch (NumberFormatException e) { - return defaultValue_mobile; - } - } - - public boolean hasStoragePermission() { - return Compatibility.hasStoragePermission(mXmppConnectionService); - } - - public void updateConversationUi(boolean force) { - synchronized (LAST_UI_UPDATE_CALL) { - if (force || SystemClock.elapsedRealtime() - LAST_UI_UPDATE_CALL.get() >= UI_REFRESH_THRESHOLD) { - LAST_UI_UPDATE_CALL.set(SystemClock.elapsedRealtime()); - mXmppConnectionService.updateConversationUi(); - } - } - } - - public PowerManager.WakeLock createWakeLock(final String name) { - final PowerManager powerManager = ContextCompat.getSystemService(mXmppConnectionService, PowerManager.class); - return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name); - } - - public static class Extension { - public final String main; - public final String secondary; - - private Extension(String main, String secondary) { - this.main = main; - this.secondary = secondary; - } - - public String getExtension() { - if (VALID_CRYPTO_EXTENSIONS.contains(main)) { - return secondary; - } else { - return main; - } - } - - public static Extension of(String path) { - //TODO accept List pathSegments - final int pos = path.lastIndexOf('/'); - final String filename = path.substring(pos + 1).toLowerCase(); - final String[] parts = filename.split("\\."); - final String main = parts.length >= 2 ? parts[parts.length - 1] : null; - final String secondary = parts.length >= 3 ? parts[parts.length - 2] : null; - return new Extension(main, secondary); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java b/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java deleted file mode 100644 index 1ea34142e..000000000 --- a/src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java +++ /dev/null @@ -1,26 +0,0 @@ -package eu.siacs.conversations.services; - -public abstract class AbstractQuickConversationsService { - - protected final XmppConnectionService service; - - public AbstractQuickConversationsService(XmppConnectionService service) { - this.service = service; - } - - public abstract void considerSync(); - - public static boolean isQuicksy() { - return true; - } - - public static boolean isConversations() { - return true; - } - - public abstract void signalAccountStateChange(); - - public abstract boolean isSynchronizing(); - - public abstract void considerSyncBackground(boolean force); -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/AlarmReceiver.java b/src/main/java/eu/siacs/conversations/services/AlarmReceiver.java deleted file mode 100644 index d99f3a4d5..000000000 --- a/src/main/java/eu/siacs/conversations/services/AlarmReceiver.java +++ /dev/null @@ -1,23 +0,0 @@ -package eu.siacs.conversations.services; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.Compatibility; - -public class AlarmReceiver extends BroadcastReceiver { - public static final int SCHEDULE_ALARM_REQUEST_CODE = 523976483; - - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().contains("exportlogs")) { - Log.d(Config.LOGTAG, "Received alarm broadcast to export logs"); - final Intent backupIntent = new Intent(context, ExportBackupService.class); - backupIntent.putExtra("NOTIFY_ON_BACKUP_COMPLETE", false); - Compatibility.startService(context, backupIntent); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java b/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java deleted file mode 100644 index 79e7e4d04..000000000 --- a/src/main/java/eu/siacs/conversations/services/AppRTCAudioManager.java +++ /dev/null @@ -1,641 +0,0 @@ -/* - * Copyright 2014 The WebRTC Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ -package eu.siacs.conversations.services; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageManager; -import android.media.AudioDeviceInfo; -import android.media.AudioFormat; -import android.media.AudioManager; -import android.media.AudioRecord; -import android.media.MediaRecorder; -import android.os.Build; -import android.util.Log; -import eu.siacs.conversations.xmpp.jingle.Media; -import androidx.annotation.Nullable; - -import org.webrtc.ThreadUtils; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.CountDownLatch; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.AppRTCUtils; - -/** - * AppRTCAudioManager manages all audio related parts of the AppRTC demo. - */ -public class AppRTCAudioManager { - - private static CountDownLatch microphoneLatch; - - private final Context apprtcContext; - // Contains speakerphone setting: auto, true or false - @Nullable - private SpeakerPhonePreference speakerPhonePreference; - // Handles all tasks related to Bluetooth headset devices. - private final AppRTCBluetoothManager bluetoothManager; - @Nullable - private AudioManager audioManager; - @Nullable - private AudioManagerEvents audioManagerEvents; - private AudioManagerState amState; - private boolean savedIsSpeakerPhoneOn; - private boolean savedIsMicrophoneMute; - private boolean hasWiredHeadset; - // Default audio device; speaker phone for video calls or earpiece for audio - // only calls. - private AudioDevice defaultAudioDevice; - // Contains the currently selected audio device. - // This device is changed automatically using a certain scheme where e.g. - // a wired headset "wins" over speaker phone. It is also possible for a - // user to explicitly select a device (and overrid any predefined scheme). - // See |userSelectedAudioDevice| for details. - private AudioDevice selectedAudioDevice; - // Contains the user-selected audio device which overrides the predefined - // selection scheme. - // TODO(henrika): always set to AudioDevice.NONE today. Add support for - // explicit selection based on choice by userSelectedAudioDevice. - private AudioDevice userSelectedAudioDevice; - // Proximity sensor object. It measures the proximity of an object in cm - // relative to the view screen of a device and can therefore be used to - // assist device switching (close to ear <=> use headset earpiece if - // available, far from ear <=> use speaker phone). - @Nullable - private AppRTCProximitySensor proximitySensor; - // Contains a list of available audio devices. A Set collection is used to - // avoid duplicate elements. - private Set audioDevices = new HashSet<>(); - // Broadcast receiver for wired headset intent broadcasts. - private BroadcastReceiver wiredHeadsetReceiver; - // Callback method for changes in audio focus. - @Nullable - private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; - - private AppRTCAudioManager(Context context, final SpeakerPhonePreference speakerPhonePreference) { - Log.d(Config.LOGTAG, "ctor"); - ThreadUtils.checkIsOnMainThread(); - apprtcContext = context; - audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE)); - bluetoothManager = AppRTCBluetoothManager.create(context, this); - wiredHeadsetReceiver = new WiredHeadsetReceiver(); - amState = AudioManagerState.UNINITIALIZED; - this.speakerPhonePreference = speakerPhonePreference; - if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) { - defaultAudioDevice = AudioDevice.EARPIECE; - } else { - defaultAudioDevice = AudioDevice.SPEAKER_PHONE; - } - // Create and initialize the proximity sensor. - // Tablet devices (e.g. Nexus 7) does not support proximity sensors. - // Note that, the sensor will not be active until start() has been called. - proximitySensor = AppRTCProximitySensor.create(context, - // This method will be called each time a state change is detected. - // Example: user holds his hand over the device (closer than ~5 cm), - // or removes his hand from the device. - this::onProximitySensorChangedState); - Log.d(Config.LOGTAG, "defaultAudioDevice: " + defaultAudioDevice); - AppRTCUtils.logDeviceInfo(Config.LOGTAG); - } - public void switchSpeakerPhonePreference(final SpeakerPhonePreference speakerPhonePreference) { - this.speakerPhonePreference = speakerPhonePreference; - if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) { - defaultAudioDevice = AudioDevice.EARPIECE; - } else { - defaultAudioDevice = AudioDevice.SPEAKER_PHONE; - } - updateAudioDeviceState(); - } - - - /** - * Construction. - */ - public static AppRTCAudioManager create(Context context, SpeakerPhonePreference speakerPhonePreference) { - return new AppRTCAudioManager(context, speakerPhonePreference); - } - - public static boolean isMicrophoneAvailable() { - microphoneLatch = new CountDownLatch(1); - AudioRecord audioRecord = null; - boolean available = true; - try { - final int sampleRate = 44100; - final int channel = AudioFormat.CHANNEL_IN_MONO; - final int format = AudioFormat.ENCODING_PCM_16BIT; - final int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channel, format); - audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRate, channel, format, bufferSize); - audioRecord.startRecording(); - final short[] buffer = new short[bufferSize]; - final int audioStatus = audioRecord.read(buffer, 0, bufferSize); - if (audioStatus == AudioRecord.ERROR_INVALID_OPERATION || audioStatus == AudioRecord.STATE_UNINITIALIZED) - available = false; - } catch (Exception e) { - available = false; - } finally { - release(audioRecord); - - } - microphoneLatch.countDown(); - return available; - } - - private static void release(final AudioRecord audioRecord) { - if (audioRecord == null) { - return; - } - try { - audioRecord.release(); - } catch (Exception e) { - //ignore - } - } - - /** - * This method is called when the proximity sensor reports a state change, - * e.g. from "NEAR to FAR" or from "FAR to NEAR". - */ - private void onProximitySensorChangedState() { - if (speakerPhonePreference != SpeakerPhonePreference.AUTO) { - return; - } - // The proximity sensor should only be activated when there are exactly two - // available audio devices. - if (audioDevices.size() == 2 && audioDevices.contains(AppRTCAudioManager.AudioDevice.EARPIECE) - && audioDevices.contains(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE)) { - if (proximitySensor.sensorReportsNearState()) { - // Sensor reports that a "handset is being held up to a person's ear", - // or "something is covering the light sensor". - setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.EARPIECE); - } else { - // Sensor reports that a "handset is removed from a person's ear", or - // "the light sensor is no longer covered". - setAudioDeviceInternal(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE); - } - } - } - - @SuppressWarnings("deprecation") - public void start(AudioManagerEvents audioManagerEvents) { - Log.d(Config.LOGTAG, AppRTCAudioManager.class.getName() + ".start()"); - ThreadUtils.checkIsOnMainThread(); - if (amState == AudioManagerState.RUNNING) { - Log.e(Config.LOGTAG, "AudioManager is already active"); - return; - } - awaitMicrophoneLatch(); - this.audioManagerEvents = audioManagerEvents; - amState = AudioManagerState.RUNNING; - // Store current audio state so we can restore it when stop() is called. - savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn(); - savedIsMicrophoneMute = audioManager.isMicrophoneMute(); - hasWiredHeadset = hasWiredHeadset(); - // Create an AudioManager.OnAudioFocusChangeListener instance. - audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { - // Called on the listener to notify if the audio focus for this listener has been changed. - // The |focusChange| value indicates whether the focus was gained, whether the focus was lost, - // and whether that loss is transient, or whether the new focus holder will hold it for an - // unknown amount of time. - // TODO(henrika): possibly extend support of handling audio-focus changes. Only contains - // logging for now. - @Override - public void onAudioFocusChange(int focusChange) { - final String typeOfChange; - switch (focusChange) { - case AudioManager.AUDIOFOCUS_GAIN: - typeOfChange = "AUDIOFOCUS_GAIN"; - break; - case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT: - typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT"; - break; - case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE: - typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE"; - break; - case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: - typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK"; - break; - case AudioManager.AUDIOFOCUS_LOSS: - typeOfChange = "AUDIOFOCUS_LOSS"; - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: - typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT"; - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: - typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK"; - break; - default: - typeOfChange = "AUDIOFOCUS_INVALID"; - break; - } - Log.d(Config.LOGTAG, "onAudioFocusChange: " + typeOfChange); - } - }; - // Request audio playout focus (without ducking) and install listener for changes in focus. - int result = audioManager.requestAudioFocus(audioFocusChangeListener, - AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); - if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - Log.d(Config.LOGTAG, "Audio focus request granted for VOICE_CALL streams"); - } else { - Log.e(Config.LOGTAG, "Audio focus request failed"); - } - // Start by setting MODE_IN_COMMUNICATION as default audio mode. It is - // required to be in this mode when playout and/or recording starts for - // best possible VoIP performance. - audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); - // Always disable microphone mute during a WebRTC call. - setMicrophoneMute(false); - // Set initial device states. - userSelectedAudioDevice = AudioDevice.NONE; - selectedAudioDevice = AudioDevice.NONE; - audioDevices.clear(); - // Initialize and start Bluetooth if a BT device is available or initiate - // detection of new (enabled) BT devices. - bluetoothManager.start(); - // Do initial selection of audio device. This setting can later be changed - // either by adding/removing a BT or wired headset or by covering/uncovering - // the proximity sensor. - updateAudioDeviceState(); - // Register receiver for broadcast intents related to adding/removing a - // wired headset. - registerReceiver(wiredHeadsetReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); - Log.d(Config.LOGTAG, "AudioManager started"); - } - - private void awaitMicrophoneLatch() { - final CountDownLatch latch = microphoneLatch; - if (latch == null) { - return; - } - try { - latch.await(); - } catch (InterruptedException e) { - //ignore - } - } - - @SuppressWarnings("deprecation") - public void stop() { - Log.d(Config.LOGTAG, AppRTCAudioManager.class.getName() + ".stop()"); - ThreadUtils.checkIsOnMainThread(); - if (amState != AudioManagerState.RUNNING) { - Log.e(Config.LOGTAG, "Trying to stop AudioManager in incorrect state: " + amState); - return; - } - amState = AudioManagerState.UNINITIALIZED; - unregisterReceiver(wiredHeadsetReceiver); - bluetoothManager.stop(); - // Restore previously stored audio states. - setSpeakerphoneOn(savedIsSpeakerPhoneOn); - setMicrophoneMute(savedIsMicrophoneMute); - audioManager.setMode(AudioManager.MODE_NORMAL); - // Abandon audio focus. Gives the previous focus owner, if any, focus. - audioManager.abandonAudioFocus(audioFocusChangeListener); - audioFocusChangeListener = null; - Log.d(Config.LOGTAG, "Abandoned audio focus for VOICE_CALL streams"); - if (proximitySensor != null) { - proximitySensor.stop(); - proximitySensor = null; - } - audioManagerEvents = null; - } - - /** - * Changes selection of the currently active audio device. - */ - private void setAudioDeviceInternal(AudioDevice device) { - Log.d(Config.LOGTAG, "setAudioDeviceInternal(device=" + device + ")"); - AppRTCUtils.assertIsTrue(audioDevices.contains(device)); - switch (device) { - case SPEAKER_PHONE: - setSpeakerphoneOn(true); - break; - case EARPIECE: - case WIRED_HEADSET: - case BLUETOOTH: - setSpeakerphoneOn(false); - break; - default: - Log.e(Config.LOGTAG, "Invalid audio device selection"); - break; - } - selectedAudioDevice = device; - } - - /** - * Changes default audio device. - * TODO(henrika): add usage of this method in the AppRTCMobile client. - */ - public void setDefaultAudioDevice(AudioDevice defaultDevice) { - ThreadUtils.checkIsOnMainThread(); - switch (defaultDevice) { - case SPEAKER_PHONE: - defaultAudioDevice = defaultDevice; - break; - case EARPIECE: - if (hasEarpiece()) { - defaultAudioDevice = defaultDevice; - } else { - defaultAudioDevice = AudioDevice.SPEAKER_PHONE; - } - break; - default: - Log.e(Config.LOGTAG, "Invalid default audio device selection"); - break; - } - Log.d(Config.LOGTAG, "setDefaultAudioDevice(device=" + defaultAudioDevice + ")"); - updateAudioDeviceState(); - } - - /** - * Changes selection of the currently active audio device. - */ - public void selectAudioDevice(AudioDevice device) { - ThreadUtils.checkIsOnMainThread(); - if (!audioDevices.contains(device)) { - Log.e(Config.LOGTAG, "Can not select " + device + " from available " + audioDevices); - } - userSelectedAudioDevice = device; - updateAudioDeviceState(); - } - - /** - * Returns current set of available/selectable audio devices. - */ - public Set getAudioDevices() { - ThreadUtils.checkIsOnMainThread(); - return Collections.unmodifiableSet(new HashSet<>(audioDevices)); - } - - /** - * Returns the currently selected audio device. - */ - public AudioDevice getSelectedAudioDevice() { - ThreadUtils.checkIsOnMainThread(); - return selectedAudioDevice; - } - - /** - * Helper method for receiver registration. - */ - private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { - apprtcContext.registerReceiver(receiver, filter); - } - - /** - * Helper method for unregistration of an existing receiver. - */ - private void unregisterReceiver(BroadcastReceiver receiver) { - apprtcContext.unregisterReceiver(receiver); - } - - /** - * Sets the speaker phone mode. - */ - private void setSpeakerphoneOn(boolean on) { - boolean wasOn = audioManager.isSpeakerphoneOn(); - if (wasOn == on) { - return; - } - audioManager.setSpeakerphoneOn(on); - } - - /** - * Sets the microphone mute state. - */ - private void setMicrophoneMute(boolean on) { - boolean wasMuted = audioManager.isMicrophoneMute(); - if (wasMuted == on) { - return; - } - audioManager.setMicrophoneMute(on); - } - - /** - * Gets the current earpiece state. - */ - private boolean hasEarpiece() { - return apprtcContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY); - } - - /** - * Checks whether a wired headset is connected or not. - * This is not a valid indication that audio playback is actually over - * the wired headset as audio routing depends on other conditions. We - * only use it as an early indicator (during initialization) of an attached - * wired headset. - */ - @Deprecated - private boolean hasWiredHeadset() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - return audioManager.isWiredHeadsetOn(); - } else { - final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL); - for (AudioDeviceInfo device : devices) { - final int type = device.getType(); - if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) { - Log.d(Config.LOGTAG, "hasWiredHeadset: found wired headset"); - return true; - } else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) { - Log.d(Config.LOGTAG, "hasWiredHeadset: found USB audio device"); - return true; - } - } - return false; - } - } - - /** - * Updates list of possible audio devices and make new device selection. - * TODO(henrika): add unit test to verify all state transitions. - */ - public void updateAudioDeviceState() { - ThreadUtils.checkIsOnMainThread(); - Log.d(Config.LOGTAG, "--- updateAudioDeviceState: " - + "wired headset=" + hasWiredHeadset + ", " - + "BT state=" + bluetoothManager.getState()); - Log.d(Config.LOGTAG, "Device status: " - + "available=" + audioDevices + ", " - + "selected=" + selectedAudioDevice + ", " - + "user selected=" + userSelectedAudioDevice); - // Check if any Bluetooth headset is connected. The internal BT state will - // change accordingly. - // TODO(henrika): perhaps wrap required state into BT manager. - if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE - || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE - || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_DISCONNECTING) { - bluetoothManager.updateDevice(); - } - // Update the set of available audio devices. - Set newAudioDevices = new HashSet<>(); - if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED - || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING - || bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE) { - newAudioDevices.add(AudioDevice.BLUETOOTH); - } - if (hasWiredHeadset) { - // If a wired headset is connected, then it is the only possible option. - newAudioDevices.add(AudioDevice.WIRED_HEADSET); - } else { - // No wired headset, hence the audio-device list can contain speaker - // phone (on a tablet), or speaker phone and earpiece (on mobile phone). - newAudioDevices.add(AudioDevice.SPEAKER_PHONE); - if (hasEarpiece()) { - newAudioDevices.add(AudioDevice.EARPIECE); - } - } - // Store state which is set to true if the device list has changed. - boolean audioDeviceSetUpdated = !audioDevices.equals(newAudioDevices); - // Update the existing audio device set. - audioDevices = newAudioDevices; - // Correct user selected audio devices if needed. - if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_UNAVAILABLE - && userSelectedAudioDevice == AudioDevice.BLUETOOTH) { - // If BT is not available, it can't be the user selection. - userSelectedAudioDevice = AudioDevice.NONE; - } - if (hasWiredHeadset && userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE) { - // If user selected speaker phone, but then plugged wired headset then make - // wired headset as user selected device. - userSelectedAudioDevice = AudioDevice.WIRED_HEADSET; - } - if (!hasWiredHeadset && userSelectedAudioDevice == AudioDevice.WIRED_HEADSET) { - // If user selected wired headset, but then unplugged wired headset then make - // speaker phone as user selected device. - userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE; - } - // Need to start Bluetooth if it is available and user either selected it explicitly or - // user did not select any output device. - boolean needBluetoothAudioStart = - bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE - && (userSelectedAudioDevice == AudioDevice.NONE - || userSelectedAudioDevice == AudioDevice.BLUETOOTH); - // Need to stop Bluetooth audio if user selected different device and - // Bluetooth SCO connection is established or in the process. - boolean needBluetoothAudioStop = - (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED - || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING) - && (userSelectedAudioDevice != AudioDevice.NONE - && userSelectedAudioDevice != AudioDevice.BLUETOOTH); - if (bluetoothManager.getState() == AppRTCBluetoothManager.State.HEADSET_AVAILABLE - || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTING - || bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) { - Log.d(Config.LOGTAG, "Need BT audio: start=" + needBluetoothAudioStart + ", " - + "stop=" + needBluetoothAudioStop + ", " - + "BT state=" + bluetoothManager.getState()); - } - // Start or stop Bluetooth SCO connection given states set earlier. - if (needBluetoothAudioStop) { - bluetoothManager.stopScoAudio(); - bluetoothManager.updateDevice(); - } - if (needBluetoothAudioStart && !needBluetoothAudioStop) { - // Attempt to start Bluetooth SCO audio (takes a few second to start). - if (!bluetoothManager.startScoAudio()) { - // Remove BLUETOOTH from list of available devices since SCO failed. - audioDevices.remove(AudioDevice.BLUETOOTH); - audioDeviceSetUpdated = true; - } - } - // Update selected audio device. - final AudioDevice newAudioDevice; - if (bluetoothManager.getState() == AppRTCBluetoothManager.State.SCO_CONNECTED) { - // If a Bluetooth is connected, then it should be used as output audio - // device. Note that it is not sufficient that a headset is available; - // an active SCO channel must also be up and running. - newAudioDevice = AudioDevice.BLUETOOTH; - } else if (hasWiredHeadset) { - // If a wired headset is connected, but Bluetooth is not, then wired headset is used as - // audio device. - newAudioDevice = AudioDevice.WIRED_HEADSET; - } else { - // No wired headset and no Bluetooth, hence the audio-device list can contain speaker - // phone (on a tablet), or speaker phone and earpiece (on mobile phone). - // |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or AudioDevice.EARPIECE - // depending on the user's selection. - newAudioDevice = defaultAudioDevice; - } - // Switch to new device but only if there has been any changes. - if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) { - // Do the required device switch. - setAudioDeviceInternal(newAudioDevice); - Log.d(Config.LOGTAG, "New device status: " - + "available=" + audioDevices + ", " - + "selected=" + newAudioDevice); - if (audioManagerEvents != null) { - // Notify a listening client that audio device has been changed. - audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices); - } - } - Log.d(Config.LOGTAG, "--- updateAudioDeviceState done"); - } - - /** - * AudioDevice is the names of possible audio devices that we currently - * support. - */ - public enum AudioDevice {SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE} - - /** - * AudioManager state. - */ - public enum AudioManagerState { - UNINITIALIZED, - PREINITIALIZED, - RUNNING, - } - - public enum SpeakerPhonePreference { - AUTO, EARPIECE, SPEAKER; - - public static SpeakerPhonePreference of(final Set media) { - if (media.contains(Media.VIDEO)) { - return SPEAKER; - } else { - return EARPIECE; - } - } - } - - /** - * Selected audio device change event. - */ - public interface AudioManagerEvents { - // Callback fired once audio device is changed or list of available audio devices changed. - void onAudioDeviceChanged( - AudioDevice selectedAudioDevice, Set availableAudioDevices); - } - - /* Receiver which handles changes in wired headset availability. */ - private class WiredHeadsetReceiver extends BroadcastReceiver { - private static final int STATE_UNPLUGGED = 0; - private static final int STATE_PLUGGED = 1; - private static final int HAS_NO_MIC = 0; - private static final int HAS_MIC = 1; - - @Override - public void onReceive(Context context, Intent intent) { - int state = intent.getIntExtra("state", STATE_UNPLUGGED); - int microphone = intent.getIntExtra("microphone", HAS_NO_MIC); - String name = intent.getStringExtra("name"); - Log.d(Config.LOGTAG, "WiredHeadsetReceiver.onReceive" + AppRTCUtils.getThreadInfo() + ": " - + "a=" + intent.getAction() + ", s=" - + (state == STATE_UNPLUGGED ? "unplugged" : "plugged") + ", m=" - + (microphone == HAS_MIC ? "mic" : "no mic") + ", n=" + name + ", sb=" - + isInitialStickyBroadcast()); - hasWiredHeadset = (state == STATE_PLUGGED); - updateAudioDeviceState(); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java b/src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java deleted file mode 100644 index 94481537c..000000000 --- a/src/main/java/eu/siacs/conversations/services/AppRTCBluetoothManager.java +++ /dev/null @@ -1,575 +0,0 @@ -/* - * Copyright 2016 The WebRTC Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ -package eu.siacs.conversations.services; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothHeadset; -import android.bluetooth.BluetoothProfile; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageManager; -import android.media.AudioManager; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; - -import androidx.annotation.Nullable; -import androidx.core.app.ActivityCompat; - -import com.google.common.collect.ImmutableList; - -import org.webrtc.ThreadUtils; - -import java.util.Collections; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.AppRTCUtils; - -/** AppRTCProximitySensor manages functions related to Bluetoth devices in the AppRTC demo. */ -public class AppRTCBluetoothManager { - // Timeout interval for starting or stopping audio to a Bluetooth SCO device. - private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000; - // Maximum number of SCO connection attempts. - private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2; - private final Context apprtcContext; - private final AppRTCAudioManager apprtcAudioManager; - @Nullable private final AudioManager audioManager; - private final Handler handler; - private final BluetoothProfile.ServiceListener bluetoothServiceListener; - private final BroadcastReceiver bluetoothHeadsetReceiver; - int scoConnectionAttempts; - private State bluetoothState; - @Nullable private BluetoothAdapter bluetoothAdapter; - @Nullable private BluetoothHeadset bluetoothHeadset; - @Nullable private BluetoothDevice bluetoothDevice; - // Runs when the Bluetooth timeout expires. We use that timeout after calling - // startScoAudio() or stopScoAudio() because we're not guaranteed to get a - // callback after those calls. - private final Runnable bluetoothTimeoutRunnable = - new Runnable() { - @Override - public void run() { - bluetoothTimeout(); - } - }; - - protected AppRTCBluetoothManager(Context context, AppRTCAudioManager audioManager) { - Log.d(Config.LOGTAG, "ctor"); - ThreadUtils.checkIsOnMainThread(); - apprtcContext = context; - apprtcAudioManager = audioManager; - this.audioManager = getAudioManager(context); - bluetoothState = State.UNINITIALIZED; - bluetoothServiceListener = new BluetoothServiceListener(); - bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver(); - handler = new Handler(Looper.getMainLooper()); - } - - /** Construction. */ - static AppRTCBluetoothManager create(Context context, AppRTCAudioManager audioManager) { - Log.d(Config.LOGTAG, "create" + AppRTCUtils.getThreadInfo()); - return new AppRTCBluetoothManager(context, audioManager); - } - - /** Returns the internal state. */ - public State getState() { - ThreadUtils.checkIsOnMainThread(); - return bluetoothState; - } - - /** - * Activates components required to detect Bluetooth devices and to enable BT SCO (audio is - * routed via BT SCO) for the headset profile. The end state will be HEADSET_UNAVAILABLE but a - * state machine has started which will start a state change sequence where the final outcome - * depends on if/when the BT headset is enabled. Example of state change sequence when start() - * is called while BT device is connected and enabled: UNINITIALIZED --> HEADSET_UNAVAILABLE --> - * HEADSET_AVAILABLE --> SCO_CONNECTING --> SCO_CONNECTED <==> audio is now routed via BT SCO. - * Note that the AppRTCAudioManager is also involved in driving this state change. - */ - public void start() { - ThreadUtils.checkIsOnMainThread(); - if (bluetoothState != State.UNINITIALIZED) { - Log.w(Config.LOGTAG, "Invalid BT state"); - return; - } - bluetoothHeadset = null; - bluetoothDevice = null; - scoConnectionAttempts = 0; - // Get a handle to the default local Bluetooth adapter. - bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - if (bluetoothAdapter == null) { - Log.w(Config.LOGTAG, "Device does not support Bluetooth"); - return; - } - // Ensure that the device supports use of BT SCO audio for off call use cases. - if (this.audioManager == null || !audioManager.isBluetoothScoAvailableOffCall()) { - Log.e(Config.LOGTAG, "Bluetooth SCO audio is not available off call"); - return; - } - // Establish a connection to the HEADSET profile (includes both Bluetooth Headset and - // Hands-Free) proxy object and install a listener. - if (!getBluetoothProfileProxy( - apprtcContext, bluetoothServiceListener, BluetoothProfile.HEADSET)) { - Log.e(Config.LOGTAG, "BluetoothAdapter.getProfileProxy(HEADSET) failed"); - return; - } - // Register receivers for BluetoothHeadset change notifications. - IntentFilter bluetoothHeadsetFilter = new IntentFilter(); - // Register receiver for change in connection state of the Headset profile. - bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); - // Register receiver for change in audio connection state of the Headset profile. - bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED); - registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter); - if (hasBluetoothConnectPermission()) { - Log.d( - Config.LOGTAG, - "HEADSET profile state: " - + stateToString( - bluetoothAdapter.getProfileConnectionState( - BluetoothProfile.HEADSET))); - } - Log.d(Config.LOGTAG, "Bluetooth proxy for headset profile has started"); - bluetoothState = State.HEADSET_UNAVAILABLE; - Log.d(Config.LOGTAG, "start done: BT state=" + bluetoothState); - } - - /** Stops and closes all components related to Bluetooth audio. */ - public void stop() { - ThreadUtils.checkIsOnMainThread(); - Log.d(Config.LOGTAG, "stop: BT state=" + bluetoothState); - if (bluetoothAdapter == null) { - return; - } - // Stop BT SCO connection with remote device if needed. - stopScoAudio(); - // Close down remaining BT resources. - if (bluetoothState == State.UNINITIALIZED) { - return; - } - unregisterReceiver(bluetoothHeadsetReceiver); - cancelTimer(); - if (bluetoothHeadset != null) { - bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset); - bluetoothHeadset = null; - } - bluetoothAdapter = null; - bluetoothDevice = null; - bluetoothState = State.UNINITIALIZED; - Log.d(Config.LOGTAG, "stop done: BT state=" + bluetoothState); - } - - /** - * Starts Bluetooth SCO connection with remote device. Note that the phone application always - * has the priority on the usage of the SCO connection for telephony. If this method is called - * while the phone is in call it will be ignored. Similarly, if a call is received or sent while - * an application is using the SCO connection, the connection will be lost for the application - * and NOT returned automatically when the call ends. Also note that: up to and including API - * version JELLY_BEAN_MR1, this method initiates a virtual voice call to the Bluetooth headset. - * After API version JELLY_BEAN_MR2 only a raw SCO audio connection is established. - * TODO(henrika): should we add support for virtual voice call to BT headset also for JBMR2 and - * higher. It might be required to initiates a virtual voice call since many devices do not - * accept SCO audio without a "call". - */ - public boolean startScoAudio() { - ThreadUtils.checkIsOnMainThread(); - Log.d( - Config.LOGTAG, - "startSco: BT state=" - + bluetoothState - + ", " - + "attempts: " - + scoConnectionAttempts - + ", " - + "SCO is on: " - + isScoOn()); - if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) { - Log.e(Config.LOGTAG, "BT SCO connection fails - no more attempts"); - return false; - } - if (bluetoothState != State.HEADSET_AVAILABLE) { - Log.e(Config.LOGTAG, "BT SCO connection fails - no headset available"); - return false; - } - // Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED. - Log.d(Config.LOGTAG, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_CHANGED..."); - // The SCO connection establishment can take several seconds, hence we cannot rely on the - // connection to be available when the method returns but instead register to receive the - // intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be - // SCO_AUDIO_STATE_CONNECTED. - bluetoothState = State.SCO_CONNECTING; - audioManager.startBluetoothSco(); - audioManager.setBluetoothScoOn(true); - scoConnectionAttempts++; - startTimer(); - Log.d( - Config.LOGTAG, - "startScoAudio done: BT state=" - + bluetoothState - + ", " - + "SCO is on: " - + isScoOn()); - return true; - } - - /** Stops Bluetooth SCO connection with remote device. */ - public void stopScoAudio() { - ThreadUtils.checkIsOnMainThread(); - Log.d( - Config.LOGTAG, - "stopScoAudio: BT state=" + bluetoothState + ", " + "SCO is on: " + isScoOn()); - if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) { - return; - } - cancelTimer(); - audioManager.stopBluetoothSco(); - audioManager.setBluetoothScoOn(false); - bluetoothState = State.SCO_DISCONNECTING; - Log.d( - Config.LOGTAG, - "stopScoAudio done: BT state=" + bluetoothState + ", " + "SCO is on: " + isScoOn()); - } - - /** - * Use the BluetoothHeadset proxy object (controls the Bluetooth Headset Service via IPC) to - * update the list of connected devices for the HEADSET profile. The internal state will change - * to HEADSET_UNAVAILABLE or to HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the - * connected device if available. - */ - @SuppressLint("MissingPermission") - public void updateDevice() { - if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) { - return; - } - Log.d(Config.LOGTAG, "updateDevice"); - // Get connected devices for the headset profile. Returns the set of - // devices which are in state STATE_CONNECTED. The BluetoothDevice class - // is just a thin wrapper for a Bluetooth hardware address. - final List devices; - if (hasBluetoothConnectPermission()) { - devices = bluetoothHeadset.getConnectedDevices(); - } else { - devices = ImmutableList.of(); - } - if (devices.isEmpty()) { - bluetoothDevice = null; - bluetoothState = State.HEADSET_UNAVAILABLE; - Log.d(Config.LOGTAG, "No connected bluetooth headset"); - } else { - // Always use first device in list. Android only supports one device. - bluetoothDevice = devices.get(0); - bluetoothState = State.HEADSET_AVAILABLE; - Log.d( - Config.LOGTAG, - "Connected bluetooth headset: " - + "name=" - + bluetoothDevice.getName() - + ", " - + "state=" - + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) - + ", SCO audio=" - + bluetoothHeadset.isAudioConnected(bluetoothDevice)); - } - Log.d(Config.LOGTAG, "updateDevice done: BT state=" + bluetoothState); - } - - /** Stubs for test mocks. */ - @Nullable - protected AudioManager getAudioManager(Context context) { - return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - } - - protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { - apprtcContext.registerReceiver(receiver, filter); - } - - protected void unregisterReceiver(BroadcastReceiver receiver) { - apprtcContext.unregisterReceiver(receiver); - } - - protected boolean getBluetoothProfileProxy( - Context context, BluetoothProfile.ServiceListener listener, int profile) { - return bluetoothAdapter.getProfileProxy(context, listener, profile); - } - - protected boolean hasBluetoothConnectPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - return ActivityCompat.checkSelfPermission( - apprtcContext, Manifest.permission.BLUETOOTH_CONNECT) - == PackageManager.PERMISSION_GRANTED; - } else { - return true; - } - } - - /** Ensures that the audio manager updates its list of available audio devices. */ - private void updateAudioDeviceState() { - ThreadUtils.checkIsOnMainThread(); - Log.d(Config.LOGTAG, "updateAudioDeviceState"); - apprtcAudioManager.updateAudioDeviceState(); - } - - /** Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds. */ - private void startTimer() { - ThreadUtils.checkIsOnMainThread(); - Log.d(Config.LOGTAG, "startTimer"); - handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS); - } - - /** Cancels any outstanding timer tasks. */ - private void cancelTimer() { - ThreadUtils.checkIsOnMainThread(); - Log.d(Config.LOGTAG, "cancelTimer"); - handler.removeCallbacks(bluetoothTimeoutRunnable); - } - - /** - * Called when start of the BT SCO channel takes too long time. Usually happens when the BT - * device has been turned on during an ongoing call. - */ - @SuppressLint("MissingPermission") - private void bluetoothTimeout() { - ThreadUtils.checkIsOnMainThread(); - if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) { - return; - } - Log.d( - Config.LOGTAG, - "bluetoothTimeout: BT state=" - + bluetoothState - + ", " - + "attempts: " - + scoConnectionAttempts - + ", " - + "SCO is on: " - + isScoOn()); - if (bluetoothState != State.SCO_CONNECTING) { - return; - } - // Bluetooth SCO should be connecting; check the latest result. - boolean scoConnected = false; - final List devices; - if (hasBluetoothConnectPermission()) { - devices = bluetoothHeadset.getConnectedDevices(); - } else { - devices = Collections.emptyList(); - } - if (devices.size() > 0) { - bluetoothDevice = devices.get(0); - if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) { - Log.d(Config.LOGTAG, "SCO connected with " + bluetoothDevice.getName()); - scoConnected = true; - } else { - Log.d(Config.LOGTAG, "SCO is not connected with " + bluetoothDevice.getName()); - } - } - if (scoConnected) { - // We thought BT had timed out, but it's actually on; updating state. - bluetoothState = State.SCO_CONNECTED; - scoConnectionAttempts = 0; - } else { - // Give up and "cancel" our request by calling stopBluetoothSco(). - Log.w(Config.LOGTAG, "BT failed to connect after timeout"); - stopScoAudio(); - } - updateAudioDeviceState(); - Log.d(Config.LOGTAG, "bluetoothTimeout done: BT state=" + bluetoothState); - } - - /** Checks whether audio uses Bluetooth SCO. */ - private boolean isScoOn() { - return audioManager.isBluetoothScoOn(); - } - - /** Converts BluetoothAdapter states into local string representations. */ - private String stateToString(int state) { - switch (state) { - case BluetoothAdapter.STATE_DISCONNECTED: - return "DISCONNECTED"; - case BluetoothAdapter.STATE_CONNECTED: - return "CONNECTED"; - case BluetoothAdapter.STATE_CONNECTING: - return "CONNECTING"; - case BluetoothAdapter.STATE_DISCONNECTING: - return "DISCONNECTING"; - case BluetoothAdapter.STATE_OFF: - return "OFF"; - case BluetoothAdapter.STATE_ON: - return "ON"; - case BluetoothAdapter.STATE_TURNING_OFF: - // Indicates the local Bluetooth adapter is turning off. Local clients should - // immediately - // attempt graceful disconnection of any remote links. - return "TURNING_OFF"; - case BluetoothAdapter.STATE_TURNING_ON: - // Indicates the local Bluetooth adapter is turning on. However local clients should - // wait - // for STATE_ON before attempting to use the adapter. - return "TURNING_ON"; - default: - return "INVALID"; - } - } - - // Bluetooth connection state. - public enum State { - // Bluetooth is not available; no adapter or Bluetooth is off. - UNINITIALIZED, - // Bluetooth error happened when trying to start Bluetooth. - ERROR, - // Bluetooth proxy object for the Headset profile exists, but no connected headset devices, - // SCO is not started or disconnected. - HEADSET_UNAVAILABLE, - // Bluetooth proxy object for the Headset profile connected, connected Bluetooth headset - // present, but SCO is not started or disconnected. - HEADSET_AVAILABLE, - // Bluetooth audio SCO connection with remote device is closing. - SCO_DISCONNECTING, - // Bluetooth audio SCO connection with remote device is initiated. - SCO_CONNECTING, - // Bluetooth audio SCO connection with remote device is established. - SCO_CONNECTED - } - - /** - * Implementation of an interface that notifies BluetoothProfile IPC clients when they have been - * connected to or disconnected from the service. - */ - private class BluetoothServiceListener implements BluetoothProfile.ServiceListener { - @Override - // Called to notify the client when the proxy object has been connected to the service. - // Once we have the profile proxy object, we can use it to monitor the state of the - // connection and perform other operations that are relevant to the headset profile. - public void onServiceConnected(int profile, BluetoothProfile proxy) { - if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { - return; - } - Log.d( - Config.LOGTAG, - "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState); - // Android only supports one connected Bluetooth Headset at a time. - bluetoothHeadset = (BluetoothHeadset) proxy; - updateAudioDeviceState(); - Log.d(Config.LOGTAG, "onServiceConnected done: BT state=" + bluetoothState); - } - - @Override - /** Notifies the client when the proxy object has been disconnected from the service. */ - public void onServiceDisconnected(int profile) { - if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { - return; - } - Log.d( - Config.LOGTAG, - "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState); - stopScoAudio(); - bluetoothHeadset = null; - bluetoothDevice = null; - bluetoothState = State.HEADSET_UNAVAILABLE; - updateAudioDeviceState(); - Log.d(Config.LOGTAG, "onServiceDisconnected done: BT state=" + bluetoothState); - } - } - - // Intent broadcast receiver which handles changes in Bluetooth device availability. - // Detects headset changes and Bluetooth SCO state changes. - private class BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - if (bluetoothState == State.UNINITIALIZED) { - return; - } - final String action = intent.getAction(); - // Change in connection state of the Headset profile. Note that the - // change does not tell us anything about whether we're streaming - // audio to BT over SCO. Typically received when user turns on a BT - // headset while audio is active using another audio device. - if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { - final int state = - intent.getIntExtra( - BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED); - Log.d( - Config.LOGTAG, - "BluetoothHeadsetBroadcastReceiver.onReceive: " - + "a=ACTION_CONNECTION_STATE_CHANGED, " - + "s=" - + stateToString(state) - + ", " - + "sb=" - + isInitialStickyBroadcast() - + ", " - + "BT state: " - + bluetoothState); - if (state == BluetoothHeadset.STATE_CONNECTED) { - scoConnectionAttempts = 0; - updateAudioDeviceState(); - } else if (state == BluetoothHeadset.STATE_CONNECTING) { - // No action needed. - } else if (state == BluetoothHeadset.STATE_DISCONNECTING) { - // No action needed. - } else if (state == BluetoothHeadset.STATE_DISCONNECTED) { - // Bluetooth is probably powered off during the call. - stopScoAudio(); - updateAudioDeviceState(); - } - // Change in the audio (SCO) connection state of the Headset profile. - // Typically received after call to startScoAudio() has finalized. - } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { - final int state = - intent.getIntExtra( - BluetoothHeadset.EXTRA_STATE, - BluetoothHeadset.STATE_AUDIO_DISCONNECTED); - Log.d( - Config.LOGTAG, - "BluetoothHeadsetBroadcastReceiver.onReceive: " - + "a=ACTION_AUDIO_STATE_CHANGED, " - + "s=" - + stateToString(state) - + ", " - + "sb=" - + isInitialStickyBroadcast() - + ", " - + "BT state: " - + bluetoothState); - if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { - cancelTimer(); - if (bluetoothState == State.SCO_CONNECTING) { - Log.d(Config.LOGTAG, "+++ Bluetooth audio SCO is now connected"); - bluetoothState = State.SCO_CONNECTED; - scoConnectionAttempts = 0; - updateAudioDeviceState(); - } else { - Log.w( - Config.LOGTAG, - "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED"); - } - } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) { - Log.d(Config.LOGTAG, "+++ Bluetooth audio SCO is now connecting..."); - } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { - Log.d(Config.LOGTAG, "+++ Bluetooth audio SCO is now disconnected"); - if (isInitialStickyBroadcast()) { - Log.d( - Config.LOGTAG, - "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast."); - return; - } - updateAudioDeviceState(); - } - } - Log.d(Config.LOGTAG, "onReceive done: BT state=" + bluetoothState); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/services/AppRTCProximitySensor.java b/src/main/java/eu/siacs/conversations/services/AppRTCProximitySensor.java deleted file mode 100644 index 2f24787c0..000000000 --- a/src/main/java/eu/siacs/conversations/services/AppRTCProximitySensor.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright 2014 The WebRTC Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ -package eu.siacs.conversations.services; - -import android.content.Context; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.os.Build; -import android.util.Log; - -import androidx.annotation.Nullable; - -import org.webrtc.ThreadUtils; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.AppRTCUtils; - -/** - * AppRTCProximitySensor manages functions related to the proximity sensor in - * the AppRTC demo. - * On most device, the proximity sensor is implemented as a boolean-sensor. - * It returns just two values "NEAR" or "FAR". Thresholding is done on the LUX - * value i.e. the LUX value of the light sensor is compared with a threshold. - * A LUX-value more than the threshold means the proximity sensor returns "FAR". - * Anything less than the threshold value and the sensor returns "NEAR". - */ -public class AppRTCProximitySensor implements SensorEventListener { - // This class should be created, started and stopped on one thread - // (e.g. the main thread). We use |nonThreadSafe| to ensure that this is - // the case. Only active when |DEBUG| is set to true. - private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker(); - private final Runnable onSensorStateListener; - private final SensorManager sensorManager; - @Nullable - private Sensor proximitySensor; - private boolean lastStateReportIsNear; - - private AppRTCProximitySensor(Context context, Runnable sensorStateListener) { - Log.d(Config.LOGTAG, "AppRTCProximitySensor" + AppRTCUtils.getThreadInfo()); - onSensorStateListener = sensorStateListener; - sensorManager = ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE)); - } - - /** - * Construction - */ - static AppRTCProximitySensor create(Context context, Runnable sensorStateListener) { - return new AppRTCProximitySensor(context, sensorStateListener); - } - - /** - * Activate the proximity sensor. Also do initialization if called for the - * first time. - */ - public boolean start() { - threadChecker.checkIsOnValidThread(); - Log.d(Config.LOGTAG, "start" + AppRTCUtils.getThreadInfo()); - if (!initDefaultSensor()) { - // Proximity sensor is not supported on this device. - return false; - } - sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL); - return true; - } - - /** - * Deactivate the proximity sensor. - */ - public void stop() { - threadChecker.checkIsOnValidThread(); - Log.d(Config.LOGTAG, "stop" + AppRTCUtils.getThreadInfo()); - if (proximitySensor == null) { - return; - } - sensorManager.unregisterListener(this, proximitySensor); - } - - /** - * Getter for last reported state. Set to true if "near" is reported. - */ - public boolean sensorReportsNearState() { - threadChecker.checkIsOnValidThread(); - return lastStateReportIsNear; - } - - @Override - public final void onAccuracyChanged(Sensor sensor, int accuracy) { - threadChecker.checkIsOnValidThread(); - AppRTCUtils.assertIsTrue(sensor.getType() == Sensor.TYPE_PROXIMITY); - if (accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) { - Log.e(Config.LOGTAG, "The values returned by this sensor cannot be trusted"); - } - } - - @Override - public final void onSensorChanged(SensorEvent event) { - threadChecker.checkIsOnValidThread(); - AppRTCUtils.assertIsTrue(event.sensor.getType() == Sensor.TYPE_PROXIMITY); - // As a best practice; do as little as possible within this method and - // avoid blocking. - float distanceInCentimeters = event.values[0]; - if (distanceInCentimeters < proximitySensor.getMaximumRange()) { - Log.d(Config.LOGTAG, "Proximity sensor => NEAR state"); - lastStateReportIsNear = true; - } else { - Log.d(Config.LOGTAG, "Proximity sensor => FAR state"); - lastStateReportIsNear = false; - } - // Report about new state to listening client. Client can then call - // sensorReportsNearState() to query the current state (NEAR or FAR). - if (onSensorStateListener != null) { - onSensorStateListener.run(); - } - Log.d(Config.LOGTAG, "onSensorChanged" + AppRTCUtils.getThreadInfo() + ": " - + "accuracy=" + event.accuracy + ", timestamp=" + event.timestamp + ", distance=" - + event.values[0]); - } - - /** - * Get default proximity sensor if it exists. Tablet devices (e.g. Nexus 7) - * does not support this type of sensor and false will be returned in such - * cases. - */ - private boolean initDefaultSensor() { - if (proximitySensor != null) { - return true; - } - proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); - if (proximitySensor == null) { - return false; - } - logProximitySensorInfo(); - return true; - } - - /** - * Helper method for logging information about the proximity sensor. - */ - private void logProximitySensorInfo() { - if (proximitySensor == null) { - return; - } - StringBuilder info = new StringBuilder("Proximity sensor: "); - info.append("name=").append(proximitySensor.getName()); - info.append(", vendor: ").append(proximitySensor.getVendor()); - info.append(", power: ").append(proximitySensor.getPower()); - info.append(", resolution: ").append(proximitySensor.getResolution()); - info.append(", max range: ").append(proximitySensor.getMaximumRange()); - info.append(", min delay: ").append(proximitySensor.getMinDelay()); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { - // Added in API level 20. - info.append(", type: ").append(proximitySensor.getStringType()); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - // Added in API level 21. - info.append(", max delay: ").append(proximitySensor.getMaxDelay()); - info.append(", reporting mode: ").append(proximitySensor.getReportingMode()); - info.append(", isWakeUpSensor: ").append(proximitySensor.isWakeUpSensor()); - } - Log.d(Config.LOGTAG, info.toString()); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java b/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java deleted file mode 100644 index 1c346e295..000000000 --- a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java +++ /dev/null @@ -1,283 +0,0 @@ -package eu.siacs.conversations.services; - -import android.content.Context; -import android.content.SharedPreferences; -import android.media.MediaMetadataRetriever; -import android.net.Uri; -import android.os.ParcelFileDescriptor; -import android.preference.PreferenceManager; -import android.util.Log; - -import androidx.annotation.NonNull; - -import com.otaliastudios.transcoder.Transcoder; -import com.otaliastudios.transcoder.TranscoderListener; -import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy; - -import org.jetbrains.annotations.NotNull; - -import java.io.File; -import java.io.FileDescriptor; -import java.io.FileNotFoundException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.Objects; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.PgpEngine; -import eu.siacs.conversations.entities.Conversation; -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.MimeUtils; -import eu.siacs.conversations.utils.TranscoderStrategies; - -public class AttachFileToConversationRunnable implements Runnable, TranscoderListener { - - private final XmppConnectionService mXmppConnectionService; - private final Message message; - private final Conversation conversation; - private final Uri uri; - private final String type; - private final UiCallback callback; - private final long maxUploadSize; - private final boolean isVideoMessage; - private final long originalFileSize; - private int currentProgress = -1; - public static String[] isCompressingVideo = new String[]{null,"0"}; - - public AttachFileToConversationRunnable(XmppConnectionService xmppConnectionService, Uri uri, String type, Message message, Conversation conversation, UiCallback callback, long maxUploadSize) { - this.uri = uri; - this.type = type; - this.mXmppConnectionService = xmppConnectionService; - this.message = message; - this.conversation = conversation; - this.callback = callback; - this.maxUploadSize = maxUploadSize; - final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type); - this.originalFileSize = FileBackend.getFileSize(mXmppConnectionService, uri); - this.isVideoMessage = !getFileBackend().useFileAsIs(uri) - && (mimeType != null && mimeType.startsWith("video/")) - && originalFileSize > Config.VIDEO_FAST_UPLOAD_SIZE - && !"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 Exception { - Log.d(Config.LOGTAG, "processing file as video"); - mXmppConnectionService.startForcingForegroundNotification(); - isCompressingVideo = new String[]{conversation.getUuid(),"0"}; - final SimpleDateFormat fileDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); - final String filename = "Sent/" + fileDateFormat.format(new Date(message.getTimeSent())) + "_" + message.getUuid().substring(0, 4) + "_komp"; - mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, String.format("%s.%s", filename, "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 ParcelFileDescriptor parcelFileDescriptor = mXmppConnectionService.getContentResolver().openFileDescriptor(uri, "r"); - if (parcelFileDescriptor == null) { - throw new FileNotFoundException("Parcel File Descriptor was null"); - } - final FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); - final Future future = getVideoCompression(fileDescriptor, file, maxUploadSize, originalFileSize); - try { - future.get(); - } catch (InterruptedException e) { - mXmppConnectionService.stopForcingForegroundNotification(); - isCompressingVideo = new String[]{null,"0"};; - throw new AssertionError(e); - } catch (ExecutionException e) { - if (e.getCause() instanceof Error) { - mXmppConnectionService.stopForcingForegroundNotification(); - isCompressingVideo = new String[]{null,"0"};; - processAsFile(); - } else { - Log.d(Config.LOGTAG, "ignoring execution exception. Should get handled by onTranscodeFailed() instead", e); - } - } - } - - private Future getVideoCompression(final FileDescriptor fileDescriptor, final File file, final long maxUploadSize, final long originalFileSize) throws Exception { - final MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); - try { - mediaMetadataRetriever.setDataSource(fileDescriptor); - } catch (Exception e) { - e.printStackTrace(); - throw new Exception(e); - } - long videoDuration; - final long estimatedFileSize = maxUploadSize / 2; // keep estimated filesize half as big as maxUploadSize - try { - videoDuration = Long.parseLong(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) / 1000; //in seconds - } catch (NumberFormatException e) { - videoDuration = -1; - } - final int bitrateAfterCompression = safeLongToInt(mXmppConnectionService.getCompressVideoBitratePreference() / 8); //in bytes - final long sizeAfterCompression = videoDuration * bitrateAfterCompression; - - if (sizeAfterCompression >= originalFileSize && originalFileSize <= Config.VIDEO_FAST_UPLOAD_SIZE) { - processAsFile(); - onTranscodeCanceled(); - return null; - } else if (estimatedFileSize >= sizeAfterCompression && sizeAfterCompression <= originalFileSize) { - return Transcoder.into(file.getAbsolutePath()) - .addDataSource(mXmppConnectionService, uri) - .setVideoTrackStrategy(TranscoderStrategies.VIDEO(mXmppConnectionService.getCompressVideoBitratePreference(), mXmppConnectionService.getCompressVideoResolutionPreference())) - .setAudioTrackStrategy(mXmppConnectionService.getCompressAudioPreference()) - .setListener(this) - .transcode(); - } else { - DefaultAudioStrategy audioStrategy; - int newBitrate = safeLongToInt((estimatedFileSize / videoDuration) * 8); // in bits/sec - int newResoloution = 0; - if (newBitrate <= mXmppConnectionService.getResources().getInteger(R.integer.verylow_video_bitrate)) { - newResoloution = mXmppConnectionService.getResources().getInteger(R.integer.verylow_video_res); - audioStrategy = TranscoderStrategies.AUDIO_LQ; - } else if (newBitrate > mXmppConnectionService.getResources().getInteger(R.integer.verylow_video_bitrate) && newBitrate <= mXmppConnectionService.getResources().getInteger(R.integer.low_video_bitrate)) { - newResoloution = mXmppConnectionService.getResources().getInteger(R.integer.low_video_res); - audioStrategy = TranscoderStrategies.AUDIO_LQ; - } else if (newBitrate > mXmppConnectionService.getResources().getInteger(R.integer.low_video_bitrate) && newBitrate <= mXmppConnectionService.getResources().getInteger(R.integer.mid_video_bitrate)) { - newResoloution = mXmppConnectionService.getResources().getInteger(R.integer.mid_video_res); - audioStrategy = TranscoderStrategies.AUDIO_MQ; - } else if (newBitrate > mXmppConnectionService.getResources().getInteger(R.integer.mid_video_bitrate) && newBitrate <= mXmppConnectionService.getResources().getInteger(R.integer.high_video_bitrate)) { - newResoloution = mXmppConnectionService.getResources().getInteger(R.integer.high_video_res); - audioStrategy = TranscoderStrategies.AUDIO_HQ; - } else { - newResoloution = mXmppConnectionService.getResources().getInteger(R.integer.high_video_res); - audioStrategy = TranscoderStrategies.AUDIO_HQ; - } - return Transcoder.into(file.getAbsolutePath()) - .addDataSource(mXmppConnectionService, uri) - .setVideoTrackStrategy(TranscoderStrategies.VIDEO(newBitrate, newResoloution)) - .setAudioTrackStrategy(audioStrategy) - .setListener(this) - .transcode(); - } - } - - private static int safeLongToInt(long l) { - if (l < Integer.MIN_VALUE || l > Integer.MAX_VALUE) { - throw new IllegalArgumentException(l + " cannot be cast to int without changing its value."); - } - return (int) l; - } - - @Override - public void onTranscodeProgress(double progress) { - final int p = (int) Math.round(progress * 100); - if (p > currentProgress) { - currentProgress = p; - mXmppConnectionService.getNotificationService().updateFileAddingNotification(p, message); - isCompressingVideo = new String[]{conversation.getUuid(), String.valueOf(currentProgress)}; - callback.progress(currentProgress); - mXmppConnectionService.getHttpConnectionManager().updateConversationUi(false); - } - } - - @Override - public void onTranscodeCompleted(int successCode) { - mXmppConnectionService.stopForcingForegroundNotification(); - isCompressingVideo = new String[]{null,"100"}; - 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(); - isCompressingVideo = new String[]{null,"0"}; - processAsFile(); - } - - @Override - public void onTranscodeFailed(@NonNull @NotNull Throwable exception) { - mXmppConnectionService.stopForcingForegroundNotification(); - isCompressingVideo = new String[]{null,"0"}; - Log.d(Config.LOGTAG, "video transcoding failed", exception); - processAsFile(); - } - - @Override - public void run() { - if (this.isVideoMessage()) { - try { - processAsVideo(); - } catch (Exception e) { - processAsFile(); - e.printStackTrace(); - } - } 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)); - } - - public FileBackend getFileBackend() { - return mXmppConnectionService.fileBackend; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/AudioPlayer.java b/src/main/java/eu/siacs/conversations/services/AudioPlayer.java deleted file mode 100644 index 7f48edbdb..000000000 --- a/src/main/java/eu/siacs/conversations/services/AudioPlayer.java +++ /dev/null @@ -1,474 +0,0 @@ -package eu.siacs.conversations.services; - -import android.Manifest; -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.res.ColorStateList; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.media.AudioManager; -import android.os.Build; -import android.os.Handler; -import android.os.PowerManager; -import android.util.Log; -import android.view.View; -import android.widget.ImageButton; -import android.widget.RelativeLayout; -import android.widget.SeekBar; -import android.widget.TextView; - -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; - -import java.lang.ref.WeakReference; -import java.util.Locale; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.ui.ConversationsActivity; -import eu.siacs.conversations.ui.adapter.MessageAdapter; -import eu.siacs.conversations.ui.util.PendingItem; -import eu.siacs.conversations.utils.ThemeHelper; -import eu.siacs.conversations.utils.WeakReferenceSet; - -public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompletionListener, SeekBar.OnSeekBarChangeListener, Runnable, SensorEventListener, AudioManager.OnAudioFocusChangeListener { - - private static final int REFRESH_INTERVAL = 250; - private static final Object LOCK = new Object(); - private static MediaPlayer player = null; - private static Message currentlyPlayingMessage = null; - private static PowerManager.WakeLock wakeLock; - private final MessageAdapter messageAdapter; - private final WeakReferenceSet audioPlayerLayouts = new WeakReferenceSet<>(); - private final SensorManager sensorManager; - private final Sensor proximitySensor; - private final PendingItem> pendingOnClickView = new PendingItem<>(); - private final ExecutorService executor = Executors.newSingleThreadExecutor(); - private boolean isEarpieceBefore = false; - - private final Handler handler = new Handler(); - - public AudioPlayer(MessageAdapter adapter) { - final Context context = adapter.getContext(); - this.messageAdapter = adapter; - this.sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); - this.proximitySensor = this.sensorManager == null ? null : this.sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); - initializeProximityWakeLock(context); - synchronized (AudioPlayer.LOCK) { - if (AudioPlayer.player != null) { - AudioPlayer.player.setOnCompletionListener(this); - if (AudioPlayer.player.isPlaying() && sensorManager != null) { - sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_FASTEST); - } - } - } - } - - private static String formatTime(int ms) { - return String.format(Locale.ENGLISH, "%d:%02d", ms / 60000, Math.min(Math.round((ms % 60000) / 1000f), 59)); - } - - private void initializeProximityWakeLock(Context context) { - if (Build.VERSION.SDK_INT >= 21) { - synchronized (AudioPlayer.LOCK) { - if (AudioPlayer.wakeLock == null) { - final PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - AudioPlayer.wakeLock = powerManager == null ? null : powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, AudioPlayer.class.getSimpleName()); - AudioPlayer.wakeLock.setReferenceCounted(false); - } - } - } else { - AudioPlayer.wakeLock = null; - } - } - - public void init(RelativeLayout audioPlayer, Message message) { - synchronized (AudioPlayer.LOCK) { - audioPlayer.setTag(message); - if (init(ViewHolder.get(audioPlayer), message)) { - this.audioPlayerLayouts.addWeakReferenceTo(audioPlayer); - executor.execute(() -> this.stopRefresher(true)); - } else { - this.audioPlayerLayouts.removeWeakReferenceTo(audioPlayer); - } - } - } - - private boolean init(ViewHolder viewHolder, Message message) { - messageAdapter.getActivity().setVolumeControlStream(AudioManager.STREAM_MUSIC); - viewHolder.progress.setOnSeekBarChangeListener(this); - ColorStateList color = ThemeHelper.AudioPlayerColor(messageAdapter.getContext()); - viewHolder.progress.setThumbTintList(color); - viewHolder.progress.setProgressTintList(color); - viewHolder.playPause.setAlpha(viewHolder.darkBackground ? 0.7f : 0.57f); - viewHolder.playPause.setOnClickListener(this); - if (message == currentlyPlayingMessage) { - if (AudioPlayer.player != null && AudioPlayer.player.isPlaying()) { - viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp); - viewHolder.progress.setEnabled(true); - } else { - viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp); - viewHolder.progress.setEnabled(false); - } - return true; - } else { - viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp); - viewHolder.runtime.setText(formatTime(message.getFileParams().runtime)); - viewHolder.progress.setProgress(0); - viewHolder.progress.setEnabled(false); - return false; - } - } - - @Override - public synchronized void onClick(View v) { - if (v.getId() == R.id.play_pause) { - synchronized (LOCK) { - startStop((ImageButton) v); - } - } - } - - private void startStop(ImageButton playPause) { - if (ContextCompat.checkSelfPermission(messageAdapter.getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(messageAdapter.getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - pendingOnClickView.push(new WeakReference<>(playPause)); - ActivityCompat.requestPermissions(messageAdapter.getActivity(), new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, ConversationsActivity.REQUEST_PLAY_PAUSE); - return; - } - initializeProximityWakeLock(playPause.getContext()); - final RelativeLayout audioPlayer = (RelativeLayout) playPause.getParent().getParent(); - final ViewHolder viewHolder = ViewHolder.get(audioPlayer); - final Message message = (Message) audioPlayer.getTag(); - if (startStop(viewHolder, message)) { - this.audioPlayerLayouts.clear(); - this.audioPlayerLayouts.addWeakReferenceTo(audioPlayer); - stopRefresher(true); - } - } - - private boolean playPauseCurrent(ViewHolder viewHolder) { - viewHolder.playPause.setAlpha(viewHolder.darkBackground ? 0.7f : 0.57f); - if (player.isPlaying()) { - viewHolder.progress.setEnabled(false); - player.pause(); - releaseAudioFocus(); - messageAdapter.flagScreenOff(); - releaseProximityWakeLock(); - viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp); - } else { - viewHolder.progress.setEnabled(true); - requestAudioFocus(); - player.start(); - messageAdapter.flagScreenOn(); - acquireProximityWakeLock(); - this.stopRefresher(true); - viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp); - } - return false; - } - - private void play(ViewHolder viewHolder, Message message, boolean earpiece, double progress) { - if (play(viewHolder, message, earpiece)) { - if (messageAdapter.autoPauseVoice() && (isEarpieceBefore && !earpiece)) { - playPauseCurrent(viewHolder); - } - AudioPlayer.player.seekTo((int) (AudioPlayer.player.getDuration() * progress)); - isEarpieceBefore = earpiece; - } - } - - private boolean play(ViewHolder viewHolder, Message message, boolean earpiece) { - AudioPlayer.player = new MediaPlayer(); - try { - AudioPlayer.currentlyPlayingMessage = message; - AudioPlayer.player.setAudioStreamType(earpiece ? AudioManager.STREAM_VOICE_CALL : AudioManager.STREAM_MUSIC); - AudioPlayer.player.setDataSource(messageAdapter.getFileBackend().getFile(message).getAbsolutePath()); - AudioPlayer.player.setOnCompletionListener(this); - AudioPlayer.player.prepare(); - requestAudioFocus(); - AudioPlayer.player.start(); - messageAdapter.flagScreenOn(); - acquireProximityWakeLock(); - viewHolder.progress.setEnabled(true); - viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_pause_white_36dp : R.drawable.ic_pause_black_36dp); - sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_FASTEST); - return true; - } catch (Exception e) { - messageAdapter.flagScreenOff(); - releaseProximityWakeLock(); - AudioPlayer.currentlyPlayingMessage = null; - sensorManager.unregisterListener(this); - return false; - } - } - - public void startStopPending() { - WeakReference reference = pendingOnClickView.pop(); - if (reference != null) { - ImageButton imageButton = reference.get(); - if (imageButton != null) { - startStop(imageButton); - } - } - } - - private boolean startStop(ViewHolder viewHolder, Message message) { - if (message == currentlyPlayingMessage && player != null) { - return playPauseCurrent(viewHolder); - } - if (AudioPlayer.player != null) { - stopCurrent(); - } - return play(viewHolder, message, false); - } - - private void stopCurrent() { - if (AudioPlayer.player.isPlaying()) { - AudioPlayer.player.stop(); - } - releaseAudioFocus(); - AudioPlayer.player.release(); - messageAdapter.flagScreenOff(); - releaseProximityWakeLock(); - AudioPlayer.player = null; - resetPlayerUi(); - } - - private void resetPlayerUi() { - for (WeakReference audioPlayer : audioPlayerLayouts) { - resetPlayerUi(audioPlayer.get()); - } - } - - private void resetPlayerUi(RelativeLayout audioPlayer) { - if (audioPlayer == null) { - return; - } - final ViewHolder viewHolder = ViewHolder.get(audioPlayer); - final Message message = (Message) audioPlayer.getTag(); - viewHolder.playPause.setImageResource(viewHolder.darkBackground ? R.drawable.ic_play_arrow_white_36dp : R.drawable.ic_play_arrow_black_36dp); - if (message != null) { - viewHolder.runtime.setText(formatTime(message.getFileParams().runtime)); - } - viewHolder.progress.setProgress(0); - viewHolder.progress.setEnabled(false); - } - - @Override - public void onCompletion(android.media.MediaPlayer mediaPlayer) { - synchronized (AudioPlayer.LOCK) { - this.stopRefresher(false); - if (AudioPlayer.player == mediaPlayer) { - AudioPlayer.currentlyPlayingMessage = null; - AudioPlayer.player = null; - } - mediaPlayer.release(); - messageAdapter.flagScreenOff(); - releaseProximityWakeLock(); - resetPlayerUi(); - sensorManager.unregisterListener(this); - } - } - - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - synchronized (AudioPlayer.LOCK) { - final RelativeLayout audioPlayer = (RelativeLayout) seekBar.getParent(); - final Message message = (Message) audioPlayer.getTag(); - if (fromUser && message == AudioPlayer.currentlyPlayingMessage) { - float percent = progress / 100f; - int duration = AudioPlayer.player.getDuration(); - int seekTo = Math.round(duration * percent); - AudioPlayer.player.seekTo(seekTo); - } - } - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - } - - public void stop() { - synchronized (AudioPlayer.LOCK) { - stopRefresher(false); - if (AudioPlayer.player != null) { - stopCurrent(); - } - AudioPlayer.currentlyPlayingMessage = null; - sensorManager.unregisterListener(this); - if (wakeLock != null && wakeLock.isHeld()) { - wakeLock.release(); - } - wakeLock = null; - } - } - - private void stopRefresher(boolean runOnceMore) { - this.handler.removeCallbacks(this); - if (runOnceMore) { - this.handler.post(this); - } - } - - public void unregisterListener() { - if (sensorManager != null) { - sensorManager.unregisterListener(this); - } - } - - @Override - public void run() { - synchronized (AudioPlayer.LOCK) { - if (AudioPlayer.player != null) { - boolean renew = false; - final int current = player.getCurrentPosition(); - final int duration = player.getDuration(); - for (WeakReference audioPlayer : audioPlayerLayouts) { - renew |= refreshAudioPlayer(audioPlayer.get(), current, duration); - } - if (renew && AudioPlayer.player.isPlaying()) { - handler.postDelayed(this, REFRESH_INTERVAL); - } - } - } - } - - private boolean refreshAudioPlayer(RelativeLayout audioPlayer, int current, int duration) { - if (audioPlayer == null || audioPlayer.getVisibility() != View.VISIBLE) { - return false; - } - final ViewHolder viewHolder = ViewHolder.get(audioPlayer); - if (duration <= 0) { - viewHolder.progress.setProgress(100); - } else { - viewHolder.progress.setProgress(current * 100 / duration); - } - viewHolder.runtime.setText(String.format("%s / %s", formatTime(current), formatTime(duration))); - return true; - } - - @Override - public void onSensorChanged(SensorEvent event) { - if (event.sensor.getType() != Sensor.TYPE_PROXIMITY) { - return; - } - if (AudioPlayer.player == null || !AudioPlayer.player.isPlaying()) { - return; - } - final int streamType; - if (event.values[0] < proximitySensor.getMaximumRange()) { - streamType = AudioManager.STREAM_VOICE_CALL; - } else { - streamType = AudioManager.STREAM_MUSIC; - } - messageAdapter.setVolumeControl(streamType); - double position = AudioPlayer.player.getCurrentPosition(); - double duration = AudioPlayer.player.getDuration(); - double progress = position / duration; - if (AudioPlayer.player.getAudioStreamType() != streamType) { - synchronized (AudioPlayer.LOCK) { - AudioPlayer.player.stop(); - releaseAudioFocus(); - AudioPlayer.player.release(); - AudioPlayer.player = null; - try { - ViewHolder currentViewHolder = getCurrentViewHolder(); - if (currentViewHolder != null) { - messageAdapter.getActivity().setVolumeControlStream(streamType); - play(currentViewHolder, currentlyPlayingMessage, streamType == AudioManager.STREAM_VOICE_CALL, progress); - } - } catch (Exception e) { - Log.d(Config.LOGTAG, "AudioPlayer Exception: " + e); - } - } - } - } - - @Override - public void onAccuracyChanged(Sensor sensor, int i) { - } - - private void acquireProximityWakeLock() { - synchronized (AudioPlayer.LOCK) { - if (wakeLock != null) { - wakeLock.acquire(); - } - } - } - - private void releaseProximityWakeLock() { - synchronized (AudioPlayer.LOCK) { - if (wakeLock != null && wakeLock.isHeld()) { - wakeLock.release(); - } - } - messageAdapter.setVolumeControl(AudioManager.STREAM_MUSIC); - } - - private ViewHolder getCurrentViewHolder() { - for (WeakReference audioPlayer : audioPlayerLayouts) { - final Message message = (Message) audioPlayer.get().getTag(); - if (message == currentlyPlayingMessage) { - return ViewHolder.get(audioPlayer.get()); - } - } - return null; - } - - @Override - public void onAudioFocusChange(int focusChange) { - if (focusChange == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - Log.i(Config.LOGTAG, "Audio focus granted."); - } else if (focusChange == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { - Log.i(Config.LOGTAG, "Audio focus failed."); - } - } - - public static class ViewHolder { - private TextView runtime; - private SeekBar progress; - private ImageButton playPause; - private boolean darkBackground = false; - - public static ViewHolder get(RelativeLayout audioPlayer) { - ViewHolder viewHolder = (ViewHolder) audioPlayer.getTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER); - if (viewHolder == null) { - viewHolder = new ViewHolder(); - viewHolder.runtime = audioPlayer.findViewById(R.id.runtime); - viewHolder.progress = audioPlayer.findViewById(R.id.progress); - viewHolder.playPause = audioPlayer.findViewById(R.id.play_pause); - audioPlayer.setTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER, viewHolder); - } - return viewHolder; - } - - public void setTheme(boolean darkBackground) { - this.darkBackground = darkBackground; - } - } - - private void releaseAudioFocus() { - AudioManager am = (AudioManager) messageAdapter.getActivity().getSystemService(Context.AUDIO_SERVICE); - if (am != null) { - am.abandonAudioFocus(this); - } - } - - private void requestAudioFocus() { - AudioManager am = (AudioManager) messageAdapter.getActivity().getSystemService(Context.AUDIO_SERVICE); - if (am != null) { - am.requestAudioFocus(this, - AudioManager.STREAM_MUSIC, - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java deleted file mode 100644 index c3ccceb8e..000000000 --- a/src/main/java/eu/siacs/conversations/services/AvatarService.java +++ /dev/null @@ -1,682 +0,0 @@ -package eu.siacs.conversations.services; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffXfermode; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.Typeface; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.text.TextUtils; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.LruCache; - -import androidx.annotation.ColorInt; -import androidx.annotation.Nullable; -import androidx.core.content.res.ResourcesCompat; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Bookmark; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Conversational; -import eu.siacs.conversations.entities.ListItem; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.entities.RawBlockable; -import eu.siacs.conversations.entities.Room; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; -import eu.siacs.conversations.xmpp.XmppConnection; - -import static eu.siacs.conversations.ui.SettingsActivity.PREFER_XMPP_AVATAR; - -public class AvatarService implements OnAdvancedStreamFeaturesLoaded { - - private static final int FG_COLOR = 0xFFFAFAFA; - private static final int TRANSPARENT = 0x00000000; - private static final int PLACEHOLDER_COLOR = 0xFF202020; - - public static final int SYSTEM_UI_AVATAR_SIZE = 48; - - private static final String PREFIX_CONTACT = "contact"; - private static final String PREFIX_CONVERSATION = "conversation"; - private static final String PREFIX_ACCOUNT = "account"; - private static final String PREFIX_GENERIC = "generic"; - private static final String CHANNEL_SYMBOL = "#"; - - final private Set sizes = new HashSet<>(); - final private HashMap> conversationDependentKeys = new HashMap<>(); - - protected XmppConnectionService mXmppConnectionService = null; - - AvatarService(XmppConnectionService service) { - this.mXmppConnectionService = service; - } - - public static int getSystemUiAvatarSize(final Context context) { - return (int) (SYSTEM_UI_AVATAR_SIZE * context.getResources().getDisplayMetrics().density); - } - - public Bitmap get(final Avatarable avatarable, final int size, final boolean cachedOnly) { - if (avatarable instanceof Account) { - return get((Account) avatarable, size, cachedOnly); - } else if (avatarable instanceof Conversation) { - return get((Conversation) avatarable, size, cachedOnly); - } else if (avatarable instanceof Message) { - return get((Message) avatarable, size, cachedOnly); - } else if (avatarable instanceof ListItem) { - return get((ListItem) avatarable, size, cachedOnly); - } else if (avatarable instanceof MucOptions.User) { - return get((MucOptions.User) avatarable, size, cachedOnly); - } else if (avatarable instanceof Room) { - return get((Room) avatarable, size, cachedOnly); - } - throw new AssertionError("AvatarService does not know how to generate avatar from " + avatarable.getClass().getName()); - } - - private Bitmap get(final Room result, final int size, boolean cacheOnly) { - final Jid room = result.getRoom(); - Conversation conversation = room != null ? mXmppConnectionService.findFirstMuc(room) : null; - if (conversation != null) { - return get(conversation, size, cacheOnly); - } - return get(CHANNEL_SYMBOL, room != null ? room.asBareJid().toEscapedString() : result.getName(), size, cacheOnly); - } - - private Bitmap get(final Contact contact, final int size, boolean cachedOnly) { - if (contact.isSelf()) { - return get(contact.getAccount(), size, cachedOnly); - } - final String KEY = key(contact, size); - Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY); - if (avatar != null || cachedOnly) { - return avatar; - } - if (contact.getAvatarFilename() != null && QuickConversationsService.isQuicksy() && mXmppConnectionService.getPreferences().getBoolean(PREFER_XMPP_AVATAR, mXmppConnectionService.getResources().getBoolean(R.bool.prefer_xmpp_avatar))) { - avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatarFilename(), size); - } - if (avatar == null && contact.getAvatarFilename() != null && mXmppConnectionService.getPreferences().getBoolean(PREFER_XMPP_AVATAR, mXmppConnectionService.getResources().getBoolean(R.bool.prefer_xmpp_avatar))) { - avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatarFilename(), size); - } - if (avatar == null && contact.getProfilePhoto() != null) { - avatar = mXmppConnectionService.getFileBackend().cropCenterSquare(Uri.parse(contact.getProfilePhoto()), size); - } - if (avatar == null) { - avatar = get(contact.getDisplayName(), contact.getJid().asBareJid().toString(), size, false); - } - this.mXmppConnectionService.getBitmapCache().put(KEY, avatar); - return avatar; - } - - public Bitmap getRoundedShortcut(final Contact contact) { - return getRoundedShortcut(contact, false); - } - - public Bitmap getRoundedShortcutWithIcon(final Contact contact) { - return getRoundedShortcut(contact, true); - } - - private Bitmap getRoundedShortcut(final Contact contact, boolean withIcon) { - DisplayMetrics metrics = mXmppConnectionService.getResources().getDisplayMetrics(); - int size = Math.round(metrics.density * 48); - Bitmap bitmap = get(contact, size); - Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(output); - final Paint paint = new Paint(); - - drawAvatar(bitmap, canvas, paint); - if (withIcon) { - drawIcon(canvas, paint); - } - return output; - } - - private void drawAvatar(Bitmap bitmap, Canvas canvas, Paint paint) { - final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); - final RectF rectF = new RectF(rect); - paint.setAntiAlias(true); - canvas.drawARGB(0, 0, 0, 0); - canvas.drawRoundRect(rectF, 5, 5, paint); - paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); - canvas.drawBitmap(bitmap, rect, rect, paint); - } - - private void drawIcon(Canvas canvas, Paint paint) { - final Resources resources = mXmppConnectionService.getResources(); - final Bitmap icon = getRoundLauncherIcon(resources); - if (icon == null) { - return; - } - paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)); - - int iconSize = Math.round(canvas.getHeight() / 2.6f); - - int left = canvas.getWidth() - iconSize; - int top = canvas.getHeight() - iconSize; - final Rect rect = new Rect(left, top, left + iconSize, top + iconSize); - canvas.drawBitmap(icon, null, rect, paint); - } - - private static Bitmap getRoundLauncherIcon(Resources resources) { - - final Drawable drawable = ResourcesCompat.getDrawable(resources, R.mipmap.ic_launcher, null); - if (drawable == null) { - return null; - } - - if (drawable instanceof BitmapDrawable) { - return ((BitmapDrawable) drawable).getBitmap(); - } - - Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - drawable.draw(canvas); - - return bitmap; - } - - public Bitmap get(final MucOptions.User user, final int size, boolean cachedOnly) { - Contact c = user.getContact(); - if (c != null && (c.getProfilePhoto() != null || c.getAvatarFilename() != null || user.getAvatar() == null)) { - return get(c, size, cachedOnly); - } else { - return getImpl(user, size, cachedOnly); - } - } - - private Bitmap getImpl(final MucOptions.User user, final int size, boolean cachedOnly) { - final String KEY = key(user, size); - Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY); - if (avatar != null || cachedOnly) { - return avatar; - } - if (user.getAvatar() != null) { - avatar = mXmppConnectionService.getFileBackend().getAvatar(user.getAvatar(), size); - } - if (avatar == null) { - Contact contact = user.getContact(); - if (contact != null) { - avatar = get(contact, size, false); - } else { - String seed = user.getRealJid() != null ? user.getRealJid().asBareJid().toString() : null; - avatar = get(user.getName(), seed, size, false); - } - } - this.mXmppConnectionService.getBitmapCache().put(KEY, avatar); - return avatar; - } - - public void clear(Contact contact) { - synchronized (this.sizes) { - for (final Integer size : sizes) { - this.mXmppConnectionService.getBitmapCache().remove(key(contact, size)); - } - } - for (Conversation conversation : mXmppConnectionService.findAllConferencesWith(contact)) { - MucOptions.User user = conversation.getMucOptions().findUserByRealJid(contact.getJid().asBareJid()); - if (user != null) { - clear(user); - } - clear(conversation); - } - } - - private String key(Contact contact, int size) { - synchronized (this.sizes) { - this.sizes.add(size); - } - return PREFIX_CONTACT + - '\0' + - contact.getAccount().getJid().asBareJid() + - '\0' + - emptyOnNull(contact.getJid()) + - '\0' + - size; - } - - private String key(MucOptions.User user, int size) { - synchronized (this.sizes) { - this.sizes.add(size); - } - return PREFIX_CONTACT + - '\0' + - user.getAccount().getJid().asBareJid() + - '\0' + - emptyOnNull(user.getFullJid()) + - '\0' + - emptyOnNull(user.getRealJid()) + - '\0' + - size; - } - - public Bitmap get(ListItem item, int size) { - return get(item, size, false); - } - - public Bitmap get(ListItem item, int size, boolean cachedOnly) { - if (item instanceof RawBlockable) { - return get(item.getDisplayName(), item.getJid().toEscapedString(), size, cachedOnly); - } else if (item instanceof Contact) { - return get((Contact) item, size, cachedOnly); - } else if (item instanceof Bookmark) { - Bookmark bookmark = (Bookmark) item; - if (bookmark.getConversation() != null) { - return get(bookmark.getConversation(), size, cachedOnly); - } else { - Jid jid = bookmark.getJid(); - Account account = bookmark.getAccount(); - Contact contact = jid == null ? null : account.getRoster().getContact(jid); - if (contact != null && contact.getAvatarFilename() != null) { - return get(contact, size, cachedOnly); - } - String seed = jid != null ? jid.asBareJid().toString() : null; - return get(bookmark.getDisplayName(), seed, size, cachedOnly); - } - } else { - String seed = item.getJid() != null ? item.getJid().asBareJid().toString() : null; - return get(item.getDisplayName(), seed, size, cachedOnly); - } - } - - public Bitmap get(Conversation conversation, int size) { - return get(conversation, size, false); - } - - public Bitmap get(Conversation conversation, int size, boolean cachedOnly) { - if (conversation.getMode() == Conversation.MODE_SINGLE) { - return get(conversation.getContact(), size, cachedOnly); - } else { - return get(conversation.getMucOptions(), size, cachedOnly); - } - } - - public void clear(Conversation conversation) { - if (conversation.getMode() == Conversation.MODE_SINGLE) { - clear(conversation.getContact()); - } else { - clear(conversation.getMucOptions()); - synchronized (this.conversationDependentKeys) { - Set keys = this.conversationDependentKeys.get(conversation.getUuid()); - if (keys == null) { - return; - } - LruCache cache = this.mXmppConnectionService.getBitmapCache(); - for (String key : keys) { - cache.remove(key); - } - keys.clear(); - } - } - } - - private Bitmap get(MucOptions mucOptions, int size, boolean cachedOnly) { - final String KEY = key(mucOptions, size); - Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY); - if (bitmap != null || cachedOnly) { - return bitmap; - } - - bitmap = mXmppConnectionService.getFileBackend().getAvatar(mucOptions.getAvatar(), size); - - if (bitmap == null) { - Conversation c = mucOptions.getConversation(); - if (mucOptions.isPrivateAndNonAnonymous()) { - final List users = mucOptions.getUsersRelevantForNameAndAvatar(); - if (users.size() == 0) { - bitmap = getImpl(c.getName().toString(), c.getJid().asBareJid().toString(), size); - } else { - bitmap = getImpl(users, size); - } - } else { - bitmap = getImpl(CHANNEL_SYMBOL, c.getJid().asBareJid().toString(), size); - } - } - - this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap); - - return bitmap; - } - - private Bitmap get(List users, int size, boolean cachedOnly) { - final String KEY = key(users, size); - Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY); - if (bitmap != null || cachedOnly) { - return bitmap; - } - bitmap = getImpl(users, size); - this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap); - return bitmap; - } - - private Bitmap getImpl(List users, int size) { - int count = users.size(); - Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - bitmap.eraseColor(TRANSPARENT); - if (count == 0) { - throw new AssertionError("Unable to draw tiles for 0 users"); - } else if (count == 1) { - drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size); - drawTile(canvas, users.get(0).getAccount(), size / 2 + 1, 0, size, size); - } else if (count == 2) { - drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size); - drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size); - } else if (count == 3) { - drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size); - drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size / 2 - 1); - drawTile(canvas, users.get(2), size / 2 + 1, size / 2 + 1, size, - size); - } else if (count == 4) { - drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1); - drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size); - drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1); - drawTile(canvas, users.get(3), size / 2 + 1, size / 2 + 1, size, - size); - } else { - drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1); - drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size); - drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1); - drawTile(canvas, "\u2026", PLACEHOLDER_COLOR, size / 2 + 1, size / 2 + 1, - size, size); - } - return bitmap; - } - - public void clear(final MucOptions options) { - if (options == null) { - return; - } - synchronized (this.sizes) { - for (Integer size : sizes) { - this.mXmppConnectionService.getBitmapCache().remove(key(options, size)); - } - } - } - - private String key(final MucOptions options, int size) { - synchronized (this.sizes) { - this.sizes.add(size); - } - return PREFIX_CONVERSATION + "_" + options.getConversation().getUuid() + "_" + size; - } - - private String key(List users, int size) { - final Conversation conversation = users.get(0).getConversation(); - StringBuilder builder = new StringBuilder("TILE_"); - builder.append(conversation.getUuid()); - - for (MucOptions.User user : users) { - builder.append("\0"); - builder.append(emptyOnNull(user.getRealJid())); - builder.append("\0"); - builder.append(emptyOnNull(user.getFullJid())); - } - builder.append('\0'); - builder.append(size); - final String key = builder.toString(); - synchronized (this.conversationDependentKeys) { - Set keys; - if (this.conversationDependentKeys.containsKey(conversation.getUuid())) { - keys = this.conversationDependentKeys.get(conversation.getUuid()); - } else { - keys = new HashSet<>(); - this.conversationDependentKeys.put(conversation.getUuid(), keys); - } - keys.add(key); - } - return key; - } - - public Bitmap get(Account account, int size) { - return get(account, size, false); - } - - public Bitmap get(Account account, int size, boolean cachedOnly) { - final String KEY = key(account, size); - Bitmap avatar = mXmppConnectionService.getBitmapCache().get(KEY); - if (avatar != null || cachedOnly) { - return avatar; - } - avatar = mXmppConnectionService.getFileBackend().getAvatar(account.getAvatar(), size); - if (avatar == null) { - final String displayName = account.getDisplayName(); - final String jid = account.getJid().asBareJid().toEscapedString(); - if (QuickConversationsService.isQuicksy() && !TextUtils.isEmpty(displayName)) { - avatar = get(displayName, jid, size, false); - } else { - avatar = get(jid, null, size, false); - } - } - mXmppConnectionService.getBitmapCache().put(KEY, avatar); - return avatar; - } - - public Bitmap get(Message message, int size, boolean cachedOnly) { - final Conversational conversation = message.getConversation(); - if (message.getType() == Message.TYPE_STATUS && message.getCounterparts() != null && message.getCounterparts().size() > 1) { - return get(message.getCounterparts(), size, cachedOnly); - } else if (message.getStatus() == Message.STATUS_RECEIVED) { - Contact c = message.getContact(); - if (c != null && (c.getProfilePhoto() != null || c.getAvatarFilename() != null)) { - return get(c, size, cachedOnly); - } else if (conversation instanceof Conversation && message.getConversation().getMode() == Conversation.MODE_MULTI) { - final Jid trueCounterpart = message.getTrueCounterpart(); - final MucOptions mucOptions = ((Conversation) conversation).getMucOptions(); - MucOptions.User user; - if (trueCounterpart != null) { - user = mucOptions.findOrCreateUserByRealJid(trueCounterpart, message.getCounterpart()); - } else { - user = mucOptions.findUserByFullJid(message.getCounterpart()); - } - if (user != null) { - return getImpl(user, size, cachedOnly); - } - } else if (c != null) { - return get(c, size, cachedOnly); - } - Jid tcp = message.getTrueCounterpart(); - String seed = tcp != null ? tcp.asBareJid().toString() : null; - return get(UIHelper.getMessageDisplayName(message), seed, size, cachedOnly); - } else { - return get(conversation.getAccount(), size, cachedOnly); - } - } - - public void clear(Account account) { - synchronized (this.sizes) { - for (Integer size : sizes) { - this.mXmppConnectionService.getBitmapCache().remove(key(account, size)); - } - } - } - - public void clear(MucOptions.User user) { - synchronized (this.sizes) { - for (Integer size : sizes) { - this.mXmppConnectionService.getBitmapCache().remove(key(user, size)); - } - } - } - - private String key(Account account, int size) { - synchronized (this.sizes) { - this.sizes.add(size); - } - return PREFIX_ACCOUNT + "_" + account.getUuid() + "_" - + String.valueOf(size); - } - - /*public Bitmap get(String name, int size) { - return get(name,null, size,false); - }*/ - - public Bitmap get(final String name, String seed, final int size, boolean cachedOnly) { - final String KEY = key(seed == null ? name : name + "\0" + seed, size); - Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY); - if (bitmap != null || cachedOnly) { - return bitmap; - } - bitmap = getImpl(name, seed, size); - mXmppConnectionService.getBitmapCache().put(KEY, bitmap); - return bitmap; - } - - public static Bitmap get(final Jid jid, final int size) { - return getImpl(jid.asBareJid().toEscapedString(), null, size); - } - - private static Bitmap getImpl(final String name, final String seed, final int size) { - Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - final String trimmedName = name == null ? "" : name.trim(); - drawTile(canvas, trimmedName, seed, 0, 0, size, size); - return bitmap; - } - - private String key(String name, int size) { - synchronized (this.sizes) { - this.sizes.add(size); - } - return PREFIX_GENERIC + "_" + name + "_" + size; - } - - private static boolean drawTile(Canvas canvas, String letter, int tileColor, int left, int top, int right, int bottom) { - letter = letter.toUpperCase(Locale.getDefault()); - Paint tilePaint = new Paint(), textPaint = new Paint(); - tilePaint.setColor(tileColor); - textPaint.setFlags(Paint.ANTI_ALIAS_FLAG); - textPaint.setColor(FG_COLOR); - textPaint.setTypeface(Typeface.create("sans-serif-light", - Typeface.NORMAL)); - textPaint.setTextSize((float) ((right - left) * 0.8)); - Rect rect = new Rect(); - - canvas.drawRect(new Rect(left, top, right, bottom), tilePaint); - textPaint.getTextBounds(letter, 0, 1, rect); - float width = textPaint.measureText(letter); - canvas.drawText(letter, (right + left) / 2 - width / 2, (top + bottom) - / 2 + rect.height() / 2, textPaint); - return true; - } - - private boolean drawTile(Canvas canvas, MucOptions.User user, int left, int top, int right, int bottom) { - Contact contact = user.getContact(); - if (contact != null) { - Uri uri = null; - if (contact.getAvatarFilename() != null && QuickConversationsService.isQuicksy() && mXmppConnectionService.getPreferences().getBoolean(PREFER_XMPP_AVATAR, mXmppConnectionService.getResources().getBoolean(R.bool.prefer_xmpp_avatar))) { - uri = mXmppConnectionService.getFileBackend().getAvatarUri(contact.getAvatarFilename()); - } else if (contact.getAvatarFilename() != null && mXmppConnectionService.getPreferences().getBoolean(PREFER_XMPP_AVATAR, mXmppConnectionService.getResources().getBoolean(R.bool.prefer_xmpp_avatar))) { - uri = mXmppConnectionService.getFileBackend().getAvatarUri(contact.getAvatarFilename()); - } else if (contact.getProfilePhoto() != null) { - uri = Uri.parse(contact.getProfilePhoto()); - } - if (drawTile(canvas, uri, left, top, right, bottom)) { - return true; - } - } else if (user.getAvatar() != null) { - Uri uri = mXmppConnectionService.getFileBackend().getAvatarUri(user.getAvatar()); - if (drawTile(canvas, uri, left, top, right, bottom)) { - return true; - } - } - if (contact != null) { - String seed = contact.getJid().asBareJid().toString(); - drawTile(canvas, contact.getDisplayName(), seed, left, top, right, bottom); - } else { - String seed = user.getRealJid() == null ? null : user.getRealJid().asBareJid().toString(); - drawTile(canvas, user.getName(), seed, left, top, right, bottom); - } - return true; - } - - private boolean drawTile(Canvas canvas, Account account, int left, int top, int right, int bottom) { - String avatar = account.getAvatar(); - if (avatar != null) { - Uri uri = mXmppConnectionService.getFileBackend().getAvatarUri(avatar); - if (uri != null) { - if (drawTile(canvas, uri, left, top, right, bottom)) { - return true; - } - } - } - String name = account.getJid().asBareJid().toString(); - return drawTile(canvas, name, name, left, top, right, bottom); - } - - private static boolean drawTile(Canvas canvas, String name, String seed, int left, int top, int right, int bottom) { - if (name != null) { - final String letter = name.equals(CHANNEL_SYMBOL) ? name : getFirstLetter(name); - final int color = UIHelper.getColorForName(seed == null ? name : seed); - drawTile(canvas, letter, color, left, top, right, bottom); - return true; - } - return false; - } - - private static String getFirstLetter(String name) { - for (Character c : name.toCharArray()) { - if (Character.isLetterOrDigit(c)) { - return c.toString(); - } - } - return "X"; - } - - private boolean drawTile(Canvas canvas, Uri uri, int left, int top, int right, int bottom) { - if (uri != null) { - Bitmap bitmap = mXmppConnectionService.getFileBackend() - .cropCenter(uri, bottom - top, right - left); - if (bitmap != null) { - drawTile(canvas, bitmap, left, top, right, bottom); - return true; - } - } - return false; - } - - private boolean drawTile(Canvas canvas, Bitmap bm, int dstleft, int dsttop, int dstright, int dstbottom) { - Rect dst = new Rect(dstleft, dsttop, dstright, dstbottom); - canvas.drawBitmap(bm, null, dst, null); - return true; - } - - @Override - public void onAdvancedStreamFeaturesAvailable(Account account) { - XmppConnection.Features features = account.getXmppConnection().getFeatures(); - if (features.pep() && !features.pepPersistent()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": has pep but is not persistent"); - if (account.getAvatar() != null) { - mXmppConnectionService.republishAvatarIfNeeded(account); - } - } - } - - private static String emptyOnNull(@Nullable Jid value) { - return value == null ? "" : value.toString(); - } - - public interface Avatarable { - @ColorInt - int getAvatarBackgroundColor(); - - String getAvatarName(); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/BarcodeProvider.java b/src/main/java/eu/siacs/conversations/services/BarcodeProvider.java deleted file mode 100644 index 58f391dbd..000000000 --- a/src/main/java/eu/siacs/conversations/services/BarcodeProvider.java +++ /dev/null @@ -1,210 +0,0 @@ -package eu.siacs.conversations.services; - -import android.content.ComponentName; -import android.content.ContentProvider; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.net.Uri; -import android.os.CancellationSignal; -import android.os.IBinder; -import android.os.ParcelFileDescriptor; -import android.util.Log; - -import androidx.annotation.Nullable; - -import com.google.zxing.BarcodeFormat; -import com.google.zxing.EncodeHintType; -import com.google.zxing.common.BitMatrix; -import com.google.zxing.qrcode.QRCodeWriter; -import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.OutputStream; -import java.util.Hashtable; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.xmpp.Jid; - -public class BarcodeProvider extends ContentProvider implements ServiceConnection { - - private static final String AUTHORITY = ".barcodes"; - - private final Object lock = new Object(); - - private XmppConnectionService mXmppConnectionService; - private boolean mBindingInProcess = false; - - public static Uri getUriForAccount(Context context, Account account) { - final String packageId = context.getPackageName(); - return Uri.parse("content://" + packageId + AUTHORITY + "/" + account.getJid().asBareJid() + ".png"); - } - - public static Bitmap create2dBarcodeBitmap(String input, int size) { - try { - final QRCodeWriter barcodeWriter = new QRCodeWriter(); - final Hashtable hints = new Hashtable<>(); - hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); - hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); - final BitMatrix result = barcodeWriter.encode(input, BarcodeFormat.QR_CODE, size, size, hints); - final int width = result.getWidth(); - final int height = result.getHeight(); - final int[] pixels = new int[width * height]; - for (int y = 0; y < height; y++) { - final int offset = y * width; - for (int x = 0; x < width; x++) { - pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.WHITE; - } - } - final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - bitmap.setPixels(pixels, 0, width, 0, 0, width, height); - return bitmap; - } catch (final Exception e) { - e.printStackTrace(); - return null; - } - } - - @Override - public boolean onCreate() { - File barcodeDirectory = new File(getContext().getCacheDir().getAbsolutePath() + "/barcodes/"); - if (barcodeDirectory.exists() && barcodeDirectory.isDirectory()) { - for (File file : barcodeDirectory.listFiles()) { - if (file.isFile() && !file.isHidden()) { - if (file.delete()) { - Log.d(Config.LOGTAG, "deleted old barcode file " + file.getAbsolutePath()); - } - } - } - } - return true; - } - - @Nullable - @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { - return null; - } - - @Nullable - @Override - public String getType(Uri uri) { - return "image/png"; - } - - @Nullable - @Override - public Uri insert(Uri uri, ContentValues values) { - return null; - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - return 0; - } - - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - return 0; - } - - @Override - public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { - return openFile(uri, mode, null); - } - - @Override - public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) throws FileNotFoundException { - Log.d(Config.LOGTAG, "opening file with uri (normal): " + uri.toString()); - String path = uri.getPath(); - if (path != null && path.endsWith(".png") && path.length() >= 5) { - String jid = path.substring(1).substring(0, path.length() - 4); - Log.d(Config.LOGTAG, "account:" + jid); - if (connectAndWait()) { - Log.d(Config.LOGTAG, "connected to background service"); - try { - Account account = mXmppConnectionService.findAccountByJid(Jid.of(jid)); - if (account != null) { - String shareableUri = account.getShareableUri(); - String hash = CryptoHelper.getFingerprint(shareableUri); - File file = new File(getContext().getCacheDir().getAbsolutePath() + "/barcodes/" + hash); - if (!file.exists()) { - file.getParentFile().mkdirs(); - file.createNewFile(); - Bitmap bitmap = create2dBarcodeBitmap(account.getShareableUri(), 1024); - OutputStream outputStream = new FileOutputStream(file); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); - outputStream.close(); - outputStream.flush(); - } - return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); - } - } catch (Exception e) { - throw new FileNotFoundException(); - } - } - } - throw new FileNotFoundException(); - } - - private boolean connectAndWait() { - Intent intent = new Intent(getContext(), XmppConnectionService.class); - intent.setAction(this.getClass().getSimpleName()); - Context context = getContext(); - if (context != null) { - synchronized (this) { - if (mXmppConnectionService == null && !mBindingInProcess) { - Log.d(Config.LOGTAG, "calling to bind service"); - context.bindService(intent, this, Context.BIND_AUTO_CREATE); - this.mBindingInProcess = true; - } - } - try { - waitForService(); - return true; - } catch (InterruptedException e) { - return false; - } - } else { - Log.d(Config.LOGTAG, "context was null"); - return false; - } - } - - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - synchronized (this) { - XmppConnectionService.XmppConnectionBinder binder = (XmppConnectionService.XmppConnectionBinder) service; - mXmppConnectionService = binder.getService(); - mBindingInProcess = false; - synchronized (this.lock) { - lock.notifyAll(); - } - } - } - - @Override - public void onServiceDisconnected(ComponentName name) { - synchronized (this) { - mXmppConnectionService = null; - } - } - - private void waitForService() throws InterruptedException { - if (mXmppConnectionService == null) { - synchronized (this.lock) { - lock.wait(); - } - } else { - Log.d(Config.LOGTAG, "not waiting for service because already initialized"); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java deleted file mode 100644 index d9a965922..000000000 --- a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java +++ /dev/null @@ -1,256 +0,0 @@ -package eu.siacs.conversations.services; - -import android.util.Log; - -import androidx.annotation.NonNull; - -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Room; -import eu.siacs.conversations.http.HttpConnectionManager; -import eu.siacs.conversations.http.services.MuclumbusService; -import eu.siacs.conversations.parser.IqParser; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; -import eu.siacs.conversations.xmpp.XmppConnection; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import okhttp3.OkHttpClient; -import okhttp3.ResponseBody; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; -import retrofit2.Retrofit; -import retrofit2.converter.gson.GsonConverterFactory; - -public class ChannelDiscoveryService { - - private final XmppConnectionService service; - - - private MuclumbusService muclumbusService; - - private final Cache> cache; - - ChannelDiscoveryService(XmppConnectionService service) { - this.service = service; - this.cache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build(); - } - - void initializeMuclumbusService() { - final OkHttpClient.Builder builder = new OkHttpClient.Builder(); - if (service.useTorToConnect() || service.useI2PToConnect()) { - builder.proxy(HttpConnectionManager.getProxy(service.useI2PToConnect())); - } - Retrofit retrofit = new Retrofit.Builder() - .client(builder.build()) - .baseUrl(Config.CHANNEL_DISCOVERY) - .addConverterFactory(GsonConverterFactory.create()) - .callbackExecutor(Executors.newSingleThreadExecutor()) - .build(); - this.muclumbusService = retrofit.create(MuclumbusService.class); - } - - void cleanCache() { - cache.invalidateAll(); - } - - void discover(@NonNull final String query, Method method, OnChannelSearchResultsFound onChannelSearchResultsFound) { - final List result = cache.getIfPresent(key(method, query)); - if (result != null) { - onChannelSearchResultsFound.onChannelSearchResultsFound(result); - return; - } - if (method == Method.LOCAL_SERVER) { - discoverChannelsLocalServers(query, onChannelSearchResultsFound); - } else { - if (query.isEmpty()) { - discoverChannelsJabberNetwork(onChannelSearchResultsFound); - } else { - discoverChannelsJabberNetwork(query, onChannelSearchResultsFound); - } - } - } - - private void discoverChannelsJabberNetwork(OnChannelSearchResultsFound listener) { - Call call = muclumbusService.getRooms(1); - try { - call.enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - final MuclumbusService.Rooms body = response.body(); - if (body == null) { - listener.onChannelSearchResultsFound(Collections.emptyList()); - logError(response); - return; - } - cache.put(key(Method.JABBER_NETWORK, ""), body.items); - listener.onChannelSearchResultsFound(body.items); - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { - Log.d(Config.LOGTAG, "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY, throwable); - listener.onChannelSearchResultsFound(Collections.emptyList()); - } - }); - } catch (Exception e) { - e.printStackTrace(); - } - } - - private void discoverChannelsJabberNetwork(final String query, OnChannelSearchResultsFound listener) { - MuclumbusService.SearchRequest searchRequest = new MuclumbusService.SearchRequest(query); - Call searchResultCall = muclumbusService.search(searchRequest); - - searchResultCall.enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - final MuclumbusService.SearchResult body = response.body(); - if (body == null) { - listener.onChannelSearchResultsFound(Collections.emptyList()); - logError(response); - return; - } - cache.put(key(Method.JABBER_NETWORK, query), body.result.items); - listener.onChannelSearchResultsFound(body.result.items); - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable throwable) { - Log.d(Config.LOGTAG, "Unable to query muclumbus on " + Config.CHANNEL_DISCOVERY, throwable); - listener.onChannelSearchResultsFound(Collections.emptyList()); - } - }); - } - - private void discoverChannelsLocalServers(final String query, final OnChannelSearchResultsFound listener) { - final Map localMucService = getLocalMucServices(); - Log.d(Config.LOGTAG, "checking with " + localMucService.size() + " muc services"); - if (localMucService.size() == 0) { - listener.onChannelSearchResultsFound(Collections.emptyList()); - return; - } - if (!query.isEmpty()) { - final List cached = cache.getIfPresent(key(Method.LOCAL_SERVER, "")); - if (cached != null) { - final List results = copyMatching(cached, query); - cache.put(key(Method.LOCAL_SERVER, query), results); - listener.onChannelSearchResultsFound(results); - } - } - final AtomicInteger queriesInFlight = new AtomicInteger(); - final List rooms = new ArrayList<>(); - for (Map.Entry entry : localMucService.entrySet()) { - IqPacket itemsRequest = service.getIqGenerator().queryDiscoItems(entry.getKey()); - queriesInFlight.incrementAndGet(); - service.sendIqPacket(entry.getValue(), itemsRequest, (account, itemsResponse) -> { - if (itemsResponse.getType() == IqPacket.TYPE.RESULT) { - final List items = IqParser.items(itemsResponse); - for (Jid item : items) { - IqPacket infoRequest = service.getIqGenerator().queryDiscoInfo(item); - queriesInFlight.incrementAndGet(); - service.sendIqPacket(account, infoRequest, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket infoResponse) { - if (infoResponse.getType() == IqPacket.TYPE.RESULT) { - final Room room = IqParser.parseRoom(infoResponse); - if (room != null) { - rooms.add(room); - } - if (queriesInFlight.decrementAndGet() <= 0) { - finishDiscoSearch(rooms, query, listener); - } - } else { - queriesInFlight.decrementAndGet(); - } - } - }); - } - } - if (queriesInFlight.decrementAndGet() <= 0) { - finishDiscoSearch(rooms, query, listener); - } - }); - } - } - - private void finishDiscoSearch(List rooms, String query, OnChannelSearchResultsFound listener) { - Collections.sort(rooms); - cache.put(key(Method.LOCAL_SERVER, ""), rooms); - if (query.isEmpty()) { - listener.onChannelSearchResultsFound(rooms); - } else { - List results = copyMatching(rooms, query); - cache.put(key(Method.LOCAL_SERVER, query), results); - listener.onChannelSearchResultsFound(rooms); - } - } - - private static List copyMatching(List haystack, String needle) { - ArrayList result = new ArrayList<>(); - for (Room room : haystack) { - if (room.contains(needle)) { - result.add(room); - } - } - return result; - } - - private Map getLocalMucServices() { - final HashMap localMucServices = new HashMap<>(); - for (Account account : service.getAccounts()) { - if (account.isEnabled()) { - final XmppConnection xmppConnection = account.getXmppConnection(); - if (xmppConnection == null) { - continue; - } - for (final String mucService : xmppConnection.getMucServers()) { - Jid jid = Jid.ofEscaped(mucService); - if (!localMucServices.containsKey(jid)) { - localMucServices.put(jid, account); - } - } - } - } - return localMucServices; - } - - private static String key(Method method, String query) { - return String.format("%s\00%s", method, query); - } - - private static void logError(final Response response) { - final ResponseBody errorBody = response.errorBody(); - Log.d(Config.LOGTAG, "code from muclumbus=" + response.code()); - if (errorBody == null) { - return; - } - try { - Log.d(Config.LOGTAG, "error body=" + errorBody.string()); - } catch (IOException e) { - //ignored - } - } - - public interface OnChannelSearchResultsFound { - void onChannelSearchResultsFound(List results); - } - - public enum Method { - JABBER_NETWORK, - LOCAL_SERVER - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java b/src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java deleted file mode 100644 index c68b502af..000000000 --- a/src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java +++ /dev/null @@ -1,116 +0,0 @@ -package eu.siacs.conversations.services; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.ServiceConnection; -import android.graphics.drawable.Icon; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.service.chooser.ChooserTarget; -import android.service.chooser.ChooserTargetService; -import android.util.Log; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.ui.ConversationsActivity; -import eu.siacs.conversations.utils.Compatibility; - -@SuppressLint("Deprecated") -@TargetApi(Build.VERSION_CODES.M) -public class ContactChooserTargetService extends ChooserTargetService implements ServiceConnection { - - private final Object lock = new Object(); - private static final int MAX_TARGETS = 5; - private XmppConnectionService mXmppConnectionService; - - private static boolean textOnly(IntentFilter filter) { - for (int i = 0; i < filter.countDataTypes(); ++i) { - if (!"text/plain".equals(filter.getDataType(i))) { - return false; - } - } - return true; - } - - @Override - public List onGetChooserTargets( - final ComponentName targetActivityName, final IntentFilter matchedFilter) { - if (!EventReceiver.hasEnabledAccounts(this)) { - return Collections.emptyList(); - } - final Intent intent = new Intent(this, XmppConnectionService.class); - intent.setAction("contact_chooser"); - Compatibility.startService(this, intent); - bindService(intent, this, Context.BIND_AUTO_CREATE); - try { - waitForService(); - if (!mXmppConnectionService.areMessagesInitialized()) { - return Collections.emptyList(); - } - final ArrayList conversations = new ArrayList<>(); - mXmppConnectionService.populateWithOrderedConversations( - conversations, textOnly(matchedFilter)); - final ComponentName componentName = - new ComponentName(this, ConversationsActivity.class); - final int pixel = AvatarService.getSystemUiAvatarSize(this); - final ArrayList chooserTargets = new ArrayList<>(); - for (final Conversation conversation : conversations) { - if (conversation.sentMessagesCount() == 0) { - continue; - } - final String name = conversation.getName().toString(); - final Icon icon = - Icon.createWithBitmap( - mXmppConnectionService.getAvatarService().get(conversation, pixel)); - final float score = 1 - (1.0f / MAX_TARGETS) * chooserTargets.size(); - final Bundle extras = new Bundle(); - extras.putString(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid()); - chooserTargets.add(new ChooserTarget(name, icon, score, componentName, extras)); - if (chooserTargets.size() >= MAX_TARGETS) { - return chooserTargets; - } - } - return chooserTargets; - } catch (final InterruptedException e) { - Log.d( - Config.LOGTAG, - "Thread got interrupted before binding to XmppConnectionService", - e); - } finally { - unbindService(this); - } - return Collections.emptyList(); - } - - @Override - public void onServiceConnected(final ComponentName name, final IBinder service) { - XmppConnectionService.XmppConnectionBinder binder = - (XmppConnectionService.XmppConnectionBinder) service; - mXmppConnectionService = binder.getService(); - synchronized (this.lock) { - lock.notifyAll(); - } - } - - @Override - public void onServiceDisconnected(ComponentName name) { - mXmppConnectionService = null; - } - - private void waitForService() throws InterruptedException { - if (mXmppConnectionService == null) { - synchronized (this.lock) { - lock.wait(); - } - } - } -} diff --git a/src/main/java/eu/siacs/conversations/services/EventReceiver.java b/src/main/java/eu/siacs/conversations/services/EventReceiver.java deleted file mode 100644 index 93fe7686f..000000000 --- a/src/main/java/eu/siacs/conversations/services/EventReceiver.java +++ /dev/null @@ -1,40 +0,0 @@ -package eu.siacs.conversations.services; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.util.Log; - -import com.google.common.base.Strings; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.Compatibility; - -public class EventReceiver extends BroadcastReceiver { - - public static final String SETTING_ENABLED_ACCOUNTS = "enabled_accounts"; - public static final String EXTRA_NEEDS_FOREGROUND_SERVICE = "needs_foreground_service"; - - @Override - public void onReceive(final Context context, final Intent originalIntent) { - final Intent intentForService = new Intent(context, XmppConnectionService.class); - final String action = originalIntent.getAction(); - intentForService.setAction(Strings.isNullOrEmpty(action) ? "other" : action); - final Bundle extras = originalIntent.getExtras(); - if (extras != null) { - intentForService.putExtras(extras); - } - if ("ui".equals(action) || hasEnabledAccounts(context)) { - Compatibility.startService(context, intentForService); - } else { - Log.d(Config.LOGTAG, "EventReceiver ignored action " + intentForService.getAction()); - } - } - - public static boolean hasEnabledAccounts(final Context context) { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTING_ENABLED_ACCOUNTS, true); - } - -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java b/src/main/java/eu/siacs/conversations/services/ExportBackupService.java deleted file mode 100644 index 61943f29f..000000000 --- a/src/main/java/eu/siacs/conversations/services/ExportBackupService.java +++ /dev/null @@ -1,600 +0,0 @@ -package eu.siacs.conversations.services; - -import static eu.siacs.conversations.persistance.FileBackend.APP_DIRECTORY; -import static eu.siacs.conversations.services.NotificationService.EXPORT_BACKUP_NOTIFICATION_ID; -import static eu.siacs.conversations.utils.Compatibility.runsTwentySix; -import static eu.siacs.conversations.utils.StorageHelper.getAppLogsDirectory; -import static eu.siacs.conversations.utils.StorageHelper.getBackupDirectory; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.database.DatabaseUtils; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.os.Bundle; -import android.os.Environment; -import android.os.IBinder; -import android.os.PowerManager; -import android.preference.PreferenceManager; -import android.util.Log; -import com.google.common.base.CharMatcher; - -import androidx.core.app.NotificationCompat; - -import com.google.common.base.Strings; - -import java.io.BufferedWriter; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.FileWriter; -import java.io.IOException; -import java.io.ObjectOutputStream; -import java.io.PrintWriter; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.spec.InvalidKeySpecException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.zip.GZIPOutputStream; - -import javax.crypto.Cipher; -import javax.crypto.CipherOutputStream; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.SecretKeySpec; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.persistance.DatabaseBackend; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.utils.BackupFileHeader; -import eu.siacs.conversations.utils.Compatibility; -import eu.siacs.conversations.utils.WakeLockHelper; -import eu.siacs.conversations.xmpp.Jid; -import static eu.siacs.conversations.utils.Compatibility.s; - -public class ExportBackupService extends Service { - - private PowerManager.WakeLock wakeLock; - private PowerManager pm; - - public static final String KEYTYPE = "AES"; - public static final String CIPHERMODE = "AES/GCM/NoPadding"; - public static final String PROVIDER = "BC"; - - public static final String MIME_TYPE = "application/vnd.conversations.backup"; - - boolean ReadableLogsEnabled = false; - private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - private static final String MESSAGE_STRING_FORMAT = "(%s) %s: %s\n"; - - private static final int PAGE_SIZE = 20; - private static final AtomicBoolean RUNNING = new AtomicBoolean(false); - private DatabaseBackend mDatabaseBackend; - private List mAccounts; - private NotificationManager notificationManager; - - private static List getPossibleFileOpenIntents(final Context context, final String path) { - - //http://www.openintents.org/action/android-intent-action-view/file-directory - //do not use 'vnd.android.document/directory' since this will trigger system file manager - final Intent openIntent = new Intent(Intent.ACTION_VIEW); - openIntent.addCategory(Intent.CATEGORY_DEFAULT); - if (Compatibility.runsAndTargetsTwentyFour(context)) { - openIntent.setType("resource/folder"); - } else { - openIntent.setDataAndType(Uri.parse("file://" + path), "resource/folder"); - } - openIntent.putExtra("org.openintents.extra.ABSOLUTE_PATH", path); - - final Intent amazeIntent = new Intent(Intent.ACTION_VIEW); - amazeIntent.setDataAndType(Uri.parse("com.amaze.filemanager:" + path), "resource/folder"); - - //will open a file manager at root and user can navigate themselves - final Intent systemFallBack = new Intent(Intent.ACTION_VIEW); - systemFallBack.addCategory(Intent.CATEGORY_DEFAULT); - systemFallBack.setData(Uri.parse("content://com.android.externalstorage.documents/root/primary")); - - return Arrays.asList(openIntent, amazeIntent, systemFallBack); - } - - private static void accountExport(final SQLiteDatabase db, final String uuid, final PrintWriter writer) { - final StringBuilder builder = new StringBuilder(); - final Cursor accountCursor = db.query(Account.TABLENAME, null, Account.UUID + "=?", new String[]{uuid}, null, null, null); - while (accountCursor != null && accountCursor.moveToNext()) { - builder.append("INSERT INTO ").append(Account.TABLENAME).append("("); - for (int i = 0; i < accountCursor.getColumnCount(); ++i) { - if (i != 0) { - builder.append(','); - } - builder.append(accountCursor.getColumnName(i)); - } - builder.append(") VALUES("); - for (int i = 0; i < accountCursor.getColumnCount(); ++i) { - if (i != 0) { - builder.append(','); - } - final String value = accountCursor.getString(i); - if (value == null || Account.ROSTERVERSION.equals(accountCursor.getColumnName(i))) { - builder.append("NULL"); - } else if (Account.OPTIONS.equals(accountCursor.getColumnName(i)) && value.matches("\\d+")) { - int intValue = Integer.parseInt(value); - intValue |= 1 << Account.OPTION_DISABLED; - builder.append(intValue); - } else { - appendEscapedSQLString(builder, value); - } - } - builder.append(")"); - builder.append(';'); - builder.append('\n'); - } - if (accountCursor != null) { - accountCursor.close(); - } - writer.append(builder.toString()); - } - private static void appendEscapedSQLString(final StringBuilder sb, final String sqlString) { - DatabaseUtils.appendEscapedSQLString(sb, CharMatcher.is('\u0000').removeFrom(sqlString)); - } - private static void simpleExport(SQLiteDatabase db, String table, String column, String uuid, PrintWriter writer) { - final Cursor cursor = db.query(table, null, column + "=?", new String[]{uuid}, null, null, null); - while (cursor != null && cursor.moveToNext()) { - writer.write(cursorToString(table, cursor, PAGE_SIZE)); - } - if (cursor != null) { - cursor.close(); - } - } - - public static byte[] getKey(final String password, final byte[] salt) throws InvalidKeySpecException { - final SecretKeyFactory factory; - try { - factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } - return factory.generateSecret(new PBEKeySpec(password.toCharArray(), salt, 1024, 128)).getEncoded(); - } - - private static String cursorToString(final String table, final Cursor cursor, final int max) { - return cursorToString(table, cursor, max, false); - } - - private static String cursorToString(final String table, final Cursor cursor, int max, boolean ignore) { - final boolean identities = SQLiteAxolotlStore.IDENTITIES_TABLENAME.equals(table); - StringBuilder builder = new StringBuilder(); - builder.append("INSERT "); - if (ignore) { - builder.append("OR IGNORE "); - } - builder.append("INTO ").append(table).append("("); - int skipColumn = -1; - for (int i = 0; i < cursor.getColumnCount(); ++i) { - final String name = cursor.getColumnName(i); - if (identities && SQLiteAxolotlStore.TRUSTED.equals(name)) { - skipColumn = i; - continue; - } - if (i != 0) { - builder.append(','); - } - builder.append(name); - } - builder.append(") VALUES"); - for (int i = 0; i < max; ++i) { - if (i != 0) { - builder.append(','); - } - appendValues(cursor, builder, skipColumn); - if (i < max - 1 && !cursor.moveToNext()) { - break; - } - } - builder.append(';'); - builder.append('\n'); - return builder.toString(); - } - - private static void appendValues(final Cursor cursor, final StringBuilder builder, final int skipColumn) { - builder.append("("); - for (int i = 0; i < cursor.getColumnCount(); ++i) { - if (i == skipColumn) { - continue; - } - if (i != 0) { - builder.append(','); - } - final String value = cursor.getString(i); - if (value == null) { - builder.append("NULL"); - } else if (value.matches("[0-9]+")) { - builder.append(value); - } else { - appendEscapedSQLString(builder, value); - } - } - builder.append(")"); - } - - @Override - public void onCreate() { - mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext()); - mAccounts = mDatabaseBackend.getAccounts(); - notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - final SharedPreferences ReadableLogs = PreferenceManager.getDefaultSharedPreferences(this); - ReadableLogsEnabled = ReadableLogs.getBoolean("export_plain_text_logs", getResources().getBoolean(R.bool.plain_text_logs)); - pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Config.LOGTAG + ": ExportLogsService"); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (RUNNING.compareAndSet(false, true)) { - new Thread(() -> { - if (intent == null) { - return; - } - Bundle extras = null; - if (intent != null && intent.getExtras() != null) { - extras = intent.getExtras(); - } - boolean notify = false; - if (extras != null && extras.containsKey("NOTIFY_ON_BACKUP_COMPLETE")) { - notify = extras.getBoolean("NOTIFY_ON_BACKUP_COMPLETE"); - } - boolean success; - List files; - try { - files = export(); - success = files != null; - } catch (final Exception e) { - Log.d(Config.LOGTAG, "unable to create backup", e); - success = false; - files = Collections.emptyList(); - } - try { - if (ReadableLogsEnabled) { // todo - List conversations = mDatabaseBackend.getConversations(Conversation.STATUS_AVAILABLE); - conversations.addAll(mDatabaseBackend.getConversations(Conversation.STATUS_ARCHIVED)); - for (Conversation conversation : conversations) { - writeToFile(conversation); - Log.d(Config.LOGTAG, "Exporting readable logs for " + conversation.getJid()); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - stopForeground(true); - RUNNING.set(false); - if (success) { - notifySuccess(files, notify); - FileBackend.deleteOldBackups(new File(getBackupDirectory(null)), this.mAccounts); - } else { - notifyError(); - } - WakeLockHelper.release(wakeLock); - stopSelf(); - }).start(); - return START_STICKY; - } else { - Log.d(Config.LOGTAG, "ExportBackupService. ignoring start command because already running"); - } - return START_NOT_STICKY; - } - - private void messageExport(SQLiteDatabase db, String uuid, PrintWriter writer, Progress progress) { - Cursor cursor; - if (runsTwentySix()) { - try { - // not select and create column Message.FILE_DELETED to be compareable with conversations - // in C Message.DELETED = Message.FILE_DELETED in PAM so do not select this column, too. - cursor = db.rawQuery("select messages." + String.join(", messages.", new String[]{ - Message.UUID, Message.CONVERSATION, Message.TIME_SENT, Message.COUNTERPART, Message.TRUE_COUNTERPART, - Message.BODY, Message.ENCRYPTION, Message.STATUS, Message.TYPE, Message.RELATIVE_FILE_PATH, - Message.SERVER_MSG_ID, Message.FINGERPRINT, Message.CARBON, Message.EDITED, Message.READ, - Message.OOB, Message.ERROR_MESSAGE, Message.READ_BY_MARKERS, Message.MARKABLE, - Message.REMOTE_MSG_ID, Message.CONVERSATION - }) + " from messages join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?", new String[]{uuid}); - } catch (Exception e) { - e.printStackTrace(); - cursor = null; - } - } else { - cursor = db.rawQuery("select messages.* from messages join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?", new String[]{uuid}); - } - int size = cursor != null ? cursor.getCount() : 0; - Log.d(Config.LOGTAG, "exporting " + size + " messages for account " + uuid); - int i = 0; - int p = 0; - try { - while (cursor != null && cursor.moveToNext()) { - writer.write(cursorToString(Message.TABLENAME, cursor, PAGE_SIZE, false)); - if (i + PAGE_SIZE > size) { - i = size; - } else { - i += PAGE_SIZE; - } - final int percentage = i * 100 / size; - if (p < percentage) { - p = percentage; - notificationManager.notify(EXPORT_BACKUP_NOTIFICATION_ID, progress.build(p)); - } - } - } catch (Exception e) { - e.printStackTrace(); - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - - private List export() throws Exception { - wakeLock.acquire(15 * 60 * 1000L /*15 minutes*/); - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), NotificationService.BACKUP_CHANNEL_ID); - mBuilder.setContentTitle(getString(R.string.notification_create_backup_title)) - .setSmallIcon(R.drawable.ic_archive_white_24dp) - .setProgress(1, 0, false); - startForeground(EXPORT_BACKUP_NOTIFICATION_ID, mBuilder.build()); - int count = 0; - final int max = this.mAccounts.size(); - final SecureRandom secureRandom = new SecureRandom(); - final List files = new ArrayList<>(); - Log.d(Config.LOGTAG, "starting backup for " + max + " accounts"); - Log.d(Config.LOGTAG, "backup settings " + exportSettings()); - for (final Account account : this.mAccounts) { - try { - final String password = account.getPassword(); - if (Strings.nullToEmpty(password).trim().isEmpty()) { - Log.d(Config.LOGTAG, String.format("skipping backup for %s because password is empty. unable to encrypt", account.getJid().asBareJid())); - continue; - } - Log.d(Config.LOGTAG, String.format("exporting data for account %s (%s)", account.getJid().asBareJid(), account.getUuid())); - final byte[] IV = new byte[12]; - final byte[] salt = new byte[16]; - secureRandom.nextBytes(IV); - secureRandom.nextBytes(salt); - final BackupFileHeader backupFileHeader = new BackupFileHeader(getString(R.string.app_name), account.getJid(), System.currentTimeMillis(), IV, salt); - final Progress progress = new Progress(mBuilder, max, count); - final File file = new File(getBackupDirectory(null), account.getJid().asBareJid().toEscapedString() + "_" + ((new SimpleDateFormat("yyyy-MM-dd")).format(new Date())) + ".ceb"); - files.add(file); - final File directory = file.getParentFile(); - if (directory != null && directory.mkdirs()) { - Log.d(Config.LOGTAG, "created backup directory " + directory.getAbsolutePath()); - } - final FileOutputStream fileOutputStream = new FileOutputStream(file); - final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream); - backupFileHeader.write(dataOutputStream); - dataOutputStream.flush(); - - final Cipher cipher = Compatibility.runsTwentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER); - final byte[] key = getKey(password, salt); - Log.d(Config.LOGTAG, backupFileHeader.toString()); - SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE); - IvParameterSpec ivSpec = new IvParameterSpec(IV); - cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); - CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher); - GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream); - PrintWriter writer = new PrintWriter(gzipOutputStream); - SQLiteDatabase db = this.mDatabaseBackend.getReadableDatabase(); - final String uuid = account.getUuid(); - accountExport(db, uuid, writer); - simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, writer); - messageExport(db, uuid, writer, progress); - for (String table : Arrays.asList(SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) { - simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, writer); - } - writer.flush(); - writer.close(); - mediaScannerScanFile(file); - Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile()); - } catch (Exception e) { - Log.d(Config.LOGTAG, "backup for " + account.getJid() + " failed with " + e); - } - count++; - } - stopForeground(true); - notificationManager.cancel(EXPORT_BACKUP_NOTIFICATION_ID); - return files; - } - - private boolean exportSettings() { - boolean success = false; - ObjectOutputStream output = null; - try { - final File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + File.separator + APP_DIRECTORY + File.separator + "Database" + File.separator, "settings.dat"); - final File directory = file.getParentFile(); - if (directory != null && directory.mkdirs()) { - Log.d(Config.LOGTAG, "created backup directory " + directory.getAbsolutePath()); - } - output = new ObjectOutputStream(new FileOutputStream(file)); - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - output.writeObject(pref.getAll()); - success = true; - } catch (FileNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - try { - if (output != null) { - output.flush(); - output.close(); - } - } catch (IOException ex) { - ex.printStackTrace(); - } - } - return success; - } - - private void mediaScannerScanFile(final File file) { - final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); - intent.setData(Uri.fromFile(file)); - sendBroadcast(intent); - } - - private void notifySuccess(final List files, final boolean notify) { - if (!notify) { - return; - } - final String path = getBackupDirectory(null); - PendingIntent openFolderIntent = null; - for (final Intent intent : getPossibleFileOpenIntents(this, path)) { - if (intent.resolveActivityInfo(getPackageManager(), 0) != null) { - - openFolderIntent = PendingIntent.getActivity(this, 189, intent, s() - ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - break; - } - } - - PendingIntent shareFilesIntent = null; - if (files.size() > 0) { - final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); - ArrayList uris = new ArrayList<>(); - for (File file : files) { - uris.add(FileBackend.getUriForFile(this, file)); - } - intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.setType(MIME_TYPE); - final Intent chooser = Intent.createChooser(intent, getString(R.string.share_backup_files)); - shareFilesIntent = PendingIntent.getActivity(this, 190, chooser, s() - ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - } - - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup"); - mBuilder.setContentTitle(getString(R.string.notification_backup_created_title)) - .setContentText(getString(R.string.notification_backup_created_subtitle, path)) - .setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_backup_created_subtitle, getBackupDirectory(null)))) - .setAutoCancel(true) - .setContentIntent(openFolderIntent) - .setSmallIcon(R.drawable.ic_archive_white_24dp); - if (shareFilesIntent != null) { - mBuilder.addAction(R.drawable.ic_share_white_24dp, getString(R.string.share_backup_files), shareFilesIntent); - } - notificationManager.notify(EXPORT_BACKUP_NOTIFICATION_ID, mBuilder.build()); - } - - private void notifyError() { - final String path = getBackupDirectory(null); - - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup"); - mBuilder.setContentTitle(getString(R.string.notification_backup_failed_title)) - .setContentText(getString(R.string.notification_backup_failed_subtitle, path)) - .setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_backup_failed_subtitle, getBackupDirectory(null)))) - .setAutoCancel(true) - .setSmallIcon(R.drawable.ic_warning_white_24dp); - notificationManager.notify(EXPORT_BACKUP_NOTIFICATION_ID, mBuilder.build()); - } - - private void writeToFile(Conversation conversation) { - Jid accountJid = resolveAccountUuid(conversation.getAccountUuid()); - Jid contactJid = conversation.getJid(); - final File dir = new File(getAppLogsDirectory(), accountJid.asBareJid().toString()); - dir.mkdirs(); - - BufferedWriter bw = null; - try { - for (Message message : mDatabaseBackend.getMessagesIterable(conversation)) { - if (message == null) - continue; - if (message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) { - String date = simpleDateFormat.format(new Date(message.getTimeSent())); - if (bw == null) { - bw = new BufferedWriter(new FileWriter( - new File(dir, contactJid.asBareJid().toString() + ".txt"))); - } - String jid = null; - switch (message.getStatus()) { - case Message.STATUS_RECEIVED: - jid = getMessageCounterpart(message); - break; - case Message.STATUS_SEND: - case Message.STATUS_SEND_RECEIVED: - case Message.STATUS_SEND_DISPLAYED: - case Message.STATUS_SEND_FAILED: - jid = accountJid.asBareJid().toString(); - break; - } - if (jid != null) { - String body = message.hasFileOnRemoteHost() ? message.getFileParams().url.toString() : message.getBody(); - bw.write(String.format(MESSAGE_STRING_FORMAT, date, jid, body.replace("\\\n", "\\ \n").replace("\n", "\\ \n"))); - } - } - } - } catch (IOException e) { - e.printStackTrace(); - } finally { - try { - if (bw != null) { - bw.close(); - } - } catch (IOException e1) { - e1.printStackTrace(); - } - } - } - - private String getMessageCounterpart(Message message) { - String trueCounterpart = (String) message.getContentValues().get(Message.TRUE_COUNTERPART); - if (trueCounterpart != null) { - return trueCounterpart; - } else { - return message.getCounterpart().toString(); - } - } - - private Jid resolveAccountUuid(String accountUuid) { - for (Account account : mAccounts) { - if (account.getUuid().equals(accountUuid)) { - return account.getJid(); - } - } - return null; - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - private static class Progress { - private final NotificationCompat.Builder builder; - private final int max; - private final int count; - - private Progress(NotificationCompat.Builder builder, int max, int count) { - this.builder = builder; - this.max = max; - this.count = count; - } - - private Notification build(int percentage) { - builder.setProgress(max * 100, count * 100 + percentage, false); - return builder.build(); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/ImportBackupService.java b/src/main/java/eu/siacs/conversations/services/ImportBackupService.java deleted file mode 100644 index 00bb3f8fe..000000000 --- a/src/main/java/eu/siacs/conversations/services/ImportBackupService.java +++ /dev/null @@ -1,395 +0,0 @@ -package eu.siacs.conversations.services; - -import static eu.siacs.conversations.services.NotificationService.IMPORT_BACKUP_NOTIFICATION_ID; -import static eu.siacs.conversations.utils.StorageHelper.getBackupDirectory; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.os.Binder; -import android.os.IBinder; -import android.provider.OpenableColumns; -import android.util.Log; - -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; - -import com.google.common.base.Charsets; -import com.google.common.base.Stopwatch; -import com.google.common.io.CountingInputStream; - -import org.bouncycastle.crypto.engines.AESEngine; -import org.bouncycastle.crypto.io.CipherInputStream; -import org.bouncycastle.crypto.modes.AEADBlockCipher; -import org.bouncycastle.crypto.modes.GCMBlockCipher; -import org.bouncycastle.crypto.params.AEADParameters; -import org.bouncycastle.crypto.params.KeyParameter; - -import java.io.BufferedReader; -import java.io.DataInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.WeakHashMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.zip.GZIPInputStream; -import java.util.zip.ZipException; - -import javax.crypto.BadPaddingException; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.persistance.DatabaseBackend; -import eu.siacs.conversations.ui.ManageAccountActivity; -import eu.siacs.conversations.utils.BackupFileHeader; -import eu.siacs.conversations.utils.SerialSingleThreadExecutor; -import eu.siacs.conversations.xmpp.Jid; -import static eu.siacs.conversations.utils.Compatibility.s; - -public class ImportBackupService extends Service { - - private static final AtomicBoolean running = new AtomicBoolean(false); - private final ImportBackupServiceBinder binder = new ImportBackupServiceBinder(); - private final SerialSingleThreadExecutor executor = new SerialSingleThreadExecutor(getClass().getSimpleName()); - private final Set mOnBackupProcessedListeners = Collections.newSetFromMap(new WeakHashMap<>()); - private DatabaseBackend mDatabaseBackend; - private NotificationManager notificationManager; - - private static int count(String input, char c) { - int count = 0; - for (char aChar : input.toCharArray()) { - if (aChar == c) { - ++count; - } - } - return count; - } - - @Override - public void onCreate() { - mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext()); - notificationManager = (android.app.NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (intent == null) { - return START_NOT_STICKY; - } - final String password = intent.getStringExtra("password"); - final Uri data = intent.getData(); - final Uri uri; - if (data == null) { - final String file = intent.getStringExtra("file"); - uri = file == null ? null : Uri.fromFile(new File(file)); - } else { - uri = data; - } - - if (password == null || password.isEmpty() || uri == null) { - return START_NOT_STICKY; - } - if (running.compareAndSet(false, true)) { - executor.execute(() -> { - startForegroundService(); - final boolean success = importBackup(uri, password); - stopForeground(true); - running.set(false); - if (success) { - notifySuccess(); - } - stopSelf(); - }); - } else { - Log.d(Config.LOGTAG, "backup already running"); - } - return START_NOT_STICKY; - } - - public boolean getLoadingState() { - return running.get(); - } - - public void loadBackupFiles(final OnBackupFilesLoaded onBackupFilesLoaded) { - executor.execute(() -> { - final List accounts = mDatabaseBackend.getAccountJids(false); - final ArrayList backupFiles = new ArrayList<>(); - final Set apps = new HashSet<>(Arrays.asList(getString(R.string.app_name), "Conversations", "Quicksy", "Pix-Art Messenger")); - final List directories = new ArrayList<>(); - for (final String app : apps) { - directories.add(new File(getBackupDirectory(app))); - //final File directory = new File(getBackupDirectory(app)); - } - directories.add(new File(getBackupDirectory(null))); - for (final File directory : directories) { - if (!directory.exists() || !directory.isDirectory()) { - Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath()); - continue; - } - Log.d(Config.LOGTAG, "directory found: " + directory.getAbsolutePath()); - final File[] files = directory.listFiles(); - if (files == null) { - continue; - } - for (final File file : files) { - if (file.isFile() && file.getName().endsWith(".ceb")) { - Log.d(Config.LOGTAG, "Backup files: " + directory.getAbsolutePath() + "/" + file); - try { - final BackupFile backupFile = BackupFile.read(file); - if (accounts.contains(backupFile.getHeader().getJid())) { - Log.d(Config.LOGTAG, "skipping backup for " + backupFile.getHeader().getJid()); - } else { - backupFiles.add(backupFile); - } - } catch (IOException | IllegalArgumentException e) { - Log.d(Config.LOGTAG, "unable to read backup file ", e); - } - } - } - } - Collections.sort(backupFiles, (a, b) -> a.header.getJid().toString().compareTo(b.header.getJid().toString())); - onBackupFilesLoaded.onBackupFilesLoaded(backupFiles); - }); - } - - private void startForegroundService() { - startForeground(IMPORT_BACKUP_NOTIFICATION_ID, createImportBackupNotification(1, 0)); - } - - private void updateImportBackupNotification(final long total, final long current) { - final int max; - final int progress; - if (total == 0) { - max = 1; - progress = 0; - } else { - max = 100; - progress = (int) (current * 100 / total); - } - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); - try { - notificationManager.notify(IMPORT_BACKUP_NOTIFICATION_ID, createImportBackupNotification(max, progress)); - } catch (final RuntimeException e) { - Log.d(Config.LOGTAG, "unable to make notification", e); - } - } - - private Notification createImportBackupNotification(final int max, final int progress) { - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup"); - mBuilder.setContentTitle(getString(R.string.restoring_backup)) - .setSmallIcon(R.drawable.ic_unarchive_white_24dp) - .setProgress(max, progress, max == 1 && progress == 0); - return mBuilder.build(); - } - - private boolean importBackup(final Uri uri, final String password) { - Log.d(Config.LOGTAG, "importing backup from " + uri); - final Stopwatch stopwatch = Stopwatch.createStarted(); - try { - final SQLiteDatabase db = mDatabaseBackend.getWritableDatabase(); - final InputStream inputStream; - final String path = uri.getPath(); - final long fileSize; - if ("file".equals(uri.getScheme()) && path != null) { - final File file = new File(path); - inputStream = new FileInputStream(file); - fileSize = file.length(); - } else { - final Cursor returnCursor = getContentResolver().query(uri, null, null, null, null); - if (returnCursor == null) { - fileSize = 0; - } else { - returnCursor.moveToFirst(); - fileSize = returnCursor.getLong(returnCursor.getColumnIndex(OpenableColumns.SIZE)); - returnCursor.close(); - } - inputStream = getContentResolver().openInputStream(uri); - } - if (inputStream == null) { - synchronized (mOnBackupProcessedListeners) { - for (final OnBackupProcessed l : mOnBackupProcessedListeners) { - l.onBackupRestoreFailed(); - } - } - return false; - } - final CountingInputStream countingInputStream = new CountingInputStream(inputStream); - final DataInputStream dataInputStream = new DataInputStream(countingInputStream); - final BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); - Log.d(Config.LOGTAG, backupFileHeader.toString()); - if (mDatabaseBackend.getAccountJids(false).contains(backupFileHeader.getJid())) { - synchronized (mOnBackupProcessedListeners) { - for (OnBackupProcessed l : mOnBackupProcessedListeners) { - l.onAccountAlreadySetup(); - } - } - return false; - } - final byte[] key = ExportBackupService.getKey(password, backupFileHeader.getSalt()); - - final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); - cipher.init(false, new AEADParameters(new KeyParameter(key), 128, backupFileHeader.getIv())); - final CipherInputStream cipherInputStream = new CipherInputStream(countingInputStream, cipher); - - final GZIPInputStream gzipInputStream = new GZIPInputStream(cipherInputStream); - final BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream, Charsets.UTF_8)); - db.beginTransaction(); - String line; - StringBuilder multiLineQuery = null; - while ((line = reader.readLine()) != null) { - int count = count(line, '\''); - if (multiLineQuery != null) { - multiLineQuery.append('\n'); - multiLineQuery.append(line); - if (count % 2 == 1) { - db.execSQL(multiLineQuery.toString()); - multiLineQuery = null; - updateImportBackupNotification(fileSize, countingInputStream.getCount()); - } - } else { - if (count % 2 == 0) { - db.execSQL(line); - updateImportBackupNotification(fileSize, countingInputStream.getCount()); - } else { - multiLineQuery = new StringBuilder(line); - } - } - } - db.setTransactionSuccessful(); - db.endTransaction(); - final Jid jid = backupFileHeader.getJid(); - final Cursor countCursor = db.rawQuery("select count(messages.uuid) from messages join conversations on conversations.uuid=messages.conversationUuid join accounts on conversations.accountUuid=accounts.uuid where accounts.username=? and accounts.server=?", new String[]{jid.getEscapedLocal(), jid.getDomain().toEscapedString()}); - countCursor.moveToFirst(); - final int count = countCursor.getInt(0); - Log.d(Config.LOGTAG, String.format("restored %d messages in %s", count, stopwatch.stop().toString())); - countCursor.close(); - stopBackgroundService(); - synchronized (mOnBackupProcessedListeners) { - for (OnBackupProcessed l : mOnBackupProcessedListeners) { - l.onBackupRestored(); - } - } - return true; - } catch (final Exception e) { - final Throwable throwable = e.getCause(); - final boolean reasonWasCrypto = throwable instanceof BadPaddingException || e instanceof ZipException; - synchronized (mOnBackupProcessedListeners) { - for (OnBackupProcessed l : mOnBackupProcessedListeners) { - if (reasonWasCrypto) { - l.onBackupDecryptionFailed(); - } else { - l.onBackupRestoreFailed(); - } - } - } - Log.d(Config.LOGTAG, "error restoring backup " + uri, e); - return false; - } - } - - private void notifySuccess() { - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup"); - mBuilder.setContentTitle(getString(R.string.notification_restored_backup_title)) - .setContentText(getString(R.string.notification_restored_backup_subtitle)) - .setAutoCancel(true) - .setContentIntent(PendingIntent.getActivity(this, 145, new Intent(this, ManageAccountActivity.class), s() - ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT)) - .setSmallIcon(R.drawable.ic_unarchive_white_24dp); - notificationManager.notify(IMPORT_BACKUP_NOTIFICATION_ID, mBuilder.build()); - } - - private void stopBackgroundService() { - Intent intent = new Intent(this, XmppConnectionService.class); - stopService(intent); - } - - public void removeOnBackupProcessedListener(OnBackupProcessed listener) { - synchronized (mOnBackupProcessedListeners) { - mOnBackupProcessedListeners.remove(listener); - } - } - - public void addOnBackupProcessedListener(OnBackupProcessed listener) { - synchronized (mOnBackupProcessedListeners) { - mOnBackupProcessedListeners.add(listener); - } - } - - @Override - public IBinder onBind(Intent intent) { - return this.binder; - } - - public interface OnBackupFilesLoaded { - void onBackupFilesLoaded(List files); - } - - public interface OnBackupProcessed { - void onBackupRestored(); - - void onBackupDecryptionFailed(); - - void onBackupRestoreFailed(); - - void onAccountAlreadySetup(); - } - - public static class BackupFile { - private final Uri uri; - private final BackupFileHeader header; - - private BackupFile(Uri uri, BackupFileHeader header) { - this.uri = uri; - this.header = header; - } - - private static BackupFile read(File file) throws IOException { - final FileInputStream fileInputStream = new FileInputStream(file); - final DataInputStream dataInputStream = new DataInputStream(fileInputStream); - BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); - fileInputStream.close(); - return new BackupFile(Uri.fromFile(file), backupFileHeader); - } - - public static BackupFile read(final Context context, final Uri uri) throws IOException { - final InputStream inputStream = context.getContentResolver().openInputStream(uri); - if (inputStream == null) { - throw new FileNotFoundException(); - } - final DataInputStream dataInputStream = new DataInputStream(inputStream); - BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); - inputStream.close(); - return new BackupFile(uri, backupFileHeader); - } - - public BackupFileHeader getHeader() { - return header; - } - - public Uri getUri() { - return uri; - } - } - - public class ImportBackupServiceBinder extends Binder { - public ImportBackupService getService() { - return ImportBackupService.this; - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/MediaPlayer.java b/src/main/java/eu/siacs/conversations/services/MediaPlayer.java deleted file mode 100644 index 1fbc34517..000000000 --- a/src/main/java/eu/siacs/conversations/services/MediaPlayer.java +++ /dev/null @@ -1,15 +0,0 @@ -package eu.siacs.conversations.services; - -public class MediaPlayer extends android.media.MediaPlayer { - private int streamType; - - public int getAudioStreamType() { - return streamType; - } - - @Override - public void setAudioStreamType(int streamType) { - this.streamType = streamType; - super.setAudioStreamType(streamType); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java deleted file mode 100644 index 5d81426a5..000000000 --- a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java +++ /dev/null @@ -1,755 +0,0 @@ -/* MemorizingTrustManager - a TrustManager which asks the user about invalid - * certificates and memorizes their decision. - * - * Copyright (c) 2010 Georg Lukas - * - * MemorizingTrustManager.java contains the actual trust manager and interface - * code to create a MemorizingActivity and obtain the results. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package eu.siacs.conversations.services; - -import android.app.Application; -import android.app.NotificationManager; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.util.Base64; -import android.util.Log; -import android.util.SparseArray; - -import androidx.appcompat.app.AppCompatActivity; - -import com.google.common.base.Charsets; -import com.google.common.base.Joiner; -import com.google.common.io.ByteStreams; -import com.google.common.io.CharStreams; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.cert.Certificate; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; -import java.security.cert.CertificateParsingException; -import java.security.cert.X509Certificate; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; -import java.util.Locale; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.regex.Pattern; - -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.XmppDomainVerifier; -import eu.siacs.conversations.entities.MTMDecision; -import eu.siacs.conversations.http.HttpConnectionManager; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.ui.MemorizingActivity; - -/** - * A X509 trust manager implementation which asks the user about invalid certificates and memorizes - * their decision. - * - *

The certificate validity is checked using the system default X509 TrustManager, creating a - * query Dialog if the check fails. - * - *

WARNING: This only works if a dedicated thread is used for opening sockets! - */ -public class MemorizingTrustManager { - - private static final SimpleDateFormat DATE_FORMAT = - new SimpleDateFormat("yyyy-MM-dd", Locale.US); - - static final String DECISION_INTENT = "de.duenndns.ssl.DECISION"; - public static final String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId"; - public static final String DECISION_INTENT_CERT = DECISION_INTENT + ".cert"; - public static final String DECISION_TITLE_ID = DECISION_INTENT + ".titleId"; - static final String NO_TRUST_ANCHOR = "Trust anchor for certification path not found."; - private static final Pattern PATTERN_IPV4 = - Pattern.compile( - "\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = - Pattern.compile( - "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)" - + " ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - private static final Pattern PATTERN_IPV6_6HEX4DEC = - Pattern.compile( - "\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = - Pattern.compile( - "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z"); - private static final Pattern PATTERN_IPV6 = - Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z"); - private static final Logger LOGGER = Logger.getLogger(MemorizingTrustManager.class.getName()); - static String KEYSTORE_DIR = "KeyStore"; - static String KEYSTORE_FILE = "KeyStore.bks"; - private static int decisionId = 0; - private static SparseArray openDecisions = new SparseArray(); - Context master; - AppCompatActivity foregroundAct; - NotificationManager notificationManager; - Handler masterHandler; - private File keyStoreFile; - private KeyStore appKeyStore; - private X509TrustManager defaultTrustManager; - private X509TrustManager appTrustManager; - private String poshCacheDir; - - public static MemorizingTrustManager create(final Context context) { - final SharedPreferences preferences = - PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); - final boolean dontTrustSystemCAs = - preferences.getBoolean( - "dont_trust_system_cas", - context.getResources().getBoolean(R.bool.dont_trust_system_cas)); - if (dontTrustSystemCAs) { - return new MemorizingTrustManager(context.getApplicationContext(), null); - } else { - return new MemorizingTrustManager(context.getApplicationContext()); - } - } - /** - * Creates an instance of the MemorizingTrustManager class that falls back to a custom - * TrustManager. - * - *

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

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

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

The context is used for file management, to display the dialog / notification and for - * obtaining translated strings. - * - * @param m Context for the application. - */ - public MemorizingTrustManager(Context m) { - init(m); - this.appTrustManager = getTrustManager(appKeyStore); - this.defaultTrustManager = getTrustManager(null); - } - - private static boolean isIp(final String server) { - return server != null - && (PATTERN_IPV4.matcher(server).matches() - || PATTERN_IPV6.matcher(server).matches() - || PATTERN_IPV6_6HEX4DEC.matcher(server).matches() - || PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches() - || PATTERN_IPV6_HEXCOMPRESSED.matcher(server).matches()); - } - - private static String getBase64Hash(X509Certificate certificate, String digest) - throws CertificateEncodingException { - MessageDigest md; - try { - md = MessageDigest.getInstance(digest); - } catch (NoSuchAlgorithmException e) { - return null; - } - md.update(certificate.getEncoded()); - return Base64.encodeToString(md.digest(), Base64.NO_WRAP); - } - - private static String hexString(byte[] data) { - StringBuffer si = new StringBuffer(); - for (int i = 0; i < data.length; i++) { - si.append(String.format("%02x", data[i])); - if (i < data.length - 1) si.append(":"); - } - return si.toString(); - } - - private static String certHash(final X509Certificate cert, String digest) { - try { - MessageDigest md = MessageDigest.getInstance(digest); - md.update(cert.getEncoded()); - return hexString(md.digest()); - } catch (CertificateEncodingException | NoSuchAlgorithmException e) { - return e.getMessage(); - } - } - - public static void interactResult(int decisionId, int choice) { - MTMDecision d; - synchronized (openDecisions) { - d = openDecisions.get(decisionId); - openDecisions.remove(decisionId); - } - if (d == null) { - LOGGER.log(Level.SEVERE, "interactResult: aborting due to stale decision reference!"); - return; - } - synchronized (d) { - d.state = choice; - d.notify(); - } - } - - void init(final Context m) { - master = m; - masterHandler = new Handler(m.getMainLooper()); - notificationManager = - (NotificationManager) master.getSystemService(Context.NOTIFICATION_SERVICE); - Application app; - if (m instanceof Application) { - app = (Application) m; - } else if (m instanceof Service) { - app = ((Service) m).getApplication(); - } else if (m instanceof AppCompatActivity) { - app = ((AppCompatActivity) m).getApplication(); - } else - throw new ClassCastException( - "MemorizingTrustManager context must be either Activity or Service!"); - - File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE); - keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE); - - poshCacheDir = app.getCacheDir().getAbsolutePath() + "/posh_cache/"; - - appKeyStore = loadAppKeyStore(); - } - - /** - * Get a list of all certificate aliases stored in MTM. - * - * @return an {@link Enumeration} of all certificates - */ - public Enumeration getCertificates() { - try { - return appKeyStore.aliases(); - } catch (KeyStoreException e) { - // this should never happen, however... - throw new RuntimeException(e); - } - } - - /** - * Removes the given certificate from MTMs key store. - * - *

WARNING: this does not immediately invalidate the certificate. It is well possible - * that (a) data is transmitted over still existing connections or (b) new connections are - * created using TLS renegotiation, without a new cert check. - * - * @param alias the certificate's alias as returned by {@link #getCertificates()}. - * @throws KeyStoreException if the certificate could not be deleted. - */ - public void deleteCertificate(String alias) throws KeyStoreException { - appKeyStore.deleteEntry(alias); - keyStoreUpdated(); - } - - X509TrustManager getTrustManager(KeyStore ks) { - try { - TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); - tmf.init(ks); - for (TrustManager t : tmf.getTrustManagers()) { - if (t instanceof X509TrustManager) { - return (X509TrustManager) t; - } - } - } catch (Exception e) { - // Here, we are covering up errors. It might be more useful - // however to throw them out of the constructor so the - // embedding app knows something went wrong. - LOGGER.log(Level.SEVERE, "getTrustManager(" + ks + ")", e); - } - return null; - } - - KeyStore loadAppKeyStore() { - KeyStore ks; - try { - ks = KeyStore.getInstance(KeyStore.getDefaultType()); - } catch (KeyStoreException e) { - LOGGER.log(Level.SEVERE, "getAppKeyStore()", e); - return null; - } - FileInputStream fileInputStream = null; - try { - ks.load(null, null); - fileInputStream = new FileInputStream(keyStoreFile); - ks.load(fileInputStream, "MTM".toCharArray()); - } catch (java.io.FileNotFoundException e) { - LOGGER.log(Level.INFO, "getAppKeyStore(" + keyStoreFile + ") - file does not exist"); - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "getAppKeyStore(" + keyStoreFile + ")", e); - } finally { - FileBackend.close(fileInputStream); - } - return ks; - } - - void storeCert(String alias, Certificate cert) { - try { - appKeyStore.setCertificateEntry(alias, cert); - } catch (KeyStoreException e) { - LOGGER.log(Level.SEVERE, "storeCert(" + cert + ")", e); - return; - } - keyStoreUpdated(); - } - - void storeCert(X509Certificate cert) { - storeCert(cert.getSubjectDN().toString(), cert); - } - - void keyStoreUpdated() { - // reload appTrustManager - appTrustManager = getTrustManager(appKeyStore); - - // store KeyStore to file - java.io.FileOutputStream fos = null; - try { - fos = new java.io.FileOutputStream(keyStoreFile); - appKeyStore.store(fos, "MTM".toCharArray()); - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "storeCert(" + keyStoreFile + ")", e); - } finally { - if (fos != null) { - try { - fos.close(); - } catch (IOException e) { - LOGGER.log(Level.SEVERE, "storeCert(" + keyStoreFile + ")", e); - } - } - } - } - - // if the certificate is stored in the app key store, it is considered "known" - private boolean isCertKnown(X509Certificate cert) { - try { - return appKeyStore.getCertificateAlias(cert) != null; - } catch (KeyStoreException e) { - return false; - } - } - - - private void checkCertTrusted( - X509Certificate[] chain, - String authType, - String domain, - boolean isServer, - boolean interactive) - throws CertificateException { - LOGGER.log( - Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")"); - try { - LOGGER.log(Level.FINE, "checkCertTrusted: trying appTrustManager"); - if (isServer) appTrustManager.checkServerTrusted(chain, authType); - else appTrustManager.checkClientTrusted(chain, authType); - } catch (final CertificateException ae) { - LOGGER.log(Level.FINER, "checkCertTrusted: appTrustManager failed", ae); - if (isCertKnown(chain[0])) { - LOGGER.log( - Level.INFO, "checkCertTrusted: accepting cert already stored in keystore"); - return; - } - try { - if (defaultTrustManager == null) throw ae; - LOGGER.log(Level.FINE, "checkCertTrusted: trying defaultTrustManager"); - if (isServer) defaultTrustManager.checkServerTrusted(chain, authType); - else defaultTrustManager.checkClientTrusted(chain, authType); - } catch (final CertificateException e) { - final SharedPreferences preferences = - PreferenceManager.getDefaultSharedPreferences(master); - final boolean trustSystemCAs = - !preferences.getBoolean("dont_trust_system_cas", false); - if (domain != null - && isServer - && trustSystemCAs - && !isIp(domain) - && !domain.endsWith(".onion") - && !domain.endsWith(".i2p")) { - final String hash = getBase64Hash(chain[0], "SHA-256"); - final List fingerprints = getPoshFingerprints(domain); - if (hash != null && fingerprints.size() > 0) { - if (fingerprints.contains(hash)) { - Log.d( - Config.LOGTAG, - "trusted cert fingerprint of " + domain + " via posh"); - return; - } else { - Log.d( - Config.LOGTAG, - "fingerprint " + hash + " not found in " + fingerprints); - } - if (getPoshCacheFile(domain).delete()) { - Log.d( - Config.LOGTAG, - "deleted posh file for " - + domain - + " after not being able to verify"); - } - } - } - if (interactive) { - interactCert(chain, authType, e); - } else { - throw e; - } - } - } - } - - private List getPoshFingerprints(final String domain) { - final List cached = getPoshFingerprintsFromCache(domain); - if (cached == null) { - return getPoshFingerprintsFromServer(domain); - } else { - return cached; - } - } - - private List getPoshFingerprintsFromServer(String domain) { - return getPoshFingerprintsFromServer( - domain, "https://" + domain + "/.well-known/posh/xmpp-client.json", -1, true); - } - - private List getPoshFingerprintsFromServer( - String domain, String url, int maxTtl, boolean followUrl) { - Log.d(Config.LOGTAG, "downloading json for " + domain + " from " + url); - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(master); - final boolean useTor = - QuickConversationsService.isConversations() - && preferences.getBoolean( - "use_tor", master.getResources().getBoolean(R.bool.use_tor)); - final boolean useI2P = QuickConversationsService.isConversations() && preferences.getBoolean("use_i2p", master.getResources().getBoolean(R.bool.use_i2p)); - try { - final List results = new ArrayList<>(); - final InputStream inputStream = HttpConnectionManager.open(url, useTor, useI2P); - final String body = - CharStreams.toString( - new InputStreamReader( - ByteStreams.limit(inputStream, 10_000), Charsets.UTF_8)); - final JSONObject jsonObject = new JSONObject(body); - int expires = jsonObject.getInt("expires"); - if (expires <= 0) { - return new ArrayList<>(); - } - if (maxTtl >= 0) { - expires = Math.min(maxTtl, expires); - } - String redirect; - try { - redirect = jsonObject.getString("url"); - } catch (JSONException e) { - redirect = null; - } - if (followUrl && redirect != null && redirect.toLowerCase().startsWith("https")) { - return getPoshFingerprintsFromServer(domain, redirect, expires, false); - } - final JSONArray fingerprints = jsonObject.getJSONArray("fingerprints"); - for (int i = 0; i < fingerprints.length(); i++) { - final JSONObject fingerprint = fingerprints.getJSONObject(i); - final String sha256 = fingerprint.getString("sha-256"); - results.add(sha256); - } - writeFingerprintsToCache(domain, results, 1000L * expires + System.currentTimeMillis()); - return results; - } catch (final Exception e) { - Log.d(Config.LOGTAG, "error fetching posh", e); - return new ArrayList<>(); - } - } - - private File getPoshCacheFile(String domain) { - return new File(poshCacheDir + domain + ".json"); - } - - private void writeFingerprintsToCache(String domain, List results, long expires) { - final File file = getPoshCacheFile(domain); - file.getParentFile().mkdirs(); - try { - file.createNewFile(); - JSONObject jsonObject = new JSONObject(); - jsonObject.put("expires", expires); - jsonObject.put("fingerprints", new JSONArray(results)); - FileOutputStream outputStream = new FileOutputStream(file); - outputStream.write(jsonObject.toString().getBytes()); - outputStream.flush(); - outputStream.close(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - private List getPoshFingerprintsFromCache(String domain) { - final File file = getPoshCacheFile(domain); - try { - final InputStream inputStream = new FileInputStream(file); - final String json = - CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8)); - final JSONObject jsonObject = new JSONObject(json); - long expires = jsonObject.getLong("expires"); - long expiresIn = expires - System.currentTimeMillis(); - if (expiresIn < 0) { - file.delete(); - return null; - } else { - Log.d(Config.LOGTAG, "posh fingerprints expire in " + (expiresIn / 1000) + "s"); - } - final List result = new ArrayList<>(); - final JSONArray jsonArray = jsonObject.getJSONArray("fingerprints"); - for (int i = 0; i < jsonArray.length(); ++i) { - result.add(jsonArray.getString(i)); - } - return result; - } catch (final IOException e) { - return null; - } catch (JSONException e) { - file.delete(); - return null; - } - } - - private X509Certificate[] getAcceptedIssuers() { - return defaultTrustManager == null - ? new X509Certificate[0] - : defaultTrustManager.getAcceptedIssuers(); - } - - private int createDecisionId(MTMDecision d) { - int myId; - synchronized (openDecisions) { - myId = decisionId; - openDecisions.put(myId, d); - decisionId += 1; - } - return myId; - } - - private void certDetails( - final StringBuffer si, final X509Certificate c, final boolean showValidFor) { - - si.append("\n"); - if (showValidFor) { - try { - si.append("Valid for: "); - si.append(Joiner.on(", ").join(XmppDomainVerifier.parseValidDomains(c).all())); - } catch (final CertificateParsingException e) { - si.append("Unable to parse Certificate"); - } - si.append("\n"); - } else { - si.append(c.getSubjectDN()); - } - si.append("\n"); - si.append(DATE_FORMAT.format(c.getNotBefore())); - si.append(" - "); - si.append(DATE_FORMAT.format(c.getNotAfter())); - si.append("\nSHA-256: "); - si.append(certHash(c, "SHA-256")); - si.append("\nSHA-1: "); - si.append(certHash(c, "SHA-1")); - si.append("\nSigned by: "); - si.append(c.getIssuerDN().toString()); - si.append("\n"); - } - - private String certChainMessage(final X509Certificate[] chain, CertificateException cause) { - Throwable e = cause; - LOGGER.log(Level.FINE, "certChainMessage for " + e); - final StringBuffer si = new StringBuffer(); - if (e.getCause() != null) { - e = e.getCause(); - // HACK: there is no sane way to check if the error is a "trust anchor - // not found", so we use string comparison. - if (NO_TRUST_ANCHOR.equals(e.getMessage())) { - si.append(master.getString(R.string.mtm_trust_anchor)); - } else si.append(e.getLocalizedMessage()); - si.append("\n"); - } - si.append("\n"); - si.append(master.getString(R.string.mtm_connect_anyway)); - si.append("\n\n"); - si.append(master.getString(R.string.mtm_cert_details)); - si.append('\n'); - for (int i = 0; i < chain.length; ++i) { - certDetails(si, chain[i], i == 0); - } - return si.toString(); - } - - /** - * Returns the top-most entry of the activity stack. - * - * @return the Context of the currently bound UI or the master context if none is bound - */ - Context getUI() { - return (foregroundAct != null) ? foregroundAct : master; - } - - int interact(final String message, final int titleId) { - /* prepare the MTMDecision blocker object */ - MTMDecision choice = new MTMDecision(); - final int myId = createDecisionId(choice); - - masterHandler.post( - new Runnable() { - public void run() { - Intent ni = new Intent(master, MemorizingActivity.class); - ni.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - ni.setData(Uri.parse(MemorizingTrustManager.class.getName() + "/" + myId)); - ni.putExtra(DECISION_INTENT_ID, myId); - ni.putExtra(DECISION_INTENT_CERT, message); - ni.putExtra(DECISION_TITLE_ID, titleId); - - // we try to directly start the activity and fall back to - // making a notification - try { - getUI().startActivity(ni); - } catch (Exception e) { - LOGGER.log(Level.FINE, "startActivity(MemorizingActivity)", e); - } - } - }); - - LOGGER.log(Level.FINE, "openDecisions: " + openDecisions + ", waiting on " + myId); - try { - synchronized (choice) { - choice.wait(); - } - } catch (InterruptedException e) { - LOGGER.log(Level.FINER, "InterruptedException", e); - } - LOGGER.log(Level.FINE, "finished wait on " + myId + ": " + choice.state); - return choice.state; - } - - void interactCert(final X509Certificate[] chain, String authType, CertificateException cause) - throws CertificateException { - switch (interact(certChainMessage(chain, cause), R.string.mtm_accept_cert)) { - case MTMDecision.DECISION_ALWAYS: - storeCert(chain[0]); // only store the server cert, not the whole chain - case MTMDecision.DECISION_ONCE: - break; - default: - throw (cause); - } - } - - public X509TrustManager getNonInteractive(String domain) { - return new NonInteractiveMemorizingTrustManager(domain); - } - - public X509TrustManager getInteractive(String domain) { - return new InteractiveMemorizingTrustManager(domain); - } - - public X509TrustManager getNonInteractive() { - return new NonInteractiveMemorizingTrustManager(null); - } - - public X509TrustManager getInteractive() { - return new InteractiveMemorizingTrustManager(null); - } - - private class NonInteractiveMemorizingTrustManager implements X509TrustManager { - - private final String domain; - - public NonInteractiveMemorizingTrustManager(String domain) { - this.domain = domain; - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, false); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, false); - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return MemorizingTrustManager.this.getAcceptedIssuers(); - } - - } - - private class InteractiveMemorizingTrustManager implements X509TrustManager { - private final String domain; - - public InteractiveMemorizingTrustManager(String domain) { - this.domain = domain; - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, true); - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, true); - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return MemorizingTrustManager.this.getAcceptedIssuers(); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java deleted file mode 100644 index b4568537a..000000000 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ /dev/null @@ -1,676 +0,0 @@ -package eu.siacs.conversations.services; - -import android.util.Log; - -import org.jetbrains.annotations.NotNull; -import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; - -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Conversational; -import eu.siacs.conversations.entities.ReceiptRequest; -import eu.siacs.conversations.generator.AbstractGenerator; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; -import eu.siacs.conversations.xmpp.mam.MamReference; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; - -public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { - - private final XmppConnectionService mXmppConnectionService; - - private final HashSet queries = new HashSet<>(); - private final ArrayList pendingQueries = new ArrayList<>(); - - public enum Version { - MAM_0("urn:xmpp:mam:0", true), - MAM_1("urn:xmpp:mam:1", false), - MAM_2("urn:xmpp:mam:2", false); - - public final boolean legacy; - public final String namespace; - - Version(String namespace, boolean legacy) { - this.namespace = namespace; - this.legacy = legacy; - } - - public static Version get(Account account) { - return get(account, null); - } - - public static Version get(Account account, Conversation conversation) { - if (conversation == null || conversation.getMode() == Conversation.MODE_SINGLE) { - return get(account.getXmppConnection().getFeatures().getAccountFeatures()); - } else { - return get(conversation.getMucOptions().getFeatures()); - } - } - - private static Version get(List features) { - final Version[] values = values(); - for (int i = values.length - 1; i >= 0; --i) { - for (String feature : features) { - if (values[i].namespace.equals(feature)) { - return values[i]; - } - } - } - return MAM_0; - } - - public static boolean has(List features) { - for (String feature : features) { - for (Version version : values()) { - if (version.namespace.equals(feature)) { - return true; - } - } - } - return false; - } - - public static Element findResult(MessagePacket packet) { - for (Version version : values()) { - Element result = packet.findChild("result", version.namespace); - if (result != null) { - return result; - } - } - return null; - } - - } - - ; - - MessageArchiveService(final XmppConnectionService service) { - this.mXmppConnectionService = service; - } - - private void catchup(final Account account) { - synchronized (this.queries) { - for (Iterator iterator = this.queries.iterator(); iterator.hasNext(); ) { - Query query = iterator.next(); - if (query.getAccount() == account) { - iterator.remove(); - } - } - } - MamReference mamReference = MamReference.max( - mXmppConnectionService.databaseBackend.getLastMessageReceived(account), - mXmppConnectionService.databaseBackend.getLastClearDate(account) - ); - mamReference = MamReference.max(mamReference, mXmppConnectionService.getAutomaticMessageDeletionDate()); - long endCatchup = account.getXmppConnection().getLastSessionEstablished(); - final Query query; - if (mamReference.getTimestamp() == 0) { - return; - } else if (endCatchup - mamReference.getTimestamp() >= Config.MAM_MAX_CATCHUP) { - long startCatchup = endCatchup - Config.MAM_MAX_CATCHUP; - List conversations = mXmppConnectionService.getConversations(); - for (Conversation conversation : conversations) { - if (conversation.getMode() == Conversation.MODE_SINGLE && conversation.getAccount() == account && startCatchup > conversation.getLastMessageTransmitted().getTimestamp()) { - this.query(conversation, startCatchup, true); - } - } - query = new Query(account, new MamReference(startCatchup), 0); - } else { - query = new Query(account, mamReference, 0); - } - synchronized (this.queries) { - this.queries.add(query); - } - this.execute(query); - } - - void catchupMUC(final Conversation conversation) { - if (conversation.getLastMessageTransmitted().getTimestamp() < 0 && conversation.countMessages() == 0) { - query(conversation, - new MamReference(0), - 0, - true); - } else { - query(conversation, - conversation.getLastMessageTransmitted(), - 0, - true); - } - } - - public Query query(final Conversation conversation) { - if (conversation.getLastMessageTransmitted().getTimestamp() < 0 && conversation.countMessages() == 0) { - return query(conversation, - new MamReference(0), - System.currentTimeMillis(), - false); - } else { - return query(conversation, - conversation.getLastMessageTransmitted(), - conversation.getAccount().getXmppConnection().getLastSessionEstablished(), - false); - } - } - - public boolean isCatchingUp(Conversation conversation) { - final Account account = conversation.getAccount(); - if (account.getXmppConnection().isWaitingForSmCatchup()) { - return true; - } else { - synchronized (this.queries) { - for (Query query : this.queries) { - if (query.getAccount() == account && query.isCatchup() && ((conversation.getMode() == Conversation.MODE_SINGLE && query.getWith() == null) || query.getConversation() == conversation)) { - return true; - } - } - } - return false; - } - } - - public Query query(final Conversation conversation, long end, boolean allowCatchup) { - return this.query(conversation, conversation.getLastMessageTransmitted(), end, allowCatchup); - } - - public Query query(Conversation conversation, MamReference start, long end, boolean allowCatchup) { - synchronized (this.queries) { - final Query query; - final MamReference startActual = MamReference.max(start, mXmppConnectionService.getAutomaticMessageDeletionDate()); - if (start.getTimestamp() == 0) { - query = new Query(conversation, startActual, end, false); - query.reference = conversation.getFirstMamReference(); - } else { - if (allowCatchup) { - MamReference maxCatchup = MamReference.max(startActual, System.currentTimeMillis() - Config.MAM_MAX_CATCHUP); - if (maxCatchup.greaterThan(startActual)) { - Query reverseCatchup = new Query(conversation, startActual, maxCatchup.getTimestamp(), false); - this.queries.add(reverseCatchup); - this.execute(reverseCatchup); - } - query = new Query(conversation, maxCatchup, end, true); - } else { - query = new Query(conversation, startActual, end, false); - } - } - if (end != 0 && start.greaterThan(end)) { - return null; - } - this.queries.add(query); - this.execute(query); - return query; - } - } - - void executePendingQueries(final Account account) { - final List pending = new ArrayList<>(); - synchronized (this.pendingQueries) { - for (Iterator iterator = this.pendingQueries.iterator(); iterator.hasNext(); ) { - Query query = iterator.next(); - if (query.getAccount() == account) { - pending.add(query); - iterator.remove(); - } - } - } - for (Query query : pending) { - this.execute(query); - } - } - - private void execute(final Query query) { - final Account account = query.getAccount(); - if (account.getStatus() == Account.State.ONLINE) { - final Conversation conversation = query.getConversation(); - if (conversation != null && conversation.getStatus() == Conversation.STATUS_ARCHIVED) { - throw new IllegalStateException("Attempted to run MAM query for archived conversation"); - } - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": running mam query " + query.toString()); - final IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query); - this.mXmppConnectionService.sendIqPacket(account, packet, (a, p) -> { - final Element fin = p.findChild("fin", query.version.namespace); - if (p.getType() == IqPacket.TYPE.TIMEOUT) { - synchronized (this.queries) { - this.queries.remove(query); - if (query.hasCallback()) { - query.callback(false); - } - } - } else if (p.getType() == IqPacket.TYPE.RESULT && fin != null) { - final boolean running; - synchronized (this.queries) { - running = this.queries.contains(query); - } - if (running) { - processFin(query, fin); - } else { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": ignoring MAM iq result because query had been killed"); - } - } else if (p.getType() == IqPacket.TYPE.RESULT && query.isLegacy()) { - //do nothing - } else { - Log.d(Config.LOGTAG, a.getJid().asBareJid().toString() + ": error executing mam: " + p.toString()); - try { - finalizeQuery(query, true); - } catch (final IllegalStateException e) { - //ignored - } - } - }); - } else { - synchronized (this.pendingQueries) { - this.pendingQueries.add(query); - } - } - } - - private void finalizeQuery(final Query query, boolean done) { - synchronized (this.queries) { - if (!this.queries.remove(query)) { - throw new IllegalStateException("Unable to remove query from queries"); - } - } - final Conversation conversation = query.getConversation(); - if (conversation != null) { - conversation.sort(); - conversation.setHasMessagesLeftOnServer(!done); - } else { - for (Conversation tmp : this.mXmppConnectionService.getConversations()) { - if (tmp.getAccount() == query.getAccount()) { - tmp.sort(); - } - } - } - if (query.hasCallback()) { - query.callback(done); - } else { - this.mXmppConnectionService.updateConversationUi(); - } - } - - boolean inCatchup(Account account) { - synchronized (this.queries) { - for (Query query : queries) { - if (query.account == account && query.isCatchup() && query.getWith() == null) { - return true; - } - } - } - return false; - } - - public boolean isCatchupInProgress(Conversation conversation) { - synchronized (this.queries) { - for (Query query : queries) { - if (query.account == conversation.getAccount() && query.isCatchup()) { - final Jid with = query.getWith() == null ? null : query.getWith().asBareJid(); - if ((conversation.getMode() == Conversational.MODE_SINGLE && with == null) || (conversation.getJid().asBareJid().equals(with))) { - return true; - } - } - } - } - return false; - } - - boolean queryInProgress(Conversation conversation, XmppConnectionService.OnMoreMessagesLoaded callback) { - synchronized (this.queries) { - for (Query query : queries) { - if (query.conversation == conversation) { - if (!query.hasCallback() && callback != null) { - query.setCallback(callback); - } - return true; - } - } - return false; - } - } - - public boolean queryInProgress(Conversation conversation) { - return queryInProgress(conversation, null); - } - - public void processFinLegacy(Element fin, Jid from) { - Query query = findQuery(fin.getAttribute("queryid")); - if (query != null && query.validFrom(from)) { - processFin(query, fin); - } - } - - private void processFin(Query query, Element fin) { - boolean complete = fin.getAttributeAsBoolean("complete"); - Element set = fin.findChild("set", "http://jabber.org/protocol/rsm"); - Element last = set == null ? null : set.findChild("last"); - String count = set == null ? null : set.findChildContent("count"); - Element first = set == null ? null : set.findChild("first"); - Element relevant = query.getPagingOrder() == PagingOrder.NORMAL ? last : first; - boolean abort = (!query.isCatchup() && query.getTotalCount() >= Config.PAGE_SIZE) || query.getTotalCount() >= Config.MAM_MAX_MESSAGES; - if (query.getConversation() != null) { - query.getConversation().setFirstMamReference(first == null ? null : first.getContent()); - } - if (complete || relevant == null || abort) { - //TODO: FIX done logic to look at complete. using count is probably unreliable because it can be ommited and doesn’t work with paging. - boolean done; - if (query.isCatchup()) { - done = false; - } else { - if (count != null) { - try { - done = Integer.parseInt(count) <= query.getTotalCount(); - } catch (NumberFormatException e) { - done = false; - } - } else { - done = query.getTotalCount() == 0; - } - } - done = done || (query.getActualMessageCount() == 0 && !query.isCatchup()); - this.finalizeQuery(query, done); - Log.d(Config.LOGTAG, query.getAccount().getJid().asBareJid() + ": finished mam after " + query.getTotalCount() + "(" + query.getActualMessageCount() + ") messages. messages left=" + Boolean.toString(!done) + " count=" + count); - if (query.isCatchup() && query.getActualMessageCount() > 0) { - mXmppConnectionService.getNotificationService().finishBacklog(true,query.getAccount()); - } - processPostponed(query); - } else { - final Query nextQuery; - if (query.getPagingOrder() == PagingOrder.NORMAL) { - nextQuery = query.next(last == null ? null : last.getContent()); - } else { - nextQuery = query.prev(first == null ? null : first.getContent()); - } - this.execute(nextQuery); - this.finalizeQuery(query, false); - synchronized (this.queries) { - this.queries.add(nextQuery); - } - } - } - - void kill(final Conversation conversation) { - final ArrayList toBeKilled = new ArrayList<>(); - synchronized (this.pendingQueries) { - for (final Iterator iterator = this.pendingQueries.iterator(); iterator.hasNext(); ) { - final Query query = iterator.next(); - if (query.getConversation() == conversation) { - iterator.remove(); - Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": killed pending MAM query for archived conversation"); - } - } - } - synchronized (this.queries) { - for (final Query q : queries) { - if (q.conversation == conversation) { - toBeKilled.add(q); - } - } - } - for (final Query q : toBeKilled) { - kill(q); - } - } - - private void kill(Query query) { - Log.d(Config.LOGTAG, query.getAccount().getJid().asBareJid() + ": killing mam query prematurely"); - query.callback = null; - this.finalizeQuery(query, false); - if (query.isCatchup() && query.getActualMessageCount() > 0) { - mXmppConnectionService.getNotificationService().finishBacklog(true, query.getAccount()); - } - this.processPostponed(query); - } - - private void processPostponed(Query query) { - query.account.getAxolotlService().processPostponed(); - query.pendingReceiptRequests.removeAll(query.receiptRequests); - Log.d(Config.LOGTAG, query.getAccount().getJid().asBareJid() + ": found " + query.pendingReceiptRequests.size() + " pending receipt requests"); - Iterator iterator = query.pendingReceiptRequests.iterator(); - while (iterator.hasNext()) { - ReceiptRequest rr = iterator.next(); - mXmppConnectionService.sendMessagePacket(query.account, mXmppConnectionService.getMessageGenerator().received(query.account, rr.getJid(), rr.getId())); - iterator.remove(); - } - } - - public Query findQuery(String id) { - if (id == null) { - return null; - } - synchronized (this.queries) { - for (Query query : this.queries) { - if (query.getQueryId().equals(id)) { - return query; - } - } - return null; - } - } - - @Override - public void onAdvancedStreamFeaturesAvailable(Account account) { - if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().mam()) { - this.catchup(account); - } - } - - public enum PagingOrder { - NORMAL, - REVERSE - } - - public class Query { - private HashSet pendingReceiptRequests = new HashSet<>(); - private HashSet receiptRequests = new HashSet<>(); - private int totalCount = 0; - private int actualCount = 0; - private int actualInThisQuery = 0; - private long start; - private long end; - private String queryId; - private String reference = null; - private Account account; - private Conversation conversation; - private PagingOrder pagingOrder = PagingOrder.NORMAL; - private XmppConnectionService.OnMoreMessagesLoaded callback = null; - private boolean catchup = true; - public final Version version; - - - Query(Conversation conversation, MamReference start, long end, boolean catchup) { - this(conversation.getAccount(), Version.get(conversation.getAccount(), conversation), catchup ? start : start.timeOnly(), end); - this.conversation = conversation; - this.pagingOrder = catchup ? PagingOrder.NORMAL : PagingOrder.REVERSE; - this.catchup = catchup; - } - - Query(Account account, MamReference start, long end) { - this(account, Version.get(account), start, end); - } - - Query(Account account, Version version, MamReference start, long end) { - this.account = account; - if (start.getReference() != null) { - this.reference = start.getReference(); - } else { - this.start = start.getTimestamp(); - } - this.end = end; - this.queryId = new BigInteger(50, SECURE_RANDOM).toString(32); - this.version = version; - } - - private Query page(String reference) { - Query query = new Query(this.account, this.version, new MamReference(this.start, reference), this.end); - query.conversation = conversation; - query.totalCount = totalCount; - query.actualCount = actualCount; - query.pendingReceiptRequests = pendingReceiptRequests; - query.receiptRequests = receiptRequests; - query.callback = callback; - query.catchup = catchup; - return query; - } - - public void removePendingReceiptRequest(ReceiptRequest receiptRequest) { - if (!this.pendingReceiptRequests.remove(receiptRequest)) { - this.receiptRequests.add(receiptRequest); - } - } - - public void addPendingReceiptRequest(ReceiptRequest receiptRequest) { - this.pendingReceiptRequests.add(receiptRequest); - } - - public boolean isLegacy() { - return version.legacy; - } - - public boolean safeToExtractTrueCounterpart() { - return muc() && !isLegacy(); - } - - public Query next(String reference) { - Query query = page(reference); - query.pagingOrder = PagingOrder.NORMAL; - return query; - } - - Query prev(String reference) { - Query query = page(reference); - query.pagingOrder = PagingOrder.REVERSE; - return query; - } - - public String getReference() { - return reference; - } - - public PagingOrder getPagingOrder() { - return this.pagingOrder; - } - - public String getQueryId() { - return queryId; - } - - public Jid getWith() { - return conversation == null ? null : conversation.getJid().asBareJid(); - } - - public boolean muc() { - return conversation != null && conversation.getMode() == Conversation.MODE_MULTI; - } - - public long getStart() { - return start; - } - - public boolean isCatchup() { - return catchup; - } - - public void setCallback(XmppConnectionService.OnMoreMessagesLoaded callback) { - this.callback = callback; - } - - public void callback(boolean done) { - if (this.callback != null) { - this.callback.onMoreMessagesLoaded(actualCount, conversation); - if (done) { - this.callback.informUser(R.string.no_more_history_on_server); - } - } - } - - public long getEnd() { - return end; - } - - public Conversation getConversation() { - return conversation; - } - - public Account getAccount() { - return this.account; - } - - public void incrementMessageCount() { - this.totalCount++; - } - - public void incrementActualMessageCount() { - this.actualInThisQuery++; - this.actualCount++; - } - - int getTotalCount() { - return this.totalCount; - } - - int getActualMessageCount() { - return this.actualCount; - } - - public int getActualInThisQuery() { - return this.actualInThisQuery; - } - - public boolean validFrom(Jid from) { - if (muc()) { - return getWith().equals(from); - } else { - return (from == null) || account.getJid().asBareJid().equals(from.asBareJid()); - } - } - - @NotNull - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - if (this.muc()) { - builder.append("to="); - builder.append(this.getWith().toString()); - } else { - builder.append("with="); - if (this.getWith() == null) { - builder.append("*"); - } else { - builder.append(getWith().toString()); - } - } - if (this.start != 0) { - builder.append(", start="); - builder.append(AbstractGenerator.getTimestamp(this.start)); - } - if (this.end != 0) { - builder.append(", end="); - builder.append(AbstractGenerator.getTimestamp(this.end)); - } - builder.append(", order=").append(pagingOrder.toString()); - if (this.reference != null) { - if (this.pagingOrder == PagingOrder.NORMAL) { - builder.append(", after="); - } else { - builder.append(", before="); - } - builder.append(this.reference); - } - builder.append(", catchup=").append(Boolean.toString(catchup)); - builder.append(", ns=").append(version.namespace); - return builder.toString(); - } - - boolean hasCallback() { - return this.callback != null; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/services/MessageSearchTask.java b/src/main/java/eu/siacs/conversations/services/MessageSearchTask.java deleted file mode 100644 index 01472ae39..000000000 --- a/src/main/java/eu/siacs/conversations/services/MessageSearchTask.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.services; - -import android.database.Cursor; -import android.os.SystemClock; -import android.util.Log; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Conversational; -import eu.siacs.conversations.entities.IndividualMessage; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.StubConversation; -import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable; -import eu.siacs.conversations.utils.Cancellable; -import eu.siacs.conversations.utils.MessageUtils; -import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor; -import eu.siacs.conversations.xmpp.Jid; - -public class MessageSearchTask implements Runnable, Cancellable { - - private static final ReplacingSerialSingleThreadExecutor EXECUTOR = new ReplacingSerialSingleThreadExecutor(MessageSearchTask.class.getName()); - - private final XmppConnectionService xmppConnectionService; - private final List term; - private final String uuid; - private final OnSearchResultsAvailable onSearchResultsAvailable; - - private boolean isCancelled = false; - - private MessageSearchTask(XmppConnectionService xmppConnectionService, List term, final String uuid, OnSearchResultsAvailable onSearchResultsAvailable) { - this.xmppConnectionService = xmppConnectionService; - this.term = term; - this.uuid = uuid; - this.onSearchResultsAvailable = onSearchResultsAvailable; - } - - public static void search(XmppConnectionService xmppConnectionService, List term, final String uuid, OnSearchResultsAvailable onSearchResultsAvailable) { - new MessageSearchTask(xmppConnectionService, term, uuid, onSearchResultsAvailable).executeInBackground(); - } - - public static void cancelRunningTasks() { - EXECUTOR.cancelRunningTasks(); - } - - @Override - public void cancel() { - this.isCancelled = true; - } - - @Override - public void run() { - long startTimestamp = SystemClock.elapsedRealtime(); - Cursor cursor = null; - try { - final HashMap conversationCache = new HashMap<>(); - final List result = new ArrayList<>(); - cursor = xmppConnectionService.databaseBackend.getMessageSearchCursor(term, uuid); - long dbTimer = SystemClock.elapsedRealtime(); - if (isCancelled) { - Log.d(Config.LOGTAG, "canceled search task"); - return; - } - if (cursor != null && cursor.getCount() > 0) { - cursor.moveToLast(); - final int indexBody = cursor.getColumnIndex(Message.BODY); - final int indexOob = cursor.getColumnIndex(Message.OOB); - final int indexConversation = cursor.getColumnIndex(Message.CONVERSATION); - final int indexAccount = cursor.getColumnIndex(Conversation.ACCOUNT); - final int indexContact = cursor.getColumnIndex(Conversation.CONTACTJID); - final int indexMode = cursor.getColumnIndex(Conversation.MODE); - do { - if (isCancelled) { - Log.d(Config.LOGTAG, "canceled search task"); - return; - } - final String body = cursor.getString(indexBody); - final boolean oob = cursor.getInt(indexOob) > 0; - if (MessageUtils.treatAsDownloadable(body, oob)) { - continue; - } - final String conversationUuid = cursor.getString(indexConversation); - Conversational conversation = conversationCache.get(conversationUuid); - if (conversation == null) { - String accountUuid = cursor.getString(indexAccount); - String contactJid = cursor.getString(indexContact); - int mode = cursor.getInt(indexMode); - conversation = findOrGenerateStub(conversationUuid, accountUuid, contactJid, mode); - conversationCache.put(conversationUuid, conversation); - } - Message message = IndividualMessage.fromCursor(cursor, conversation); - result.add(message); - } while (cursor.moveToPrevious()); - } - long stopTimestamp = SystemClock.elapsedRealtime(); - Log.d(Config.LOGTAG, "found " + result.size() + " messages in " + (stopTimestamp - startTimestamp) + "ms" + " (db was " + (dbTimer - startTimestamp) + "ms)"); - onSearchResultsAvailable.onSearchResultsAvailable(term, result); - } catch (Exception e) { - Log.d(Config.LOGTAG, "exception while searching ", e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - - private Conversational findOrGenerateStub(String conversationUuid, String accountUuid, String contactJid, int mode) throws Exception { - Conversation conversation = xmppConnectionService.findConversationByUuid(conversationUuid); - if (conversation != null) { - return conversation; - } - Account account = xmppConnectionService.findAccountByUuid(accountUuid); - Jid jid = Jid.of(contactJid); - if (account != null && jid != null) { - return new StubConversation(account, conversationUuid, jid.asBareJid(), mode); - } - throw new Exception("Unable to generate stub for " + contactJid); - } - - private void executeInBackground() { - EXECUTOR.execute(this); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java deleted file mode 100644 index 7f7933892..000000000 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ /dev/null @@ -1,1939 +0,0 @@ -package eu.siacs.conversations.services; - -import static eu.siacs.conversations.ui.util.MyLinkify.replaceYoutube; -import static eu.siacs.conversations.utils.Compatibility.s; - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationChannelGroup; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Typeface; -import android.media.AudioAttributes; -import android.media.Ringtone; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.Build; -import android.os.SystemClock; -import android.preference.PreferenceManager; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.style.ForegroundColorSpan; -import android.text.style.StyleSpan; -import android.util.DisplayMetrics; -import android.util.Log; -import com.google.common.base.Joiner; -import androidx.annotation.RequiresApi; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationCompat.BigPictureStyle; -import androidx.core.app.NotificationCompat.Builder; -import androidx.core.app.NotificationManagerCompat; -import androidx.core.app.Person; -import androidx.core.app.RemoteInput; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.drawable.IconCompat; - -import com.google.common.collect.Iterables; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Conversational; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.ui.ConversationsActivity; -import eu.siacs.conversations.ui.EditAccountActivity; -import eu.siacs.conversations.ui.RtpSessionActivity; -import eu.siacs.conversations.ui.TimePreference; -import eu.siacs.conversations.utils.AccountUtils; -import eu.siacs.conversations.utils.Compatibility; -import eu.siacs.conversations.utils.GeoHelper; -import eu.siacs.conversations.utils.ThemeHelper; -import eu.siacs.conversations.utils.TorServiceUtils; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xmpp.XmppConnection; -import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; -import eu.siacs.conversations.xmpp.jingle.Media; - -public class NotificationService { - - public static final Object CATCHUP_LOCK = new Object(); - public static final String MESSAGES_CHANNEL_ID = "messages"; - public static final String SILENT_MESSAGES_CHANNEL_ID = "silent_messages"; - public static final String INCOMING_CALLS_CHANNEL_ID = "incoming_calls"; - public static final String ONGOING_CALLS_CHANNEL_ID = "ongoing_calls"; - public static final String MISSED_CALLS_CHANNEL_ID = "missed_calls"; - public static final String FOREGROUND_CHANNEL_ID = "foreground"; - public static final String QUIET_HOURS_CHANNEL_ID = "quiet_hours"; - public static final String DELIVERY_FAILED_CHANNEL_ID = "delivery_failed"; - public static final String BACKUP_CHANNEL_ID = "backup"; - public static final String UPDATE_CHANNEL_ID = "appupdate"; - public static final String VIDEOCOMPRESSION_CHANNEL_ID = "compression"; - public static final String ERROR_CHANNEL_ID = "error"; - public static final String INDIVIDUAL_NOTIFICATION_PREFIX = "XxXx_"; - public static final String OLD_INDIVIDUAL_NOTIFICATION_PREFIX = "xxXx_"; // x, Xx, xXx, - private static final String DEFAULT = "default_ID"; - private static final int CALL_DAT = 120; - private static final long[] CALL_PATTERN = {0, 3 * CALL_DAT, CALL_DAT, CALL_DAT, 3 * CALL_DAT, CALL_DAT, CALL_DAT}; - private static final int MESSAGE_DAT = 70; - private static final long[] MESSAGE_PATTERN = {0, 3 * MESSAGE_DAT, MESSAGE_DAT, MESSAGE_DAT}; - - private static final String MESSAGES_GROUP = "eu.siacs.conversations.messages"; - private static final String MISSED_CALLS_GROUP = "eu.siacs.conversations.missed_calls"; - private static final int NOTIFICATION_ID_MULTIPLIER = 1024 * 1024; - public static final int NOTIFICATION_ID = 2 * NOTIFICATION_ID_MULTIPLIER; - public static final int FOREGROUND_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 4; - public static final int ERROR_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 6; - private static final int LED_COLOR = 0xff0080FF; - private static final int INCOMING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 8; - public static final int ONGOING_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 10; - private static final int DELIVERY_FAILED_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 12; - public static final int MISSED_CALL_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 14; - public static final int IMPORT_BACKUP_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 16; - public static final int EXPORT_BACKUP_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 18; - public static final int UPDATE_NOTIFICATION_ID = NOTIFICATION_ID_MULTIPLIER * 20; - private final XmppConnectionService mXmppConnectionService; - private final LinkedHashMap> notifications = new LinkedHashMap<>(); - private final LinkedHashMap mMissedCalls = - new LinkedHashMap<>(); - private final HashMap mBacklogMessageCounter = new HashMap<>(); - private Conversation mOpenConversation; - private boolean mIsInForeground; - private long mLastNotification; - - private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL = "incoming_calls_channel"; - private Ringtone currentlyPlayingRingtone = null; - private ScheduledFuture vibrationFuture; - - NotificationService(final XmppConnectionService service) { - this.mXmppConnectionService = service; - } - - private static boolean displaySnoozeAction(List messages) { - int numberOfMessagesWithoutReply = 0; - for (Message message : messages) { - if (message.getStatus() == Message.STATUS_RECEIVED) { - ++numberOfMessagesWithoutReply; - } else { - return false; - } - } - return numberOfMessagesWithoutReply >= 3; - } - - public static Pattern generateNickHighlightPattern(final String nick) { - return Pattern.compile("(?<=(^|\\s))" + Pattern.quote(nick) + "(?=\\s|$|\\p{Punct})"); - } - - @RequiresApi(api = Build.VERSION_CODES.O) - void updateChannels() { - mXmppConnectionService.mNotificationChannelExecutor.execute(this::initializeChannels); - //initializeChannels(); - } - - @RequiresApi(api = Build.VERSION_CODES.O) - void initializeChannels() { - final Context c = mXmppConnectionService; - final NotificationManager notificationManager = c.getSystemService(NotificationManager.class); - if (notificationManager == null) { - return; - } - notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("status", c.getString(R.string.notification_group_status_information))); - notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("chats", c.getString(R.string.notification_group_messages))); - notificationManager.createNotificationChannelGroup(new NotificationChannelGroup("calls", c.getString(R.string.notification_group_calls))); - - NotificationChannel foregroundServiceChannel = notificationManager.getNotificationChannel(FOREGROUND_CHANNEL_ID); - if (foregroundServiceChannel == null) { - foregroundServiceChannel = new NotificationChannel(FOREGROUND_CHANNEL_ID, - c.getString(R.string.foreground_service_channel_name), - NotificationManager.IMPORTANCE_MIN); - foregroundServiceChannel.setDescription(c.getString(R.string.foreground_service_channel_description)); - foregroundServiceChannel.setShowBadge(false); - foregroundServiceChannel.setGroup("status"); - notificationManager.createNotificationChannel(foregroundServiceChannel); - } - - NotificationChannel backupChannel = notificationManager.getNotificationChannel(BACKUP_CHANNEL_ID); - if (backupChannel == null) { - backupChannel = new NotificationChannel(BACKUP_CHANNEL_ID, - c.getString(R.string.backup_channel_name), - NotificationManager.IMPORTANCE_LOW); - backupChannel.setShowBadge(false); - backupChannel.setGroup("status"); - notificationManager.createNotificationChannel(backupChannel); - } - - NotificationChannel videoCompressionChannel = notificationManager.getNotificationChannel(VIDEOCOMPRESSION_CHANNEL_ID); - if (videoCompressionChannel == null) { - videoCompressionChannel = new NotificationChannel(VIDEOCOMPRESSION_CHANNEL_ID, - c.getString(R.string.video_compression_channel_name), - NotificationManager.IMPORTANCE_LOW); - videoCompressionChannel.setShowBadge(false); - videoCompressionChannel.setGroup("status"); - notificationManager.createNotificationChannel(videoCompressionChannel); - } - - NotificationChannel AppUpdateChannel = notificationManager.getNotificationChannel(UPDATE_CHANNEL_ID); - if (AppUpdateChannel == null) { - AppUpdateChannel = new NotificationChannel(UPDATE_CHANNEL_ID, - c.getString(R.string.app_update_channel_name), - NotificationManager.IMPORTANCE_LOW); - AppUpdateChannel.setShowBadge(false); - AppUpdateChannel.setGroup("status"); - notificationManager.createNotificationChannel(AppUpdateChannel); - } - - NotificationChannel ongoingCallsChannel = notificationManager.getNotificationChannel(ONGOING_CALLS_CHANNEL_ID); - if (ongoingCallsChannel == null) { - ongoingCallsChannel = new NotificationChannel(ONGOING_CALLS_CHANNEL_ID, - c.getString(R.string.ongoing_calls_channel_name), - NotificationManager.IMPORTANCE_LOW); - ongoingCallsChannel.setShowBadge(false); - ongoingCallsChannel.setGroup("calls"); - notificationManager.createNotificationChannel(ongoingCallsChannel); - } - - NotificationChannel missedCallsChannel = notificationManager.getNotificationChannel(MISSED_CALLS_CHANNEL_ID); - if (missedCallsChannel == null) { - missedCallsChannel = new NotificationChannel( - MISSED_CALLS_CHANNEL_ID, - c.getString(R.string.missed_calls_channel_name), - NotificationManager.IMPORTANCE_HIGH); - missedCallsChannel.setShowBadge(true); - missedCallsChannel.setSound(null, null); - missedCallsChannel.setLightColor(LED_COLOR); - missedCallsChannel.enableLights(true); - missedCallsChannel.setGroup("calls"); - notificationManager.createNotificationChannel(missedCallsChannel); - } - - NotificationChannel silentMessagesChannel = notificationManager.getNotificationChannel(SILENT_MESSAGES_CHANNEL_ID); - if (silentMessagesChannel == null) { - silentMessagesChannel = new NotificationChannel(SILENT_MESSAGES_CHANNEL_ID, - c.getString(R.string.silent_messages_channel_name), - NotificationManager.IMPORTANCE_LOW); - silentMessagesChannel.setDescription(c.getString(R.string.silent_messages_channel_description)); - silentMessagesChannel.setShowBadge(true); - silentMessagesChannel.setLightColor(LED_COLOR); - silentMessagesChannel.enableLights(true); - silentMessagesChannel.setGroup("chats"); - notificationManager.createNotificationChannel(silentMessagesChannel); - } - - NotificationChannel quietHoursChannel = notificationManager.getNotificationChannel(QUIET_HOURS_CHANNEL_ID); - if (quietHoursChannel == null) { - quietHoursChannel = new NotificationChannel(QUIET_HOURS_CHANNEL_ID, - c.getString(R.string.title_pref_quiet_hours), - NotificationManager.IMPORTANCE_LOW); - quietHoursChannel.setShowBadge(true); - quietHoursChannel.setLightColor(LED_COLOR); - quietHoursChannel.enableLights(true); - quietHoursChannel.setGroup("chats"); - quietHoursChannel.enableVibration(false); - quietHoursChannel.setSound(null, null); - notificationManager.createNotificationChannel(quietHoursChannel); - } - - NotificationChannel deliveryFailedChannel = notificationManager.getNotificationChannel(DELIVERY_FAILED_CHANNEL_ID); - if (deliveryFailedChannel == null) { - deliveryFailedChannel = new NotificationChannel(DELIVERY_FAILED_CHANNEL_ID, - c.getString(R.string.delivery_failed_channel_name), - NotificationManager.IMPORTANCE_DEFAULT); - deliveryFailedChannel.setShowBadge(false); - deliveryFailedChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT) - .build()); - deliveryFailedChannel.setGroup("chats"); - notificationManager.createNotificationChannel(deliveryFailedChannel); - } - - createDefaultMessageNotificationChannel(notificationManager); - createDefaultCallNotificationChannel(notificationManager); - - createIndividualNotificationChannels(notificationManager); - } - - // create individual notification channels for selected chats - @RequiresApi(api = Build.VERSION_CODES.O) - private void createIndividualNotificationChannels(final NotificationManager notificationManager) { - try { - final int chats = mXmppConnectionService.getConversations().size(); - for (int i = 0; i < chats; i++) { - if (mXmppConnectionService.hasIndividualNotification(mXmppConnectionService.getConversations().get(i))) { - if (mXmppConnectionService.getConversations().get(i).getMode() == Conversation.MODE_SINGLE) { - createCallNotificationChannels(notificationManager, i); - } - createMessageNotificationChannels(notificationManager, i); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - // create individual call notification channel for selected chats - @RequiresApi(api = Build.VERSION_CODES.O) - private void createCallNotificationChannels(final NotificationManager notificationManager, final int i) { - final String uuid = mXmppConnectionService.getConversations().get(i).getUuid(); - final String jid = mXmppConnectionService.getConversations().get(i).getAccount().getJid().asBareJid().toString(); - final String name = mXmppConnectionService.getConversations().get(i).getName().toString().toLowerCase(); - final String time = String.valueOf(mXmppConnectionService.getIndividualNotificationPreference(mXmppConnectionService.getConversations().get(i))); - final String channelID = INDIVIDUAL_NOTIFICATION_PREFIX + INCOMING_CALLS_CHANNEL_ID + "_" + uuid + "_" + time; - try { - final NotificationChannel incomingCallsChannel = new NotificationChannel(channelID, - mXmppConnectionService.getString(R.string.notification_group_calls), - NotificationManager.IMPORTANCE_HIGH); - incomingCallsChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE), new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) - .build()); - incomingCallsChannel.setShowBadge(false); - incomingCallsChannel.setLightColor(LED_COLOR); - incomingCallsChannel.enableLights(true); - notificationManager.createNotificationChannelGroup(new NotificationChannelGroup(INDIVIDUAL_NOTIFICATION_PREFIX + name + uuid, name + " (" + jid + ")")); - incomingCallsChannel.setGroup(INDIVIDUAL_NOTIFICATION_PREFIX + name + uuid); - incomingCallsChannel.setBypassDnd(true); - incomingCallsChannel.enableVibration(true); - incomingCallsChannel.setVibrationPattern(CALL_PATTERN); - notificationManager.createNotificationChannel(incomingCallsChannel); - } catch (Exception e) { - e.printStackTrace(); - } - } - - // create individual message notification channels for selected chat - @RequiresApi(api = Build.VERSION_CODES.O) - private void createMessageNotificationChannels(final NotificationManager notificationManager, final int i) { - final String uuid = mXmppConnectionService.getConversations().get(i).getUuid(); - final String jid = mXmppConnectionService.getConversations().get(i).getAccount().getJid().asBareJid().toString(); - final String name = mXmppConnectionService.getConversations().get(i).getName().toString().toLowerCase(); - final String time = String.valueOf(mXmppConnectionService.getIndividualNotificationPreference(mXmppConnectionService.getConversations().get(i))); - final String channelID = INDIVIDUAL_NOTIFICATION_PREFIX + MESSAGES_CHANNEL_ID + "_" + uuid + "_" + time; - try { - final NotificationChannel messagesChannel = new NotificationChannel(channelID, - mXmppConnectionService.getString(R.string.notification_group_messages), - NotificationManager.IMPORTANCE_HIGH); - messagesChannel.setShowBadge(true); - messagesChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT) - .build()); - messagesChannel.setLightColor(LED_COLOR); - messagesChannel.setVibrationPattern(MESSAGE_PATTERN); - messagesChannel.enableVibration(true); - messagesChannel.enableLights(true); - notificationManager.createNotificationChannelGroup(new NotificationChannelGroup(INDIVIDUAL_NOTIFICATION_PREFIX + name + uuid, name + " (" + jid + ")")); - messagesChannel.setGroup(INDIVIDUAL_NOTIFICATION_PREFIX + name + uuid); - notificationManager.createNotificationChannel(messagesChannel); - } catch (Exception e) { - e.printStackTrace(); - } - } - - // default message notification channel - @RequiresApi(api = Build.VERSION_CODES.O) - private void createDefaultMessageNotificationChannel(final NotificationManager notificationManager) { - final String channelID = MESSAGES_CHANNEL_ID + "_" + DEFAULT; - try { - final NotificationChannel messagesChannel = new NotificationChannel(channelID, - mXmppConnectionService.getString(R.string.notification_group_messages), - NotificationManager.IMPORTANCE_HIGH); - messagesChannel.setShowBadge(true); - messagesChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT) - .build()); - messagesChannel.setLightColor(LED_COLOR); - messagesChannel.setVibrationPattern(MESSAGE_PATTERN); - messagesChannel.enableVibration(true); - messagesChannel.enableLights(true); - messagesChannel.setGroup("chats"); - notificationManager.createNotificationChannel(messagesChannel); - } catch (Exception e) { - e.printStackTrace(); - } - } - - // default call notification channel - @RequiresApi(api = Build.VERSION_CODES.O) - private void createDefaultCallNotificationChannel(final NotificationManager notificationManager) { - final String channelID = INCOMING_CALLS_CHANNEL_ID + "_" + DEFAULT; - try { - final NotificationChannel incomingCallsChannel = new NotificationChannel(channelID, - mXmppConnectionService.getString(R.string.notification_group_calls), - NotificationManager.IMPORTANCE_HIGH); - incomingCallsChannel.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE), new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) - .build()); - incomingCallsChannel.setShowBadge(false); - incomingCallsChannel.setLightColor(LED_COLOR); - incomingCallsChannel.enableLights(true); - incomingCallsChannel.setGroup("calls"); - incomingCallsChannel.setBypassDnd(true); - incomingCallsChannel.enableVibration(true); - incomingCallsChannel.setVibrationPattern(CALL_PATTERN); - notificationManager.createNotificationChannel(incomingCallsChannel); - } catch (Exception e) { - e.printStackTrace(); - } - } - - // clean individual notification channels and groups for selected user - @RequiresApi(api = Build.VERSION_CODES.O) - public void cleanNotificationChannels(final Context context, final String uuid) { - final NotificationManager notificationManager = context.getSystemService(NotificationManager.class); - cleanCallNotificationChannels(notificationManager, uuid); - cleanMessageNotificationChannels(notificationManager, uuid); - cleanNotificationGroup(notificationManager, uuid); - } - - // clean individual notification group for user - @RequiresApi(api = Build.VERSION_CODES.O) - private void cleanNotificationGroup(final NotificationManager notificationManager, final String uuid) { - try { - final String name = mXmppConnectionService.findConversationByUuid(uuid).getName().toString().toLowerCase(); - final String groupID = INDIVIDUAL_NOTIFICATION_PREFIX + name + uuid; - final List list = notificationManager.getNotificationChannelGroups(); - final int count = list.size(); - for (int a = 0; a < count; a++) { - final NotificationChannelGroup group = list.get(a); - final String id = group.getId(); - if (id.startsWith(groupID) || id.startsWith(uuid)) { - notificationManager.deleteNotificationChannelGroup(id); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - // clean individual call notification channels for user - @RequiresApi(api = Build.VERSION_CODES.O) - private void cleanCallNotificationChannels(final NotificationManager notificationManager, final String uuid) { - final String time = String.valueOf(mXmppConnectionService.getIndividualNotificationPreference(mXmppConnectionService.findConversationByUuid(uuid))); - final String channelID = INDIVIDUAL_NOTIFICATION_PREFIX + INCOMING_CALLS_CHANNEL_ID + "_" + uuid + "_" + time; - try { - final List list = notificationManager.getNotificationChannels(); - final int count = list.size(); - for (int a = 0; a < count; a++) { - final NotificationChannel channel = list.get(a); - final String id = channel.getId(); - if (id.startsWith(channelID) || id.startsWith(INCOMING_CALLS_CHANNEL_ID + "_" + uuid)) { - notificationManager.deleteNotificationChannel(id); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - // clean individual message notification channels and group for user - @RequiresApi(api = Build.VERSION_CODES.O) - private void cleanMessageNotificationChannels(final NotificationManager notificationManager, final String uuid) { - final String time = String.valueOf(mXmppConnectionService.getIndividualNotificationPreference(mXmppConnectionService.findConversationByUuid(uuid))); - final String channelID = INDIVIDUAL_NOTIFICATION_PREFIX + MESSAGES_CHANNEL_ID + "_" + uuid + "_" + time; - try { - final List list = notificationManager.getNotificationChannels(); - final int count = list.size(); - for (int a = 0; a < count; a++) { - final NotificationChannel channel = list.get(a); - final String id = channel.getId(); - if (id.startsWith(channelID) || id.startsWith(MESSAGES_CHANNEL_ID + "_" + uuid)) { - notificationManager.deleteNotificationChannel(id); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - // clean all individual notification settings - @RequiresApi(api = Build.VERSION_CODES.O) - public void cleanAllNotificationChannels(final Context context) { - try { - final NotificationManager notificationManager = context.getSystemService(NotificationManager.class); - final int chats = mXmppConnectionService.getConversations().size(); - for (int i = 0; i < chats; i++) { - if (mXmppConnectionService.hasIndividualNotification(mXmppConnectionService.getConversations().get(i))) { - final String uuid = mXmppConnectionService.getConversations().get(i).getUuid(); - mXmppConnectionService.setIndividualNotificationPreference(mXmppConnectionService.getConversations().get(i), true); - cleanCallNotificationChannels(notificationManager, uuid); - cleanMessageNotificationChannels(notificationManager, uuid); - cleanNotificationGroup(notificationManager, uuid); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - // clean all old individual notifications - @RequiresApi(api = Build.VERSION_CODES.O) - public void cleanAllOldNotificationChannels(final Context context) { - try { - final NotificationManager notificationManager = context.getSystemService(NotificationManager.class); - final int channels = notificationManager.getNotificationChannels().size(); - for (int i1 = 0; i1 < channels; i1++) { - final String channelID = notificationManager.getNotificationChannels().get(i1).getId(); - if (channelID.startsWith(OLD_INDIVIDUAL_NOTIFICATION_PREFIX)) { - notificationManager.deleteNotificationChannel(channelID); - } - } - final int groups = notificationManager.getNotificationChannelGroups().size(); - for (int i2 = 0; i2 < groups; i2++) { - final String groupID = notificationManager.getNotificationChannelGroups().get(i2).getId(); - if (groupID.startsWith(OLD_INDIVIDUAL_NOTIFICATION_PREFIX)) { - notificationManager.deleteNotificationChannel(groupID); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - public boolean notifyMessage(final Message message) { - final Conversation conversation = (Conversation) message.getConversation(); - return message.getStatus() == Message.STATUS_RECEIVED - && !conversation.isMuted() - && (conversation.alwaysNotify() || wasHighlightedOrPrivate(message)) - && (!conversation.isWithStranger() || notificationsFromStrangers()) - && message.getType() != Message.TYPE_RTP_SESSION; - } - - private boolean notifyMissedCall(final Message message) { - return message.getType() == Message.TYPE_RTP_SESSION - && message.getStatus() == Message.STATUS_RECEIVED; - } - - public boolean notificationsFromStrangers() { - return mXmppConnectionService.getBooleanPreference("notifications_from_strangers", R.bool.notifications_from_strangers); - } - - private boolean isQuietHours() { - if (!mXmppConnectionService.getBooleanPreference("enable_quiet_hours", R.bool.enable_quiet_hours)) { - return false; - } - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); - final long startTime = TimePreference.minutesToTimestamp(preferences.getLong("quiet_hours_start", TimePreference.DEFAULT_VALUE)); - final long endTime = TimePreference.minutesToTimestamp(preferences.getLong("quiet_hours_end", TimePreference.DEFAULT_VALUE)); - final long nowTime = Calendar.getInstance().getTimeInMillis(); - - if (endTime < startTime) { - return nowTime > startTime || nowTime < endTime; - } else { - return nowTime > startTime && nowTime < endTime; - } - } - - public void pushFromBacklog(final Message message) { - if (notifyMessage(message)) { - synchronized (notifications) { - getBacklogMessageCounter((Conversation) message.getConversation()).incrementAndGet(); - pushToStack(message); - } - } else if (notifyMissedCall(message)) { - synchronized (mMissedCalls) { - pushMissedCall(message); - } - } - } - - private AtomicInteger getBacklogMessageCounter(Conversation conversation) { - synchronized (mBacklogMessageCounter) { - if (!mBacklogMessageCounter.containsKey(conversation)) { - mBacklogMessageCounter.put(conversation, new AtomicInteger(0)); - } - return mBacklogMessageCounter.get(conversation); - } - } - - void pushFromDirectReply(final Message message) { - synchronized (notifications) { - pushToStack(message); - updateNotification(false); - } - } - - public void finishBacklog(boolean notify, Account account) { - synchronized (notifications) { - mXmppConnectionService.updateUnreadCountBadge(); - if (account == null || !notify) { - updateNotification(notify); - } else { - final int count; - final List conversations; - synchronized (this.mBacklogMessageCounter) { - conversations = getBacklogConversations(account); - count = getBacklogMessageCount(account); - } - updateNotification(count > 0, conversations); - } - } - synchronized (mMissedCalls) { - updateMissedCallNotifications(mMissedCalls.keySet()); - } - } - - private List getBacklogConversations(Account account) { - final List conversations = new ArrayList<>(); - for (Map.Entry entry : mBacklogMessageCounter.entrySet()) { - if (entry.getKey().getAccount() == account) { - conversations.add(entry.getKey().getUuid()); - } - } - return conversations; - } - - private int getBacklogMessageCount(Account account) { - int count = 0; - for (Iterator> it = mBacklogMessageCounter.entrySet().iterator(); it.hasNext(); ) { - Map.Entry entry = it.next(); - if (entry.getKey().getAccount() == account) { - count += entry.getValue().get(); - it.remove(); - } - } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": backlog message count=" + count); - return count; - } - - void finishBacklog() { - finishBacklog(false, null); - } - - private void pushToStack(final Message message) { - final String conversationUuid = message.getConversationUuid(); - if (notifications.containsKey(conversationUuid)) { - notifications.get(conversationUuid).add(message); - } else { - final ArrayList mList = new ArrayList<>(); - mList.add(message); - notifications.put(conversationUuid, mList); - } - } - - public void push(final Message message) { - synchronized (CATCHUP_LOCK) { - final XmppConnection connection = message.getConversation().getAccount().getXmppConnection(); - if (connection != null && connection.isWaitingForSmCatchup()) { - connection.incrementSmCatchupMessageCounter(); - pushFromBacklog(message); - } else { - pushNow(message); - } - } - } - - public void pushFailedDelivery(final Message message) { - final Conversation conversation = (Conversation) message.getConversation(); - final boolean isScreenLocked = !mXmppConnectionService.isScreenLocked(); - if (this.mIsInForeground && isScreenLocked && this.mOpenConversation == message.getConversation()) { - Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing failed delivery notification because conversation is open"); - return; - } - final PendingIntent pendingIntent = createContentIntent(conversation); - final int notificationId = generateRequestCode(conversation, 0) + DELIVERY_FAILED_NOTIFICATION_ID; - final int failedDeliveries = conversation.countFailedDeliveries(); - final Notification notification = - new Builder(mXmppConnectionService, DELIVERY_FAILED_CHANNEL_ID) - .setContentTitle(conversation.getName()) - .setAutoCancel(true) - .setSmallIcon(R.drawable.ic_error_white_24dp) - .setContentText(mXmppConnectionService.getResources().getQuantityText(R.plurals.some_messages_could_not_be_delivered, failedDeliveries)) - .setGroup("delivery_failed") - .setContentIntent(pendingIntent).build(); - final Notification summaryNotification = - new Builder(mXmppConnectionService, DELIVERY_FAILED_CHANNEL_ID) - .setContentTitle(mXmppConnectionService.getString(R.string.failed_deliveries)) - .setContentText(mXmppConnectionService.getResources().getQuantityText(R.plurals.some_messages_could_not_be_delivered, 1024)) - .setSmallIcon(R.drawable.ic_error_white_24dp) - .setGroup("delivery_failed") - .setGroupSummary(true) - .setAutoCancel(true) - .build(); - notify(notificationId, notification); - notify(DELIVERY_FAILED_NOTIFICATION_ID, summaryNotification); - } - - public void showIncomingCallNotification(final AbstractJingleConnection.Id id, final Set media, final String uuid) { - final Intent fullScreenIntent = new Intent(mXmppConnectionService, RtpSessionActivity.class); - fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString()); - fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString()); - fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId); - fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - Builder builder; - if (mXmppConnectionService.hasIndividualNotification(mXmppConnectionService.findConversationByUuid(uuid))) { - final String time = String.valueOf(mXmppConnectionService.getIndividualNotificationPreference(mXmppConnectionService.findConversationByUuid(uuid))); - builder = new Builder(mXmppConnectionService, INDIVIDUAL_NOTIFICATION_PREFIX + INCOMING_CALLS_CHANNEL_ID + "_" + uuid + "_" + time); - } else { - builder = new Builder(mXmppConnectionService, INCOMING_CALLS_CHANNEL_ID + "_" + DEFAULT); - } - if (media.contains(Media.VIDEO)) { - builder.setSmallIcon(R.drawable.ic_videocam_white_24dp); - builder.setContentTitle(mXmppConnectionService.getString(R.string.rtp_state_incoming_video_call)); - } else { - builder.setSmallIcon(R.drawable.ic_call_white_24dp); - builder.setContentTitle(mXmppConnectionService.getString(R.string.rtp_state_incoming_call)); - } - final Contact contact = id.getContact(); - builder.setLargeIcon(mXmppConnectionService.getAvatarService().get( - contact, - AvatarService.getSystemUiAvatarSize(mXmppConnectionService)) - ); - final Uri systemAccount = contact.getSystemAccount(); - if (systemAccount != null) { - builder.addPerson(systemAccount.toString()); - } - String string = id.account.getRoster().getContact(id.with).getDisplayName(); - if (mXmppConnectionService.multipleAccounts()) { - string += " ("; - string += id.account.getJid().asBareJid().toString(); - string += ")"; - } - builder.setContentText(string); - builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - builder.setPriority(NotificationCompat.PRIORITY_HIGH); - builder.setCategory(NotificationCompat.CATEGORY_CALL); - PendingIntent pendingIntent = createPendingRtpSession(id, Intent.ACTION_VIEW, 101); - builder.setFullScreenIntent(pendingIntent, true); - builder.setContentIntent(pendingIntent); //old androids need this? - builder.setOngoing(true); - final String dismissString = mXmppConnectionService.getString(R.string.dismiss_call); - final SpannableString dismiss = new SpannableString(dismissString); - dismiss.setSpan(new ForegroundColorSpan(mXmppConnectionService.getResources().getColor(R.color.red700)), 0, dismissString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - builder.addAction(new NotificationCompat.Action.Builder( - R.drawable.ic_call_end_white_48dp, - dismiss, - createCallAction(id.sessionId, XmppConnectionService.ACTION_DISMISS_CALL, 102)) - .build()); - final String acceptString = mXmppConnectionService.getString(R.string.answer_call); - final SpannableString accept = new SpannableString(acceptString); - accept.setSpan(new ForegroundColorSpan(mXmppConnectionService.getResources().getColor(R.color.green500)), 0, acceptString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - builder.addAction(new NotificationCompat.Action.Builder( - R.drawable.ic_call_white_24dp, - accept, - createPendingRtpSession(id, RtpSessionActivity.ACTION_ACCEPT_CALL, 103)) - .build()); - modifyIncomingCall(builder); - final Notification notification = builder.build(); - notification.flags = notification.flags | Notification.FLAG_INSISTENT; - notify(INCOMING_CALL_NOTIFICATION_ID, notification); - } - - public Notification getOngoingCallNotification(final XmppConnectionService.OngoingCall ongoingCall) { - final AbstractJingleConnection.Id id = ongoingCall.id; - final NotificationCompat.Builder builder = new NotificationCompat.Builder(mXmppConnectionService, ONGOING_CALLS_CHANNEL_ID); - if (ongoingCall.media.contains(Media.VIDEO)) { - builder.setSmallIcon(R.drawable.ic_videocam_white_24dp); - 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); - 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); - builder.setPriority(NotificationCompat.PRIORITY_HIGH); - builder.setCategory(NotificationCompat.CATEGORY_CALL); - builder.setContentIntent(createPendingRtpSession(id, Intent.ACTION_VIEW, 101)); - builder.setOngoing(true); - builder.addAction(new NotificationCompat.Action.Builder( - R.drawable.ic_call_end_white_48dp, - mXmppConnectionService.getString(R.string.hang_up), - createCallAction(id.sessionId, XmppConnectionService.ACTION_END_CALL, 104)) - .build()); - return builder.build(); - } - - private PendingIntent createPendingRtpSession(final AbstractJingleConnection.Id id, final String action, final int requestCode) { - final Intent fullScreenIntent = new Intent(mXmppConnectionService, RtpSessionActivity.class); - fullScreenIntent.setAction(action); - fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.account.getJid().asBareJid().toEscapedString()); - fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_WITH, id.with.toEscapedString()); - fullScreenIntent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.sessionId); - return PendingIntent.getActivity( - mXmppConnectionService, - requestCode, - fullScreenIntent, - s() - ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - } - - public void cancelIncomingCallNotification() { - stopSoundAndVibration(); - cancel(INCOMING_CALL_NOTIFICATION_ID); - } - - public boolean stopSoundAndVibration() { - int stopped = 0; - if (this.currentlyPlayingRingtone != null) { - if (this.currentlyPlayingRingtone.isPlaying()) { - Log.d(Config.LOGTAG, "stop playing ring tone"); - ++stopped; - } - this.currentlyPlayingRingtone.stop(); - } - if (this.vibrationFuture != null && !this.vibrationFuture.isCancelled()) { - Log.d(Config.LOGTAG, "stop vibration"); - this.vibrationFuture.cancel(true); - ++stopped; - } - return stopped > 0; - } - - public static void cancelIncomingCallNotification(final Context context) { - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); - try { - notificationManager.cancel(INCOMING_CALL_NOTIFICATION_ID); - } catch (RuntimeException e) { - Log.d(Config.LOGTAG, "unable to cancel incoming call notification after crash", e); - } - } - - private void pushNow(final Message message) { - mXmppConnectionService.updateUnreadCountBadge(); - if (!notifyMessage(message)) { - if (this.mIsInForeground && this.mOpenConversation == message.getConversation()) { - mXmppConnectionService.vibrate(); - } - Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing notification because turned off"); - return; - } - final boolean isScreenLocked = mXmppConnectionService.isScreenLocked(); - if (this.mIsInForeground && !isScreenLocked && this.mOpenConversation == message.getConversation()) { - mXmppConnectionService.vibrate(); - Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": suppressing notification because conversation is open"); - return; - } - if (this.mIsInForeground) { - mXmppConnectionService.vibrate(); - } - synchronized (notifications) { - pushToStack(message); - final Conversational conversation = message.getConversation(); - final Account account = conversation.getAccount(); - final boolean doNotify = (!(this.mIsInForeground && this.mOpenConversation == null) || isScreenLocked) - && !account.inGracePeriod() - && !this.inMiniGracePeriod(account); - updateNotification(doNotify, Collections.singletonList(conversation.getUuid())); - } - } - - private void pushMissedCall(final Message message) { - final Conversational conversation = message.getConversation(); - final MissedCallsInfo info = mMissedCalls.get(conversation); - if (info == null) { - mMissedCalls.put(conversation, new MissedCallsInfo(message.getTimeSent())); - } else { - info.newMissedCall(message.getTimeSent()); - } - } - - public void pushMissedCallNow(final Message message) { - synchronized (mMissedCalls) { - pushMissedCall(message); - updateMissedCallNotifications(Collections.singleton(message.getConversation())); - } - } - - public void clear(final Conversation conversation) { - clearMessages(conversation); - clearMissedCalls(conversation); - } - - public void clearMessages() { - synchronized (notifications) { - for (ArrayList messages : notifications.values()) { - markAsReadIfHasDirectReply(messages); - } - notifications.clear(); - updateNotification(false); - } - } - - public void clearMessages(final Conversation conversation) { - synchronized (this.mBacklogMessageCounter) { - this.mBacklogMessageCounter.remove(conversation); - } - synchronized (notifications) { - markAsReadIfHasDirectReply(conversation); - if (notifications.remove(conversation.getUuid()) != null) { - cancel(conversation.getUuid(), NOTIFICATION_ID); - updateNotification(false, null, true); - } - } - } - - public void clearMissedCalls() { - synchronized (mMissedCalls) { - for (final Conversational conversation : mMissedCalls.keySet()) { - cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID); - } - mMissedCalls.clear(); - updateMissedCallNotifications(null); - } - } - - public void clearMissedCalls(final Conversation conversation) { - synchronized (mMissedCalls) { - if (mMissedCalls.remove(conversation) != null) { - cancel(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID); - updateMissedCallNotifications(null); - } - } - } - - private void markAsReadIfHasDirectReply(final Conversation conversation) { - markAsReadIfHasDirectReply(notifications.get(conversation.getUuid())); - } - - private void markAsReadIfHasDirectReply(final ArrayList messages) { - if (messages != null && messages.size() > 0) { - Message last = messages.get(messages.size() - 1); - if (last.getStatus() != Message.STATUS_RECEIVED) { - if (mXmppConnectionService.markRead((Conversation) last.getConversation(), false)) { - mXmppConnectionService.updateConversationUi(); - } - } - } - } - - private void setNotificationColor(final Builder mBuilder) { - mBuilder.setColor(ContextCompat.getColor(mXmppConnectionService, ThemeHelper.notificationColor(mXmppConnectionService))); - } - - public void updateNotification() { - synchronized (notifications) { - updateNotification(false); - } - } - - private void updateNotification(final boolean notify) { - updateNotification(notify, null, false); - } - - private void updateNotification(final boolean notify, final List conversations) { - updateNotification(notify, conversations, false); - } - - private void updateNotification(final boolean notify, final List conversations, final boolean summaryOnly) { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); - final boolean quiteHours = isQuietHours(); - final boolean notifyOnlyOneChild = notify && conversations != null && conversations.size() == 1; //if this check is changed to > 0 catchup messages will create one notification per conversation - if (notifications.size() == 0) { - cancel(NOTIFICATION_ID); - } else { - if (notify) { - this.markLastNotification(); - } - final Builder mBuilder; - if (notifications.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - mBuilder = buildSingleConversations(notifications.values().iterator().next(), notify, quiteHours); - modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences); - notify(NOTIFICATION_ID, mBuilder.build()); - } else { - mBuilder = buildMultipleConversation(notify, quiteHours); - if (notifyOnlyOneChild) { - mBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); - } - modifyForSoundVibrationAndLight(mBuilder, notify, quiteHours, preferences); - if (!summaryOnly) { - for (Map.Entry> entry : notifications.entrySet()) { - String uuid = entry.getKey(); - final boolean notifyThis = notifyOnlyOneChild ? conversations.contains(uuid) : notify; - Builder singleBuilder = buildSingleConversations(entry.getValue(), notifyThis, quiteHours); - if (!notifyOnlyOneChild) { - singleBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY); - } - modifyForSoundVibrationAndLight(singleBuilder, notifyThis, quiteHours, preferences); - singleBuilder.setGroup(MESSAGES_GROUP); - setNotificationColor(singleBuilder); - notify(entry.getKey(), NOTIFICATION_ID, singleBuilder.build()); - } - } - notify(NOTIFICATION_ID, mBuilder.build()); - } - } - } - - private void updateMissedCallNotifications(final Set update) { - if (mMissedCalls.isEmpty()) { - cancel(MISSED_CALL_NOTIFICATION_ID); - return; - } - if (mMissedCalls.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - final Conversational conversation = mMissedCalls.keySet().iterator().next(); - final MissedCallsInfo info = mMissedCalls.values().iterator().next(); - final Notification notification = missedCall(conversation, info); - notify(MISSED_CALL_NOTIFICATION_ID, notification); - } else { - final Notification summary = missedCallsSummary(); - notify(MISSED_CALL_NOTIFICATION_ID, summary); - if (update != null) { - for (final Conversational conversation : update) { - final MissedCallsInfo info = mMissedCalls.get(conversation); - if (info != null) { - final Notification notification = missedCall(conversation, info); - notify(conversation.getUuid(), MISSED_CALL_NOTIFICATION_ID, notification); - } - } - } - } - } - - private void modifyForSoundVibrationAndLight(Builder mBuilder, boolean notify, boolean quietHours, SharedPreferences preferences) { - final Resources resources = mXmppConnectionService.getResources(); - final String ringtone = preferences.getString("notification_ringtone", resources.getString(R.string.notification_ringtone)); - final boolean vibrate = preferences.getBoolean("vibrate_on_notification", resources.getBoolean(R.bool.vibrate_on_notification)); - final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led)); - final boolean headsup = preferences.getBoolean("notification_headsup", resources.getBoolean(R.bool.headsup_notifications)); - if (notify && !quietHours) { - if (vibrate) { - mBuilder.setVibrate(MESSAGE_PATTERN); - } else { - mBuilder.setVibrate(new long[]{0}); - } - Uri uri = Uri.parse(ringtone); - try { - mBuilder.setSound(fixRingtoneUri(uri)); - } catch (SecurityException e) { - Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString()); - } - } else { - mBuilder.setLocalOnly(true); - } - mBuilder.setCategory(Notification.CATEGORY_MESSAGE); - mBuilder.setPriority(notify ? (headsup ? NotificationCompat.PRIORITY_HIGH : NotificationCompat.PRIORITY_DEFAULT) : NotificationCompat.PRIORITY_LOW); - setNotificationColor(mBuilder); - mBuilder.setDefaults(0); - if (led) { - mBuilder.setLights(LED_COLOR, 2000, 3000); - } - } - - private void modifyIncomingCall(Builder mBuilder) { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); - final Resources resources = mXmppConnectionService.getResources(); - final String ringtone = preferences.getString("call_ringtone", resources.getString(R.string.incoming_call_ringtone)); - mBuilder.setVibrate(CALL_PATTERN); - final Uri uri = Uri.parse(ringtone); - try { - mBuilder.setSound(fixRingtoneUri(uri)); - } catch (SecurityException e) { - Log.d(Config.LOGTAG, "unable to use custom notification sound " + uri.toString()); - } - mBuilder.setPriority(NotificationCompat.PRIORITY_HIGH); - setNotificationColor(mBuilder); - mBuilder.setLights(LED_COLOR, 2000, 3000); - } - - private Uri fixRingtoneUri(Uri uri) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && "file".equals(uri.getScheme())) { - return FileBackend.getUriForFile(mXmppConnectionService, new File(uri.getPath())); - } else { - return uri; - } - } - - private Notification missedCallsSummary() { - final Builder publicBuilder = buildMissedCallsSummary(true); - final Builder builder = buildMissedCallsSummary(false); - builder.setPublicVersion(publicBuilder.build()); - return builder.build(); - } - - private Builder buildMissedCallsSummary(boolean publicVersion) { - final Builder builder = - new NotificationCompat.Builder(mXmppConnectionService, MISSED_CALLS_CHANNEL_ID); - int totalCalls = 0; - final List names = new ArrayList<>(); - long lastTime = 0; - for (final Map.Entry entry : mMissedCalls.entrySet()) { - final Conversational conversation = entry.getKey(); - final MissedCallsInfo missedCallsInfo = entry.getValue(); - names.add(conversation.getContact().getDisplayName()); - totalCalls += missedCallsInfo.getNumberOfCalls(); - lastTime = Math.max(lastTime, missedCallsInfo.getLastTime()); - } - final String title = - (totalCalls == 1) - ? mXmppConnectionService.getString(R.string.missed_call) - : (mMissedCalls.size() == 1) - ? mXmppConnectionService - .getResources() - .getQuantityString( - R.plurals.n_missed_calls, totalCalls, totalCalls) - : mXmppConnectionService - .getResources() - .getQuantityString( - R.plurals.n_missed_calls_from_m_contacts, - mMissedCalls.size(), - totalCalls, - mMissedCalls.size()); - builder.setContentTitle(title); - builder.setTicker(title); - if (!publicVersion) { - builder.setContentText(Joiner.on(", ").join(names)); - } - builder.setSmallIcon(R.drawable.ic_missed_call_notification); - builder.setGroupSummary(true); - builder.setGroup(MISSED_CALLS_GROUP); - builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN); - builder.setCategory(NotificationCompat.CATEGORY_CALL); - builder.setWhen(lastTime); - if (!mMissedCalls.isEmpty()) { - final Conversational firstConversation = mMissedCalls.keySet().iterator().next(); - builder.setContentIntent(createContentIntent(firstConversation)); - } - builder.setDeleteIntent(createMissedCallsDeleteIntent(null)); - modifyMissedCall(builder); - return builder; - } - - private Notification missedCall(final Conversational conversation, final MissedCallsInfo info) { - final Builder publicBuilder = buildMissedCall(conversation, info, true); - final Builder builder = buildMissedCall(conversation, info, false); - builder.setPublicVersion(publicBuilder.build()); - return builder.build(); - } - - private Builder buildMissedCall( - final Conversational conversation, final MissedCallsInfo info, boolean publicVersion) { - final Builder builder = - new NotificationCompat.Builder(mXmppConnectionService, MISSED_CALLS_CHANNEL_ID); - final String title = - (info.getNumberOfCalls() == 1) - ? mXmppConnectionService.getString(R.string.missed_call) - : mXmppConnectionService - .getResources() - .getQuantityString( - R.plurals.n_missed_calls, - info.getNumberOfCalls(), - info.getNumberOfCalls()); - builder.setContentTitle(title); - final String name = conversation.getContact().getDisplayName(); - if (publicVersion) { - builder.setTicker(title); - } else { - builder.setTicker( - mXmppConnectionService - .getResources() - .getQuantityString( - R.plurals.n_missed_calls_from_x, - info.getNumberOfCalls(), - info.getNumberOfCalls(), - name)); - builder.setContentText(name); - } - builder.setSmallIcon(R.drawable.ic_missed_call_notification); - builder.setGroup(MISSED_CALLS_GROUP); - builder.setCategory(NotificationCompat.CATEGORY_CALL); - builder.setWhen(info.getLastTime()); - builder.setContentIntent(createContentIntent(conversation)); - builder.setDeleteIntent(createMissedCallsDeleteIntent(conversation)); - if (!publicVersion && conversation instanceof Conversation) { - builder.setLargeIcon( - mXmppConnectionService - .getAvatarService() - .get( - (Conversation) conversation, - AvatarService.getSystemUiAvatarSize(mXmppConnectionService))); - } - modifyMissedCall(builder); - return builder; - } - - private void modifyMissedCall(final Builder builder) { - final SharedPreferences preferences = - PreferenceManager.getDefaultSharedPreferences(mXmppConnectionService); - final Resources resources = mXmppConnectionService.getResources(); - final boolean led = preferences.getBoolean("led", resources.getBoolean(R.bool.led)); - if (led) { - builder.setLights(LED_COLOR, 2000, 3000); - } - builder.setPriority(NotificationCompat.PRIORITY_HIGH); - builder.setSound(null); - setNotificationColor(builder); - } - - private Builder buildMultipleConversation(final boolean notify, final boolean quietHours) { - Builder mBuilder = null; - final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); - style.setBigContentTitle(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_unread_conversations, notifications.size(), notifications.size())); - final List names = new ArrayList<>(); - Conversation conversation = null; - for (final ArrayList messages : notifications.values()) { - if (messages.isEmpty()) { - continue; - } - conversation = (Conversation) messages.get(0).getConversation(); - if (quietHours) { - mBuilder = new Builder(mXmppConnectionService, QUIET_HOURS_CHANNEL_ID); - } else if (notify) { - if (mXmppConnectionService.hasIndividualNotification(conversation)) { - final String time = String.valueOf(mXmppConnectionService.getIndividualNotificationPreference(conversation)); - mBuilder = new Builder(mXmppConnectionService, INDIVIDUAL_NOTIFICATION_PREFIX + MESSAGES_CHANNEL_ID + "_" + conversation.getUuid() + "_" + time); - } else { - mBuilder = new Builder(mXmppConnectionService, MESSAGES_CHANNEL_ID + "_" + DEFAULT); - } - } else { - mBuilder = new Builder(mXmppConnectionService, SILENT_MESSAGES_CHANNEL_ID); - } - final String name = conversation.getName().toString(); - SpannableString styledString; - if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) { - int count = messages.size(); - styledString = new SpannableString(name + ": " + mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count)); - styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0); - style.addLine(styledString); - } else { - styledString = new SpannableString(name + ": " + UIHelper.getMessagePreview(mXmppConnectionService, messages.get(0)).first); - styledString.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length(), 0); - style.addLine(styledString); - } - names.add(name); - } - final String contentTitle = mXmppConnectionService.getResources().getQuantityString(R.plurals.x_unread_conversations, notifications.size(), notifications.size()); - mBuilder.setContentTitle(contentTitle); - mBuilder.setTicker(contentTitle); - mBuilder.setContentText(Joiner.on(", ").join(names)); - mBuilder.setStyle(style); - if (conversation != null) { - mBuilder.setContentIntent(createOpenConversationsIntent()); - } - mBuilder.setGroupSummary(true); - mBuilder.setGroup(MESSAGES_GROUP); - mBuilder.setDeleteIntent(createDeleteIntent(null)); - mBuilder.setSmallIcon(R.drawable.ic_notification); - return mBuilder; - } - - private Builder buildSingleConversations(final ArrayList messages, final boolean notify, final boolean quietHours) { - Builder mBuilder = null; - if (messages.size() >= 1) { - final Conversation conversation = (Conversation) messages.get(0).getConversation(); - if (quietHours) { - mBuilder = new Builder(mXmppConnectionService, QUIET_HOURS_CHANNEL_ID); - } else { - if (notify) { - if (mXmppConnectionService.hasIndividualNotification(conversation)) { - final String time = String.valueOf(mXmppConnectionService.getIndividualNotificationPreference(conversation)); - mBuilder = new Builder(mXmppConnectionService, INDIVIDUAL_NOTIFICATION_PREFIX + MESSAGES_CHANNEL_ID + "_" + conversation.getUuid() + "_" + time); - } else { - mBuilder = new Builder(mXmppConnectionService, MESSAGES_CHANNEL_ID + "_" + DEFAULT); - } - } else { - mBuilder = new Builder(mXmppConnectionService, SILENT_MESSAGES_CHANNEL_ID); - } - } - mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService().get(conversation, AvatarService.getSystemUiAvatarSize(mXmppConnectionService))); - mBuilder.setContentTitle(conversation.getName()); - if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) { - int count = messages.size(); - mBuilder.setContentText(mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count)); - } else { - Message message; - //TODO starting with Android 9 we might want to put images in MessageStyle - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P && (message = getImage(messages)) != null) { - modifyForImage(mBuilder, message, messages); - } else { - modifyForTextOnly(mBuilder, messages); - } - RemoteInput remoteInput = new RemoteInput.Builder("text_reply").setLabel(UIHelper.getMessageHint(mXmppConnectionService, conversation)).build(); - PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation); - NotificationCompat.Action markReadAction = new NotificationCompat.Action.Builder( - R.drawable.ic_email_open_outline_white_24dp, - mXmppConnectionService.getString(R.string.mark_as_read), - markAsReadPendingIntent) - .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) - .setShowsUserInterface(false) - .build(); - final String replyLabel = mXmppConnectionService.getString(R.string.reply); - final String lastMessageUuid = Iterables.getLast(messages).getUuid(); - final NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder( - R.drawable.ic_reply_white_24dp, - replyLabel, - createReplyIntent(conversation, lastMessageUuid, false)) - .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) - .setShowsUserInterface(false) - .addRemoteInput(remoteInput).build(); - final NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply, - replyLabel, - createReplyIntent(conversation, lastMessageUuid, true)).addRemoteInput(remoteInput).build(); - mBuilder.extend(new NotificationCompat.WearableExtender().addAction(wearReplyAction)); - int addedActionsCount = 1; - mBuilder.addAction(markReadAction); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mBuilder.addAction(replyAction); - ++addedActionsCount; - } - if (displaySnoozeAction(messages)) { - String label = mXmppConnectionService.getString(R.string.snooze); - PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation); - NotificationCompat.Action snoozeAction = new NotificationCompat.Action.Builder( - R.drawable.ic_notifications_paused_white_24dp, - label, - pendingSnoozeIntent).build(); - mBuilder.addAction(snoozeAction); - ++addedActionsCount; - } - if (addedActionsCount < 3) { - final Message firstLocationMessage = getFirstLocationMessage(messages); - if (firstLocationMessage != null) { - final PendingIntent pendingShowLocationIntent = createShowLocationIntent(firstLocationMessage); - if (pendingShowLocationIntent != null) { - final String label = mXmppConnectionService.getResources().getString(R.string.show_location); - NotificationCompat.Action locationAction = new NotificationCompat.Action.Builder( - R.drawable.ic_room_white_24dp, - label, - pendingShowLocationIntent).build(); - mBuilder.addAction(locationAction); - ++addedActionsCount; - } - } - } - if (addedActionsCount < 3) { - Message firstDownloadableMessage = getFirstDownloadableMessage(messages); - if (firstDownloadableMessage != null) { - String label = mXmppConnectionService.getResources().getString(R.string.download_x_file, UIHelper.getFileDescriptionString(mXmppConnectionService, firstDownloadableMessage)); - PendingIntent pendingDownloadIntent = createDownloadIntent(firstDownloadableMessage); - NotificationCompat.Action downloadAction = new NotificationCompat.Action.Builder( - R.drawable.ic_file_download_white_24dp, - label, - pendingDownloadIntent).build(); - mBuilder.addAction(downloadAction); - ++addedActionsCount; - } - } - } - if (conversation.getMode() == Conversation.MODE_SINGLE) { - Contact contact = conversation.getContact(); - Uri systemAccount = contact.getSystemAccount(); - if (systemAccount != null) { - mBuilder.addPerson(systemAccount.toString()); - } - } - mBuilder.setWhen(conversation.getLatestMessage().getTimeSent()); - mBuilder.setSmallIcon(R.drawable.ic_notification); - mBuilder.setDeleteIntent(createDeleteIntent(conversation)); - mBuilder.setContentIntent(createContentIntent(conversation)); - } - return mBuilder; - } - - private void modifyForImage(final Builder builder, final Message message, final ArrayList messages) { - try { - final Bitmap bitmap = mXmppConnectionService.getFileBackend().getThumbnail(message, getPixel(288), false); - final ArrayList tmp = new ArrayList<>(); - for (final Message msg : messages) { - if (msg.getType() == Message.TYPE_TEXT - && msg.getTransferable() == null) { - tmp.add(msg); - } - } - final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle(); - bigPictureStyle.bigPicture(bitmap); - if (tmp.size() > 0) { - CharSequence text = getMergedBodies(tmp); - bigPictureStyle.setSummaryText(text); - builder.setContentText(text); - builder.setTicker(text); - } else { - final String description = UIHelper.getFileDescriptionString(mXmppConnectionService, message); - builder.setContentText(description); - builder.setTicker(description); - } - builder.setStyle(bigPictureStyle); - } catch (final IOException e) { - modifyForTextOnly(builder, messages); - } - } - - private Person getPerson(Message message) { - final Contact contact = message.getContact(); - final Person.Builder builder = new Person.Builder(); - if (contact != null) { - builder.setName(contact.getDisplayName()); - final Uri uri = contact.getSystemAccount(); - if (uri != null) { - builder.setUri(uri.toString()); - } - } else { - builder.setName(UIHelper.getColoredUsername(mXmppConnectionService, message)); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - builder.setIcon(IconCompat.createWithBitmap(mXmppConnectionService.getAvatarService().get(message, AvatarService.getSystemUiAvatarSize(mXmppConnectionService), false))); - } - return builder.build(); - } - - private void modifyForTextOnly(final Builder builder, final ArrayList messages) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - final Conversation conversation = (Conversation) messages.get(0).getConversation(); - final Person.Builder meBuilder = new Person.Builder().setName(mXmppConnectionService.getString(R.string.me)); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - meBuilder.setIcon(IconCompat.createWithBitmap(mXmppConnectionService.getAvatarService().get(conversation.getAccount(), AvatarService.getSystemUiAvatarSize(mXmppConnectionService)))); - } - final Person me = meBuilder.build(); - NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(me); - final boolean multiple = conversation.getMode() == Conversation.MODE_MULTI; - if (multiple) { - messagingStyle.setConversationTitle(conversation.getName()); - } - for (Message message : messages) { - final Person sender = message.getStatus() == Message.STATUS_RECEIVED ? getPerson(message) : null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && isImageMessage(message)) { - final Uri dataUri = FileBackend.getMediaUri(mXmppConnectionService, mXmppConnectionService.getFileBackend().getFile(message)); - NotificationCompat.MessagingStyle.Message imageMessage = new NotificationCompat.MessagingStyle.Message(UIHelper.getMessagePreview(mXmppConnectionService, message).first, message.getTimeSent(), sender); - if (dataUri != null) { - imageMessage.setData(message.getMimeType(), dataUri); - } - messagingStyle.addMessage(imageMessage); - } else { - messagingStyle.addMessage(UIHelper.getMessagePreview(mXmppConnectionService, message).first, message.getTimeSent(), sender); - } - } - messagingStyle.setGroupConversation(multiple); - builder.setStyle(messagingStyle); - } else { - if (messages.get(0).getConversation().getMode() == Conversation.MODE_SINGLE) { - builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages))); - final CharSequence preview = UIHelper.getMessagePreview(mXmppConnectionService, messages.get(messages.size() - 1)).first; - builder.setContentText(preview); - builder.setTicker(preview); - builder.setNumber(messages.size()); - } else { - final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); - SpannableString styledString; - for (Message message : messages) { - final SpannableString name = UIHelper.getColoredUsername(mXmppConnectionService, message); - styledString = new SpannableString(name + ": " + replaceYoutube(mXmppConnectionService, message.getBody())); - style.addLine(styledString); - } - builder.setStyle(style); - int count = messages.size(); - if (count == 1) { - final SpannableString name = UIHelper.getColoredUsername(mXmppConnectionService, messages.get(0)); - styledString = new SpannableString(name + ": " + replaceYoutube(mXmppConnectionService, messages.get(0).getBody())); - builder.setContentText(styledString); - builder.setTicker(styledString); - } else { - final String text = mXmppConnectionService.getResources().getQuantityString(R.plurals.x_messages, count, count); - builder.setContentText(text); - builder.setTicker(text); - } - } - } - } - - private Message getImage(final Iterable messages) { - Message image = null; - for (final Message message : messages) { - if (message.getStatus() != Message.STATUS_RECEIVED) { - return null; - } - if (isImageMessage(message)) { - image = message; - } - } - return image; - } - - private static boolean isImageMessage(Message message) { - return message.getType() != Message.TYPE_TEXT - && message.getTransferable() == null - && !message.isFileDeleted() - && message.getEncryption() != Message.ENCRYPTION_PGP - && message.getFileParams().height > 0; - } - - private Message getFirstDownloadableMessage(final Iterable messages) { - for (final Message message : messages) { - if (message.getTransferable() != null || (message.getType() == Message.TYPE_TEXT && message.treatAsDownloadable())) { - return message; - } - } - return null; - } - - private Message getFirstLocationMessage(final Iterable messages) { - for (final Message message : messages) { - if (message.isGeoUri()) { - return message; - } - } - return null; - } - - private CharSequence getMergedBodies(final ArrayList messages) { - final StringBuilder text = new StringBuilder(); - for (Message message : messages) { - if (text.length() != 0) { - text.append("\n"); - } - text.append(UIHelper.getMessagePreview(mXmppConnectionService, message).first); - } - return text.toString(); - } - - private PendingIntent createShowLocationIntent(final Message message) { - Iterable intents = GeoHelper.createGeoIntentsFromMessage(mXmppConnectionService, message); - for (final Intent intent : intents) { - if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) { - return PendingIntent.getActivity(mXmppConnectionService, generateRequestCode(message.getConversation(), 18), intent, s() ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - } - } - return null; - } - - private PendingIntent createContentIntent(final String conversationUuid, final String downloadMessageUuid) { - final Intent viewConversationIntent = new Intent(mXmppConnectionService, ConversationsActivity.class); - viewConversationIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION); - viewConversationIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversationUuid); - if (downloadMessageUuid != null) { - viewConversationIntent.putExtra(ConversationsActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid); - return PendingIntent.getActivity(mXmppConnectionService, - generateRequestCode(conversationUuid, 8), - viewConversationIntent, - s() - ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - } else { - return PendingIntent.getActivity(mXmppConnectionService, - generateRequestCode(conversationUuid, 10), - viewConversationIntent, - s() - ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - } - } - - private int generateRequestCode(String uuid, int actionId) { - return (actionId * NOTIFICATION_ID_MULTIPLIER) + (uuid.hashCode() % NOTIFICATION_ID_MULTIPLIER); - } - - private int generateRequestCode(Conversational conversation, int actionId) { - return generateRequestCode(conversation.getUuid(), actionId); - } - - private PendingIntent createDownloadIntent(final Message message) { - return createContentIntent(message.getConversationUuid(), message.getUuid()); - } - - private PendingIntent createContentIntent(final Conversational conversation) { - return createContentIntent(conversation.getUuid(), null); - } - - private PendingIntent createDeleteIntent(final Conversation conversation) { - final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_CLEAR_MESSAGE_NOTIFICATION); - if (conversation != null) { - intent.putExtra("uuid", conversation.getUuid()); - return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 20), intent, s() - ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - } - return PendingIntent.getService(mXmppConnectionService, 0, intent, s() - ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - } - - private PendingIntent createMissedCallsDeleteIntent(final Conversational conversation) { - final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_CLEAR_MISSED_CALL_NOTIFICATION); - if (conversation != null) { - intent.putExtra("uuid", conversation.getUuid()); - return PendingIntent.getService( - mXmppConnectionService, - generateRequestCode(conversation, 21), - intent, - s() - ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - } - return PendingIntent.getService( - mXmppConnectionService, - 1, - intent, - s() - ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - } - - 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, s() - ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - } - - private PendingIntent createReadPendingIntent(Conversation conversation) { - final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_MARK_AS_READ); - intent.putExtra("uuid", conversation.getUuid()); - intent.setPackage(mXmppConnectionService.getPackageName()); - return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 16), intent, s() - ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - } - - private PendingIntent createCallAction(String sessionId, final String action, int requestCode) { - final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); - intent.setAction(action); - intent.setPackage(mXmppConnectionService.getPackageName()); - intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, sessionId); - return PendingIntent.getService(mXmppConnectionService, requestCode, intent, s() - ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - } - - private PendingIntent createSnoozeIntent(Conversation conversation) { - final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_SNOOZE); - intent.putExtra("uuid", conversation.getUuid()); - intent.setPackage(mXmppConnectionService.getPackageName()); - return PendingIntent.getService(mXmppConnectionService, generateRequestCode(conversation, 22), intent, s() - ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - } - - private PendingIntent createTryAgainIntent() { - final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN); - return PendingIntent.getService(mXmppConnectionService, 45, intent, s() - ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - } - - private PendingIntent createDismissErrorIntent() { - final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS); - return PendingIntent.getService(mXmppConnectionService, 69, intent, s() - ? PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - } - - private boolean wasHighlightedOrPrivate(final Message message) { - if (message.getConversation() instanceof Conversation) { - Conversation conversation = (Conversation) message.getConversation(); - final String nick = conversation.getMucOptions().getActualNick(); - final Pattern highlight = generateNickHighlightPattern(nick); - if (message.getBody() == null || nick == null) { - return false; - } - final Matcher m = highlight.matcher(message.getBody()); - return (m.find() || message.isPrivateMessage()); - } else { - return false; - } - } - - public void setOpenConversation(final Conversation conversation) { - this.mOpenConversation = conversation; - } - - public void setIsInForeground(final boolean foreground) { - this.mIsInForeground = foreground; - } - - private int getPixel(final int dp) { - final DisplayMetrics metrics = mXmppConnectionService.getResources() - .getDisplayMetrics(); - return ((int) (dp * metrics.density)); - } - - private void markLastNotification() { - this.mLastNotification = SystemClock.elapsedRealtime(); - } - - private boolean inMiniGracePeriod(final Account account) { - final int miniGrace = account.getStatus() == Account.State.ONLINE ? Config.MINI_GRACE_PERIOD - : Config.MINI_GRACE_PERIOD * 2; - return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace); - } - - Notification createForegroundNotification() { - final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService); - mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service)); - String status; - final List accounts = mXmppConnectionService.getAccounts(); - int enabled = 0; - int connected = 0; - if (accounts != null) { - for (Account account : accounts) { - if (account.isOnlineAndConnected()) { - connected++; - enabled++; - } else if (account.isEnabled()) { - enabled++; - } - } - if (accounts.size() == 1) { - Account mAccount = accounts.get(0); - if (mAccount.getStatus() == Account.State.ONLINE) { - status = "(" + mXmppConnectionService.getString(R.string.account_status_online) + ")"; - status = " " + status; - Log.d(Config.LOGTAG, "Status: " + status); - mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service) + status); - } else if (mAccount.getStatus() == Account.State.CONNECTING) { - status = "(" + mXmppConnectionService.getString(R.string.account_status_connecting) + ")"; - status = " " + status; - Log.d(Config.LOGTAG, "Status: " + status); - mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service) + status); - } else { - status = "(" + mXmppConnectionService.getString(R.string.account_status_offline) + ")"; - status = " " + status; - Log.d(Config.LOGTAG, "Status: " + status); - mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service) + status); - } - } else if (accounts.size() > 1) { - mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service)); - } else { - status = "(" + mXmppConnectionService.getString(R.string.account_status_offline) + ")"; - status = " " + status; - Log.d(Config.LOGTAG, "Status: " + status); - mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service) + status); - } - } - mBuilder.setContentText(mXmppConnectionService.getString(R.string.connected_accounts, connected, enabled)); - mBuilder.setContentIntent(createOpenConversationsIntent()); - final PendingIntent openIntent = createOpenConversationsIntent(); - if (openIntent != null) { - mBuilder.setContentIntent(openIntent); - } - mBuilder.setWhen(0); - mBuilder.setPriority(Notification.PRIORITY_MIN); - mBuilder.setSmallIcon(connected > 0 ? R.drawable.ic_link_white_24dp : R.drawable.ic_link_off_white_24dp); - mBuilder.setLocalOnly(true); - if (Compatibility.runsTwentySix()) { - mBuilder.setChannelId(FOREGROUND_CHANNEL_ID); - } - return mBuilder.build(); - } - - private PendingIntent createOpenConversationsIntent() { - try { - return PendingIntent.getActivity( - mXmppConnectionService, - 0, - new Intent(mXmppConnectionService, ConversationsActivity.class), - s() - ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - } catch (RuntimeException e) { - e.printStackTrace(); - return null; - } - } - - void updateErrorNotification() { - if (Config.SUPPRESS_ERROR_NOTIFICATION) { - cancel(ERROR_NOTIFICATION_ID); - return; - } - final boolean showAllErrors = QuickConversationsService.isConversations(); - final List errors = new ArrayList<>(); - boolean torNotAvailable = false; - for (final Account account : mXmppConnectionService.getAccounts()) { - if (account.hasErrorStatus() && account.showErrorNotification() && (showAllErrors || account.getLastErrorStatus() == Account.State.UNAUTHORIZED)) { - errors.add(account); - torNotAvailable |= account.getStatus() == Account.State.TOR_NOT_AVAILABLE; - } - } - if (mXmppConnectionService.foregroundNotificationNeedsUpdatingWhenErrorStateChanges()) { - notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification()); - } - final Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService); - if (errors.size() == 0) { - cancel(ERROR_NOTIFICATION_ID); - return; - } else if (errors.size() == 1) { - mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_account)); - mBuilder.setContentText(errors.get(0).getJid().asBareJid().toEscapedString()); - } else { - mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_accounts)); - mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix)); - } - mBuilder.addAction(R.drawable.ic_autorenew_white_24dp, - mXmppConnectionService.getString(R.string.try_again), - createTryAgainIntent() - ); - if (torNotAvailable) { - if (TorServiceUtils.isOrbotInstalled(mXmppConnectionService)) { - mBuilder.addAction( - R.drawable.ic_play_circle_filled_white_48dp, - mXmppConnectionService.getString(R.string.start_orbot), - PendingIntent.getActivity( - mXmppConnectionService, - 147, - TorServiceUtils.LAUNCH_INTENT, - s() - ? PendingIntent.FLAG_IMMUTABLE - | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT)); - } else { - mBuilder.addAction( - R.drawable.ic_file_download_white_24dp, - mXmppConnectionService.getString(R.string.install_orbot), - PendingIntent.getActivity( - mXmppConnectionService, - 146, - TorServiceUtils.INSTALL_INTENT, - s() - ? PendingIntent.FLAG_IMMUTABLE - | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT)); - } - } - mBuilder.setDeleteIntent(createDismissErrorIntent()); - mBuilder.setVisibility(Notification.VISIBILITY_PRIVATE); - mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp); - mBuilder.setLocalOnly(true); - mBuilder.setPriority(Notification.PRIORITY_LOW); - final Intent intent; - if (AccountUtils.MANAGE_ACCOUNT_ACTIVITY != null) { - intent = new Intent(mXmppConnectionService, AccountUtils.MANAGE_ACCOUNT_ACTIVITY); - } else { - intent = new Intent(mXmppConnectionService, EditAccountActivity.class); - intent.putExtra("jid", errors.get(0).getJid().asBareJid().toEscapedString()); - intent.putExtra(EditAccountActivity.EXTRA_OPENED_FROM_NOTIFICATION, true); - } - mBuilder.setContentIntent(PendingIntent.getActivity(mXmppConnectionService, 145, intent, s() - ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT)); - if (Compatibility.runsTwentySix()) { - mBuilder.setChannelId(ERROR_CHANNEL_ID); - } - notify(ERROR_NOTIFICATION_ID, mBuilder.build()); - } - - void updateFileAddingNotification(int current, Message message) { - Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService); - mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.transcoding_video)); - mBuilder.setProgress(100, current, false); - mBuilder.setSmallIcon(R.drawable.ic_hourglass_empty_white_24dp); - mBuilder.setContentIntent(createContentIntent(message.getConversation())); - mBuilder.setOngoing(true); - if (Compatibility.runsTwentySix()) { - mBuilder.setChannelId(VIDEOCOMPRESSION_CHANNEL_ID); - } - Notification notification = mBuilder.build(); - notify(FOREGROUND_NOTIFICATION_ID, notification); - } - - Notification AppUpdateNotification(PendingIntent intent, String version, String filesize) { - Notification.Builder mBuilder = new Notification.Builder(mXmppConnectionService); - mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.app_name)); - mBuilder.setContentText(String.format(mXmppConnectionService.getString(R.string.update_available), version, filesize)); - mBuilder.setSmallIcon(R.drawable.ic_update_notification); - mBuilder.setContentIntent(intent); - mBuilder.setOngoing(true); - if (Compatibility.runsTwentySix()) { - mBuilder.setChannelId(UPDATE_CHANNEL_ID); - } - return mBuilder.build(); - } - - public void AppUpdateServiceNotification(Notification notification) { - notify(UPDATE_NOTIFICATION_ID, notification); - } - - private void notify(String tag, int id, Notification notification) { - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService); - try { - notificationManager.notify(tag, id, notification); - } catch (RuntimeException e) { - Log.d(Config.LOGTAG, "unable to make notification", e); - } - } - - public void notify(int id, Notification notification) { - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService); - try { - notificationManager.notify(id, notification); - } catch (RuntimeException e) { - Log.d(Config.LOGTAG, "unable to make notification", e); - } - } - - public void cancel(int id) { - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService); - try { - notificationManager.cancel(id); - } catch (RuntimeException e) { - Log.d(Config.LOGTAG, "unable to cancel notification", e); - } - } - - private void cancel(String tag, int id) { - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService); - try { - notificationManager.cancel(tag, id); - } catch (RuntimeException e) { - Log.d(Config.LOGTAG, "unable to cancel notification", e); - } - } - - private static class MissedCallsInfo { - private int numberOfCalls; - private long lastTime; - - MissedCallsInfo(final long time) { - numberOfCalls = 1; - lastTime = time; - } - - public void newMissedCall(final long time) { - ++numberOfCalls; - lastTime = time; - } - - public int getNumberOfCalls() { - return numberOfCalls; - } - - public long getLastTime() { - return lastTime; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/services/ProviderService.java b/src/main/java/eu/siacs/conversations/services/ProviderService.java deleted file mode 100644 index 3947c97b9..000000000 --- a/src/main/java/eu/siacs/conversations/services/ProviderService.java +++ /dev/null @@ -1,118 +0,0 @@ -package eu.siacs.conversations.services; - -import android.os.AsyncTask; -import android.util.Log; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.Objects; -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.http.HttpConnectionManager; - -public class ProviderService extends AsyncTask { - public static List providers = new ArrayList<>(); - - // in accordance with cat B (https://invent.kde.org/melvo/xmpp-providers/) - public static boolean REGISTRATION = true; - public static boolean FREE = true; - public static int COMPLIANCE = 90; - public static String RATING = "A"; - - public ProviderService() { - } - - public static List getProviders() { - final HashSet provider = new HashSet<>(Config.DOMAIN.DOMAINS); - if (!providers.isEmpty()) { - provider.addAll(providers); - } - return new ArrayList<>(provider); - } - - @Override - protected Boolean doInBackground(String... params) { - StringBuilder jsonString = new StringBuilder(); - boolean isError = false; - try { - Log.d(Config.LOGTAG, "ProviderService: Updating provider list from " + Config.PROVIDER_URL); - final InputStream is = HttpConnectionManager.open(Config.PROVIDER_URL, false, false); - BufferedReader reader = new BufferedReader(new InputStreamReader(is)); - String line; - while ((line = reader.readLine()) != null) { - jsonString.append(line); - } - is.close(); - reader.close(); - } catch (Exception e) { - e.printStackTrace(); - isError = true; - } - - try { - parseJson(new JSONObject(jsonString.toString())); - } catch (JSONException e) { - e.printStackTrace(); - isError = true; - } - if (isError) { - Log.d(Config.LOGTAG, "ProviderService: Updating provider list failed"); - } - return !isError; - } - - private void parseJson(JSONObject jsonObject) { - if (jsonObject != null) { - try { - for (int i = 0; i < jsonObject.length(); i++) { - boolean inBandRegistration = false; - boolean freeOfCharge = false; - String ratingC2S = null; - String ratingS2S = null; - int ratingXmppComplianceTester = 0; - final String provider = Objects.requireNonNull(jsonObject.names()).getString(i); - if (provider.length() > 0) { - final JSONObject json = new JSONObject(jsonObject.get(provider).toString()); - for (int ii = 0; ii < json.length(); ii++) { - String featureName = Objects.requireNonNull(json.names()).getString(ii); - final JSONObject subjson = new JSONObject(json.get(Objects.requireNonNull(json.names()).getString(ii)).toString()); - if (featureName.equals("inBandRegistration")) { - inBandRegistration = subjson.getBoolean("content"); - } - if (featureName.equals("ratingXmppComplianceTester")) { - ratingXmppComplianceTester = subjson.getInt("content"); - } - if (featureName.equals("freeOfCharge")) { - freeOfCharge = subjson.getBoolean("content"); - } - if (featureName.equals("ratingImObservatoryClientToServer")) { - ratingC2S = subjson.getString("content"); - } - if (featureName.equals("ratingImObservatoryServerToServer")) { - ratingS2S = subjson.getString("content"); - } - if (!Config.DOMAIN.BLACKLISTED_DOMAINS.contains(provider) - && inBandRegistration == REGISTRATION - && ratingXmppComplianceTester >= COMPLIANCE - && freeOfCharge == FREE - && (ratingC2S != null && ratingC2S.equalsIgnoreCase(RATING)) - && (ratingS2S != null && ratingS2S.equalsIgnoreCase(RATING))) { - //Log.d(Config.LOGTAG, "ProviderService: Updating provider list. Adding " + provider + " (Registration: " + inBandRegistration + " Compliance: " + ratingXmppComplianceTester + " Free: " + freeOfCharge + " Rating C2S/S2S: " + ratingC2S + "/" + ratingS2S + ")"); - providers.add(provider); - } - } - } - } - } catch (JSONException e) { - e.printStackTrace(); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/QuickConversationsService.java b/src/main/java/eu/siacs/conversations/services/QuickConversationsService.java deleted file mode 100644 index 1d83a5cb5..000000000 --- a/src/main/java/eu/siacs/conversations/services/QuickConversationsService.java +++ /dev/null @@ -1,28 +0,0 @@ -package eu.siacs.conversations.services; - -public class QuickConversationsService extends AbstractQuickConversationsService { - - QuickConversationsService(XmppConnectionService xmppConnectionService) { - super(xmppConnectionService); - } - - @Override - public void considerSync() { - - } - - @Override - public void signalAccountStateChange() { - - } - - @Override - public boolean isSynchronizing() { - return false; - } - - @Override - public void considerSyncBackground(boolean force) { - - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/ShortcutService.java b/src/main/java/eu/siacs/conversations/services/ShortcutService.java deleted file mode 100644 index 6b3cd8b20..000000000 --- a/src/main/java/eu/siacs/conversations/services/ShortcutService.java +++ /dev/null @@ -1,164 +0,0 @@ -package eu.siacs.conversations.services; - -import android.annotation.TargetApi; -import android.content.Intent; -import android.content.pm.ShortcutInfo; -import android.content.pm.ShortcutManager; -import android.graphics.Bitmap; -import android.graphics.drawable.Icon; -import android.net.Uri; -import android.os.Build; -import android.util.Log; - -import androidx.annotation.NonNull; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.ui.StartConversationActivity; -import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor; -import eu.siacs.conversations.xmpp.Jid; - -public class ShortcutService { - private final XmppConnectionService xmppConnectionService; - private final ReplacingSerialSingleThreadExecutor replacingSerialSingleThreadExecutor = new ReplacingSerialSingleThreadExecutor(ShortcutService.class.getSimpleName()); - - public ShortcutService(XmppConnectionService xmppConnectionService) { - this.xmppConnectionService = xmppConnectionService; - } - - public void refresh() { - refresh(false); - } - - public void refresh(final boolean forceUpdate) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - final Runnable r = new Runnable() { - @Override - public void run() { - refreshImpl(forceUpdate); - } - }; - replacingSerialSingleThreadExecutor.execute(r); - } - } - - @TargetApi(25) - public void report(Contact contact) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - ShortcutManager shortcutManager = xmppConnectionService.getSystemService(ShortcutManager.class); - shortcutManager.reportShortcutUsed(getShortcutId(contact)); - } - } - - @TargetApi(25) - private void refreshImpl(boolean forceUpdate) { - List frequentContacts = xmppConnectionService.databaseBackend.getFrequentContacts(30); - HashMap accounts = new HashMap<>(); - for (Account account : xmppConnectionService.getAccounts()) { - accounts.put(account.getUuid(), account); - } - List contacts = new ArrayList<>(); - for (FrequentContact frequentContact : frequentContacts) { - Account account = accounts.get(frequentContact.account); - if (account != null) { - contacts.add(account.getRoster().getContact(frequentContact.contact)); - } - } - ShortcutManager shortcutManager = xmppConnectionService.getSystemService(ShortcutManager.class); - boolean needsUpdate = forceUpdate || contactsChanged(contacts, shortcutManager.getDynamicShortcuts()); - if (!needsUpdate) { - Log.d(Config.LOGTAG, "skipping shortcut update"); - return; - } - List newDynamicShortCuts = new ArrayList<>(); - for (Contact contact : contacts) { - ShortcutInfo shortcut = getShortcutInfo(contact); - newDynamicShortCuts.add(shortcut); - } - if (shortcutManager.setDynamicShortcuts(newDynamicShortCuts)) { - Log.d(Config.LOGTAG, "updated dynamic shortcuts"); - } else { - Log.d(Config.LOGTAG, "unable to update dynamic shortcuts"); - } - } - - @TargetApi(Build.VERSION_CODES.N_MR1) - private ShortcutInfo getShortcutInfo(Contact contact) { - return new ShortcutInfo.Builder(xmppConnectionService, getShortcutId(contact)) - .setShortLabel(contact.getDisplayName()) - .setIntent(getShortcutIntent(contact)) - .setIcon(Icon.createWithBitmap(xmppConnectionService.getAvatarService().getRoundedShortcut(contact))) - .build(); - } - - private static boolean contactsChanged(List needles, List haystack) { - for (Contact needle : needles) { - if (!contactExists(needle, haystack)) { - return true; - } - } - return needles.size() != haystack.size(); - } - - @TargetApi(25) - private static boolean contactExists(Contact needle, List haystack) { - for (ShortcutInfo shortcutInfo : haystack) { - if (getShortcutId(needle).equals(shortcutInfo.getId()) && needle.getDisplayName().equals(shortcutInfo.getShortLabel())) { - return true; - } - } - return false; - } - - private static String getShortcutId(Contact contact) { - return contact.getAccount().getJid().asBareJid().toEscapedString() + "#" + contact.getJid().asBareJid().toEscapedString(); - } - - private Intent getShortcutIntent(Contact contact) { - Intent intent = new Intent(xmppConnectionService, StartConversationActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse("xmpp:" + contact.getJid().asBareJid().toEscapedString())); - intent.putExtra("account", contact.getAccount().getJid().asBareJid().toString()); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); - return intent; - } - - @NonNull - public Intent createShortcut(Contact contact, boolean legacy) { - Intent intent; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !legacy) { - ShortcutInfo shortcut = getShortcutInfo(contact); - ShortcutManager shortcutManager = xmppConnectionService.getSystemService(ShortcutManager.class); - intent = shortcutManager.createShortcutResultIntent(shortcut); - } else { - intent = createShortcutResultIntent(contact); - } - return intent; - } - - @NonNull - private Intent createShortcutResultIntent(Contact contact) { - AvatarService avatarService = xmppConnectionService.getAvatarService(); - Bitmap icon = avatarService.getRoundedShortcutWithIcon(contact); - Intent intent = new Intent(); - intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, contact.getDisplayName()); - intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon); - intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, getShortcutIntent(contact)); - return intent; - } - - public static class FrequentContact { - private final String account; - private final Jid contact; - - public FrequentContact(String account, Jid contact) { - this.account = account; - this.contact = contact; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java deleted file mode 100644 index 7d2d90dd5..000000000 --- a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java +++ /dev/null @@ -1,333 +0,0 @@ -package eu.siacs.conversations.services; - -import android.content.ComponentName; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.preference.PreferenceManager; -import android.util.Log; -import com.google.common.base.Optional; -import com.google.common.base.Strings; -import com.google.common.collect.Iterables; -import com.google.common.io.BaseEncoding; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.parser.AbstractParser; -import eu.siacs.conversations.persistance.UnifiedPushDatabase; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import eu.siacs.conversations.xmpp.stanzas.PresencePacket; -import java.nio.charset.StandardCharsets; -import java.text.ParseException; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -public class UnifiedPushBroker { - - // time to expiration before a renewal attempt is made (24 hours) - public static final long TIME_TO_RENEW = 86_400_000L; - - // interval for the 'cron tob' that attempts renewals for everything that expires is lass than - // `TIME_TO_RENEW` - public static final long RENEWAL_INTERVAL = 3_600_000L; - - private static final ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1); - - private final XmppConnectionService service; - - public UnifiedPushBroker(final XmppConnectionService xmppConnectionService) { - this.service = xmppConnectionService; - SCHEDULER.scheduleAtFixedRate( - this::renewUnifiedPushEndpoints, - RENEWAL_INTERVAL, - RENEWAL_INTERVAL, - TimeUnit.MILLISECONDS); - } - - public void renewUnifiedPushEndpointsOnBind(final Account account) { - final Optional transportOptional = getTransport(); - if (transportOptional.isPresent()) { - final Transport transport = transportOptional.get(); - final Account transportAccount = transport.account; - if (transportAccount != null && transportAccount.getUuid().equals(account.getUuid())) { - final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(service); - if (database.hasEndpoints(transport)) { - sendDirectedPresence(transportAccount, transport.transport); - } - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": trigger endpoint renewal on bind"); - renewUnifiedEndpoint(transportOptional.get()); - } - } - } - - private void sendDirectedPresence(final Account account, Jid to) { - final PresencePacket presence = new PresencePacket(); - presence.setTo(to); - service.sendPresencePacket(account, presence); - } - - public Optional renewUnifiedPushEndpoints() { - final Optional transportOptional = getTransport(); - if (transportOptional.isPresent()) { - final Transport transport = transportOptional.get(); - if (transport.account.isEnabled()) { - renewUnifiedEndpoint(transportOptional.get()); - } else { - Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. Account is disabled"); - } - } else { - Log.d(Config.LOGTAG, "skipping UnifiedPush endpoint renewal. No transport selected"); - } - return transportOptional; - } - - private void renewUnifiedEndpoint(final Transport transport) { - final Account account = transport.account; - final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service); - final List renewals = - unifiedPushDatabase.getRenewals( - account.getUuid(), transport.transport.toEscapedString()); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": " - + renewals.size() - + " UnifiedPush endpoints scheduled for renewal on " - + transport.transport); - for (final UnifiedPushDatabase.PushTarget renewal : renewals) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": try to renew UnifiedPush " + renewal); - final String hashedApplication = - UnifiedPushDistributor.hash(account.getUuid(), renewal.application); - final String hashedInstance = - UnifiedPushDistributor.hash(account.getUuid(), renewal.instance); - final IqPacket registration = new IqPacket(IqPacket.TYPE.SET); - registration.setTo(transport.transport); - final Element register = registration.addChild("register", Namespace.UNIFIED_PUSH); - register.setAttribute("application", hashedApplication); - register.setAttribute("instance", hashedInstance); - this.service.sendIqPacket( - account, - registration, - (a, response) -> processRegistration(transport, renewal, response)); - } - } - - private void processRegistration( - final Transport transport, - final UnifiedPushDatabase.PushTarget renewal, - final IqPacket response) { - if (response.getType() == IqPacket.TYPE.RESULT) { - final Element registered = response.findChild("registered", Namespace.UNIFIED_PUSH); - if (registered == null) { - return; - } - final String endpoint = registered.getAttribute("endpoint"); - if (Strings.isNullOrEmpty(endpoint)) { - Log.w(Config.LOGTAG, "endpoint was null in up registration"); - return; - } - final long expiration; - try { - expiration = AbstractParser.getTimestamp(registered.getAttribute("expiration")); - } catch (final IllegalArgumentException | ParseException e) { - Log.d(Config.LOGTAG, "could not parse expiration", e); - return; - } - renewUnifiedPushEndpoint(transport, renewal, endpoint, expiration); - } else { - Log.d(Config.LOGTAG, "could not register UP endpoint " + response.getErrorCondition()); - } - } - - private void renewUnifiedPushEndpoint( - final Transport transport, - final UnifiedPushDatabase.PushTarget renewal, - final String endpoint, - final long expiration) { - Log.d(Config.LOGTAG, "registered endpoint " + endpoint + " expiration=" + expiration); - final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service); - final boolean modified = - unifiedPushDatabase.updateEndpoint( - renewal.instance, - transport.account.getUuid(), - transport.transport.toEscapedString(), - endpoint, - expiration); - if (modified) { - Log.d( - Config.LOGTAG, - "endpoint for " - + renewal.application - + "/" - + renewal.instance - + " was updated to " - + endpoint); - broadcastEndpoint( - renewal.instance, - new UnifiedPushDatabase.ApplicationEndpoint(renewal.application, endpoint)); - } - } - - public boolean reconfigurePushDistributor() { - final boolean enabled = getTransport().isPresent(); - setUnifiedPushDistributorEnabled(enabled); - return enabled; - } - - private void setUnifiedPushDistributorEnabled(final boolean enabled) { - final PackageManager packageManager = service.getPackageManager(); - final ComponentName componentName = - new ComponentName(service, UnifiedPushDistributor.class); - if (enabled) { - packageManager.setComponentEnabledSetting( - componentName, - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, - PackageManager.DONT_KILL_APP); - Log.d(Config.LOGTAG, "UnifiedPushDistributor has been enabled"); - } else { - packageManager.setComponentEnabledSetting( - componentName, - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, - PackageManager.DONT_KILL_APP); - Log.d(Config.LOGTAG, "UnifiedPushDistributor has been disabled"); - } - } - - public boolean processPushMessage( - final Account account, final Jid transport, final Element push) { - final String instance = push.getAttribute("instance"); - final String application = push.getAttribute("application"); - if (Strings.isNullOrEmpty(instance) || Strings.isNullOrEmpty(application)) { - return false; - } - final String content = push.getContent(); - final byte[] payload; - if (Strings.isNullOrEmpty(content)) { - payload = new byte[0]; - } else if (BaseEncoding.base64().canDecode(content)) { - payload = BaseEncoding.base64().decode(content); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": received invalid unified push payload"); - return false; - } - final Optional pushTarget = - getPushTarget(account, transport, application, instance); - if (pushTarget.isPresent()) { - final UnifiedPushDatabase.PushTarget target = pushTarget.get(); - // TODO check if app is still installed? - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": broadcasting a " - + payload.length - + " bytes push message to " - + target.application); - broadcastPushMessage(target, payload); - return true; - } else { - Log.d(Config.LOGTAG, "could not find application for push"); - return false; - } - } - - public Optional getTransport() { - final SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(service.getApplicationContext()); - final String accountPreference = - sharedPreferences.getString(UnifiedPushDistributor.PREFERENCE_ACCOUNT, "none"); - final String pushServerPreference = - sharedPreferences.getString( - UnifiedPushDistributor.PREFERENCE_PUSH_SERVER, - service.getString(R.string.default_push_server)); - if (Strings.isNullOrEmpty(accountPreference) - || "none".equalsIgnoreCase(accountPreference) - || Strings.nullToEmpty(pushServerPreference).trim().isEmpty()) { - return Optional.absent(); - } - final Jid transport; - final Jid jid; - try { - transport = Jid.ofEscaped(Strings.nullToEmpty(pushServerPreference).trim()); - jid = Jid.ofEscaped(Strings.nullToEmpty(accountPreference).trim()); - } catch (final IllegalArgumentException e) { - return Optional.absent(); - } - final Account account = service.findAccountByJid(jid); - if (account == null) { - return Optional.absent(); - } - return Optional.of(new Transport(account, transport)); - } - - private Optional getPushTarget( - final Account account, - final Jid transport, - final String application, - final String instance) { - if (transport == null || application == null || instance == null) { - return Optional.absent(); - } - final String uuid = account.getUuid(); - final List pushTargets = - UnifiedPushDatabase.getInstance(service) - .getPushTargets(uuid, transport.toEscapedString()); - return Iterables.tryFind( - pushTargets, - pt -> - UnifiedPushDistributor.hash(uuid, pt.application).equals(application) - && UnifiedPushDistributor.hash(uuid, pt.instance).equals(instance)); - } - - private void broadcastPushMessage( - final UnifiedPushDatabase.PushTarget target, final byte[] payload) { - final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_MESSAGE); - updateIntent.setPackage(target.application); - updateIntent.putExtra("token", target.instance); - updateIntent.putExtra("bytesMessage", payload); - updateIntent.putExtra("message", new String(payload, StandardCharsets.UTF_8)); - service.sendBroadcast(updateIntent); - } - - private void broadcastEndpoint( - final String instance, final UnifiedPushDatabase.ApplicationEndpoint endpoint) { - Log.d(Config.LOGTAG, "broadcasting endpoint to " + endpoint.application); - final Intent updateIntent = new Intent(UnifiedPushDistributor.ACTION_NEW_ENDPOINT); - updateIntent.setPackage(endpoint.application); - updateIntent.putExtra("token", instance); - updateIntent.putExtra("endpoint", endpoint.endpoint); - service.sendBroadcast(updateIntent); - } - - public void rebroadcastEndpoint(final String instance, final Transport transport) { - final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(service); - final UnifiedPushDatabase.ApplicationEndpoint endpoint = - unifiedPushDatabase.getEndpoint( - transport.account.getUuid(), - transport.transport.toEscapedString(), - instance); - if (endpoint != null) { - broadcastEndpoint(instance, endpoint); - } - } - - public static class Transport { - public final Account account; - public final Jid transport; - - public Transport(Account account, Jid transport) { - this.account = account; - this.transport = transport; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java deleted file mode 100644 index 64c16dbcd..000000000 --- a/src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java +++ /dev/null @@ -1,152 +0,0 @@ -package eu.siacs.conversations.services; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ResolveInfo; -import android.net.Uri; -import android.util.Log; - -import com.google.common.base.Charsets; -import com.google.common.base.Joiner; -import com.google.common.base.Strings; -import com.google.common.collect.Lists; -import com.google.common.hash.Hashing; -import com.google.common.io.BaseEncoding; - -import java.util.Arrays; -import java.util.Collection; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.persistance.UnifiedPushDatabase; -import eu.siacs.conversations.utils.Compatibility; - -public class UnifiedPushDistributor extends BroadcastReceiver { - - public static final String ACTION_REGISTER = "org.unifiedpush.android.distributor.REGISTER"; - public static final String ACTION_UNREGISTER = "org.unifiedpush.android.distributor.UNREGISTER"; - public static final String ACTION_BYTE_MESSAGE = - "org.unifiedpush.android.distributor.feature.BYTES_MESSAGE"; - public static final String ACTION_REGISTRATION_FAILED = - "org.unifiedpush.android.connector.REGISTRATION_FAILED"; - public static final String ACTION_MESSAGE = "org.unifiedpush.android.connector.MESSAGE"; - public static final String ACTION_NEW_ENDPOINT = - "org.unifiedpush.android.connector.NEW_ENDPOINT"; - - public static final String PREFERENCE_ACCOUNT = "up_push_account"; - public static final String PREFERENCE_PUSH_SERVER = "up_push_server"; - - public static final List PREFERENCES = - Arrays.asList(PREFERENCE_ACCOUNT, PREFERENCE_PUSH_SERVER); - - @Override - public void onReceive(final Context context, final Intent intent) { - if (intent == null) { - return; - } - final String action = intent.getAction(); - final String application = intent.getStringExtra("application"); - final String instance = intent.getStringExtra("token"); - final List features = intent.getStringArrayListExtra("features"); - switch (Strings.nullToEmpty(action)) { - case ACTION_REGISTER: - register(context, application, instance, features); - break; - case ACTION_UNREGISTER: - unregister(context, instance); - break; - case Intent.ACTION_PACKAGE_FULLY_REMOVED: - unregisterApplication(context, intent.getData()); - break; - default: - Log.d(Config.LOGTAG, "UnifiedPushDistributor received unknown action " + action); - break; - } - } - - private void register( - final Context context, - final String application, - final String instance, - final Collection features) { - if (Strings.isNullOrEmpty(application) || Strings.isNullOrEmpty(instance)) { - Log.w(Config.LOGTAG, "ignoring invalid UnifiedPush registration"); - return; - } - final List receivers = getBroadcastReceivers(context, application); - if (receivers.contains(application)) { - final boolean byteMessage = features != null && features.contains(ACTION_BYTE_MESSAGE); - Log.d( - Config.LOGTAG, - "received up registration from " - + application - + "/" - + instance - + " features: " - + features); - if (UnifiedPushDatabase.getInstance(context).register(application, instance)) { - Log.d( - Config.LOGTAG, - "successfully created UnifiedPush entry. waking up XmppConnectionService"); - final Intent serviceIntent = new Intent(context, XmppConnectionService.class); - serviceIntent.setAction(XmppConnectionService.ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS); - serviceIntent.putExtra("instance", instance); - Compatibility.startService(context, serviceIntent); - } else { - Log.d(Config.LOGTAG, "not successful. sending error message back to application"); - final Intent registrationFailed = new Intent(ACTION_REGISTRATION_FAILED); - registrationFailed.setPackage(application); - registrationFailed.putExtra("token", instance); - context.sendBroadcast(registrationFailed); - } - } else { - Log.d( - Config.LOGTAG, - "ignoring invalid UnifiedPush registration. Unknown application " - + application); - } - } - - private List getBroadcastReceivers(final Context context, final String application) { - final Intent messageIntent = new Intent(ACTION_MESSAGE); - messageIntent.setPackage(application); - final List resolveInfo = - context.getPackageManager().queryBroadcastReceivers(messageIntent, 0); - return Lists.transform( - resolveInfo, ri -> ri.activityInfo == null ? null : ri.activityInfo.packageName); - } - - private void unregister(final Context context, final String instance) { - if (Strings.isNullOrEmpty(instance)) { - Log.w(Config.LOGTAG, "ignoring invalid UnifiedPush un-registration"); - return; - } - final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(context); - if (unifiedPushDatabase.deleteInstance(instance)) { - Log.d(Config.LOGTAG, "successfully removed " + instance + " from UnifiedPush"); - } - } - - private void unregisterApplication(final Context context, final Uri uri) { - if (uri != null && "package".equalsIgnoreCase(uri.getScheme())) { - final String application = uri.getSchemeSpecificPart(); - if (Strings.isNullOrEmpty(application)) { - return; - } - Log.d(Config.LOGTAG, "app " + application + " has been removed from the system"); - final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(context); - if (database.deleteApplication(application)) { - Log.d(Config.LOGTAG, "successfully removed " + application + " from UnifiedPush"); - } - } - } - - public static String hash(String... components) { - return BaseEncoding.base64() - .encode( - Hashing.sha256() - .hashString(Joiner.on('\0').join(components), Charsets.UTF_8) - .asBytes()); - } -} diff --git a/src/main/java/eu/siacs/conversations/services/UpdateService.java b/src/main/java/eu/siacs/conversations/services/UpdateService.java deleted file mode 100644 index daca32144..000000000 --- a/src/main/java/eu/siacs/conversations/services/UpdateService.java +++ /dev/null @@ -1,244 +0,0 @@ -package eu.siacs.conversations.services; - -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; -import static eu.siacs.conversations.utils.Compatibility.s; -import static eu.siacs.conversations.http.HttpConnectionManager.getProxy; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.net.URL; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; - -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; - -import eu.siacs.conversations.BuildConfig; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.http.NoSSLv3SocketFactory; -import eu.siacs.conversations.ui.UpdaterActivity; -import me.drakeet.support.toast.ToastCompat; - -public class UpdateService extends AsyncTask { - private boolean mUseTor; - private boolean mUseI2P; - private Context context; - private String store; - private NotificationService getNotificationService; - - public UpdateService(Context context, String Store, XmppConnectionService mXmppConnectionService) { - this.context = context; - this.store = Store; - this.mUseTor = mXmppConnectionService.useTorToConnect(); - this.mUseI2P = mXmppConnectionService.useI2PToConnect(); - this.getNotificationService = mXmppConnectionService.getNotificationService(); - } - - @Override - protected Wrapper doInBackground(String... params) { - StringBuilder jsonString = new StringBuilder(); - boolean UpdateAvailable = false; - boolean interactive = false; - boolean isError = false; - - if (params[0].equals("true")) { - interactive = true; - } - SSLContext sslcontext = null; - SSLSocketFactory NoSSLv3Factory = null; - try { - sslcontext = SSLContext.getInstance("TLSv1"); - if (sslcontext != null) { - sslcontext.init(null, null, null); - NoSSLv3Factory = new NoSSLv3SocketFactory(sslcontext.getSocketFactory()); - } - } catch (NoSuchAlgorithmException | KeyManagementException e) { - e.printStackTrace(); - } - HttpsURLConnection.setDefaultSSLSocketFactory(NoSSLv3Factory); - HttpsURLConnection connection = null; - try { - URL url = new URL(Config.UPDATE_URL); - if (mUseTor && !mUseI2P) { - connection = (HttpsURLConnection) url.openConnection(getProxy(false)); - } else if (mUseI2P) { - connection = (HttpsURLConnection) url.openConnection(getProxy(true)); - } else { - connection = (HttpsURLConnection) url.openConnection(); - } - connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); - connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); - connection.setRequestProperty("User-agent", System.getProperty("http.agent")); - BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); - String line; - while ((line = reader.readLine()) != null) { - jsonString.append(line); - } - } catch (Exception e) { - e.printStackTrace(); - isError = true; - } finally { - if (connection != null) { - connection.disconnect(); - } - } - - try { - JSONObject json = new JSONObject(jsonString.toString()); - if (json.getBoolean("success") && json.has("latestVersion") && json.has("appURI") && json.has("filesize")) { - String version = json.getString("latestVersion"); - String ownVersion = BuildConfig.VERSION_NAME; - String url = json.getString("appURI"); - String filesize = json.getString("filesize"); - if (BuildConfig.APPLICATION_ID.equals("de.monocles.chat")) { - url = json.getString("appURIPS"); - filesize = json.getString("filesizePS"); - } - String changelog = ""; - if (json.has("changelog")) { - changelog = json.getString("changelog"); - } - if (checkVersion(version, ownVersion) >= 1) { - Log.d(Config.LOGTAG, "AppUpdater: Version " + ownVersion + " should be updated to " + version); - UpdateAvailable = true; - if (interactive) { - showUpdateDialog(url, changelog); - } else { - showNotification(url, changelog, version, filesize, store); - } - } else { - Log.d(Config.LOGTAG, "AppUpdater: Version " + ownVersion + " is up to date"); - UpdateAvailable = false; - } - } - } catch (JSONException e) { - e.printStackTrace(); - } - Wrapper w = new Wrapper(); - w.isError = isError; - w.UpdateAvailable = UpdateAvailable; - w.show = interactive; - return w; - } - - private void showUpdateDialog(final String url, final String changelog) { - Intent intent = new Intent(context, UpdaterActivity.class); - intent.putExtra("update", "MonoclesMessenger_UpdateService"); - intent.putExtra("url", url); - intent.putExtra("changelog", changelog); - intent.putExtra("store", store); - context.startActivity(intent); - } - - @Override - protected void onPostExecute(Wrapper w) { - super.onPostExecute(w); - if (w.isError) { - showToastMessage(true, true); - return; - } - showToastMessage(w.show, false); - } - - private void showToastMessage(boolean show, final boolean error) { - if (!show) { - return; - } - Handler handler = new Handler(Looper.getMainLooper()); - handler.post(() -> { - String ToastMessage = ""; - if (error) { - ToastMessage = context.getString(R.string.failed); - } else { - ToastMessage = context.getString(R.string.no_update_available); - } - ToastCompat.makeText(context, ToastMessage, ToastCompat.LENGTH_LONG).show(); - }); - } - - private void showNotification(String url, String changelog, String version, String filesize, String store) { - Intent intent = new Intent(context, UpdaterActivity.class); - intent.putExtra("update", "MonoclesMessenger_UpdateService"); - intent.putExtra("url", url); - intent.putExtra("changelog", changelog); - intent.putExtra("store", store); - PendingIntent pi = PendingIntent.getActivity(context, 0, intent, s() - ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - getNotificationService.AppUpdateServiceNotification(getNotificationService.AppUpdateNotification(pi, version, filesize)); - } - - private int checkVersion(String remoteVersion, String installedVersion) { - // Use this instead of String.compareTo() for a non-lexicographical - // comparison that works for version strings. e.g. "1.10".compareTo("1.6"). - // - // @param str1 a string of ordinal numbers separated by decimal points. - // @param str2 a string of ordinal numbers separated by decimal points. - // @return The result is a negative integer if str1 is _numerically_ less than str2. - // The result is a positive integer if str1 is _numerically_ greater than str2. - // The result is zero if the strings are _numerically_ equal. - // @note It does not work if "1.10" is supposed to be equal to "1.10.0". - - String[] remote = null; - String[] installed = null; - String[] remoteV = null; - String[] installedV = null; - try { - installedV = installedVersion.split("[ |\\-]"); - Log.d(Config.LOGTAG, "AppUpdater: Version installed: " + installedV[0]); - installed = installedV[0].split("\\."); - } catch (Exception e) { - e.printStackTrace(); - } - try { - remoteV = remoteVersion.split(" "); - if (installedV != null && installedV.length > 1) { - if (installedV[1] != null && installedV[1].toLowerCase().contains("beta")) { - remoteV[0] = remoteV[0] + ".1"; - } - } - Log.d(Config.LOGTAG, "AppUpdater: Version on server: " + remoteV[0]); - remote = remoteV[0].split("\\."); - } catch (Exception e) { - e.printStackTrace(); - } - int i = 0; - // set index to first non-equal ordinal or length of shortest localVersion string - try { - if (remote != null && installed != null) { - while (i < remote.length && i < installed.length && remote[i].equals(installed[i])) { - i++; - } - // compare first non-equal ordinal number - if (i < remote.length && i < installed.length) { - int diff = Integer.valueOf(remote[i]).compareTo(Integer.valueOf(installed[i])); - return Integer.signum(diff); - } - // the strings are equal or one string is a substring of the other - // e.g. "1.2.3" = "1.2.3" or "1.2.3" < "1.2.3.4" - return Integer.signum(remote.length - installed.length); - } - } catch (Exception e) { - showToastMessage(true, true); - e.printStackTrace(); - return 0; - } - return 0; - } - - class Wrapper { - boolean UpdateAvailable = false; - boolean show = false; - boolean isError = false; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java deleted file mode 100644 index 5a1ce6ae9..000000000 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ /dev/null @@ -1,6911 +0,0 @@ -package eu.siacs.conversations.services; - -import static eu.siacs.conversations.persistance.FileBackend.AUDIOS; -import static eu.siacs.conversations.persistance.FileBackend.FILES; -import static eu.siacs.conversations.persistance.FileBackend.IMAGES; -import static eu.siacs.conversations.persistance.FileBackend.VIDEOS; -import static eu.siacs.conversations.ui.SettingsActivity.ALLOW_MESSAGE_CORRECTION; -import static eu.siacs.conversations.ui.SettingsActivity.ALLOW_MESSAGE_RETRACTION; -import static eu.siacs.conversations.ui.SettingsActivity.AUTOMATIC_ATTACHMENT_DELETION; -import static eu.siacs.conversations.ui.SettingsActivity.CHAT_STATES; -import static eu.siacs.conversations.ui.SettingsActivity.CONFIRM_MESSAGES; -import static eu.siacs.conversations.ui.SettingsActivity.ENABLE_MULTI_ACCOUNTS; -import static eu.siacs.conversations.ui.SettingsActivity.INDICATE_RECEIVED; -import static eu.siacs.conversations.ui.SettingsActivity.SHOW_OWN_ACCOUNTS; -import static eu.siacs.conversations.ui.SettingsActivity.USE_INNER_STORAGE; -import static eu.siacs.conversations.utils.RichPreview.RICH_LINK_METADATA; -import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; -import static eu.siacs.conversations.utils.StorageHelper.getAppMediaDirectory; -import android.content.res.Resources; -import android.os.Handler; -import eu.siacs.conversations.ui.UiCallback; -import static eu.siacs.conversations.utils.Compatibility.s; -import android.Manifest; -import androidx.annotation.RequiresApi; -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.app.ActivityManager; -import android.app.AlarmManager; -import android.app.KeyguardManager; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.database.ContentObserver; -import android.graphics.Bitmap; -import android.media.AudioManager; -import android.net.ConnectivityManager; -import android.net.Network; -import android.net.NetworkCapabilities; -import android.net.NetworkInfo; -import android.net.Uri; -import java.security.SecureRandom; -import android.os.Binder; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.os.IBinder; -import android.os.PowerManager; -import android.os.PowerManager.WakeLock; -import android.os.SystemClock; -import android.os.Vibrator; -import android.preference.PreferenceManager; -import android.provider.ContactsContract; -import android.security.KeyChain; -import android.telephony.PhoneStateListener; -import android.telephony.TelephonyManager; -import android.text.TextUtils; -import android.util.DisplayMetrics; -import android.util.Log; -import android.util.LruCache; -import android.util.Pair; -import net.java.otr4j.OtrException; -import net.java.otr4j.session.Session; -import net.java.otr4j.session.SessionID; -import net.java.otr4j.session.SessionImpl; -import net.java.otr4j.session.SessionStatus; -import eu.siacs.conversations.xmpp.jid.OtrJidHelper; -import eu.siacs.conversations.xmpp.Jid; - -import androidx.annotation.BoolRes; -import androidx.annotation.IntegerRes; -import androidx.core.app.RemoteInput; -import androidx.core.content.ContextCompat; -import androidx.annotation.NonNull; -import com.google.common.base.Objects; -import com.google.common.base.Strings; -import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy; - -import org.conscrypt.Conscrypt; -import org.openintents.openpgp.IOpenPgpService2; -import org.openintents.openpgp.util.OpenPgpApi; -import org.openintents.openpgp.util.OpenPgpServiceConnection; -import com.google.common.base.Optional; -import java.text.ParseException; -import eu.siacs.conversations.persistance.UnifiedPushDatabase; -import eu.siacs.conversations.utils.AccountUtils; - -import java.io.File; -import java.security.Security; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.TimeZone; -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.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; - -import eu.siacs.conversations.BuildConfig; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.android.JabberIdContact; -import eu.siacs.conversations.crypto.OmemoSetting; -import eu.siacs.conversations.crypto.PgpDecryptionService; -import eu.siacs.conversations.crypto.PgpEngine; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; -import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Blockable; -import eu.siacs.conversations.entities.Bookmark; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Conversational; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.entities.MucOptions.OnRenameListener; -import eu.siacs.conversations.entities.Presence; -import eu.siacs.conversations.entities.PresenceTemplate; -import eu.siacs.conversations.entities.Roster; -import eu.siacs.conversations.entities.ServiceDiscoveryResult; -import eu.siacs.conversations.generator.AbstractGenerator; -import eu.siacs.conversations.generator.IqGenerator; -import eu.siacs.conversations.generator.MessageGenerator; -import eu.siacs.conversations.generator.PresenceGenerator; -import eu.siacs.conversations.http.HttpConnectionManager; -import eu.siacs.conversations.parser.AbstractParser; -import eu.siacs.conversations.parser.IqParser; -import eu.siacs.conversations.parser.MessageParser; -import eu.siacs.conversations.parser.PresenceParser; -import eu.siacs.conversations.persistance.DatabaseBackend; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.ui.ChooseAccountForProfilePictureActivity; -import eu.siacs.conversations.ui.RtpSessionActivity; -import eu.siacs.conversations.ui.SettingsActivity; -import eu.siacs.conversations.ui.UiCallback; -import eu.siacs.conversations.ui.interfaces.OnAvatarPublication; -import eu.siacs.conversations.ui.interfaces.OnMediaLoaded; -import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable; -import eu.siacs.conversations.utils.Compatibility; -import eu.siacs.conversations.utils.ConversationsFileObserver; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.EasyOnboardingInvite; -import eu.siacs.conversations.utils.ExceptionHelper; -import eu.siacs.conversations.utils.MimeUtils; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.utils.PhoneHelper; -import eu.siacs.conversations.utils.QuickLoader; -import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor; -import eu.siacs.conversations.utils.ReplacingTaskManager; -import eu.siacs.conversations.utils.Resolver; -import eu.siacs.conversations.utils.SerialSingleThreadExecutor; -import eu.siacs.conversations.utils.StorageHelper; -import eu.siacs.conversations.utils.StringUtils; -import eu.siacs.conversations.utils.TorServiceUtils; -import eu.siacs.conversations.utils.TranscoderStrategies; -import eu.siacs.conversations.utils.WakeLockHelper; -import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xml.LocalizedContent; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnBindListener; -import eu.siacs.conversations.xmpp.OnContactStatusChanged; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; -import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; -import eu.siacs.conversations.xmpp.OnMessageAcknowledged; -import eu.siacs.conversations.xmpp.OnMessagePacketReceived; -import eu.siacs.conversations.xmpp.OnPresencePacketReceived; -import eu.siacs.conversations.xmpp.OnStatusChanged; -import eu.siacs.conversations.xmpp.OnUpdateBlocklist; -import eu.siacs.conversations.xmpp.XmppConnection; -import eu.siacs.conversations.xmpp.chatstate.ChatState; -import eu.siacs.conversations.xmpp.forms.Data; -import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; -import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; -import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; -import eu.siacs.conversations.xmpp.jingle.Media; -import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; -import eu.siacs.conversations.xmpp.mam.MamReference; -import eu.siacs.conversations.xmpp.pep.Avatar; -import eu.siacs.conversations.xmpp.pep.PublishOptions; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; -import eu.siacs.conversations.xmpp.stanzas.PresencePacket; -import me.leolin.shortcutbadger.ShortcutBadger; - -import java.io.File; -import java.security.Security; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -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; - - -import org.conscrypt.Conscrypt; -import org.openintents.openpgp.IOpenPgpService2; -import org.openintents.openpgp.util.OpenPgpApi; -import org.openintents.openpgp.util.OpenPgpServiceConnection; - -public class XmppConnectionService extends Service { - - public static final String ACTION_REPLY_TO_CONVERSATION = "reply_to_conversations"; - public static final String ACTION_MARK_AS_READ = "mark_as_read"; - public static final String ACTION_SNOOZE = "snooze"; - public static final String ACTION_CLEAR_MESSAGE_NOTIFICATION = "clear_message_notification"; - public static final String ACTION_CLEAR_MISSED_CALL_NOTIFICATION = - "clear_missed_call_notification"; - public static final String ACTION_DISMISS_ERROR_NOTIFICATIONS = "dismiss_error"; - public static final String ACTION_TRY_AGAIN = "try_again"; - public static final String ACTION_IDLE_PING = "idle_ping"; - public static final String ACTION_FCM_TOKEN_REFRESH = "fcm_token_refresh"; - public static final String ACTION_FCM_MESSAGE_RECEIVED = "fcm_message_received"; - public static final String ACTION_DISMISS_CALL = "dismiss_call"; - public static final String ACTION_END_CALL = "end_call"; - public static final String ACTION_PROVISION_ACCOUNT = "provision_account"; - private static final String ACTION_POST_CONNECTIVITY_CHANGE = - "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE"; - public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS = - "eu.siacs.conversations.UNIFIED_PUSH_RENEW"; - public static final String FDroid = "org.fdroid.fdroid"; - public static final String PlayStore = "com.android.vending"; - private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp"; - - public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1); - private static final Executor FILE_OBSERVER_EXECUTOR = Executors.newSingleThreadExecutor(); - private static final Executor FILE_ATTACHMENT_EXECUTOR = Executors.newSingleThreadExecutor(); - private static final 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"); - public final Executor mWebPreviewExecutor = Executors.newFixedThreadPool(3); - public final SerialSingleThreadExecutor mNotificationChannelExecutor = new SerialSingleThreadExecutor("updateNotificationChannels"); - public final SerialSingleThreadExecutor mMessageResendTaskExecuter = new SerialSingleThreadExecutor("MessageResender"); - private final ReplacingTaskManager mRosterSyncTaskManager = new ReplacingTaskManager(); - private final IBinder mBinder = new XmppConnectionBinder(); - private final List conversations = new CopyOnWriteArrayList<>(); - private final IqGenerator mIqGenerator = new IqGenerator(this); - private final Set mInProgressAvatarFetches = new HashSet<>(); - private final Set mOmittedPepAvatarFetches = new HashSet<>(); - private final HashSet mLowPingTimeoutMode = new HashSet<>(); - private final OnIqPacketReceived mDefaultIqHandler = - (account, packet) -> { - if (packet.getType() != IqPacket.TYPE.RESULT) { - Element error = packet.findChild("error"); - String text = error != null ? error.findChildContent("text") : null; - if (text != null) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": received iq error - " + text); - } - } - }; - public DatabaseBackend databaseBackend; - private final ReplacingSerialSingleThreadExecutor mContactMergerExecutor = - new ReplacingSerialSingleThreadExecutor("ContactMerger"); - private long mLastActivity = 0; - public final FileBackend fileBackend = new FileBackend(this); - private MemorizingTrustManager mMemorizingTrustManager; - private final NotificationService mNotificationService = new NotificationService(this); - private final UnifiedPushBroker unifiedPushBroker = new UnifiedPushBroker(this); - private final ChannelDiscoveryService mChannelDiscoveryService = - new ChannelDiscoveryService(this); - private final ShortcutService mShortcutService = new ShortcutService(this); - private final AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false); - private final AtomicBoolean mForceForegroundService = new AtomicBoolean(false); - private final AtomicBoolean mForceDuringOnCreate = new AtomicBoolean(false); - private final AtomicReference ongoingCall = new AtomicReference<>(); - private final OnMessagePacketReceived mMessageParser = new MessageParser(this); - private final OnPresencePacketReceived mPresenceParser = new PresenceParser(this); - private final IqParser mIqParser = new IqParser(this); - private final MessageGenerator mMessageGenerator = new MessageGenerator(this); - public OnContactStatusChanged onContactStatusChanged = - (contact, online) -> { - Conversation conversation = find(getConversations(), contact); - if (conversation != null) { - if (online) { - if (contact.getPresences().size() == 1) { - sendUnsentMessages(conversation); - } - } else { - //check if the resource we are having a conversation with is still online - if (conversation.hasValidOtrSession()) { - String otrResource = conversation.getOtrSession().getSessionID().getUserID(); - if (!(Arrays.asList(contact.getPresences().toResourceArray()).contains(otrResource))) { - conversation.endOtrIfNeeded(); - } - } - } - } - }; - private final PresenceGenerator mPresenceGenerator = new PresenceGenerator(this); - private List accounts; - private final JingleConnectionManager mJingleConnectionManager = - new JingleConnectionManager(this); - public final HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(this); - private final AvatarService mAvatarService = new AvatarService(this); - private final MessageArchiveService mMessageArchiveService = new MessageArchiveService(this); - private final PushManagementService mPushManagementService = new PushManagementService(this); - private final QuickConversationsService mQuickConversationsService = - new QuickConversationsService(this); - private final ConversationsFileObserver fileObserver = - new ConversationsFileObserver( - Environment.getExternalStorageDirectory().getAbsolutePath()) { - @Override - public void onEvent(final int event, final File file) { - markFileDeleted(file); - } - }; - private final OnMessageAcknowledged mOnMessageAcknowledgedListener = - new OnMessageAcknowledged() { - - @Override - public boolean onMessageAcknowledged( - final Account account, final Jid to, final String id) { - if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) { - final String sessionId = - id.substring( - JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX - .length()); - mJingleConnectionManager.updateProposedSessionDiscovered( - account, - to, - sessionId, - JingleConnectionManager.DeviceDiscoveryState - .SEARCHING_ACKNOWLEDGED); - } - final Jid bare = to.asBareJid(); - - for (final Conversation conversation : getConversations()) { - if (conversation.getAccount() == account - && conversation.getJid().asBareJid().equals(bare)) { - final Message message = conversation.findUnsentMessageWithUuid(id); - if (message != null) { - message.setStatus(Message.STATUS_SEND); - message.setErrorMessage(null); - databaseBackend.updateMessage(message, false); - return true; - } - } - } - return false; - } - }; - private final AtomicBoolean isPhoneInCall = new AtomicBoolean(false); - private final PhoneStateListener phoneStateListener = - new PhoneStateListener() { - @Override - public void onCallStateChanged(final int state, final String phoneNumber) { - isPhoneInCall.set(state != TelephonyManager.CALL_STATE_IDLE); - if (state == TelephonyManager.CALL_STATE_OFFHOOK) { - mJingleConnectionManager.notifyPhoneCallStarted(); - } - } - }; - // Ui callback listeners - private final Set mOnConversationUpdates = - Collections.newSetFromMap(new WeakHashMap()); - private final Set mOnShowErrorToasts = - Collections.newSetFromMap(new WeakHashMap()); - private final Set mOnAccountUpdates = - Collections.newSetFromMap(new WeakHashMap()); - private final Set mOnCaptchaRequested = - Collections.newSetFromMap(new WeakHashMap()); - private final Set mOnRosterUpdates = - Collections.newSetFromMap(new WeakHashMap()); - private final Set mOnUpdateBlocklist = - Collections.newSetFromMap(new WeakHashMap()); - private final Set mOnMucRosterUpdate = - Collections.newSetFromMap(new WeakHashMap()); - private final Set mOnKeyStatusUpdated = - Collections.newSetFromMap(new WeakHashMap()); - private final Set onJingleRtpConnectionUpdate = - Collections.newSetFromMap(new WeakHashMap()); - - private final Object LISTENER_LOCK = new Object(); - - public final Set FILENAMES_TO_IGNORE_DELETION = new HashSet<>(); - - private final OnBindListener mOnBindListener = - new OnBindListener() { - @Override - public void onBind(final Account account) { - synchronized (mInProgressAvatarFetches) { - for (Iterator iterator = mInProgressAvatarFetches.iterator(); - iterator.hasNext(); ) { - final String KEY = iterator.next(); - if (KEY.startsWith(account.getJid().asBareJid() + "_")) { - iterator.remove(); - } - } - } - boolean loggedInSuccessfully = - account.setOption(Account.OPTION_LOGGED_IN_SUCCESSFULLY, true); - boolean gainedFeature = - account.setOption( - Account.OPTION_HTTP_UPLOAD_AVAILABLE, - account.getXmppConnection().getFeatures().httpUpload(0)); - if (loggedInSuccessfully || gainedFeature) { - databaseBackend.updateAccount(account); - } - if (loggedInSuccessfully) { - if (!TextUtils.isEmpty(account.getDisplayName())) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": display name wasn't empty on first log in." - + " publishing"); - publishDisplayName(account); - } - } - - account.getRoster().clearPresences(); - synchronized (account.inProgressConferenceJoins) { - account.inProgressConferenceJoins.clear(); - } - synchronized (account.inProgressConferencePings) { - account.inProgressConferencePings.clear(); - } - mJingleConnectionManager.notifyRebound(account); - mQuickConversationsService.considerSyncBackground(false); - fetchRosterFromServer(account); - final XmppConnection connection = account.getXmppConnection(); - - if (connection.getFeatures().bookmarks2()) { - fetchBookmarks2(account); - } else if (!account.getXmppConnection().getFeatures().bookmarksConversion()) { - fetchBookmarks(account); - } - final boolean flexible = - account.getXmppConnection() - .getFeatures() - .flexibleOfflineMessageRetrieval(); - final boolean catchup = getMessageArchiveService().inCatchup(account); - if (flexible - && catchup - && account.getXmppConnection().isMamPreferenceAlways()) { - sendIqPacket( - account, - mIqGenerator.purgeOfflineMessages(), - (acc, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Log.d( - Config.LOGTAG, - acc.getJid().asBareJid() - + ": successfully purged offline messages"); - } - }); - } - sendPresence(account); - if (mPushManagementService.available(account)) { - mPushManagementService.registerPushTokenOnServer(account); - } - connectMultiModeConversations(account); - syncDirtyContacts(account); - - unifiedPushBroker.renewUnifiedPushEndpointsOnBind(account); - } - }; - private boolean destroyed = false; - private int unreadCount = -1; - private final AtomicLong mLastExpiryRun = new AtomicLong(0); - private SecureRandom mRandom; - private final LruCache, ServiceDiscoveryResult> discoCache = - new LruCache<>(20); - private final OnStatusChanged statusListener = - new OnStatusChanged() { - - @Override - public void onStatusChanged(final Account account) { - XmppConnection connection = account.getXmppConnection(); - updateAccountUi(); - - - if (account.getStatus() == Account.State.ONLINE - || account.getStatus().isError()) { - mQuickConversationsService.signalAccountStateChange(); - } - - if (account.getStatus() == Account.State.ONLINE) { - synchronized (mLowPingTimeoutMode) { - if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": leaving low ping timeout mode"); - } - } - if (account.setShowErrorNotification(true)) { - databaseBackend.updateAccount(account); - } - mMessageArchiveService.executePendingQueries(account); - if (connection != null && connection.getFeatures().csi()) { - if (checkListeners()) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + " sending csi//inactive"); - connection.sendInactive(); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + " sending csi//active"); - connection.sendActive(); - } - } - List conversations = getConversations(); - for (Conversation conversation : conversations) { - final boolean inProgressJoin; - synchronized (account.inProgressConferenceJoins) { - inProgressJoin = - account.inProgressConferenceJoins.contains(conversation); - } - final boolean pendingJoin; - synchronized (account.pendingConferenceJoins) { - pendingJoin = account.pendingConferenceJoins.contains(conversation); - } - if (conversation.getAccount() == account - && !pendingJoin - && !inProgressJoin) { - sendUnsentMessages(conversation); - } - } - final List pendingLeaves; - synchronized (account.pendingConferenceLeaves) { - pendingLeaves = new ArrayList<>(account.pendingConferenceLeaves); - account.pendingConferenceLeaves.clear(); - } - for (Conversation conversation : pendingLeaves) { - leaveMuc(conversation); - } - final List pendingJoins; - synchronized (account.pendingConferenceJoins) { - pendingJoins = new ArrayList<>(account.pendingConferenceJoins); - account.pendingConferenceJoins.clear(); - } - for (Conversation conversation : pendingJoins) { - joinMuc(conversation); - } - scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode()); - } else if (account.getStatus() == Account.State.OFFLINE - || account.getStatus() == Account.State.DISABLED) { - resetSendingToWaiting(account); - if (account.isEnabled() && isInLowPingTimeoutMode(account)) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": went into offline state during low ping mode." - + " reconnecting now"); - reconnectAccount(account, true, false); - } else { - final int timeToReconnect = SECURE_RANDOM.nextInt(10) + 2; - scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode()); - } - } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) { - databaseBackend.updateAccount(account); - reconnectAccount(account, true, false); - } else if (account.getStatus() != Account.State.CONNECTING - && account.getStatus() != Account.State.NO_INTERNET) { - resetSendingToWaiting(account); - if (connection != null && account.getStatus().isAttemptReconnect()) { - final int next = connection.getTimeToNextAttempt(); - final boolean lowPingTimeoutMode = isInLowPingTimeoutMode(account); - if (next <= 0) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": error connecting account. reconnecting now." - + " lowPingTimeout=" - + lowPingTimeoutMode); - reconnectAccount(account, true, false); - } else { - final int attempt = connection.getAttempt() + 1; - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": error connecting account. try again in " - + next - + "s for the " - + attempt - + " time. lowPingTimeout=" - + lowPingTimeoutMode); - scheduleWakeUpCall(next, account.getUuid().hashCode()); - } - } - } - getNotificationService().updateErrorNotification(); - } - }; - private OpenPgpServiceConnection pgpServiceConnection; - private PgpEngine mPgpEngine = null; - private WakeLock wakeLock; - private LruCache mBitmapCache; - private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver(); - private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver(); - - private static String generateFetchKey(Account account, final Avatar avatar) { - return account.getJid().asBareJid() + "_" + avatar.owner + "_" + avatar.sha1sum; - } - - private boolean isInLowPingTimeoutMode(Account account) { - synchronized (mLowPingTimeoutMode) { - return mLowPingTimeoutMode.contains(account.getJid().asBareJid()); - } - } - - public void startForcingForegroundNotification() { - mForceForegroundService.set(true); - toggleForegroundService(); - } - - public void stopForcingForegroundNotification() { - mForceForegroundService.set(false); - toggleForegroundService(); - } - - public boolean areMessagesInitialized() { - return this.restoredFromDatabaseLatch.getCount() == 0; - } - - public PgpEngine getPgpEngine() { - if (!Config.supportOpenPgp()) { - return null; - } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) { - if (this.mPgpEngine == null) { - this.mPgpEngine = - new PgpEngine( - new OpenPgpApi( - getApplicationContext(), pgpServiceConnection.getService()), - this); - } - return mPgpEngine; - } else { - return null; - } - } - - public OpenPgpApi getOpenPgpApi() { - if (!Config.supportOpenPgp()) { - return null; - } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) { - return new OpenPgpApi(this, pgpServiceConnection.getService()); - } else { - return null; - } - } - - public FileBackend getFileBackend() { - return this.fileBackend; - } - - public AvatarService getAvatarService() { - return this.mAvatarService; - } - - public void attachLocationToConversation( - final Conversation conversation, final Uri uri, final UiCallback callback) { - int encryption = conversation.getNextEncryption(); - if (encryption == Message.ENCRYPTION_PGP) { - encryption = Message.ENCRYPTION_DECRYPTED; - } - Message message = new Message(conversation, uri.toString(), encryption); - Message.configurePrivateMessage(message); - if (encryption == Message.ENCRYPTION_DECRYPTED) { - getPgpEngine().encrypt(message, callback); - } else { - sendMessage(message); - callback.success(message); - } - } - - public void attachFileToConversation( - final Conversation conversation, - final Uri uri, - final String type, - final UiCallback callback) { - final Message message; - if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { - message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED); - } else { - message = new Message(conversation, "", conversation.getNextEncryption()); - } - if (!Message.configurePrivateFileMessage(message)) { - message.setCounterpart(conversation.getNextCounterpart()); - message.setType(Message.TYPE_FILE); - } - Log.d(Config.LOGTAG, "attachFile: type=" + message.getType()); - Log.d(Config.LOGTAG, "counterpart=" + message.getCounterpart()); - final AttachFileToConversationRunnable runnable = - new AttachFileToConversationRunnable(this, uri, type, message, conversation, callback, getMaxHttpUploadSize(conversation)); - if (runnable.isVideoMessage()) { - VIDEO_COMPRESSION_EXECUTOR.execute(runnable); - } else { - FILE_ATTACHMENT_EXECUTOR.execute(runnable); - } - } - - public long getMaxHttpUploadSize(Conversation conversation) { - final XmppConnection connection = conversation.getAccount().getXmppConnection(); - return connection == null ? -1 : connection.getFeatures().getMaxHttpUploadSize(); - } - - public void attachImageToConversation( - final Conversation conversation, - final Uri uri, - final String type, - final UiCallback callback) { - final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(this, uri, type); - final boolean compressPictures = getCompressImageResolutionPreference() != 0; - if (!compressPictures - || getFileBackend().useImageAsIs(uri) - || (mimeType != null && mimeType.endsWith("/gif")) - || getFileBackend().unusualBounds(uri)) { - Log.d( - Config.LOGTAG, - conversation.getAccount().getJid().asBareJid() - + ": not compressing picture. sending as file"); - attachFileToConversation(conversation, uri, mimeType, callback); - return; - } - final Message message; - if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { - message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED); - } else { - message = new Message(conversation, "", conversation.getNextEncryption()); - } - if (!Message.configurePrivateFileMessage(message)) { - message.setCounterpart(conversation.getNextCounterpart()); - message.setType(Message.TYPE_IMAGE); - } - Log.d(Config.LOGTAG, "attachImage: type=" + message.getType()); - FILE_ATTACHMENT_EXECUTOR.execute( - () -> { - try { - getFileBackend().copyImageToPrivateStorage(message, uri); - } catch (FileBackend.ImageCompressionException e) { - Log.d( - Config.LOGTAG, - "unable to compress image. fall back to file transfer", - e); - attachFileToConversation(conversation, uri, mimeType, callback); - return; - } catch (final FileBackend.FileCopyException e) { - callback.error(e.getResId(), message); - return; - } - if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { - final PgpEngine pgpEngine = getPgpEngine(); - if (pgpEngine != null) { - pgpEngine.encrypt(message, callback); - } else if (callback != null) { - callback.error(R.string.unable_to_connect_to_keychain, null); - } - } else { - sendMessage(message); - callback.success(message); - } - }); - } - - public Conversation find(Bookmark bookmark) { - return find(bookmark.getAccount(), bookmark.getJid()); - } - - public Conversation find(final Account account, final Jid jid) { - return find(getConversations(), account, jid); - } - - public boolean isMuc(final Account account, final Jid jid) { - final Conversation c = find(account, jid); - return c != null && c.getMode() == Conversational.MODE_MULTI; - } - - public void search( - final List term, - final String uuid, - final OnSearchResultsAvailable onSearchResultsAvailable) { - MessageSearchTask.search(this, term, uuid, onSearchResultsAvailable); - } - - @SuppressLint("InvalidWakeLockTag") - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - final String action = intent == null ? null : intent.getAction(); - final boolean needsForegroundService = - intent != null - && intent.getBooleanExtra( - EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE, false); - if (needsForegroundService) { - Log.d( - Config.LOGTAG, - "toggle forced foreground service after receiving event (action=" - + action - + ")"); - toggleForegroundService(true); - } - String pushedAccountHash = null; - boolean interactive = false; - if (action != null) { - final String uuid = intent.getStringExtra("uuid"); - switch (action) { - case ConnectivityManager.CONNECTIVITY_ACTION: - if (hasInternetConnection()) { - if (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0) { - schedulePostConnectivityChange(); - } - if (Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) { - resetAllAttemptCounts(true, false); - } - Resolver.clearCache(); - } - break; - case Intent.ACTION_SHUTDOWN: - logoutAndSave(true); - return START_NOT_STICKY; - case ACTION_CLEAR_MESSAGE_NOTIFICATION: - mNotificationExecutor.execute( - () -> { - try { - final Conversation c = findConversationByUuid(uuid); - if (c != null) { - mNotificationService.clearMessages(c); - } else { - mNotificationService.clearMessages(); - } - restoredFromDatabaseLatch.await(); - - } catch (InterruptedException e) { - Log.d( - Config.LOGTAG, - "unable to process clear message notification"); - } - }); - break; - case ACTION_CLEAR_MISSED_CALL_NOTIFICATION: - mNotificationExecutor.execute( - () -> { - try { - final Conversation c = findConversationByUuid(uuid); - if (c != null) { - mNotificationService.clearMissedCalls(c); - } else { - mNotificationService.clearMissedCalls(); - } - restoredFromDatabaseLatch.await(); - - } catch (InterruptedException e) { - Log.d( - Config.LOGTAG, - "unable to process clear missed call notification"); - } - }); - break; - case ACTION_DISMISS_CALL: - { - final String sessionId = - intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID); - Log.d( - Config.LOGTAG, - "received intent to dismiss call with session id " + sessionId); - mJingleConnectionManager.rejectRtpSession(sessionId); - break; - } - case TorServiceUtils.ACTION_STATUS: - final String status = intent.getStringExtra(TorServiceUtils.EXTRA_STATUS); - // TODO port and host are in 'extras' - but this may not be a reliable source? - if ("ON".equals(status)) { - handleOrbotStartedEvent(); - return START_STICKY; - } - break; - case ACTION_END_CALL: - { - final String sessionId = - intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID); - Log.d( - Config.LOGTAG, - "received intent to end call with session id " + sessionId); - mJingleConnectionManager.endRtpSession(sessionId); - } - break; - case ACTION_PROVISION_ACCOUNT: - { - final String address = intent.getStringExtra("address"); - final String password = intent.getStringExtra("password"); - if (QuickConversationsService.isQuicksy() - || Strings.isNullOrEmpty(address) - || Strings.isNullOrEmpty(password)) { - break; - } - provisionAccount(address, password); - break; - } - case ACTION_DISMISS_ERROR_NOTIFICATIONS: - dismissErrorNotifications(); - break; - case ACTION_TRY_AGAIN: - resetAllAttemptCounts(false, true); - interactive = true; - break; - case ACTION_REPLY_TO_CONVERSATION: - Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); - if (remoteInput == null) { - break; - } - final CharSequence body = remoteInput.getCharSequence("text_reply"); - final boolean dismissNotification = - intent.getBooleanExtra("dismiss_notification", false); - final String lastMessageUuid = intent.getStringExtra("last_message_uuid"); - if (body == null || body.length() <= 0) { - break; - } - mNotificationExecutor.execute( - () -> { - try { - restoredFromDatabaseLatch.await(); - final Conversation c = findConversationByUuid(uuid); - if (c != null) { - directReply( - c, - body.toString(), - lastMessageUuid, - dismissNotification); - } - } catch (InterruptedException e) { - Log.d(Config.LOGTAG, "unable to process direct reply"); - } - }); - break; - case ACTION_MARK_AS_READ: - mNotificationExecutor.execute( - () -> { - final Conversation c = findConversationByUuid(uuid); - if (c == null) { - Log.d( - Config.LOGTAG, - "received mark read intent for unknown conversation (" - + uuid - + ")"); - return; - } - try { - restoredFromDatabaseLatch.await(); - sendReadMarker(c, null); - } catch (InterruptedException e) { - Log.d( - Config.LOGTAG, - "unable to process notification read marker for" - + " conversation " - + c.getName()); - } - }); - break; - case ACTION_SNOOZE: - mNotificationExecutor.execute( - () -> { - final Conversation c = findConversationByUuid(uuid); - if (c == null) { - Log.d( - Config.LOGTAG, - "received snooze intent for unknown conversation (" - + uuid - + ")"); - return; - } - c.setMutedTill(System.currentTimeMillis() + 30 * 60 * 1000); - mNotificationService.clearMessages(c); - updateConversation(c); - }); - case AudioManager.RINGER_MODE_CHANGED_ACTION: - case NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED: - if (dndOnSilentMode()) { - refreshAllPresences(); - } - break; - case Intent.ACTION_SCREEN_ON: - deactivateGracePeriod(); - case Intent.ACTION_USER_PRESENT: - case Intent.ACTION_SCREEN_OFF: - if (awayWhenScreenLocked()) { - refreshAllPresences(); - } - break; - case ACTION_FCM_TOKEN_REFRESH: - refreshAllFcmTokens(); - break; - case ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS: - final String instance = intent.getStringExtra("instance"); - final Optional transport = - renewUnifiedPushEndpoints(); - if (instance != null && transport.isPresent()) { - unifiedPushBroker.rebroadcastEndpoint(instance, transport.get()); - } - break; - case ACTION_IDLE_PING: - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - scheduleNextIdlePing(); - } - break; - case ACTION_FCM_MESSAGE_RECEIVED: - pushedAccountHash = intent.getStringExtra("account"); - Log.d( - Config.LOGTAG, - "push message arrived in service. account=" + pushedAccountHash); - break; - case Intent.ACTION_SEND: - Uri uri = intent.getData(); - if (uri != null) { - Log.d(Config.LOGTAG, "received uri permission for " + uri); - } - return START_STICKY; - } - } - synchronized (this) { - WakeLockHelper.acquire(wakeLock); - boolean pingNow = - ConnectivityManager.CONNECTIVITY_ACTION.equals(action) - || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0 - && ACTION_POST_CONNECTIVITY_CHANGE.equals(action)); - final HashSet pingCandidates = new HashSet<>(); - final String androidId = PhoneHelper.getAndroidId(this); - for (Account account : accounts) { - final boolean pushWasMeantForThisAccount = - CryptoHelper.getAccountFingerprint(account, androidId) - .equals(pushedAccountHash); - pingNow |= - processAccountState( - account, - interactive, - "ui".equals(action), - pushWasMeantForThisAccount, - pingCandidates); - } - if (pingNow) { - for (Account account : pingCandidates) { - List conversations = getConversations(); - for (Conversation conversation : conversations) { - if (conversation.getAccount() == account && !account.pendingConferenceJoins.contains(conversation)) { - resendFailedFileMessages(conversation); - resendFailedMessages(conversation); - } - } - final boolean lowTimeout = isInLowPingTimeoutMode(account); - account.getXmppConnection().sendPing(); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + " send ping (action=" - + action - + ",lowTimeout=" - + lowTimeout - + ")"); - scheduleWakeUpCall( - lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT, - account.getUuid().hashCode()); - } - } - WakeLockHelper.release(wakeLock); - } - if (SystemClock.elapsedRealtime() - mLastExpiryRun.get() >= Config.EXPIRY_INTERVAL) { - expireOldMessages(); - expireOldFiles(); - deleteWebpreviewCache(); - } - // move files from /monocles chat/ --> /Android/data/ for Android >= 30 - if (Compatibility.runsThirty() && (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED - && ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)) { - StorageHelper.migrateStorage(this); - } - return START_STICKY; - } - - private void handleOrbotStartedEvent() { - for (final Account account : accounts) { - if (account.getStatus() == Account.State.TOR_NOT_AVAILABLE || account.getStatus() == Account.State.I2P_NOT_AVAILABLE) { - reconnectAccount(account, true, false); - } - } - } - public int maxResendTime(){ - return Integer.parseInt(getPreferences().getString(SettingsActivity.MAX_RESEND_TIME, getResources().getString(R.string.max_resend_time))); - } - private void deleteWebpreviewCache() { - new Thread(() -> { - try { - long start = SystemClock.elapsedRealtime(); - final Calendar time = Calendar.getInstance(); - time.add(Calendar.DAY_OF_YEAR, -7); - final File directory = new File(getCacheDir().getAbsolutePath(), File.separator + RICH_LINK_METADATA); - if (!directory.exists()) { - return; - } - final File[] files = directory.listFiles(); - if (files != null) { - int count = 0; - for (File file : files) { - Date lastModified = new Date(file.lastModified()); - if (lastModified.before(time.getTime())) { - file.delete(); - count++; - } - } - Log.d(Config.LOGTAG, "Deleted " + count + " expired webpreview cache files in " + (SystemClock.elapsedRealtime() - start) + "ms"); - } - } catch (Exception e) { - Log.d(Config.LOGTAG, "Deleted no expired webpreview cache files because of " + e); - } - }).start(); - } - - private boolean processAccountState( - Account account, - boolean interactive, - boolean isUiAction, - boolean isAccountPushed, - HashSet pingCandidates) { - storeNumberOfAccounts(this.getAccounts().size()); - boolean pingNow = false; - if (account.getStatus().isAttemptReconnect()) { - if (!hasInternetConnection()) { - account.setStatus(Account.State.NO_INTERNET); - if (statusListener != null) { - statusListener.onStatusChanged(account); - } - } else { - if (account.getStatus() == Account.State.NO_INTERNET) { - account.setStatus(Account.State.OFFLINE); - if (statusListener != null) { - statusListener.onStatusChanged(account); - } - } - if (account.getStatus() == Account.State.ONLINE) { - synchronized (mLowPingTimeoutMode) { - long lastReceived = account.getXmppConnection().getLastPacketReceived(); - long lastSent = account.getXmppConnection().getLastPingSent(); - long pingInterval = - isUiAction - ? Config.PING_MIN_INTERVAL * 1000 - : Config.PING_MAX_INTERVAL * 1000; - long msToNextPing = - (Math.max(lastReceived, lastSent) + pingInterval) - - SystemClock.elapsedRealtime(); - int pingTimeout = - mLowPingTimeoutMode.contains(account.getJid().asBareJid()) - ? Config.LOW_PING_TIMEOUT * 1000 - : Config.PING_TIMEOUT * 1000; - long pingTimeoutIn = - (lastSent + pingTimeout) - SystemClock.elapsedRealtime(); - if (lastSent > lastReceived) { - if (pingTimeoutIn < 0) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": ping timeout"); - this.reconnectAccount(account, true, interactive); - } else { - int secs = (int) (pingTimeoutIn / 1000); - this.scheduleWakeUpCall(secs, account.getUuid().hashCode()); - } - } else { - pingCandidates.add(account); - if (isAccountPushed) { - pingNow = true; - if (mLowPingTimeoutMode.add(account.getJid().asBareJid())) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": entering low ping timeout mode"); - } - } else if (msToNextPing <= 0) { - pingNow = true; - } else { - this.scheduleWakeUpCall( - (int) (msToNextPing / 1000), account.getUuid().hashCode()); - if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": leaving low ping timeout mode"); - } - } - } - } - } else if (account.getStatus() == Account.State.OFFLINE) { - reconnectAccount(account, true, interactive); - } else if (account.getStatus() == Account.State.CONNECTING) { - long secondsSinceLastConnect = - (SystemClock.elapsedRealtime() - - account.getXmppConnection().getLastConnect()) - / 1000; - long secondsSinceLastDisco = - (SystemClock.elapsedRealtime() - - account.getXmppConnection().getLastDiscoStarted()) - / 1000; - long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco; - long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect; - if (timeout < 0) { - Log.d( - Config.LOGTAG, - account.getJid() - + ": time out during connect reconnecting" - + " (secondsSinceLast=" - + secondsSinceLastConnect - + ")"); - account.getXmppConnection().resetAttemptCount(false); - reconnectAccount(account, true, interactive); - } else if (discoTimeout < 0) { - account.getXmppConnection().sendDiscoTimeout(); - scheduleWakeUpCall( - (int) Math.min(timeout, discoTimeout), - account.getUuid().hashCode()); - } else { - scheduleWakeUpCall( - (int) Math.min(timeout, discoTimeout), - account.getUuid().hashCode()); - } - } else { - if (account.getXmppConnection().getTimeToNextAttempt() <= 0) { - reconnectAccount(account, true, interactive); - } - } - } - } - return pingNow; - } - - private void storeNumberOfAccounts(int accounts) { - final SharedPreferences.Editor editor = getPreferences().edit(); - Log.d(Config.LOGTAG, "Number of accounts is " + accounts); - editor.putInt(SettingsActivity.NUMBER_OF_ACCOUNTS, accounts); - editor.apply(); - if (accounts > 1 && !multipleAccounts()) { - editor.putBoolean(ENABLE_MULTI_ACCOUNTS, true); - } - } - - public boolean processUnifiedPushMessage( - final Account account, final Jid transport, final Element push) { - return unifiedPushBroker.processPushMessage(account, transport, push); - } - - public void reinitializeMuclumbusService() { - mChannelDiscoveryService.initializeMuclumbusService(); - } - - public void discoverChannels( - String query, - ChannelDiscoveryService.Method method, - ChannelDiscoveryService.OnChannelSearchResultsFound onChannelSearchResultsFound) { - mChannelDiscoveryService.discover( - Strings.nullToEmpty(query).trim(), method, onChannelSearchResultsFound); - } - - public boolean isDataSaverDisabled() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - final ConnectivityManager connectivityManager = - (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); - return !connectivityManager.isActiveNetworkMetered() - || Compatibility.getRestrictBackgroundStatus(connectivityManager) - == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED; - } else { - return true; - } - } - - 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() { - @Override - public void success(Message message) { - if (dismissAfterReply) { - markRead((Conversation) message.getConversation(), true); - } else { - mNotificationService.pushFromDirectReply(message); - } - } - @Override - public void error(int errorCode, Message object) {} - - @Override - public void userInputRequired(PendingIntent pi, Message object) {} - - @Override - public void progress(int progress) {} - }); - } else { - sendMessage(message); - if (dismissAfterReply) { - markRead(conversation, true); - } else { - mNotificationService.pushFromDirectReply(message); - } - } - } - - public boolean pauseVoiceOnMoveFromEar() { - return getBooleanPreference(SettingsActivity.PAUSE_VOICE, R.bool.pause_voice); - } - - public boolean easyDownloader() { - return getBooleanPreference(SettingsActivity.EASY_DOWNLOADER, R.bool.easy_downloader); - } - - private boolean dndOnSilentMode() { - return getBooleanPreference(SettingsActivity.DND_ON_SILENT_MODE, R.bool.dnd_on_silent_mode); - } - - private boolean manuallyChangePresence() { - return getBooleanPreference( - SettingsActivity.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence); - } - - private boolean treatVibrateAsSilent() { - return getBooleanPreference( - SettingsActivity.TREAT_VIBRATE_AS_SILENT, R.bool.treat_vibrate_as_silent); - } - - private boolean awayWhenScreenLocked() { - return getBooleanPreference( - SettingsActivity.AWAY_WHEN_SCREEN_IS_OFF, R.bool.away_when_screen_off); - } - - private String getCompressPicturesPreference() { - return getPreferences() - .getString( - "image_compression", - getResources().getString(R.string.image_compression)); - } - - public boolean alternativeVoiceSettings() { - return getBooleanPreference("alternative_voice_settings", R.bool.alternative_voice_settings); - } - - public boolean colored_muc_names() { - return getBooleanPreference("colored_muc_names", R.bool.use_colored_muc_names); - } - - public int getCompressImageResolutionPreference() { - switch (getPreferences().getString("image_compression", getResources().getString(R.string.image_compression))) { - case "low": - return 720; - case "mid": - return 1920; - case "high": - return 3840; - case "uncompressed": - return 0; - default: - return 1920; - } - } - - public int getCompressImageSizePreference() { - switch (getPreferences().getString("image_compression", getResources().getString(R.string.image_compression))) { - case "low": - return 209715; // 0.2 * 1024 * 1024 = 209715 (0.2 MiB) - case "mid": - return 524288; // 0.5 * 1024 * 1024 = 524288 (0.5 MiB) - case "high": - return 1048576; // 1 * 1024 * 1024 = 1048576 (1 MiB) - case "uncompressed": - return 0; - default: - return 524288; - } - } - - public int getCompressVideoResolutionPreference() { - switch (getPreferences().getString("video_compression", getResources().getString(R.string.video_compression))) { - case "verylow": - return getResources().getInteger(R.integer.verylow_video_res); - case "low": - return getResources().getInteger(R.integer.low_video_res); - case "mid": - return getResources().getInteger(R.integer.mid_video_res); - case "high": - return getResources().getInteger(R.integer.high_video_res); - case "uncompressed": - return 0; - default: - return getResources().getInteger(R.integer.mid_video_res); - } - } - - public int getCompressVideoBitratePreference() { - switch (getPreferences().getString("video_compression", getResources().getString(R.string.video_compression))) { - case "verylow": - return getResources().getInteger(R.integer.verylow_video_bitrate); - case "low": - return getResources().getInteger(R.integer.low_video_bitrate); - case "mid": - return getResources().getInteger(R.integer.mid_video_bitrate); - case "high": - return getResources().getInteger(R.integer.high_video_bitrate); - case "uncompressed": - return 0; - default: - return getResources().getInteger(R.integer.mid_video_bitrate); - } - } - - public DefaultAudioStrategy getCompressAudioPreference() { - switch (getPreferences().getString("video_compression", getResources().getString(R.string.video_compression))) { - case "verylow": - return TranscoderStrategies.AUDIO_LQ; - case "low": - return TranscoderStrategies.AUDIO_LQ; - case "mid": - return TranscoderStrategies.AUDIO_MQ; - case "high": - return TranscoderStrategies.AUDIO_HQ; - case "uncompressed": - return TranscoderStrategies.AUDIO_HQ; - default: - return TranscoderStrategies.AUDIO_MQ; - } - } - - public boolean getAttachmentChoicePreference() { - return getBooleanPreference(SettingsActivity.QUICK_SHARE_ATTACHMENT_CHOICE, R.bool.quick_share_attachment_choice); - } - - public long getIndividualNotificationPreference(final Conversation conversation) { - final String uuid = conversation.getUuid(); - return getPreferences().getLong(SettingsActivity.INDIVIDUAL_NOTIFICATION_PREFIX + uuid, 0L); - } - - public void setIndividualNotificationPreference(final Conversation conversation, final boolean reset) { - final String uuid = conversation.getUuid(); - long value = System.currentTimeMillis(); - if (reset) { - value = 0; - } - getPreferences().edit().putLong(SettingsActivity.INDIVIDUAL_NOTIFICATION_PREFIX + uuid, value).apply(); - } - - public boolean hasIndividualNotification(Conversation conversation) { - if (conversation == null) { - return false; - } - return getIndividualNotificationPreference(conversation) > 0L; - } - - private Presence.Status getTargetPresence() { - if (dndOnSilentMode() && isPhoneSilenced()) { - return Presence.Status.DND; - } else if (awayWhenScreenLocked() && isScreenLocked()) { - return Presence.Status.AWAY; - } else { - return Presence.Status.ONLINE; - } - } - - public boolean isScreenLocked() { - final KeyguardManager keyguardManager = - (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); - final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); - final boolean locked = keyguardManager != null && keyguardManager.isKeyguardLocked(); - final boolean interactive = powerManager != null && powerManager.isInteractive(); - return locked || !interactive; - } - - private boolean isPhoneSilenced() { - final boolean notificationDnd; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - final NotificationManager notificationManager = - getSystemService(NotificationManager.class); - final int filter = - notificationManager == null - ? NotificationManager.INTERRUPTION_FILTER_UNKNOWN - : notificationManager.getCurrentInterruptionFilter(); - notificationDnd = filter >= NotificationManager.INTERRUPTION_FILTER_PRIORITY; - } else { - notificationDnd = false; - } - final AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - final int ringerMode = - audioManager == null - ? AudioManager.RINGER_MODE_NORMAL - : audioManager.getRingerMode(); - try { - if (treatVibrateAsSilent()) { - return notificationDnd || ringerMode != AudioManager.RINGER_MODE_NORMAL; - } else { - return notificationDnd || ringerMode == AudioManager.RINGER_MODE_SILENT; - } - } catch (Throwable throwable) { - Log.d( - Config.LOGTAG, - "platform bug in isPhoneSilenced (" + throwable.getMessage() + ")"); - return notificationDnd; - } - } - - private void resetAllAttemptCounts(boolean reallyAll, boolean retryImmediately) { - Log.d(Config.LOGTAG, "resetting all attempt counts"); - for (Account account : accounts) { - if (account.hasErrorStatus() || reallyAll) { - final XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - connection.resetAttemptCount(retryImmediately); - } - } - if (account.setShowErrorNotification(true)) { - mDatabaseWriterExecutor.execute(() -> databaseBackend.updateAccount(account)); - } - } - mNotificationService.updateErrorNotification(); - } - - private void dismissErrorNotifications() { - for (final Account account : this.accounts) { - if (account.hasErrorStatus()) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": dismissing error notification"); - if (account.setShowErrorNotification(false)) { - mDatabaseWriterExecutor.execute(() -> databaseBackend.updateAccount(account)); - } - } - } - } - - private void expireOldMessages() { - expireOldMessages(false); - } - - public void expireOldMessages(final boolean resetHasMessagesLeftOnServer) { - mLastExpiryRun.set(SystemClock.elapsedRealtime()); - mDatabaseWriterExecutor.execute( - () -> { - long timestamp = getAutomaticMessageDeletionDate(); - if (timestamp > 0) { - databaseBackend.expireOldMessages(timestamp); - synchronized (XmppConnectionService.this.conversations) { - for (Conversation conversation : - XmppConnectionService.this.conversations) { - conversation.expireOldMessages(timestamp); - if (resetHasMessagesLeftOnServer) { - conversation.messagesLoaded.set(true); - conversation.setHasMessagesLeftOnServer(true); - } - } - } - updateConversationUi(); - } - }); - } - - public boolean hasInternetConnection() { - final ConnectivityManager cm = - ContextCompat.getSystemService(this, ConnectivityManager.class); - if (cm == null) { - return true; // if internet connection can not be checked it is probably best to just - // try - } - try { - if (Compatibility.runsTwentyNine()) { - final Network activeNetwork = cm.getActiveNetwork(); - final NetworkCapabilities capabilities = - activeNetwork == null ? null : cm.getNetworkCapabilities(activeNetwork); - return capabilities != null - && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); - } else { - final NetworkInfo networkInfo = cm.getActiveNetworkInfo(); - return networkInfo != null - && (networkInfo.isConnected() - || networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET); - } - } catch (final RuntimeException e) { - Log.d(Config.LOGTAG, "unable to check for internet connection", e); - return true; // if internet connection can not be checked it is probably best to just - // try - } - } - - public boolean isWIFI() { - final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - try { - final NetworkInfo activeNetwork = cm == null ? null : cm.getActiveNetworkInfo(); - if (activeNetwork != null) { // connected to the internet - if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) { - return true; - } - } - } catch (RuntimeException e) { - Log.d(Config.LOGTAG, "unable to check for WIFI connection", e); - return false; - } - return false; - } - - public boolean isMobile() { - final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - try { - final NetworkInfo activeNetwork = cm == null ? null : cm.getActiveNetworkInfo(); - if (activeNetwork != null) { // connected to the internet - if (activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE) { - if (!activeNetwork.isRoaming()) { - return true; - } - } - } - } catch (RuntimeException e) { - Log.d(Config.LOGTAG, "unable to check for mobile connection", e); - return false; - } - return false; - } - - public boolean isMobileRoaming() { - final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - try { - final NetworkInfo activeNetwork = cm == null ? null : cm.getActiveNetworkInfo(); - if (activeNetwork != null) { // connected to the internet - if (activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE) { - if (activeNetwork.isRoaming()) { - return true; - } - } - } - } catch (RuntimeException e) { - Log.d(Config.LOGTAG, "unable to check for roaming connection", e); - return false; - } - return false; - } - - @SuppressLint("TrulyRandom") - @Override - public void onCreate() { - updateNotificationChannels(); - if (Compatibility.runsTwentySix()) { - cleanOldNotificationChannels(); - } - mChannelDiscoveryService.initializeMuclumbusService(); - mForceDuringOnCreate.set(Compatibility.runsAndTargetsTwentySix(this)); - toggleForegroundService(); - this.destroyed = false; - OmemoSetting.load(this); - ExceptionHelper.init(getApplicationContext()); - try { - Security.insertProviderAt(Conscrypt.newProvider(), 1); - } catch (Throwable throwable) { - Log.e(Config.LOGTAG, "unable to initialize security provider", throwable); - } - Resolver.init(this); - this.mRandom = new SecureRandom(); - updateMemorizingTrustmanager(); - final int DEFAULT_CACHE_SIZE_PROPORTION = 8; - ActivityManager manager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE); - int memoryClass = manager.getMemoryClass(); - int memoryClassInKilobytes = memoryClass * 1024; - int cacheSize = memoryClassInKilobytes / DEFAULT_CACHE_SIZE_PROPORTION; - this.mBitmapCache = - new LruCache(cacheSize) { - @Override - protected int sizeOf(final String key, final Bitmap bitmap) { - return bitmap.getByteCount() / 1024; - } - }; - if (mLastActivity == 0) { - mLastActivity = - getPreferences().getLong(SETTING_LAST_ACTIVITY_TS, System.currentTimeMillis()); - } - - Log.d(Config.LOGTAG, "initializing database..."); - this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext()); - Log.d(Config.LOGTAG, "restoring accounts..."); - this.accounts = databaseBackend.getAccounts(); - - final SharedPreferences.Editor editor = getPreferences().edit(); - if (this.accounts.size() == 0 - && Arrays.asList("Sony", "Sony Ericsson").contains(Build.MANUFACTURER)) { - editor.putBoolean(SettingsActivity.SHOW_FOREGROUND_SERVICE, true); - Log.d( - Config.LOGTAG, - Build.MANUFACTURER + " is on blacklist. enabling foreground service"); - } - final boolean hasEnabledAccounts = hasEnabledAccounts(); - editor.putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply(); - editor.apply(); - toggleSetProfilePictureActivity(hasEnabledAccounts); - reconfigurePushDistributor(); - restoreFromDatabase(); - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M - || ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) - == PackageManager.PERMISSION_GRANTED) { - startContactObserver(); - } - FileBackend.switchStorage(usingInnerStorage()); - FILE_OBSERVER_EXECUTOR.execute(fileBackend::deleteHistoricAvatarPath); - if (Compatibility.hasStoragePermission(this)) { - Log.d(Config.LOGTAG, "starting file observer"); - 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() { - @Override - public void onBound(IOpenPgpService2 service) { - for (Account account : accounts) { - final PgpDecryptionService pgp = - account.getPgpDecryptionService(); - if (pgp != null) { - pgp.continueDecryption(true); - } - } - } - - @Override - public void onError(Exception e) {} - }); - this.pgpServiceConnection.bindToService(); - } - - final PowerManager pm = ContextCompat.getSystemService(this, PowerManager.class); - this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Config.LOGTAG + ":Service"); - toggleForegroundService(); - updateUnreadCountBadge(); - toggleScreenEventReceiver(); - final IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(TorServiceUtils.ACTION_STATUS); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - scheduleNextIdlePing(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); - } - intentFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED); - } - registerReceiver(this.mInternalEventReceiver, intentFilter); - mForceDuringOnCreate.set(false); - toggleForegroundService(); - setupPhoneStateListener(); - //start export log service every day at given time - ScheduleAutomaticExport(); - // cancel scheduled exporter - CancelAutomaticExport(false); - } - - public void updateNotificationChannels() { - if (Compatibility.runsTwentySix()) { - new Thread(mNotificationService::updateChannels).start(); - } - } - @RequiresApi(api = Build.VERSION_CODES.O) - public void cleanOldNotificationChannels() { - new Thread(() -> { - try { - mNotificationService.cleanAllOldNotificationChannels(this); - } catch (Exception e) { - e.printStackTrace(); - } - }).start(); - } - - private void setupPhoneStateListener() { - final TelephonyManager telephonyManager = - (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); - if (telephonyManager == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - return; - } - telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); - } - - public boolean isPhoneInCall() { - return isPhoneInCall.get(); - } - - private void checkForDeletedFiles() { - if (destroyed) { - Log.d( - Config.LOGTAG, - "Do not check for deleted files because service has been destroyed"); - return; - } - final long start = SystemClock.elapsedRealtime(); - final List relativeFilePaths = - databaseBackend.getFilePathInfo(); - final List changed = new ArrayList<>(); - for (final DatabaseBackend.FilePathInfo filePath : relativeFilePaths) { - if (destroyed) { - Log.d( - Config.LOGTAG, - "Stop checking for deleted files because service has been destroyed"); - return; - } - final File file = fileBackend.getFileForPath(filePath.path); - if (file != null && filePath.setFileDeleted(!file.exists())) { - changed.add(filePath); - } - } - final long duration = SystemClock.elapsedRealtime() - start; - Log.d( - Config.LOGTAG, - "found " - + changed.size() - + " changed files on start up. total=" - + relativeFilePaths.size() - + ". (" - + duration - + "ms)"); - if (changed.size() > 0) { - databaseBackend.markFilesAsChanged(changed); - markChangedFiles(changed); - } - } - - public void startContactObserver() { - getContentResolver() - .registerContentObserver( - ContactsContract.Contacts.CONTENT_URI, - true, - new ContentObserver(null) { - @Override - public void onChange(boolean selfChange) { - super.onChange(selfChange); - if (restoredFromDatabaseLatch.getCount() == 0) { - loadPhoneContacts(); - } - } - }); - } - - @Override - public void onTrimMemory(int level) { - super.onTrimMemory(level); - if (level >= TRIM_MEMORY_COMPLETE) { - Log.d(Config.LOGTAG, "clear cache due to low memory"); - getBitmapCache().evictAll(); - } - } - - @Override - public void onDestroy() { - try { - unregisterReceiver(this.mInternalEventReceiver); - unregisterReceiver(this.mInternalScreenEventReceiver); - } catch (final IllegalArgumentException e) { - // ignored - } - destroyed = false; - fileObserver.stopWatching(); - super.onDestroy(); - // cancel scheduled exporter - CancelAutomaticExport(true); - } - - public void restartFileObserver() { - Log.d(Config.LOGTAG, "restarting file observer"); - FILE_OBSERVER_EXECUTOR.execute(this.fileObserver::restartWatching); - FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles); - } - - public void toggleScreenEventReceiver() { - if (awayWhenScreenLocked() && !manuallyChangePresence()) { - final IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_SCREEN_ON); - filter.addAction(Intent.ACTION_SCREEN_OFF); - filter.addAction(Intent.ACTION_USER_PRESENT); - registerReceiver(this.mInternalScreenEventReceiver, filter); - } else { - try { - unregisterReceiver(this.mInternalScreenEventReceiver); - } catch (IllegalArgumentException e) { - //ignored - } - } - } - - public void toggleForegroundService() { - toggleForegroundService(false); - } - - public void setOngoingCall( - AbstractJingleConnection.Id id, Set media, final boolean reconnecting) { - ongoingCall.set(new OngoingCall(id, media, reconnecting)); - toggleForegroundService(false); - } - - public void removeOngoingCall() { - ongoingCall.set(null); - toggleForegroundService(false); - } - - private void toggleForegroundService(boolean force) { - final boolean status; - final OngoingCall ongoing = ongoingCall.get(); - if (force - || mForceDuringOnCreate.get() - || mForceForegroundService.get() - || ongoing != null - || (Compatibility.keepForegroundService(this) && hasEnabledAccounts())) { - final Notification notification; - final int id; - if (ongoing != null) { - notification = this.mNotificationService.getOngoingCallNotification(ongoing); - id = NotificationService.ONGOING_CALL_NOTIFICATION_ID; - startForeground(id, notification); - mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID); - } else { - notification = this.mNotificationService.createForegroundNotification(); - id = NotificationService.FOREGROUND_NOTIFICATION_ID; - startForeground(id, notification); - } - - if (!mForceForegroundService.get()) { - mNotificationService.notify(id, notification); - } - status = true; - } else { - stopForeground(true); - status = false; - } - if (!mForceForegroundService.get()) { - mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID); - } - if (ongoing == null) { - mNotificationService.cancel(NotificationService.ONGOING_CALL_NOTIFICATION_ID); - } - Log.d(Config.LOGTAG, "ForegroundService: " + (status ? "on" : "off")); - } - - public boolean foregroundNotificationNeedsUpdatingWhenErrorStateChanges() { - return !mForceForegroundService.get() - && ongoingCall.get() == null - && Compatibility.keepForegroundService(this) - && hasEnabledAccounts(); - } - - @Override - public void onTaskRemoved(final Intent rootIntent) { - super.onTaskRemoved(rootIntent); - if ((Compatibility.keepForegroundService(this) && hasEnabledAccounts()) - || mForceForegroundService.get() - || ongoingCall.get() != null) { - Log.d(Config.LOGTAG, "ignoring onTaskRemoved because foreground service is activated"); - } else { - this.logoutAndSave(false); - } - } - - private void logoutAndSave(boolean stop) { - int activeAccounts = 0; - if (accounts != null) { - for (final Account account : accounts) { - if (account.getStatus() != Account.State.DISABLED) { - databaseBackend.writeRoster(account.getRoster()); - activeAccounts++; - } - if (account.getXmppConnection() != null) { - new Thread(() -> disconnect(account, false)).start(); - } - } - } - if (stop || activeAccounts == 0) { - Log.d(Config.LOGTAG, "good bye"); - stopSelf(); - } - } - - private void schedulePostConnectivityChange() { - final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - if (alarmManager == null) { - return; - } - - final long triggerAtMillis = - SystemClock.elapsedRealtime() - + (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL * 1000); - final Intent intent = new Intent(this, EventReceiver.class); - intent.setAction(ACTION_POST_CONNECTIVITY_CHANGE); - try { - final PendingIntent pendingIntent = - PendingIntent.getBroadcast( - this, - 1, - intent, - s() - ? PendingIntent.FLAG_IMMUTABLE - | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - alarmManager.setAndAllowWhileIdle( - AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent); - } else { - alarmManager.set( - AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent); - } - } catch (RuntimeException e) { - Log.e(Config.LOGTAG, "unable to schedule alarm for post connectivity change", e); - } - } - - public void scheduleWakeUpCall(int seconds, int requestCode) { - final long timeToWake = - SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000L; - final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - if (alarmManager == null) { - return; - } - final Intent intent = new Intent(this, EventReceiver.class); - intent.setAction("ping"); - try { - final PendingIntent pendingIntent; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - pendingIntent = - PendingIntent.getBroadcast( - this, requestCode, intent, PendingIntent.FLAG_IMMUTABLE); - } else { - pendingIntent = PendingIntent.getBroadcast(this, requestCode, intent, 0); - } - alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent); - } catch (RuntimeException e) { - Log.e(Config.LOGTAG, "unable to schedule alarm for ping", e); - } catch (Exception e) { - Log.e(Config.LOGTAG, "unable to schedule alarm for ping", e); - } - } - - @TargetApi(Build.VERSION_CODES.M) - private void scheduleNextIdlePing() { - final long timeToWake = SystemClock.elapsedRealtime() + (Config.IDLE_PING_INTERVAL * 1000); - final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - if (alarmManager == null) { - return; - } - final Intent intent = new Intent(this, EventReceiver.class); - intent.setAction(ACTION_IDLE_PING); - try { - final PendingIntent pendingIntent = - PendingIntent.getBroadcast( - this, - 0, - intent, - s() - ? PendingIntent.FLAG_IMMUTABLE - | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - alarmManager.setAndAllowWhileIdle( - AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent); - } catch (RuntimeException e) { - Log.d(Config.LOGTAG, "unable to schedule alarm for idle ping", e); - } - } - - public XmppConnection createConnection(final Account account) { - final XmppConnection connection = new XmppConnection(account, this); - connection.setOnMessagePacketReceivedListener(this.mMessageParser); - connection.setOnStatusChangedListener(this.statusListener); - connection.setOnPresencePacketReceivedListener(this.mPresenceParser); - connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser); - connection.setOnJinglePacketReceivedListener((mJingleConnectionManager::deliverPacket)); - connection.setOnBindListener(this.mOnBindListener); - connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); - connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService); - connection.addOnAdvancedStreamFeaturesAvailableListener(this.mAvatarService); - AxolotlService axolotlService = account.getAxolotlService(); - if (axolotlService != null) { - connection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService); - } - return connection; - } - - public void sendChatState(Conversation conversation) { - if (sendChatStates()) { - MessagePacket packet = mMessageGenerator.generateChatState(conversation); - sendMessagePacket(conversation.getAccount(), packet); - } - } - - private void sendFileMessage(final Message message, final boolean delay) { - Log.d(Config.LOGTAG, "send file message"); - final Account account = message.getConversation().getAccount(); - if (account.httpUploadAvailable(fileBackend.getFile(message, false).getSize()) - || message.getConversation().getMode() == Conversation.MODE_MULTI) { - mHttpConnectionManager.createNewUploadConnection(message, delay); - } else { - mJingleConnectionManager.startJingleFileTransfer(message); - } - } - - public void sendMessage(final Message message) { - sendMessage(message, false, false); - } - - private void sendMessage(final Message message, final boolean resend, final boolean delay) { - if (resend) { - message.setTime(System.currentTimeMillis()); - } - final Account account = message.getConversation().getAccount(); - if (account.setShowErrorNotification(true)) { - databaseBackend.updateAccount(account); - mNotificationService.updateErrorNotification(); - } - final Conversation conversation = (Conversation) message.getConversation(); - account.deactivateGracePeriod(); - if (QuickConversationsService.isQuicksy() - && conversation.getMode() == Conversation.MODE_SINGLE) { - final Contact contact = conversation.getContact(); - if (!contact.showInRoster() && contact.getOption(Contact.Options.SYNCED_VIA_OTHER)) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": adding " - + contact.getJid() - + " on sending message"); - createContact(contact, true); - } - } - MessagePacket packet = null; - final boolean addToConversation = !message.edited(); - boolean saveInDb = addToConversation; - message.setStatus(Message.STATUS_WAITING); - - if (message.getEncryption() != Message.ENCRYPTION_NONE - && conversation.getMode() == Conversation.MODE_MULTI - && conversation.isPrivateAndNonAnonymous()) { - if (conversation.setAttribute( - Conversation.ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, true)) { - databaseBackend.updateConversation(conversation); - } - } - if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) { - conversation.endOtrIfNeeded(); - conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, - message1 -> markMessage(message1, Message.STATUS_SEND_FAILED)); - } - - final boolean inProgressJoin = isJoinInProgress(conversation); - - if (account.isOnlineAndConnected() && !inProgressJoin) { - switch (message.getEncryption()) { - case Message.ENCRYPTION_NONE: - if (message.needsUploading()) { - if (account.httpUploadAvailable( - fileBackend.getFile(message, false).getSize()) - || conversation.getMode() == Conversation.MODE_MULTI - || message.fixCounterpart()) { - this.sendFileMessage(message, delay); - } else { - break; - } - } else { - packet = mMessageGenerator.generateChat(message); - } - break; - case Message.ENCRYPTION_PGP: - case Message.ENCRYPTION_DECRYPTED: - if (message.needsUploading()) { - if (account.httpUploadAvailable( - fileBackend.getFile(message, false).getSize()) - || conversation.getMode() == Conversation.MODE_MULTI - || message.fixCounterpart()) { - this.sendFileMessage(message, delay); - } else { - break; - } - } else { - packet = mMessageGenerator.generatePgpChat(message); - } - break; - case Message.ENCRYPTION_OTR: - SessionImpl otrSession = conversation.getOtrSession(); - if (otrSession != null && otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) { - try { - message.setCounterpart(OtrJidHelper.fromSessionID(otrSession.getSessionID())); - } catch (IllegalArgumentException e) { - break; - } - if (message.needsUploading()) { - mJingleConnectionManager.startJingleFileTransfer(message); - } else { - packet = mMessageGenerator.generateOtrChat(message); - } - } else if (otrSession == null) { - if (message.fixCounterpart()) { - conversation.startOtrSession(message.getCounterpart().getResource(), true); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not fix counterpart for OTR message to contact " + message.getCounterpart()); - break; - } - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + " OTR session with " + message.getContact() + " is in wrong state: " + otrSession.getSessionStatus().toString()); - } - break; - case Message.ENCRYPTION_AXOLOTL: - message.setFingerprint(account.getAxolotlService().getOwnFingerprint()); - if (message.needsUploading()) { - if (account.httpUploadAvailable( - fileBackend.getFile(message, false).getSize()) - || conversation.getMode() == Conversation.MODE_MULTI - || message.fixCounterpart()) { - this.sendFileMessage(message, delay); - } else { - break; - } - } else { - XmppAxolotlMessage axolotlMessage = - account.getAxolotlService().fetchAxolotlMessageFromCache(message); - if (axolotlMessage == null) { - account.getAxolotlService().preparePayloadMessage(message, delay); - } else { - packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage); - } - } - break; - - } - if (packet != null) { - if (account.getXmppConnection().getFeatures().sm() - || (conversation.getMode() == Conversation.MODE_MULTI - && message.getCounterpart().isBareJid())) { - message.setStatus(Message.STATUS_UNSEND); - } else { - message.setStatus(Message.STATUS_SEND); - } - } - } else { - switch (message.getEncryption()) { - case Message.ENCRYPTION_DECRYPTED: - if (!message.needsUploading()) { - String pgpBody = message.getEncryptedBody(); - String decryptedBody = message.getBody(); - message.setBody(pgpBody); // TODO might throw NPE - message.setEncryption(Message.ENCRYPTION_PGP); - if (message.edited()) { - message.setBody(decryptedBody); - message.setEncryption(Message.ENCRYPTION_DECRYPTED); - if (!databaseBackend.updateMessage(message, message.getEditedId())) { - Log.e(Config.LOGTAG, "error updated message in DB after edit"); - } - updateConversationUi(); - return; - } else { - databaseBackend.createMessage(message); - saveInDb = false; - message.setBody(decryptedBody); - message.setEncryption(Message.ENCRYPTION_DECRYPTED); - } - } - break; - case Message.ENCRYPTION_OTR: - if (!conversation.hasValidOtrSession() && message.getCounterpart() != null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": create otr session without starting for " + message.getContact().getJid()); - conversation.startOtrSession(message.getCounterpart().getResource(), false); - } - break; - case Message.ENCRYPTION_AXOLOTL: - message.setFingerprint(account.getAxolotlService().getOwnFingerprint()); - break; - } - } - - boolean mucMessage = - conversation.getMode() == Conversation.MODE_MULTI && !message.isPrivateMessage(); - if (mucMessage) { - message.setCounterpart(conversation.getMucOptions().getSelf().getFullJid()); - } - - if (resend) { - if (packet != null && addToConversation) { - if (account.getXmppConnection().getFeatures().sm() || mucMessage) { - markMessage(message, Message.STATUS_UNSEND); - } else { - markMessage(message, Message.STATUS_SEND); - } - } - } else { - if (addToConversation) { - conversation.add(message); - } - if (saveInDb) { - databaseBackend.createMessage(message); - } else if (message.edited()) { - if (!databaseBackend.updateMessage(message, message.getEditedId())) { - Log.e(Config.LOGTAG, "error updated message in DB after edit"); - } - } - updateConversationUi(); - } - if (packet != null) { - if (delay) { - mMessageGenerator.addDelay(packet, message.getTimeSent()); - } - if (conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) { - if (this.sendChatStates()) { - packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); - } - } - sendMessagePacket(account, packet); - } - } - - private boolean isJoinInProgress(final Conversation conversation) { - final Account account = conversation.getAccount(); - synchronized (account.inProgressConferenceJoins) { - if (conversation.getMode() == Conversational.MODE_MULTI) { - final boolean inProgress = account.inProgressConferenceJoins.contains(conversation); - final boolean pending = account.pendingConferenceJoins.contains(conversation); - final boolean inProgressJoin = inProgress || pending; - if (inProgressJoin) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": holding back message to group. inProgress=" - + inProgress - + ", pending=" - + pending); - } - return inProgressJoin; - } else { - return false; - } - } - } - - private void sendUnsentMessages(final Conversation conversation) { - final Runnable runnable = () -> { - conversation.findWaitingMessages(message -> resendMessage(message, true)); - }; - mDatabaseWriterExecutor.execute((runnable)); - - } - private void resendFailedMessages(final Conversation conversation) { - final Runnable runnable = () -> { - conversation.findResendAbleFailedMessage(message -> { - if (message.increaseResendCount() < maxResendTime()) { - Log.d(Config.LOGTAG, "Resend failed message " + message.getErrorMessage() + " at times " + message.getResendCount() + " bytes for " + conversation.getJid()); - // because it'll use a custom delay here, only the last time will delay the message. - resendFailedMessages(message); - } - }); - }; - mMessageResendTaskExecuter.execute((runnable)); - } - private void resendFailedFileMessages(final Conversation conversation) { - final Runnable runnable = () -> { - conversation.findFailedMessagesWithFiles(message -> { - if (mHttpConnectionManager.getAutoAcceptFileSize() >= message.getFileParams().size) { - Log.d(Config.LOGTAG, "Resend failed message with size " + message.getFileParams().size + " bytes for " + conversation.getJid()); - resendMessage(message, true); - } - }); - }; - mDatabaseWriterExecutor.execute((runnable)); - } - - public void resendMessage(final Message message, final boolean delay) { - sendMessage(message, true, delay); - } - - public void requestEasyOnboardingInvite( - final Account account, final EasyOnboardingInvite.OnInviteRequested callback) { - final XmppConnection connection = account.getXmppConnection(); - final Jid jid = - connection == null - ? null - : connection.getJidForCommand(Namespace.EASY_ONBOARDING_INVITE); - if (jid == null) { - callback.inviteRequestFailed( - getString(R.string.server_does_not_support_easy_onboarding_invites)); - return; - } - final IqPacket request = new IqPacket(IqPacket.TYPE.SET); - request.setTo(jid); - final Element command = request.addChild("command", Namespace.COMMANDS); - command.setAttribute("node", Namespace.EASY_ONBOARDING_INVITE); - command.setAttribute("action", "execute"); - sendIqPacket( - account, - request, - (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - final Element resultCommand = - response.findChild("command", Namespace.COMMANDS); - final Element x = - resultCommand == null - ? null - : resultCommand.findChild("x", Namespace.DATA); - if (x != null) { - final Data data = Data.parse(x); - final String uri = data.getValue("uri"); - final String landingUrl = data.getValue("landing-url"); - if (uri != null) { - final EasyOnboardingInvite invite = - new EasyOnboardingInvite( - jid.getDomain().toEscapedString(), uri, landingUrl); - callback.inviteRequested(invite); - return; - } - } - callback.inviteRequestFailed(getString(R.string.unable_to_parse_invite)); - Log.d(Config.LOGTAG, response.toString()); - } else if (response.getType() == IqPacket.TYPE.ERROR) { - callback.inviteRequestFailed(IqParser.errorMessage(response)); - } else { - callback.inviteRequestFailed(getString(R.string.remote_server_timeout)); - } - }); - - } - - public void fetchRosterFromServer(final Account account) { - final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET); - if (!"".equals(account.getRosterVersion())) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": fetching roster version " - + account.getRosterVersion()); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching roster"); - } - iqPacket.query(Namespace.ROSTER).setAttribute("ver", account.getRosterVersion()); - sendIqPacket(account, iqPacket, mIqParser); - } - - public void fetchBookmarks(final Account account) { - final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET); - final Element query = iqPacket.query("jabber:iq:private"); - query.addChild("storage", Namespace.BOOKMARKS); - final OnIqPacketReceived callback = - (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - final Element query1 = response.query(); - final Element storage = query1.findChild("storage", "storage:bookmarks"); - Map bookmarks = Bookmark.parseFromStorage(storage, account); - processBookmarksInitial(a, bookmarks, false); - } else { - Log.d( - Config.LOGTAG, - a.getJid().asBareJid() + ": could not fetch bookmarks"); - } - }; - sendIqPacket(account, iqPacket, callback); - } - - public void fetchBookmarks2(final Account account) { - final IqPacket retrieve = mIqGenerator.retrieveBookmarks(); - sendIqPacket( - account, - retrieve, - new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(final Account account, final IqPacket response) { - if (response.getType() == IqPacket.TYPE.RESULT) { - final Element pubsub = response.findChild("pubsub", Namespace.PUB_SUB); - final Map bookmarks = - Bookmark.parseFromPubsub(pubsub, account); - processBookmarksInitial(account, bookmarks, true); - } - } - }); - } - - public void processBookmarksInitial( - Account account, Map bookmarks, final boolean pep) { - final Set previousBookmarks = account.getBookmarkedJids(); - final boolean synchronizeWithBookmarks = synchronizeWithBookmarks(); - for (Bookmark bookmark : bookmarks.values()) { - previousBookmarks.remove(bookmark.getJid().asBareJid()); - processModifiedBookmark(bookmark, pep, synchronizeWithBookmarks); - } - if (pep && synchronizeWithBookmarks) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": " - + previousBookmarks.size() - + " bookmarks have been removed"); - for (Jid jid : previousBookmarks) { - processDeletedBookmark(account, jid); - } - } - account.setBookmarks(bookmarks); - } - - public void processDeletedBookmark(Account account, Jid jid) { - final Conversation conversation = find(account, jid); - if (conversation != null - && conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": archiving destroyed conference (" - + conversation.getJid() - + ") after receiving pep"); - archiveConversation(conversation, false); - } - } - - private void processModifiedBookmark( - Bookmark bookmark, final boolean pep, final boolean synchronizeWithBookmarks) { - final Account account = bookmark.getAccount(); - Conversation conversation = find(bookmark); - if (conversation != null) { - if (conversation.getMode() != Conversation.MODE_MULTI) { - return; - } - bookmark.setConversation(conversation); - if (pep && synchronizeWithBookmarks && !bookmark.autojoin()) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": archiving conference (" - + conversation.getJid() - + ") after receiving pep"); - archiveConversation(conversation, false); - } else { - final MucOptions mucOptions = conversation.getMucOptions(); - if (mucOptions.getError() == MucOptions.Error.NICK_IN_USE) { - final String current = mucOptions.getActualNick(); - final String proposed = mucOptions.getProposedNick(); - if (current != null && !current.equals(proposed)) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": proposed nick changed after bookmark push " - + current - + "->" - + proposed); - joinMuc(conversation); - } - } - } - } else if (synchronizeWithBookmarks && bookmark.autojoin()) { - conversation = - findOrCreateConversation(account, bookmark.getFullJid(), true, true, false); - bookmark.setConversation(conversation); - } - } - - public void processModifiedBookmark(Bookmark bookmark) { - final boolean synchronizeWithBookmarks = synchronizeWithBookmarks(); - processModifiedBookmark(bookmark, true, synchronizeWithBookmarks); - } - - public void createBookmark(final Account account, final Bookmark bookmark) { - account.putBookmark(bookmark); - final XmppConnection connection = account.getXmppConnection(); - if (connection == null) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": no connection. ignoring bookmark creation"); - } else if (connection != null && connection.getFeatures().bookmarks2()) { - final Element item = mIqGenerator.publishBookmarkItem(bookmark); - pushNodeAndEnforcePublishOptions( - account, - Namespace.BOOKMARKS2, - item, - bookmark.getJid().asBareJid().toEscapedString(), - PublishOptions.persistentWhitelistAccessMaxItems()); - } else if (connection != null && connection.getFeatures().bookmarksConversion()) { - pushBookmarksPep(account); - } else { - pushBookmarksPrivateXml(account); - } - } - - public void deleteBookmark(final Account account, final Bookmark bookmark) { - account.removeBookmark(bookmark); - final XmppConnection connection = account.getXmppConnection(); - if (connection != null && connection.getFeatures().bookmarks2()) { - IqPacket request = - mIqGenerator.deleteItem( - Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toEscapedString()); - sendIqPacket( - account, - request, - (a, response) -> { - if (response.getType() == IqPacket.TYPE.ERROR) { - Log.d( - Config.LOGTAG, - a.getJid().asBareJid() - + ": unable to delete bookmark " - + response.getErrorCondition()); - } - }); - } else if (connection != null && connection.getFeatures().bookmarksConversion()) { - pushBookmarksPep(account); - } else { - pushBookmarksPrivateXml(account); - } - } - - public void pushBookmarks(Account account) { - if (account.getXmppConnection().getFeatures().bookmarksConversion()) { - pushBookmarksPep(account); - } else { - pushBookmarksPrivateXml(account); - } - } - - private void pushBookmarksPrivateXml(Account account) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via private xml"); - IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET); - Element query = iqPacket.query("jabber:iq:private"); - Element storage = query.addChild("storage", "storage:bookmarks"); - for (final Bookmark bookmark : account.getBookmarks()) { - storage.addChild(bookmark); - } - sendIqPacket(account, iqPacket, mDefaultIqHandler); - } - - private void pushBookmarksPep(Account account) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via pep"); - final Element storage = new Element("storage", "storage:bookmarks"); - for (final Bookmark bookmark : account.getBookmarks()) { - storage.addChild(bookmark); - } - pushNodeAndEnforcePublishOptions( - account, - Namespace.BOOKMARKS, - storage, - "current", - PublishOptions.persistentWhitelistAccess()); - } - - private void pushNodeAndEnforcePublishOptions( - final Account account, - final String node, - final Element element, - final String id, - final Bundle options) { - pushNodeAndEnforcePublishOptions(account, node, element, id, options, true); - } - - private void pushNodeAndEnforcePublishOptions( - final Account account, - final String node, - final Element element, - final String id, - final Bundle options, - final boolean retry) { - final IqPacket packet = mIqGenerator.publishElement(node, element, id, options); - sendIqPacket( - account, - packet, - (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - return; - } - if (retry && PublishOptions.preconditionNotMet(response)) { - pushNodeConfiguration( - account, - node, - options, - new OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - pushNodeAndEnforcePublishOptions( - account, node, element, id, options, false); - } - @Override - public void onPushFailed() { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": unable to push node configuration (" - + node - + ")"); - } - }); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": error publishing bookmarks (retry=" - + retry - + ") " - + response); - } - }); - } - - private void restoreFromDatabase() { - synchronized (this.conversations) { - try { - final Map accountLookupTable = new Hashtable<>(); - for (Account account : this.accounts) { - accountLookupTable.put(account.getUuid(), account); - } - Log.d(Config.LOGTAG, "restoring conversations..."); - final long startTimeConversationsRestore = SystemClock.elapsedRealtime(); - this.conversations.addAll( - databaseBackend.getConversations(Conversation.STATUS_AVAILABLE)); - for (Iterator iterator = conversations.listIterator(); - iterator.hasNext(); ) { - Conversation conversation = iterator.next(); - Account account = accountLookupTable.get(conversation.getAccountUuid()); - if (account != null) { - conversation.setAccount(account); - } else { - Log.e( - Config.LOGTAG, - "unable to restore Conversations with " + conversation.getJid()); - iterator.remove(); - } - } - long diffConversationsRestore = - SystemClock.elapsedRealtime() - startTimeConversationsRestore; - Log.d( - Config.LOGTAG, - "finished restoring conversations in " + diffConversationsRestore + "ms"); - Runnable runnable = - () -> { - 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)); - databaseBackend.expireOldMessages(deletionDate); - } - Log.d(Config.LOGTAG, "restoring roster..."); - for (final Account account : accounts) { - databaseBackend.readRoster(account.getRoster()); - account.initAccountServices( - XmppConnectionService - .this); // roster needs to be loaded at this stage - } - getBitmapCache().evictAll(); - loadPhoneContacts(); - Log.d(Config.LOGTAG, "restoring messages..."); - final long startMessageRestore = SystemClock.elapsedRealtime(); - final Conversation quickLoad = QuickLoader.get(this.conversations); - if (quickLoad != null) { - restoreMessages(quickLoad); - updateConversationUi(); - final long diffMessageRestore = - SystemClock.elapsedRealtime() - startMessageRestore; - Log.d( - Config.LOGTAG, - "quickly restored " - + quickLoad.getName() - + " after " - + diffMessageRestore - + "ms"); - } - for (Conversation conversation : this.conversations) { - if (quickLoad != conversation) { - restoreMessages(conversation); - } - } - mNotificationService.finishBacklog(); - restoredFromDatabaseLatch.countDown(); - final long diffMessageRestore = - SystemClock.elapsedRealtime() - startMessageRestore; - Log.d( - Config.LOGTAG, - "finished restoring messages in " + diffMessageRestore + "ms"); - updateConversationUi(); - }; - mDatabaseReaderExecutor.execute( - runnable); // will contain one write command (expiry) but that's fine - } catch (Exception e) { - Log.d(Config.LOGTAG, "error restoring messages: " + e); - } - } - } - - private void restoreMessages(Conversation conversation) { - conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE)); - conversation.findUnsentTextMessages( - message -> markMessage(message, Message.STATUS_WAITING)); - conversation.findUnreadMessagesAndCalls(mNotificationService::pushFromBacklog); - } - - public void loadPhoneContacts() { - mContactMergerExecutor.execute( - () -> { - final Map contacts = JabberIdContact.load(this); - Log.d(Config.LOGTAG, "start merging phone contacts with roster"); - for (final Account account : accounts) { - final List withSystemAccounts = - account.getRoster().getWithSystemAccounts(JabberIdContact.class); - for (final JabberIdContact jidContact : contacts.values()) { - final Contact contact = - account.getRoster().getContact(jidContact.getJid()); - boolean needsCacheClean = contact.setPhoneContact(jidContact); - if (needsCacheClean) { - getAvatarService().clear(contact); - } - withSystemAccounts.remove(contact); - } - for (final Contact contact : withSystemAccounts) { - boolean needsCacheClean = - contact.unsetPhoneContact(JabberIdContact.class); - if (needsCacheClean) { - getAvatarService().clear(contact); - } - } - } - Log.d(Config.LOGTAG, "finished merging phone contacts"); - mShortcutService.refresh( - mInitialAddressbookSyncCompleted.compareAndSet(false, true)); - updateRosterUi(); - mQuickConversationsService.considerSync(); - }); - } - - public void syncRoster(final Account account) { - mRosterSyncTaskManager.execute( - account, () -> databaseBackend.writeRoster(account.getRoster())); - } - - public List getConversations() { - return this.conversations; - } - - private void markFileDeleted(final File file) { - synchronized (FILENAMES_TO_IGNORE_DELETION) { - if (FILENAMES_TO_IGNORE_DELETION.remove(file.getAbsolutePath())) { - Log.d(Config.LOGTAG, "ignored deletion of " + file.getAbsolutePath()); - return; - } - } - final boolean isInternalFile = fileBackend.isInternalFile(file); - final List uuids = databaseBackend.markFileAsDeleted(file, isInternalFile); - Log.d( - Config.LOGTAG, - "deleted file " - + file.getAbsolutePath() - + " internal=" - + isInternalFile - + ", database hits=" - + uuids.size()); - markUuidsAsDeletedFiles(uuids); - } - - private void markUuidsAsDeletedFiles(List uuids) { - boolean deleted = false; - for (Conversation conversation : getConversations()) { - deleted |= conversation.markAsDeleted(uuids); - } - for (final String uuid : uuids) { - evictPreview(uuid); - } - if (deleted) { - updateConversationUi(); - } - } - - private void markChangedFiles(List infos) { - boolean changed = false; - for (Conversation conversation : getConversations()) { - changed |= conversation.markAsChanged(infos); - } - if (changed) { - updateConversationUi(); - } - } - - public void populateWithOrderedConversations(final List list) { - populateWithOrderedConversations(list, true, true); - } - - public void populateWithOrderedConversations( - final List list, final boolean includeNoFileUpload) { - populateWithOrderedConversations(list, includeNoFileUpload, true); - } - - public void populateWithOrderedConversations( - final List list, final boolean includeNoFileUpload, final boolean sort) { - final List orderedUuids; - if (sort) { - orderedUuids = null; - } else { - orderedUuids = new ArrayList<>(); - for (Conversation conversation : list) { - orderedUuids.add(conversation.getUuid()); - } - } - list.clear(); - if (includeNoFileUpload) { - list.addAll(getConversations()); - } else { - for (Conversation conversation : getConversations()) { - if (conversation.getMode() == Conversation.MODE_SINGLE - || (conversation.getAccount().httpUploadAvailable() - && conversation.getMucOptions().participating())) { - list.add(conversation); - } - } - } - try { - if (orderedUuids != null) { - Collections.sort( - list, - (a, b) -> { - final int indexA = orderedUuids.indexOf(a.getUuid()); - final int indexB = orderedUuids.indexOf(b.getUuid()); - if (indexA == -1 || indexB == -1 || indexA == indexB) { - return a.compareTo(b); - } - return indexA - indexB; - }); - } else { - Collections.sort(list); - } - } catch (IllegalArgumentException e) { - // ignore - } - } - - public void loadMoreMessages( - final Conversation conversation, - final long timestamp, - final OnMoreMessagesLoaded callback) { - if (XmppConnectionService.this - .getMessageArchiveService() - .queryInProgress(conversation, callback)) { - return; - } else if (timestamp == 0) { - return; - } - Log.d( - Config.LOGTAG, - "load more messages for " - + conversation.getName() - + " prior to " - + MessageGenerator.getTimestamp(timestamp)); - final Runnable runnable = - () -> { - final Account account = conversation.getAccount(); - List messages = - databaseBackend.getMessages(conversation, 50, timestamp); - if (messages.size() > 0) { - conversation.addAll(0, messages); - callback.onMoreMessagesLoaded(messages.size(), conversation); - } else if (conversation.hasMessagesLeftOnServer() - && account.isOnlineAndConnected() - && conversation.getLastClearHistory().getTimestamp() == 0) { - final boolean mamAvailable; - if (conversation.getMode() == Conversation.MODE_SINGLE) { - mamAvailable = - account.getXmppConnection().getFeatures().mam() - && !conversation.getContact().isBlocked(); - } else { - mamAvailable = conversation.getMucOptions().mamSupport(); - } - if (mamAvailable) { - MessageArchiveService.Query query = - getMessageArchiveService() - .query( - conversation, - new MamReference(0), - timestamp, - false); - if (query != null) { - query.setCallback(callback); - callback.informUser(R.string.fetching_history_from_server); - } else { - callback.informUser(R.string.not_fetching_history_retention_period); - } - } - } - }; - mDatabaseReaderExecutor.execute(runnable); - } - - public List getAccounts() { - return this.accounts; - } - - /** - * This will find all conferences with the contact as member and also the conference that is the - * contact (that 'fake' contact is used to store the avatar) - */ - public List findAllConferencesWith(Contact contact) { - final ArrayList results = new ArrayList<>(); - for (final Conversation c : conversations) { - if (c.getMode() != Conversation.MODE_MULTI) { - continue; - } - final MucOptions mucOptions = c.getMucOptions(); - if (c.getJid().asBareJid().equals(contact.getJid().asBareJid()) - || (mucOptions != null && mucOptions.isContactInRoom(contact))) { - results.add(c); - } - } - return results; - } - - public Conversation find(final Iterable haystack, final Contact contact) { - for (final Conversation conversation : haystack) { - if (conversation.getContact() == contact) { - return conversation; - } - } - return null; - } - - public Conversation find( - final Iterable haystack, final Account account, final Jid jid) { - if (jid == null) { - return null; - } - for (final Conversation conversation : haystack) { - if ((account == null || conversation.getAccount() == account) - && (conversation.getJid().asBareJid().equals(jid.asBareJid()))) { - return conversation; - } - } - return null; - } - - public boolean isConversationsListEmpty(final Conversation ignore) { - synchronized (this.conversations) { - final int size = this.conversations.size(); - return size == 0 || size == 1 && this.conversations.get(0) == ignore; - } - } - - public boolean isConversationStillOpen(final Conversation conversation) { - synchronized (this.conversations) { - for (Conversation current : this.conversations) { - if (current == conversation) { - return true; - } - } - } - return false; - } - - - public Conversation findOrCreateConversation( - Account account, Jid jid, boolean muc, final boolean async) { - return this.findOrCreateConversation(account, jid, muc, false, async); - } - - public Conversation findOrCreateConversation( - final Account account, - final Jid jid, - final boolean muc, - final boolean joinAfterCreate, - final boolean async) { - return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, null, async); - } - - public Conversation findOrCreateConversation( - final Account account, - final Jid jid, - final boolean muc, - final boolean joinAfterCreate, - final MessageArchiveService.Query query, - final boolean async) { - synchronized (this.conversations) { - Conversation conversation = find(account, jid); - if (conversation != null) { - return conversation; - } - conversation = databaseBackend.findConversation(account, jid); - final boolean loadMessagesFromDb; - if (conversation != null) { - conversation.setStatus(Conversation.STATUS_AVAILABLE); - conversation.setAccount(account); - if (muc) { - conversation.setMode(Conversation.MODE_MULTI); - conversation.setContactJid(jid); - } else { - conversation.setMode(Conversation.MODE_SINGLE); - conversation.setContactJid(jid.asBareJid()); - } - databaseBackend.updateConversation(conversation); - loadMessagesFromDb = conversation.messagesLoaded.compareAndSet(true, false); - } else { - String conversationName; - Contact contact = account.getRoster().getContact(jid); - if (contact != null) { - conversationName = contact.getDisplayName(); - } else { - conversationName = jid.getLocal(); - } - if (muc) { - conversation = - new Conversation( - conversationName, account, jid, Conversation.MODE_MULTI); - } else { - conversation = - new Conversation( - conversationName, - account, - jid.asBareJid(), - Conversation.MODE_SINGLE); - } - this.databaseBackend.createConversation(conversation); - loadMessagesFromDb = false; - } - final Conversation c = conversation; - final Runnable runnable = - () -> { - if (loadMessagesFromDb) { - c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE)); - updateConversationUi(); - c.messagesLoaded.set(true); - } - if (account.getXmppConnection() != null - && !c.getContact().isBlocked() - && account.getXmppConnection().getFeatures().mam() - && !muc) { - if (query == null) { - mMessageArchiveService.query(c); - } else { - if (query.getConversation() == null) { - mMessageArchiveService.query( - c, query.getStart(), query.isCatchup()); - } - } - } - if (joinAfterCreate) { - joinMuc(c); - } - }; - if (async) { - mDatabaseReaderExecutor.execute(runnable); - } else { - runnable.run(); - } - this.conversations.add(conversation); - updateConversationUi(); - return conversation; - } - } - - public Conversation findConversation(final Account account, final Jid jid, final boolean muc) { - synchronized (this.conversations) { - Conversation conversation = find(account, jid); - if (conversation != null) { - return conversation; - } - conversation = databaseBackend.findConversation(account, jid); - if (conversation != null) { - conversation.setStatus(Conversation.STATUS_AVAILABLE); - conversation.setAccount(account); - if (muc) { - conversation.setMode(Conversation.MODE_MULTI); - conversation.setContactJid(jid); - } else { - conversation.setMode(Conversation.MODE_SINGLE); - conversation.setContactJid(jid.asBareJid()); - } - databaseBackend.updateConversation(conversation); - } else { - String conversationName; - Contact contact = account.getRoster().getContact(jid); - if (contact != null) { - conversationName = contact.getDisplayName(); - } else { - conversationName = jid.getLocal(); - } - if (muc) { - conversation = new Conversation(conversationName, account, jid, - Conversation.MODE_MULTI); - } else { - conversation = new Conversation(conversationName, account, jid.asBareJid(), - Conversation.MODE_SINGLE); - } - } - return conversation; - } - } - - public void archiveConversation(Conversation conversation) { - archiveConversation(conversation, true); - } - - private void archiveConversation( - Conversation conversation, final boolean maySynchronizeWithBookmarks) { - getNotificationService().clear(conversation); - conversation.setStatus(Conversation.STATUS_ARCHIVED); - conversation.setNextMessage(null); - if (Compatibility.runsTwentySix()) { - try { - mNotificationService.cleanNotificationChannels(this, conversation.getUuid()); - } catch (Exception e) { - e.printStackTrace(); - } - } - synchronized (this.conversations) { - getMessageArchiveService().kill(conversation); - if (conversation.getMode() == Conversation.MODE_MULTI) { - if (conversation.getAccount().getStatus() == Account.State.ONLINE) { - final Bookmark bookmark = conversation.getBookmark(); - if (maySynchronizeWithBookmarks - && bookmark != null - && synchronizeWithBookmarks()) { - if (conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) { - Account account = bookmark.getAccount(); - bookmark.setConversation(null); - deleteBookmark(account, bookmark); - } else if (bookmark.autojoin()) { - bookmark.setAutojoin(false); - createBookmark(bookmark.getAccount(), bookmark); - } - } - } - leaveMuc(conversation); - } else { - if (conversation - .getContact() - .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { - stopPresenceUpdatesTo(conversation.getContact()); - } - } - updateConversation(conversation); - this.conversations.remove(conversation); - updateConversationUi(); - } - } - - public void stopPresenceUpdatesTo(Contact contact) { - Log.d(Config.LOGTAG, "Canceling presence request from " + contact.getJid().toString()); - sendPresencePacket(contact.getAccount(), mPresenceGenerator.stopPresenceUpdatesTo(contact)); - contact.resetOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); - } - - public void createAccount(final Account account) { - account.initAccountServices(this); - databaseBackend.createAccount(account); - this.accounts.add(account); - this.reconnectAccountInBackground(account); - updateAccountUi(); - syncEnabledAccountSetting(); - toggleForegroundService(); - } - - private void syncEnabledAccountSetting() { - final boolean hasEnabledAccounts = hasEnabledAccounts(); - getPreferences() - .edit() - .putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts) - .apply(); - toggleSetProfilePictureActivity(hasEnabledAccounts); - } - - private void toggleSetProfilePictureActivity(final boolean enabled) { - try { - final ComponentName name = - new ComponentName(this, ChooseAccountForProfilePictureActivity.class); - final int targetState = - enabled - ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED - : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; - getPackageManager() - .setComponentEnabledSetting(name, targetState, PackageManager.DONT_KILL_APP); - } catch (IllegalStateException e) { - Log.d(Config.LOGTAG, "unable to toggle profile picture activity"); - } - } - public boolean reconfigurePushDistributor() { - return this.unifiedPushBroker.reconfigurePushDistributor(); - } - - public Optional renewUnifiedPushEndpoints() { - return this.unifiedPushBroker.renewUnifiedPushEndpoints(); - } - - private void provisionAccount(final String address, final String password) { - final Jid jid = Jid.ofEscaped(address); - final Account account = new Account(jid, password); - account.setOption(Account.OPTION_DISABLED, true); - Log.d(Config.LOGTAG, jid.asBareJid().toEscapedString() + ": provisioning account"); - createAccount(account); - } - - public void createAccountFromKey(final String alias, final OnAccountCreated callback) { - new Thread( - () -> { - try { - final X509Certificate[] chain = - KeyChain.getCertificateChain(this, alias); - final X509Certificate cert = - chain != null && chain.length > 0 ? chain[0] : null; - if (cert == null) { - callback.informUser(R.string.unable_to_parse_certificate); - return; - } - Pair info = CryptoHelper.extractJidAndName(cert); - if (info == null) { - callback.informUser(R.string.certificate_does_not_contain_jid); - return; - } - if (findAccountByJid(info.first) == null) { - final Account account = new Account(info.first, ""); - account.setPrivateKeyAlias(alias); - account.setOption(Account.OPTION_DISABLED, true); - account.setOption(Account.OPTION_FIXED_USERNAME, true); - account.setDisplayName(info.second); - createAccount(account); - callback.onAccountCreated(account); - if (Config.X509_VERIFICATION) { - try { - getMemorizingTrustManager() - .getNonInteractive(account.getServer()) - .checkClientTrusted(chain, "RSA"); - } catch (CertificateException e) { - callback.informUser( - R.string.certificate_chain_is_not_trusted); - } - } - } else { - callback.informUser(R.string.account_already_exists); - } - } catch (Exception e) { - callback.informUser(R.string.unable_to_parse_certificate); - } - }) - .start(); - } - - public void updateKeyInAccount(final Account account, final String alias) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": update key in account " + alias); - try { - X509Certificate[] chain = - KeyChain.getCertificateChain(XmppConnectionService.this, alias); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + " loaded certificate chain"); - Pair info = CryptoHelper.extractJidAndName(chain[0]); - if (info == null) { - showErrorToastInUi(R.string.certificate_does_not_contain_jid); - return; - } - if (account.getJid().asBareJid().equals(info.first)) { - account.setPrivateKeyAlias(alias); - account.setDisplayName(info.second); - databaseBackend.updateAccount(account); - if (Config.X509_VERIFICATION) { - try { - getMemorizingTrustManager() - .getNonInteractive() - .checkClientTrusted(chain, "RSA"); - } catch (CertificateException e) { - showErrorToastInUi(R.string.certificate_chain_is_not_trusted); - } - account.getAxolotlService().regenerateKeys(true); - } - } else { - showErrorToastInUi(R.string.jid_does_not_match_certificate); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - public boolean updateAccount(final Account account) { - if (databaseBackend.updateAccount(account)) { - account.setShowErrorNotification(true); - this.statusListener.onStatusChanged(account); - databaseBackend.updateAccount(account); - reconnectAccountInBackground(account); - updateAccountUi(); - getNotificationService().updateErrorNotification(); - toggleForegroundService(); - syncEnabledAccountSetting(); - mChannelDiscoveryService.cleanCache(); - return true; - } else { - return false; - } - } - - public void updateAccountPasswordOnServer( - final Account account, - final String newPassword, - final OnAccountPasswordChanged callback) { - final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword); - sendIqPacket( - account, - iq, - (a, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - a.setPassword(newPassword); - a.setOption(Account.OPTION_MAGIC_CREATE, false); - databaseBackend.updateAccount(a); - callback.onPasswordChangeSucceeded(); - } else { - callback.onPasswordChangeFailed(); - } - }); - } - - public void deleteAccountFromServer(final Account account) { - account.getXmppConnection().sendDeleteRequest(); - deleteAccount(account); - } - - public void deleteAccount(final Account account) { - final boolean connected = account.getStatus() == Account.State.ONLINE; - synchronized (this.conversations) { - if (connected) { - account.getAxolotlService().deleteOmemoIdentity(); - } - for (final Conversation conversation : conversations) { - if (conversation.getAccount() == account) { - if (conversation.getMode() == Conversation.MODE_MULTI) { - if (Compatibility.runsTwentySix()) { - try { - mNotificationService.cleanNotificationChannels(this, conversation.getUuid()); - } catch (Exception e) { - e.printStackTrace(); - } - } - if (connected) { - leaveMuc(conversation); - } - } - conversations.remove(conversation); - mNotificationService.clear(conversation); - } - } - if (account.getXmppConnection() != null) { - new Thread(() -> disconnect(account, !connected)).start(); - } - final Runnable runnable = - () -> { - if (!databaseBackend.deleteAccount(account)) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": unable to delete account"); - } - }; - mDatabaseWriterExecutor.execute(runnable); - this.accounts.remove(account); - this.mRosterSyncTaskManager.clear(account); - updateAccountUi(); - mNotificationService.updateErrorNotification(); - syncEnabledAccountSetting(); - toggleForegroundService(); - } - } - - public void setOnConversationListChangedListener(OnConversationUpdate listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - remainingListeners = checkListeners(); - if (!this.mOnConversationUpdates.add(listener)) { - Log.w( - Config.LOGTAG, - listener.getClass().getName() - + " is already registered as ConversationListChangedListener"); - } - this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0); - } - if (remainingListeners) { - switchToForeground(); - } - } - - public void removeOnConversationListChangedListener(OnConversationUpdate listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - this.mOnConversationUpdates.remove(listener); - this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0); - remainingListeners = checkListeners(); - } - if (remainingListeners) { - switchToBackground(); - } - } - - public void setOnShowErrorToastListener(OnShowErrorToast listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - remainingListeners = checkListeners(); - if (!this.mOnShowErrorToasts.add(listener)) { - Log.w( - Config.LOGTAG, - listener.getClass().getName() - + " is already registered as OnShowErrorToastListener"); - } - } - if (remainingListeners) { - switchToForeground(); - } - } - - public void removeOnShowErrorToastListener(OnShowErrorToast onShowErrorToast) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - this.mOnShowErrorToasts.remove(onShowErrorToast); - remainingListeners = checkListeners(); - } - if (remainingListeners) { - switchToBackground(); - } - } - - public void setOnAccountListChangedListener(OnAccountUpdate listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - remainingListeners = checkListeners(); - if (!this.mOnAccountUpdates.add(listener)) { - Log.w( - Config.LOGTAG, - listener.getClass().getName() - + " is already registered as OnAccountListChangedtListener"); - } - } - if (remainingListeners) { - switchToForeground(); - } - } - - public void removeOnAccountListChangedListener(OnAccountUpdate listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - this.mOnAccountUpdates.remove(listener); - remainingListeners = checkListeners(); - } - if (remainingListeners) { - switchToBackground(); - } - } - - public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - remainingListeners = checkListeners(); - if (!this.mOnCaptchaRequested.add(listener)) { - Log.w( - Config.LOGTAG, - listener.getClass().getName() - + " is already registered as OnCaptchaRequestListener"); - } - } - if (remainingListeners) { - switchToForeground(); - } - } - - public void removeOnCaptchaRequestedListener(OnCaptchaRequested listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - this.mOnCaptchaRequested.remove(listener); - remainingListeners = checkListeners(); - } - if (remainingListeners) { - switchToBackground(); - } - } - - public void setOnRosterUpdateListener(final OnRosterUpdate listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - remainingListeners = checkListeners(); - if (!this.mOnRosterUpdates.add(listener)) { - Log.w( - Config.LOGTAG, - listener.getClass().getName() - + " is already registered as OnRosterUpdateListener"); - } - } - if (remainingListeners) { - switchToForeground(); - } - } - - public void removeOnRosterUpdateListener(final OnRosterUpdate listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - this.mOnRosterUpdates.remove(listener); - remainingListeners = checkListeners(); - } - if (remainingListeners) { - switchToBackground(); - } - } - - public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - remainingListeners = checkListeners(); - if (!this.mOnUpdateBlocklist.add(listener)) { - Log.w( - Config.LOGTAG, - listener.getClass().getName() - + " is already registered as OnUpdateBlocklistListener"); - } - } - if (remainingListeners) { - switchToForeground(); - } - } - - public void removeOnUpdateBlocklistListener(final OnUpdateBlocklist listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - this.mOnUpdateBlocklist.remove(listener); - remainingListeners = checkListeners(); - } - if (remainingListeners) { - switchToBackground(); - } - } - - public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - remainingListeners = checkListeners(); - if (!this.mOnKeyStatusUpdated.add(listener)) { - Log.w( - Config.LOGTAG, - listener.getClass().getName() - + " is already registered as OnKeyStatusUpdateListener"); - } - } - if (remainingListeners) { - switchToForeground(); - } - } - - public void removeOnNewKeysAvailableListener(final OnKeyStatusUpdated listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - this.mOnKeyStatusUpdated.remove(listener); - remainingListeners = checkListeners(); - } - if (remainingListeners) { - switchToBackground(); - } - } - - public void setOnRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - remainingListeners = checkListeners(); - if (!this.onJingleRtpConnectionUpdate.add(listener)) { - Log.w( - Config.LOGTAG, - listener.getClass().getName() - + " is already registered as OnJingleRtpConnectionUpdate"); - } - } - if (remainingListeners) { - switchToForeground(); - } - } - - public void removeRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - this.onJingleRtpConnectionUpdate.remove(listener); - remainingListeners = checkListeners(); - } - if (remainingListeners) { - switchToBackground(); - } - } - - public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - remainingListeners = checkListeners(); - if (!this.mOnMucRosterUpdate.add(listener)) { - Log.w( - Config.LOGTAG, - listener.getClass().getName() - + " is already registered as OnMucRosterListener"); - } - } - if (remainingListeners) { - switchToForeground(); - } - } - - public void removeOnMucRosterUpdateListener(final OnMucRosterUpdate listener) { - final boolean remainingListeners; - synchronized (LISTENER_LOCK) { - this.mOnMucRosterUpdate.remove(listener); - remainingListeners = checkListeners(); - } - if (remainingListeners) { - switchToBackground(); - } - } - - public boolean checkListeners() { - return (this.mOnAccountUpdates.size() == 0 - && this.mOnConversationUpdates.size() == 0 - && this.mOnRosterUpdates.size() == 0 - && this.mOnCaptchaRequested.size() == 0 - && this.mOnMucRosterUpdate.size() == 0 - && this.mOnUpdateBlocklist.size() == 0 - && this.mOnShowErrorToasts.size() == 0 - && this.onJingleRtpConnectionUpdate.size() == 0 - && this.mOnKeyStatusUpdated.size() == 0); - } - - private void switchToForeground() { - final boolean broadcastLastActivity = broadcastLastActivity(); - for (Conversation conversation : getConversations()) { - if (conversation.getMode() == Conversation.MODE_MULTI) { - conversation.getMucOptions().resetChatState(); - } else { - conversation.setIncomingChatState(Config.DEFAULT_CHAT_STATE); - } - } - for (Account account : getAccounts()) { - if (account.getStatus() == Account.State.ONLINE) { - account.deactivateGracePeriod(); - final XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - if (connection.getFeatures().csi()) { - connection.sendActive(); - } - if (broadcastLastActivity) { - sendPresence( - account, - false); // send new presence but don't include idle because we are - // not - } - } - } - } - Log.d(Config.LOGTAG, "app switched into foreground"); - } - - private void switchToBackground() { - final boolean broadcastLastActivity = broadcastLastActivity(); - if (broadcastLastActivity) { - mLastActivity = System.currentTimeMillis(); - final SharedPreferences.Editor editor = getPreferences().edit(); - editor.putLong(SETTING_LAST_ACTIVITY_TS, mLastActivity); - editor.apply(); - } - for (Account account : getAccounts()) { - if (account.getStatus() == Account.State.ONLINE) { - XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - if (broadcastLastActivity) { - sendPresence(account, true); - } - if (connection.getFeatures().csi()) { - connection.sendInactive(); - } - } - } - } - this.mNotificationService.setIsInForeground(false); - Log.d(Config.LOGTAG, "app switched into background"); - } - - private void connectMultiModeConversations(Account account) { - List conversations = getConversations(); - for (Conversation conversation : conversations) { - if (conversation.getMode() == Conversation.MODE_MULTI - && conversation.getAccount() == account) { - joinMuc(conversation); - } - } - } - - public void mucSelfPingAndRejoin(final Conversation conversation) { - final Account account = conversation.getAccount(); - synchronized (account.inProgressConferenceJoins) { - if (account.inProgressConferenceJoins.contains(conversation)) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": canceling muc self ping because join is already under way"); - return; - } - } - synchronized (account.inProgressConferencePings) { - if (!account.inProgressConferencePings.add(conversation)) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": canceling muc self ping because ping is already under way"); - return; - } - } - final Jid self = conversation.getMucOptions().getSelf().getFullJid(); - final IqPacket ping = new IqPacket(IqPacket.TYPE.GET); - ping.setTo(self); - ping.addChild("ping", Namespace.PING); - sendIqPacket( - conversation.getAccount(), - ping, - (a, response) -> { - if (response.getType() == IqPacket.TYPE.ERROR) { - Element error = response.findChild("error"); - if (error == null - || error.hasChild("service-unavailable") - || error.hasChild("feature-not-implemented") - || error.hasChild("item-not-found")) { - Log.d( - Config.LOGTAG, - a.getJid().asBareJid() - + ": ping to " - + self - + " came back as ignorable error"); - } else { - Log.d( - Config.LOGTAG, - a.getJid().asBareJid() - + ": ping to " - + self - + " failed. attempting rejoin"); - joinMuc(conversation); - } - } else if (response.getType() == IqPacket.TYPE.RESULT) { - Log.d( - Config.LOGTAG, - a.getJid().asBareJid() + ": ping to " + self + " came back fine"); - } - synchronized (account.inProgressConferencePings) { - account.inProgressConferencePings.remove(conversation); - } - }); - } - - public void joinMuc(Conversation conversation) { - joinMuc(conversation, null, false); - } - - public void joinMuc(Conversation conversation, boolean followedInvite) { - joinMuc(conversation, null, followedInvite); - } - - private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) { - joinMuc(conversation, onConferenceJoined, false); - } - - private void joinMuc( - Conversation conversation, - final OnConferenceJoined onConferenceJoined, - final boolean followedInvite) { - final Account account = conversation.getAccount(); - synchronized (account.pendingConferenceJoins) { - account.pendingConferenceJoins.remove(conversation); - } - synchronized (account.pendingConferenceLeaves) { - account.pendingConferenceLeaves.remove(conversation); - } - if (account.getStatus() == Account.State.ONLINE) { - synchronized (account.inProgressConferenceJoins) { - account.inProgressConferenceJoins.add(conversation); - } - if (Config.MUC_LEAVE_BEFORE_JOIN) { - sendPresencePacket(account, mPresenceGenerator.leave(conversation.getMucOptions())); - } - conversation.resetMucOptions(); - if (onConferenceJoined != null) { - conversation.getMucOptions().flagNoAutoPushConfiguration(); - } - conversation.setHasMessagesLeftOnServer(false); - fetchConferenceConfiguration( - conversation, - new OnConferenceConfigurationFetched() { - - private void join(Conversation conversation) { - Account account = conversation.getAccount(); - final MucOptions mucOptions = conversation.getMucOptions(); - - if (mucOptions.nonanonymous() - && !mucOptions.membersOnly() - && !conversation.getBooleanAttribute( - "accept_non_anonymous", false)) { - synchronized (account.inProgressConferenceJoins) { - account.inProgressConferenceJoins.remove(conversation); - } - mucOptions.setError(MucOptions.Error.NON_ANONYMOUS); - updateConversationUi(); - if (onConferenceJoined != null) { - onConferenceJoined.onConferenceJoined(conversation); - } - return; - } - - final Jid joinJid = mucOptions.getSelf().getFullJid(); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid().toString() - + ": joining conversation " - + joinJid.toString()); - PresencePacket packet = - mPresenceGenerator.selfPresence( - account, - Presence.Status.ONLINE, - mucOptions.nonanonymous() - || onConferenceJoined != null); - packet.setTo(joinJid); - Element x = packet.addChild("x", "http://jabber.org/protocol/muc"); - if (conversation.getMucOptions().getPassword() != null) { - x.addChild("password").setContent(mucOptions.getPassword()); - } - - if (mucOptions.mamSupport()) { - // Use MAM instead of the limited muc history to get history - x.addChild("history").setAttribute("maxchars", "0"); - } else { - // Fallback to muc history - x.addChild("history") - .setAttribute( - "since", - PresenceGenerator.getTimestamp( - conversation - .getLastMessageTransmitted() - .getTimestamp())); - } - sendPresencePacket(account, packet); - if (onConferenceJoined != null) { - onConferenceJoined.onConferenceJoined(conversation); - } - if (!joinJid.equals(conversation.getJid())) { - conversation.setContactJid(joinJid); - databaseBackend.updateConversation(conversation); - } - - if (mucOptions.mamSupport()) { - getMessageArchiveService().catchupMUC(conversation); - } - if (mucOptions.isPrivateAndNonAnonymous()) { - fetchConferenceMembers(conversation); - - if (followedInvite) { - final Bookmark bookmark = conversation.getBookmark(); - if (bookmark != null) { - if (!bookmark.autojoin()) { - bookmark.setAutojoin(true); - createBookmark(account, bookmark); - } - } else { - saveConversationAsBookmark(conversation, null); - } - } - } - synchronized (account.inProgressConferenceJoins) { - account.inProgressConferenceJoins.remove(conversation); - sendUnsentMessages(conversation); - } - } - - @Override - public void onConferenceConfigurationFetched(Conversation conversation) { - if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": conversation (" - + conversation.getJid() - + ") got archived before IQ result"); - return; - } - join(conversation); - } - - @Override - public void onFetchFailed( - final Conversation conversation, final String errorCondition) { - if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": conversation (" - + conversation.getJid() - + ") got archived before IQ result"); - return; - } - if ("remote-server-not-found".equals(errorCondition)) { - synchronized (account.inProgressConferenceJoins) { - account.inProgressConferenceJoins.remove(conversation); - } - conversation - .getMucOptions() - .setError(MucOptions.Error.SERVER_NOT_FOUND); - updateConversationUi(); - } else { - join(conversation); - fetchConferenceConfiguration(conversation); - } - } - }); - updateConversationUi(); - } else { - synchronized (account.pendingConferenceJoins) { - account.pendingConferenceJoins.add(conversation); - } - conversation.resetMucOptions(); - conversation.setHasMessagesLeftOnServer(false); - updateConversationUi(); - } - } - - private void fetchConferenceMembers(final Conversation conversation) { - final Account account = conversation.getAccount(); - final AxolotlService axolotlService = account.getAxolotlService(); - final String[] affiliations = {"member", "admin", "owner"}; - OnIqPacketReceived callback = - new OnIqPacketReceived() { - - private int i = 0; - private boolean success = true; - - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - final boolean omemoEnabled = - conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL; - Element query = packet.query("http://jabber.org/protocol/muc#admin"); - if (packet.getType() == IqPacket.TYPE.RESULT && query != null) { - for (Element child : query.getChildren()) { - if ("item".equals(child.getName())) { - MucOptions.User user = - AbstractParser.parseItem(conversation, child); - if (!user.realJidMatchesAccount()) { - boolean isNew = - conversation.getMucOptions().updateUser(user); - Contact contact = user.getContact(); - if (omemoEnabled - && isNew - && user.getRealJid() != null - && (contact == null - || !contact.mutualPresenceSubscription()) - && axolotlService.hasEmptyDeviceList( - user.getRealJid())) { - axolotlService.fetchDeviceIds(user.getRealJid()); - } - } - } - } - } else { - success = false; - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": could not request affiliation " - + affiliations[i] - + " in " - + conversation.getJid().asBareJid()); - } - ++i; - if (i >= affiliations.length) { - List members = conversation.getMucOptions().getMembers(true); - if (success) { - List cryptoTargets = conversation.getAcceptedCryptoTargets(); - boolean changed = false; - for (ListIterator iterator = cryptoTargets.listIterator(); - iterator.hasNext(); ) { - Jid jid = iterator.next(); - if (!members.contains(jid) - && !members.contains(jid.getDomain())) { - iterator.remove(); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": removed " - + jid - + " from crypto targets of " - + conversation.getName()); - changed = true; - } - } - if (changed) { - conversation.setAcceptedCryptoTargets(cryptoTargets); - updateConversation(conversation); - } - } - getAvatarService().clear(conversation); - updateMucRosterUi(); - updateConversationUi(); - } - } - }; - for (String affiliation : affiliations) { - sendIqPacket( - account, mIqGenerator.queryAffiliation(conversation, affiliation), callback); - } - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": fetching members for " + conversation.getName()); - } - - public void providePasswordForMuc(Conversation conversation, String password) { - if (conversation.getMode() == Conversation.MODE_MULTI) { - conversation.getMucOptions().setPassword(password); - if (conversation.getBookmark() != null) { - final Bookmark bookmark = conversation.getBookmark(); - if (synchronizeWithBookmarks()) { - bookmark.setAutojoin(true); - } - createBookmark(conversation.getAccount(), bookmark); - } - updateConversation(conversation); - joinMuc(conversation); - } - } - - public void deleteAvatar(final Account account) { - final AtomicBoolean executed = new AtomicBoolean(false); - final Runnable onDeleted = - () -> { - if (executed.compareAndSet(false, true)) { - account.setAvatar(null); - databaseBackend.updateAccount(account); - getAvatarService().clear(account); - updateAccountUi(); - } - }; - deleteVcardAvatar(account, onDeleted); - deletePepNode(account, Namespace.AVATAR_DATA); - deletePepNode(account, Namespace.AVATAR_METADATA, onDeleted); - } - - public void deletePepNode(final Account account, final String node) { - deletePepNode(account, node, null); - } - - private void deletePepNode(final Account account, final String node, final Runnable runnable) { - final IqPacket request = mIqGenerator.deleteNode(node); - sendIqPacket( - account, - request, - (a, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Log.d( - Config.LOGTAG, - a.getJid().asBareJid() + ": successfully deleted pep node " + node); - if (runnable != null) { - runnable.run(); - } - } else { - Log.d( - Config.LOGTAG, - a.getJid().asBareJid() + ": failed to delete " + packet); - } - }); - } - - private void deleteVcardAvatar(final Account account, @NonNull final Runnable runnable) { - final IqPacket retrieveVcard = - mIqGenerator.retrieveVcardAvatar(account.getJid().asBareJid()); - sendIqPacket( - account, - retrieveVcard, - (a, response) -> { - if (response.getType() != IqPacket.TYPE.RESULT) { - Log.d( - Config.LOGTAG, - a.getJid().asBareJid() + ": no vCard set. nothing to do"); - return; - } - final Element vcard = response.findChild("vCard", "vcard-temp"); - if (vcard == null) { - Log.d( - Config.LOGTAG, - a.getJid().asBareJid() + ": no vCard set. nothing to do"); - return; - } - Element photo = vcard.findChild("PHOTO"); - if (photo == null) { - photo = vcard.addChild("PHOTO"); - } - photo.clearChildren(); - IqPacket publication = new IqPacket(IqPacket.TYPE.SET); - publication.setTo(a.getJid().asBareJid()); - publication.addChild(vcard); - sendIqPacket( - account, - publication, - (a1, publicationResponse) -> { - if (publicationResponse.getType() == IqPacket.TYPE.RESULT) { - Log.d( - Config.LOGTAG, - a1.getJid().asBareJid() - + ": successfully deleted vcard avatar"); - runnable.run(); - } else { - Log.d( - Config.LOGTAG, - "failed to publish vcard " - + publicationResponse.getErrorCondition()); - } - }); - }); - } - - - private boolean hasEnabledAccounts() { - if (this.accounts == null) { - return false; // set to false if accounts could not be fetched - used for notifications - } - for (Account account : this.accounts) { - if (account.isEnabled()) { - return true; - } - } - return false; - } - - public void getAttachments( - final Conversation conversation, int limit, final OnMediaLoaded onMediaLoaded) { - getAttachments( - conversation.getAccount(), conversation.getJid().asBareJid(), limit, onMediaLoaded); - } - - public void getAttachments( - final Account account, - final Jid jid, - final int limit, - final OnMediaLoaded onMediaLoaded) { - getAttachments(account.getUuid(), jid.asBareJid(), limit, onMediaLoaded); - } - - public void getAttachments( - final String account, - final Jid jid, - final int limit, - final OnMediaLoaded onMediaLoaded) { - new Thread( - () -> - onMediaLoaded.onMediaLoaded( - fileBackend.convertToAttachments( - databaseBackend.getRelativeFilePaths( - account, jid, limit)))) - .start(); - } - - public void persistSelfNick(MucOptions.User self) { - final Conversation conversation = self.getConversation(); - final boolean tookProposedNickFromBookmark = - conversation.getMucOptions().isTookProposedNickFromBookmark(); - Jid full = self.getFullJid(); - if (!full.equals(conversation.getJid())) { - Log.d(Config.LOGTAG, "nick changed. updating"); - conversation.setContactJid(full); - databaseBackend.updateConversation(conversation); - } - final Bookmark bookmark = conversation.getBookmark(); - final String bookmarkedNick = bookmark == null ? null : bookmark.getNick(); - if (bookmark != null - && (tookProposedNickFromBookmark || TextUtils.isEmpty(bookmarkedNick)) - && !full.getResource().equals(bookmarkedNick)) { - final Account account = conversation.getAccount(); - final String defaultNick = MucOptions.defaultNick(account); - if (TextUtils.isEmpty(bookmarkedNick) && full.getResource().equals(defaultNick)) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": do not overwrite empty bookmark nick with default nick for " - + conversation.getJid().asBareJid()); - return; - } - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": persist nick '" - + full.getResource() - + "' into bookmark for " - + conversation.getJid().asBareJid()); - bookmark.setNick(full.getResource()); - createBookmark(bookmark.getAccount(), bookmark); - } - } - - public boolean renameInMuc( - final Conversation conversation, - final String nick, - final UiCallback callback) { - final MucOptions options = conversation.getMucOptions(); - final Jid joinJid = options.createJoinJid(nick); - if (joinJid == null) { - return false; - } - if (options.online()) { - Account account = conversation.getAccount(); - options.setOnRenameListener( - new OnRenameListener() { - - @Override - public void onSuccess() { - callback.success(conversation); - } - - @Override - public void onFailure() { - callback.error(R.string.nick_in_use, conversation); - } - }); - final PresencePacket packet = - mPresenceGenerator.selfPresence( - account, Presence.Status.ONLINE, options.nonanonymous()); - packet.setTo(joinJid); - sendPresencePacket(account, packet); - } else { - conversation.setContactJid(joinJid); - databaseBackend.updateConversation(conversation); - if (conversation.getAccount().getStatus() == Account.State.ONLINE) { - Bookmark bookmark = conversation.getBookmark(); - if (bookmark != null) { - bookmark.setNick(nick); - createBookmark(bookmark.getAccount(), bookmark); - } - joinMuc(conversation); - } - } - return true; - } - - public void leaveMuc(Conversation conversation) { - leaveMuc(conversation, false); - } - - private void leaveMuc(Conversation conversation, boolean now) { - if (Compatibility.runsTwentySix()) { - try { - mNotificationService.cleanNotificationChannels(this, conversation.getUuid()); - } catch (Exception e) { - e.printStackTrace(); - } - } - final Account account = conversation.getAccount(); - synchronized (account.pendingConferenceJoins) { - account.pendingConferenceJoins.remove(conversation); - } - synchronized (account.pendingConferenceLeaves) { - account.pendingConferenceLeaves.remove(conversation); - } - if (account.getStatus() == Account.State.ONLINE || now) { - sendPresencePacket( - conversation.getAccount(), - mPresenceGenerator.leave(conversation.getMucOptions())); - conversation.getMucOptions().setOffline(); - Bookmark bookmark = conversation.getBookmark(); - if (bookmark != null) { - bookmark.setConversation(null); - } - Log.d( - Config.LOGTAG, - conversation.getAccount().getJid().asBareJid() - + ": leaving muc " - + conversation.getJid()); - } else { - synchronized (account.pendingConferenceLeaves) { - account.pendingConferenceLeaves.add(conversation); - } - } - } - - public String findConferenceServer(final Account account) { - String server; - if (account.getXmppConnection() != null) { - server = account.getXmppConnection().getMucServer(); - if (server != null) { - return server; - } - } - for (Account other : getAccounts()) { - if (other != account && other.getXmppConnection() != null) { - server = other.getXmppConnection().getMucServer(); - if (server != null) { - return server; - } - } - } - return null; - } - - public void createPublicChannel( - final Account account, - final String name, - final Jid address, - final UiCallback callback) { - joinMuc( - findOrCreateConversation(account, address, true, false, true), - conversation -> { - final Bundle configuration = IqGenerator.defaultChannelConfiguration(); - if (!TextUtils.isEmpty(name)) { - configuration.putString("muc#roomconfig_roomname", name); - } - pushConferenceConfiguration( - conversation, - configuration, - new OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - saveConversationAsBookmark(conversation, name); - callback.success(conversation); - } - - @Override - public void onPushFailed() { - if (conversation - .getMucOptions() - .getSelf() - .getAffiliation() - .ranks(MucOptions.Affiliation.OWNER)) { - callback.error( - R.string.unable_to_set_channel_configuration, - conversation); - } else { - callback.error( - R.string.joined_an_existing_channel, conversation); - } - } - }); - }); - } - - public boolean createAdhocConference( - final Account account, - final String name, - final Iterable jids, - final UiCallback callback) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid().toString() - + ": creating adhoc conference with " - + jids.toString()); - if (account.getStatus() == Account.State.ONLINE) { - try { - String server = findConferenceServer(account); - if (server == null) { - if (callback != null) { - callback.error(R.string.no_conference_server_found, null); - - } - return false; - } - final Jid jid = Jid.of(CryptoHelper.pronounceable(), server, null); - final Conversation conversation = - findOrCreateConversation(account, jid, true, false, true); - joinMuc( - conversation, - new OnConferenceJoined() { - - @Override - public void onConferenceJoined(final Conversation conversation) { - final Bundle configuration = - IqGenerator.defaultGroupChatConfiguration(); - if (!TextUtils.isEmpty(name)) { - configuration.putString("muc#roomconfig_roomname", name); - } - pushConferenceConfiguration( - conversation, - configuration, - new OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - for (Jid invite : jids) { - invite(conversation, invite); - } - for (String resource : - account.getSelfContact() - .getPresences() - .toResourceArray()) { - Jid other = - account.getJid().withResource(resource); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": sending direct invite to " - + other); - directInvite(conversation, other); - } - saveConversationAsBookmark(conversation, name); - if (callback != null) { - callback.success(conversation); - } - } - - @Override - public void onPushFailed() { - archiveConversation(conversation); - if (callback != null) { - callback.error( - R.string.conference_creation_failed, - conversation); - } - } - }); - } - }); - return true; - } catch (IllegalArgumentException e) { - if (callback != null) { - callback.error(R.string.conference_creation_failed, null); - } - return false; - } - } else { - if (callback != null) { - callback.error(R.string.not_connected_try_again, null); - } - return false; - } - } - - public void fetchConferenceConfiguration(final Conversation conversation) { - fetchConferenceConfiguration(conversation, null); - } - - public void fetchConferenceConfiguration( - final Conversation conversation, final OnConferenceConfigurationFetched callback) { - IqPacket request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid()); - sendIqPacket( - conversation.getAccount(), - request, - new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - final MucOptions mucOptions = conversation.getMucOptions(); - final Bookmark bookmark = conversation.getBookmark(); - final boolean sameBefore = - StringUtils.equals( - bookmark == null ? null : bookmark.getBookmarkName(), - mucOptions.getName()); - - if (mucOptions.updateConfiguration( - new ServiceDiscoveryResult(packet))) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": muc configuration changed for " - + conversation.getJid().asBareJid()); - updateConversation(conversation); - } - - if (bookmark != null - && (sameBefore || bookmark.getBookmarkName() == null)) { - if (bookmark.setBookmarkName( - StringUtils.nullOnEmpty(mucOptions.getName()))) { - createBookmark(account, bookmark); - } - } - if (callback != null) { - callback.onConferenceConfigurationFetched(conversation); - } - - updateConversationUi(); - } else if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": received timeout waiting for conference" - + " configuration fetch"); - } else { - if (callback != null) { - callback.onFetchFailed(conversation, packet.getErrorCondition()); - } - } - } - }); - } - - public void pushNodeConfiguration( - Account account, - final String node, - final Bundle options, - final OnConfigurationPushed callback) { - pushNodeConfiguration(account, account.getJid().asBareJid(), node, options, callback); - } - - public void pushNodeConfiguration( - Account account, - final Jid jid, - final String node, - final Bundle options, - final OnConfigurationPushed callback) { - Log.d(Config.LOGTAG, "pushing node configuration"); - sendIqPacket( - account, - mIqGenerator.requestPubsubConfiguration(jid, node), - new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element pubsub = - packet.findChild( - "pubsub", "http://jabber.org/protocol/pubsub#owner"); - Element configuration = - pubsub == null ? null : pubsub.findChild("configure"); - Element x = - configuration == null - ? null - : configuration.findChild("x", Namespace.DATA); - if (x != null) { - Data data = Data.parse(x); - data.submit(options); - sendIqPacket( - account, - mIqGenerator.publishPubsubConfiguration(jid, node, data), - new OnIqPacketReceived() { - @Override - public void onIqPacketReceived( - Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT - && callback != null) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": successfully changed node" - + " configuration for node " - + node); - callback.onPushSucceeded(); - } else if (packet.getType() == IqPacket.TYPE.ERROR - && callback != null) { - callback.onPushFailed(); - } - } - }); - } else if (callback != null) { - callback.onPushFailed(); - } - } else if (packet.getType() == IqPacket.TYPE.ERROR && callback != null) { - callback.onPushFailed(); - } - } - }); - } - - public void pushConferenceConfiguration( - final Conversation conversation, - final Bundle options, - final OnConfigurationPushed callback) { - if (options.getString("muc#roomconfig_whois", "moderators").equals("anyone")) { - conversation.setAttribute("accept_non_anonymous", true); - updateConversation(conversation); - } - if (options.containsKey("muc#roomconfig_moderatedroom")) { - final boolean moderated = "1".equals(options.getString("muc#roomconfig_moderatedroom")); - options.putString("members_by_default", moderated ? "0" : "1"); - } - final IqPacket request = new IqPacket(IqPacket.TYPE.GET); - request.setTo(conversation.getJid().asBareJid()); - request.query("http://jabber.org/protocol/muc#owner"); - sendIqPacket( - conversation.getAccount(), - request, - new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - final Data data = - Data.parse(packet.query().findChild("x", Namespace.DATA)); - data.submit(options); - final IqPacket set = new IqPacket(IqPacket.TYPE.SET); - set.setTo(conversation.getJid().asBareJid()); - set.query("http://jabber.org/protocol/muc#owner").addChild(data); - sendIqPacket( - account, - set, - new OnIqPacketReceived() { - @Override - public void onIqPacketReceived( - Account account, IqPacket packet) { - if (callback != null) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - callback.onPushSucceeded(); - } else { - callback.onPushFailed(); - } - } - } - }); - } else { - if (callback != null) { - callback.onPushFailed(); - } - } - } - }); - } - - public void pushSubjectToConference(final Conversation conference, final String subject) { - MessagePacket packet = - this.getMessageGenerator() - .conferenceSubject(conference, StringUtils.nullOnEmpty(subject)); - this.sendMessagePacket(conference.getAccount(), packet); - } - - public void changeAffiliationInConference( - final Conversation conference, - Jid user, - final MucOptions.Affiliation affiliation, - final OnAffiliationChanged callback) { - final Jid jid = user.asBareJid(); - 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 { - 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 changeRoleInConference( - final Conversation conference, final String nick, MucOptions.Role role) { - IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.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); - } - }); - } - - public void destroyRoom(final Conversation conversation, final OnRoomDestroy callback) { - try { - IqPacket request = new IqPacket(IqPacket.TYPE.SET); - request.setTo(conversation.getJid().asBareJid()); - request.query("http://jabber.org/protocol/muc#owner").addChild("destroy"); - sendIqPacket( - conversation.getAccount(), - request, - new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - if (callback != null) { - callback.onRoomDestroySucceeded(); - } - } else if (packet.getType() == IqPacket.TYPE.ERROR) { - if (callback != null) { - callback.onRoomDestroyFailed(); - } - } - } - }); - } catch (Exception e) { - e.printStackTrace(); - } - } - - private void disconnect(Account account, boolean force) { - if ((account.getStatus() == Account.State.ONLINE) - || (account.getStatus() == Account.State.DISABLED)) { - final XmppConnection connection = account.getXmppConnection(); - if (!force) { - List conversations = getConversations(); - for (Conversation conversation : conversations) { - if (conversation.getAccount() == account) { - if (conversation.getMode() == Conversation.MODE_MULTI) { - leaveMuc(conversation, true); - } else { - if (conversation.endOtrIfNeeded()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() - + ": ended otr session with " - + conversation.getJid()); - } - } - } - } - sendOfflinePresence(account); - } - connection.disconnect(force); - } - } - - @Override - public IBinder onBind(Intent intent) { - return mBinder; - } - - public void updateMessage(Message message) { - updateMessage(message, true); - } - - public void updateMessage(Message message, boolean includeBody) { - databaseBackend.updateMessage(message, includeBody); - updateConversationUi(); - } - - public void createMessageAsync(final Message message) { - mDatabaseWriterExecutor.execute(() -> databaseBackend.createMessage(message)); - } - - public void updateMessage(Message message, String uuid) { - if (!databaseBackend.updateMessage(message, uuid)) { - Log.e(Config.LOGTAG, "error updated message in DB after edit"); - } - updateConversationUi(); - } - - protected void syncDirtyContacts(Account account) { - for (Contact contact : account.getRoster().getContacts()) { - if (contact.getOption(Contact.Options.DIRTY_PUSH)) { - pushContactToServer(contact); - } - if (contact.getOption(Contact.Options.DIRTY_DELETE)) { - deleteContactOnServer(contact); - } - } - } - - public void createContact(final Contact contact, final boolean autoGrant) { - createContact(contact, autoGrant, null); - } - - public void createContact( - final Contact contact, final boolean autoGrant, final String preAuth) { - if (autoGrant) { - contact.setOption(Contact.Options.PREEMPTIVE_GRANT); - contact.setOption(Contact.Options.ASKING); - } - pushContactToServer(contact, preAuth); - } - public void onOtrSessionEstablished(Conversation conversation) { - final Account account = conversation.getAccount(); - final Session otrSession = conversation.getOtrSession(); - Log.d(Config.LOGTAG, - account.getJid().asBareJid() + " otr session established with " - + conversation.getJid() + "/" - + otrSession.getSessionID().getUserID()); - conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() { - - @Override - public void onMessageFound(Message message) { - SessionID id = otrSession.getSessionID(); - try { - message.setCounterpart(Jid.of(id.getAccountID() + "/" + id.getUserID())); - } catch (IllegalArgumentException e) { - return; - } - if (message.needsUploading()) { - mJingleConnectionManager.startJingleFileTransfer(message); - } else { - MessagePacket outPacket = mMessageGenerator.generateOtrChat(message); - if (outPacket != null) { - mMessageGenerator.addDelay(outPacket, message.getTimeSent()); - message.setStatus(Message.STATUS_SEND); - databaseBackend.updateMessage(message, false); - sendMessagePacket(account, outPacket); - } - } - updateConversationUi(); - } - }); - } - - public boolean renewSymmetricKey(Conversation conversation) { - Account account = conversation.getAccount(); - byte[] symmetricKey = new byte[32]; - this.mRandom.nextBytes(symmetricKey); - Session otrSession = conversation.getOtrSession(); - if (otrSession != null) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); - packet.setFrom(account.getJid()); - MessageGenerator.addMessageHints(packet); - packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/" - + otrSession.getSessionID().getUserID()); - try { - packet.setBody(otrSession - .transformSending(CryptoHelper.FILETRANSFER - + CryptoHelper.bytesToHex(symmetricKey))[0]); - sendMessagePacket(account, packet); - conversation.setSymmetricKey(symmetricKey); - return true; - } catch (OtrException e) { - return false; - } - } - return false; - } - - - public void pushContactToServer(final Contact contact) { - pushContactToServer(contact, null); - } - - private void pushContactToServer(final Contact contact, final String preAuth) { - contact.resetOption(Contact.Options.DIRTY_DELETE); - contact.setOption(Contact.Options.DIRTY_PUSH); - final Account account = contact.getAccount(); - if (account.getStatus() == Account.State.ONLINE) { - final boolean ask = contact.getOption(Contact.Options.ASKING); - final boolean sendUpdates = - contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST) - && contact.getOption(Contact.Options.PREEMPTIVE_GRANT); - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - iq.query(Namespace.ROSTER).addChild(contact.asElement()); - account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler); - if (sendUpdates) { - sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact)); - } - if (ask) { - sendPresencePacket( - account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth)); - } - } else { - syncRoster(contact.getAccount()); - } - } - - public void publishMucAvatar( - final Conversation conversation, final Uri image, final OnAvatarPublication callback) { - new Thread( - () -> { - final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; - final int size = Config.AVATAR_SIZE; - final Avatar avatar = - getFileBackend().getPepAvatar(image, size, format); - if (avatar != null) { - if (!getFileBackend().save(avatar)) { - callback.onAvatarPublicationFailed( - R.string.error_saving_avatar); - return; - } - avatar.owner = conversation.getJid().asBareJid(); - publishMucAvatar(conversation, avatar, callback); - } else { - callback.onAvatarPublicationFailed( - R.string.error_publish_avatar_converting); - } - }) - .start(); - } - - public void publishAvatar( - final Account account, final Uri image, final OnAvatarPublication callback) { - new Thread( - () -> { - final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; - final int size = Config.AVATAR_SIZE; - final Avatar avatar = - getFileBackend().getPepAvatar(image, size, format); - if (avatar != null) { - if (!getFileBackend().save(avatar)) { - Log.d(Config.LOGTAG, "unable to save vcard"); - callback.onAvatarPublicationFailed( - R.string.error_saving_avatar); - return; - } - publishAvatar(account, avatar, callback); - } else { - callback.onAvatarPublicationFailed( - R.string.error_publish_avatar_converting); - } - }) - .start(); - } - - private void publishMucAvatar( - Conversation conversation, Avatar avatar, OnAvatarPublication callback) { - final IqPacket retrieve = mIqGenerator.retrieveVcardAvatar(avatar); - sendIqPacket( - conversation.getAccount(), - retrieve, - (account, response) -> { - boolean itemNotFound = - response.getType() == IqPacket.TYPE.ERROR - && response.hasChild("error") - && response.findChild("error").hasChild("item-not-found"); - if (response.getType() == IqPacket.TYPE.RESULT || itemNotFound) { - Element vcard = response.findChild("vCard", "vcard-temp"); - if (vcard == null) { - vcard = new Element("vCard", "vcard-temp"); - } - Element photo = vcard.findChild("PHOTO"); - if (photo == null) { - photo = vcard.addChild("PHOTO"); - } - photo.clearChildren(); - photo.addChild("TYPE").setContent(avatar.type); - photo.addChild("BINVAL").setContent(avatar.image); - IqPacket publication = new IqPacket(IqPacket.TYPE.SET); - publication.setTo(conversation.getJid().asBareJid()); - publication.addChild(vcard); - sendIqPacket( - account, - publication, - (a1, publicationResponse) -> { - if (publicationResponse.getType() == IqPacket.TYPE.RESULT) { - callback.onAvatarPublicationSucceeded(); - } else { - Log.d( - Config.LOGTAG, - "failed to publish vcard " - + publicationResponse.getErrorCondition()); - callback.onAvatarPublicationFailed( - R.string.error_publish_avatar_server_reject); - } - }); - } else { - Log.d(Config.LOGTAG, "failed to request vcard " + response); - callback.onAvatarPublicationFailed( - R.string.error_publish_avatar_no_server_support); - } - }); - } - - public void publishAvatar( - Account account, final Avatar avatar, final OnAvatarPublication callback) { - final Bundle options; - if (account.getXmppConnection().getFeatures().pepPublishOptions()) { - options = PublishOptions.openAccess(); - } else { - options = null; - } - publishAvatar(account, avatar, options, true, callback); - } - - public void publishAvatar( - Account account, - final Avatar avatar, - final Bundle options, - final boolean retry, - final OnAvatarPublication callback) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": publishing avatar. options=" + options); - IqPacket packet = this.mIqGenerator.publishAvatar(avatar, options); - this.sendIqPacket( - account, - packet, - new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(Account account, IqPacket result) { - if (result.getType() == IqPacket.TYPE.RESULT) { - publishAvatarMetadata(account, avatar, options, true, callback); - } else if (retry && PublishOptions.preconditionNotMet(result)) { - pushNodeConfiguration( - account, - Namespace.AVATAR_DATA, - options, - new OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": changed node configuration for" - + " avatar node"); - publishAvatar( - account, avatar, options, false, callback); - } - - @Override - public void onPushFailed() { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": unable to change node" - + " configuration for avatar node"); - publishAvatar(account, avatar, null, false, callback); - } - }); - } else { - Element error = result.findChild("error"); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": server rejected avatar " - + (avatar.size / 1024) - + "KiB " - + (error != null ? error.toString() : "")); - if (callback != null) { - callback.onAvatarPublicationFailed( - R.string.error_publish_avatar_server_reject); - } - } - } - }); - } - - public void publishAvatarMetadata( - Account account, - final Avatar avatar, - final Bundle options, - final boolean retry, - final OnAvatarPublication callback) { - final IqPacket packet = - XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar, options); - sendIqPacket( - account, - packet, - new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket result) { - if (result.getType() == IqPacket.TYPE.RESULT) { - if (account.setAvatar(avatar.getFilename())) { - getAvatarService().clear(account); - databaseBackend.updateAccount(account); - notifyAccountAvatarHasChanged(account); - } - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": published avatar " - + (avatar.size / 1024) - + "KiB"); - if (callback != null) { - callback.onAvatarPublicationSucceeded(); - } - } else if (retry && PublishOptions.preconditionNotMet(result)) { - pushNodeConfiguration( - account, - Namespace.AVATAR_METADATA, - options, - new OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": changed node configuration for" - + " avatar meta data node"); - publishAvatarMetadata( - account, avatar, options, false, callback); - } - - @Override - public void onPushFailed() { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": unable to change node" - + " configuration for avatar meta data" - + " node"); - publishAvatarMetadata( - account, avatar, null, false, callback); - } - }); - } else { - if (callback != null) { - callback.onAvatarPublicationFailed( - R.string.error_publish_avatar_server_reject); - } - } - } - }); - } - - public void republishAvatarIfNeeded(Account account) { - if (account.getAxolotlService().isPepBroken()) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": skipping republication of avatar because pep is broken"); - return; - } - IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null); - this.sendIqPacket( - account, - packet, - new OnIqPacketReceived() { - - private Avatar parseAvatar(IqPacket packet) { - Element pubsub = - packet.findChild("pubsub", "http://jabber.org/protocol/pubsub"); - if (pubsub != null) { - Element items = pubsub.findChild("items"); - if (items != null) { - return Avatar.parseMetadata(items); - } - } - return null; - } - - - private boolean errorIsItemNotFound(IqPacket packet) { - Element error = packet.findChild("error"); - return packet.getType() == IqPacket.TYPE.ERROR - && error != null - && error.hasChild("item-not-found"); - } - - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT - || errorIsItemNotFound(packet)) { - Avatar serverAvatar = parseAvatar(packet); - if (serverAvatar == null && account.getAvatar() != null) { - Avatar avatar = fileBackend.getStoredPepAvatar(account.getAvatar()); - if (avatar != null) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": avatar on server was null. republishing"); - publishAvatar( - account, - fileBackend.getStoredPepAvatar(account.getAvatar()), - null); - } else { - Log.e( - Config.LOGTAG, - account.getJid().asBareJid() - + ": error rereading avatar"); - } - } - } - } - }); - } - - public void fetchAvatar(Account account, Avatar avatar) { - fetchAvatar(account, avatar, null); - } - - public void fetchAvatar( - Account account, final Avatar avatar, final UiCallback callback) { - final String KEY = generateFetchKey(account, avatar); - synchronized (this.mInProgressAvatarFetches) { - if (mInProgressAvatarFetches.add(KEY)) { - switch (avatar.origin) { - case PEP: - this.mInProgressAvatarFetches.add(KEY); - fetchAvatarPep(account, avatar, callback); - break; - case VCARD: - this.mInProgressAvatarFetches.add(KEY); - fetchAvatarVcard(account, avatar, callback); - break; - } - } else if (avatar.origin == Avatar.Origin.PEP) { - mOmittedPepAvatarFetches.add(KEY); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": already fetching " - + avatar.origin - + " avatar for " - + avatar.owner); - } - } - } - - private void fetchAvatarPep( - Account account, final Avatar avatar, final UiCallback callback) { - IqPacket packet = this.mIqGenerator.retrievePepAvatar(avatar); - sendIqPacket( - account, - packet, - (a, result) -> { - synchronized (mInProgressAvatarFetches) { - mInProgressAvatarFetches.remove(generateFetchKey(a, avatar)); - } - final String ERROR = - a.getJid().asBareJid() - + ": fetching avatar for " - + avatar.owner - + " failed "; - if (result.getType() == IqPacket.TYPE.RESULT) { - avatar.image = mIqParser.avatarData(result); - if (avatar.image != null) { - if (getFileBackend().save(avatar)) { - if (a.getJid().asBareJid().equals(avatar.owner)) { - if (a.setAvatar(avatar.getFilename())) { - databaseBackend.updateAccount(a); - } - getAvatarService().clear(a); - updateConversationUi(); - updateAccountUi(); - } else { - final Contact contact = a.getRoster().getContact(avatar.owner); - contact.setAvatar(avatar); - syncRoster(account); - getAvatarService().clear(contact); - updateConversationUi(); - updateRosterUi(); - } - if (callback != null) { - callback.success(avatar); - } - Log.d( - Config.LOGTAG, - a.getJid().asBareJid() - + ": successfully fetched pep avatar for " - + avatar.owner); - return; - } - } else { - Log.d(Config.LOGTAG, ERROR + "(parsing error)"); - } - } else { - Element error = result.findChild("error"); - if (error == null) { - Log.d(Config.LOGTAG, ERROR + "(server error)"); - } else { - Log.d(Config.LOGTAG, ERROR + error.toString()); - } - } - if (callback != null) { - callback.error(0, null); - } - }); - } - - private void fetchAvatarVcard( - final Account account, final Avatar avatar, final UiCallback callback) { - IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar); - this.sendIqPacket( - account, - packet, - new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - final boolean previouslyOmittedPepFetch; - synchronized (mInProgressAvatarFetches) { - final String KEY = generateFetchKey(account, avatar); - mInProgressAvatarFetches.remove(KEY); - previouslyOmittedPepFetch = mOmittedPepAvatarFetches.remove(KEY); - } - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element vCard = packet.findChild("vCard", "vcard-temp"); - Element photo = vCard != null ? vCard.findChild("PHOTO") : null; - String image = photo != null ? photo.findChildContent("BINVAL") : null; - if (image != null) { - avatar.image = image; - if (getFileBackend().save(avatar)) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": successfully fetched vCard avatar for " - + avatar.owner - + " omittedPep=" - + previouslyOmittedPepFetch); - if (avatar.owner.isBareJid()) { - if (account.getJid().asBareJid().equals(avatar.owner) - && account.getAvatar() == null) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": had no avatar. replacing with" - + " vcard"); - account.setAvatar(avatar.getFilename()); - databaseBackend.updateAccount(account); - getAvatarService().clear(account); - updateAccountUi(); - } else { - final Contact contact = - account.getRoster().getContact(avatar.owner); - contact.setAvatar(avatar, previouslyOmittedPepFetch); - syncRoster(account); - getAvatarService().clear(contact); - updateRosterUi(); - } - updateConversationUi(); - } else { - Conversation conversation = - find(account, avatar.owner.asBareJid()); - if (conversation != null - && conversation.getMode() - == Conversation.MODE_MULTI) { - MucOptions.User user = - conversation - .getMucOptions() - .findUserByFullJid(avatar.owner); - if (user != null) { - if (user.setAvatar(avatar)) { - getAvatarService().clear(user); - updateConversationUi(); - updateMucRosterUi(); - } - if (user.getRealJid() != null) { - Contact contact = - account.getRoster() - .getContact(user.getRealJid()); - contact.setAvatar(avatar); - syncRoster(account); - getAvatarService().clear(contact); - updateRosterUi(); - } - } - } - } - } - } - } - } - }); - } - - public void checkForAvatar(Account account, final UiCallback callback) { - IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null); - this.sendIqPacket( - account, - packet, - new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element pubsub = - packet.findChild("pubsub", "http://jabber.org/protocol/pubsub"); - if (pubsub != null) { - Element items = pubsub.findChild("items"); - if (items != null) { - Avatar avatar = Avatar.parseMetadata(items); - if (avatar != null) { - avatar.owner = account.getJid().asBareJid(); - if (fileBackend.isAvatarCached(avatar)) { - if (account.setAvatar(avatar.getFilename())) { - databaseBackend.updateAccount(account); - } - getAvatarService().clear(account); - callback.success(avatar); - } else { - fetchAvatarPep(account, avatar, callback); - } - return; - } - } - } - } - callback.error(0, null); - } - }); - } - - public void notifyAccountAvatarHasChanged(final Account account) { - final XmppConnection connection = account.getXmppConnection(); - if (connection != null && connection.getFeatures().bookmarksConversion()) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": avatar changed. resending presence to online group chats"); - for (Conversation conversation : conversations) { - if (conversation.getAccount() == account - && conversation.getMode() == Conversational.MODE_MULTI) { - final MucOptions mucOptions = conversation.getMucOptions(); - if (mucOptions.online()) { - PresencePacket packet = - mPresenceGenerator.selfPresence( - account, Presence.Status.ONLINE, mucOptions.nonanonymous()); - packet.setTo(mucOptions.getSelf().getFullJid()); - connection.sendPresencePacket(packet); - } - } - } - } - } - - public void deleteContactOnServer(Contact contact) { - contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); - contact.resetOption(Contact.Options.DIRTY_PUSH); - contact.setOption(Contact.Options.DIRTY_DELETE); - Account account = contact.getAccount(); - if (account.getStatus() == Account.State.ONLINE) { - IqPacket iq = new IqPacket(IqPacket.TYPE.SET); - Element item = iq.query(Namespace.ROSTER).addChild("item"); - item.setAttribute("jid", contact.getJid()); - item.setAttribute("subscription", "remove"); - account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler); - } - } - - public void updateConversation(final Conversation conversation) { - mDatabaseWriterExecutor.execute(() -> databaseBackend.updateConversation(conversation)); - } - - private void reconnectAccount(final Account account) { - account.setOption(Account.OPTION_DISABLED, true); - if (!updateAccount(account)) { - Log.d(Config.LOGTAG, getString(R.string.unable_to_update_account)); - } - account.setOption(Account.OPTION_DISABLED, false); - final XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - connection.resetEverything(); - } - if (!updateAccount(account)) { - Log.d(Config.LOGTAG, getString(R.string.unable_to_update_account)); - } - } - - - private void reconnectAccount( - final Account account, final boolean force, final boolean interactive) { - synchronized (account) { - XmppConnection connection = account.getXmppConnection(); - if (connection == null) { - connection = createConnection(account); - account.setXmppConnection(connection); - } - boolean hasInternet = hasInternetConnection(); - if (account.isEnabled() && hasInternet) { - if (!force) { - disconnect(account, false); - } - Thread thread = new Thread(connection); - connection.setInteractive(interactive); - connection.prepareNewConnection(); - connection.interrupt(); - thread.start(); - scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode()); - } else { - disconnect(account, force || account.getTrueStatus().isError() || !hasInternet); - account.getRoster().clearPresences(); - connection.resetEverything(); - final AxolotlService axolotlService = account.getAxolotlService(); - if (axolotlService != null) { - axolotlService.resetBrokenness(); - } - if (!hasInternet) { - account.setStatus(Account.State.NO_INTERNET); - } - } - } - } - - public void reconnectAccountInBackground(final Account account) { - new Thread(() -> reconnectAccount(account, false, true)).start(); - } - - public void invite(final Conversation conversation, final Jid contact) { - Log.d( - Config.LOGTAG, - conversation.getAccount().getJid().asBareJid() - + ": inviting " - + contact - + " to " - + conversation.getJid().asBareJid()); - 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); - } - - public void directInvite(Conversation conversation, Jid jid) { - MessagePacket packet = mMessageGenerator.directInvite(conversation, jid); - sendMessagePacket(conversation.getAccount(), packet); - } - - public void resetSendingToWaiting(Account account) { - for (Conversation conversation : getConversations()) { - if (conversation.getAccount() == account) { - conversation.findUnsentTextMessages( - message -> markMessage(message, Message.STATUS_WAITING)); - } - } - } - - public Message markMessage( - final Account account, final Jid recipient, final String uuid, final int status) { - return markMessage(account, recipient, uuid, status, null); - } - - public Message markMessage( - final Account account, - final Jid recipient, - final String uuid, - final int status, - String errorMessage) { - if (uuid == null) { - return null; - } - for (Conversation conversation : getConversations()) { - if (conversation.getJid().asBareJid().equals(recipient) - && conversation.getAccount() == account) { - final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid); - if (message != null) { - markMessage(message, status, errorMessage); - } - return message; - } - } - return null; - } - - public boolean markMessage( - final Conversation conversation, - final String uuid, - final int status, - final String serverMessageId) { - return markMessage(conversation, uuid, status, serverMessageId, null); - } - - public boolean markMessage( - final Conversation conversation, - final String uuid, - final int status, - final String serverMessageId, - final LocalizedContent body) { - if (uuid == null) { - return false; - } else { - final Message message = conversation.findSentMessageWithUuid(uuid); - if (message != null) { - if (message.getServerMsgId() == null) { - message.setServerMsgId(serverMessageId); - } - if (message.getEncryption() == Message.ENCRYPTION_NONE - && message.isTypeText() - && isBodyModified(message, body)) { - message.setBody(body.content); - if (body.count > 1) { - message.setBodyLanguage(body.language); - } - markMessage(message, status, null, true); - } else { - markMessage(message, status); - } - return true; - } else { - return false; - } - } - } - - private static boolean isBodyModified(final Message message, final LocalizedContent body) { - if (body == null || body.content == null) { - return false; - } - return !body.content.equals(message.getBody()); - } - - public void markMessage(Message message, int status) { - markMessage(message, status, null); - } - - public void markMessage(final Message message, final int status, final String errorMessage) { - markMessage(message, status, errorMessage, false); - } - - public void markMessage( - final Message message, - final int status, - final String errorMessage, - final boolean includeBody) { - final int oldStatus = message.getStatus(); - if (status == Message.STATUS_SEND_FAILED - && (oldStatus == Message.STATUS_SEND_RECEIVED - || oldStatus == Message.STATUS_SEND_DISPLAYED)) { - return; - } - if (status == Message.STATUS_SEND_RECEIVED && oldStatus == Message.STATUS_SEND_DISPLAYED) { - return; - } - message.setErrorMessage(errorMessage); - message.setStatus(status); - databaseBackend.updateMessage(message, includeBody); - updateConversationUi(); - if (oldStatus != status && status == Message.STATUS_SEND_FAILED) { - mNotificationService.pushFailedDelivery(message); - // resend it - mMessageResendTaskExecuter.execute(() -> { - try { - if (message.increaseResendCount() <= maxResendTime() / 2) { - Thread.sleep(resendDelay()); - resendFailedMessages(message); - } - } - catch (Exception ignore){ - // if system halt, give it up - Log.w(Config.LOGTAG,"System Halt, so the message resend give up"); - } - }); - - } - } - private long resendDelay() { - return Long.parseLong(getPreferences().getString(SettingsActivity.RESEND_DELAY, getResources().getString(R.string.resend_delay))); - } - - public SharedPreferences getPreferences() { - return PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - } - - public long getAutomaticMessageDeletionDate() { - final long timeout = - getLongPreference( - SettingsActivity.AUTOMATIC_MESSAGE_DELETION, - R.integer.automatic_message_deletion); - return timeout == 0 ? timeout : (System.currentTimeMillis() - (timeout * 1000)); - } - - public long getAutomaticAttachmentDeletionDate() { - final long timeout = getLongPreference(AUTOMATIC_ATTACHMENT_DELETION, R.integer.automatic_attachment_deletion); - return timeout == 0 ? timeout : (System.currentTimeMillis() - (timeout * 1000)); - } - - public long getLongPreference(String name, @IntegerRes int res) { - long defaultValue = getResources().getInteger(res); - try { - return Long.parseLong(getPreferences().getString(name, String.valueOf(defaultValue))); - } catch (Exception e) { - e.printStackTrace(); - return defaultValue; - } - } - - public boolean getBooleanPreference(String name, @BoolRes int res) { - return getPreferences().getBoolean(name, getResources().getBoolean(res)); - } - - public boolean confirmMessages() { - return getBooleanPreference(CONFIRM_MESSAGES, R.bool.confirm_messages); - } - - public boolean usingInnerStorage() { - return getBooleanPreference(USE_INNER_STORAGE, R.bool.use_inner_storage); - } - - public boolean allowMessageCorrection() { - return getBooleanPreference(ALLOW_MESSAGE_CORRECTION, R.bool.allow_message_correction); - } - - public boolean allowMessageRetraction() { - return getBooleanPreference(ALLOW_MESSAGE_RETRACTION, R.bool.allow_message_retraction); - } - - public boolean sendChatStates() { - return getBooleanPreference(CHAT_STATES, R.bool.chat_states); - } - - private boolean synchronizeWithBookmarks() { - return getBooleanPreference("autojoin", R.bool.autojoin); - } - - public boolean indicateReceived() { - return getBooleanPreference(INDICATE_RECEIVED, R.bool.indicate_received); - } - - public boolean useTorToConnect() { - return QuickConversationsService.isConversations() - && getBooleanPreference("use_tor", R.bool.use_tor); - } - - public boolean useI2PToConnect() { - return QuickConversationsService.isConversations() && getBooleanPreference("use_i2p", R.bool.use_i2p); - } - - public boolean showExtendedConnectionOptions() { - return QuickConversationsService.isConversations() - && getBooleanPreference("show_connection_options", R.bool.show_connection_options); - } - - public boolean warnUnecryptedChat() { - return getBooleanPreference(SettingsActivity.WARN_UNENCRYPTED_CHAT, R.bool.warn_unencrypted_chat); - } - - public boolean hideYouAreNotParticipating() { - return getBooleanPreference(SettingsActivity.HIDE_YOU_ARE_NOT_PARTICIPATING, R.bool.hide_you_are_not_participating); - } - - public boolean hideMemoryWarning() { - return getBooleanPreference(SettingsActivity.HIDE_MEMORY_WARNING, R.bool.hide_memory_warning); - } - - public boolean broadcastLastActivity() { - return getBooleanPreference(SettingsActivity.BROADCAST_LAST_ACTIVITY, R.bool.last_activity); - } - - public boolean multipleAccounts() { - return getBooleanPreference(ENABLE_MULTI_ACCOUNTS, R.bool.enable_multi_accounts); - } - - public boolean showOwnAccounts() { - return getBooleanPreference(SHOW_OWN_ACCOUNTS, R.bool.show_own_accounts); - } - - public boolean allowMergeMessages() { - return false; - //return getBooleanPreference("allowmergemessages", R.bool.allowmergemessages); - } - - public boolean showTextFormatting() { - return getBooleanPreference("showtextformatting", R.bool.showtextformatting); - } - - public int unreadCount() { - int count = 0; - for (Conversation conversation : getConversations()) { - count += conversation.unreadCount(); - } - return count; - } - - public void vibrate() { - try { - final boolean vibrateInChat = getBooleanPreference("vibrate_in_chat", R.bool.vibrate_in_chat); - if (!isPhoneSilenced() && vibrateInChat) { - Log.d(Config.LOGTAG, "Notification: short vibrate"); - Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE); - vibrator.vibrate(100); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - private List threadSafeList(Set set) { - synchronized (LISTENER_LOCK) { - return set.size() == 0 ? Collections.emptyList() : new ArrayList<>(set); - } - } - - public void showErrorToastInUi(int resId) { - for (OnShowErrorToast listener : threadSafeList(this.mOnShowErrorToasts)) { - listener.onShowErrorToast(resId); - } - } - - public void updateConversationUi() { - for (OnConversationUpdate listener : threadSafeList(this.mOnConversationUpdates)) { - listener.onConversationUpdate(); - } - } - - public void notifyJingleRtpConnectionUpdate( - final Account account, - final Jid with, - final String sessionId, - final RtpEndUserState state) { - for (OnJingleRtpConnectionUpdate listener : - threadSafeList(this.onJingleRtpConnectionUpdate)) { - listener.onJingleRtpConnectionUpdate(account, with, sessionId, state); - } - } - - public void notifyJingleRtpConnectionUpdate( - AppRTCAudioManager.AudioDevice selectedAudioDevice, - Set availableAudioDevices) { - for (OnJingleRtpConnectionUpdate listener : - threadSafeList(this.onJingleRtpConnectionUpdate)) { - listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices); - } - } - - public void updateAccountUi() { - for (OnAccountUpdate listener : threadSafeList(this.mOnAccountUpdates)) { - listener.onAccountUpdate(); - } - } - - public void updateRosterUi() { - for (OnRosterUpdate listener : threadSafeList(this.mOnRosterUpdates)) { - listener.onRosterUpdate(); - } - } - - public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) { - if (mOnCaptchaRequested.size() > 0) { - DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics(); - Bitmap scaled = - Bitmap.createScaledBitmap( - captcha, - (int) (captcha.getWidth() * metrics.scaledDensity), - (int) (captcha.getHeight() * metrics.scaledDensity), - false); - for (OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) { - listener.onCaptchaRequested(account, id, data, scaled); - } - return true; - } - return false; - } - - public void updateBlocklistUi(final OnUpdateBlocklist.Status status) { - for (OnUpdateBlocklist listener : threadSafeList(this.mOnUpdateBlocklist)) { - listener.OnUpdateBlocklist(status); - } - } - - public void updateMucRosterUi() { - for (OnMucRosterUpdate listener : threadSafeList(this.mOnMucRosterUpdate)) { - listener.onMucRosterUpdate(); - } - } - - public void keyStatusUpdated(AxolotlService.FetchStatus report) { - for (OnKeyStatusUpdated listener : threadSafeList(this.mOnKeyStatusUpdated)) { - listener.onKeyStatusUpdated(report); - } - } - - public Account findAccountByJid(final Jid jid) { - for (final Account account : this.accounts) { - if (account.getJid().asBareJid().equals(jid.asBareJid())) { - return account; - } - } - return null; - } - - public Account findAccountByUuid(final String uuid) { - for (Account account : this.accounts) { - if (account.getUuid().equals(uuid)) { - return account; - } - } - return null; - } - - public Conversation findConversationByUuid(String uuid) { - for (Conversation conversation : getConversations()) { - if (conversation.getUuid().equals(uuid)) { - return conversation; - } - } - return null; - } - - public Conversation findUniqueConversationByJid(XmppUri xmppUri) { - List findings = new ArrayList<>(); - for (Conversation c : getConversations()) { - if (c.getAccount().isEnabled() - && c.getJid().asBareJid().equals(xmppUri.getJid()) - && ((c.getMode() == Conversational.MODE_MULTI) - == xmppUri.isAction(XmppUri.ACTION_JOIN))) { - findings.add(c); - } - } - return findings.size() == 1 ? findings.get(0) : null; - } - - public boolean markRead(final Conversation conversation, boolean dismiss) { - return markRead(conversation, null, dismiss).size() > 0; - } - - public void markRead(final Conversation conversation) { - markRead(conversation, null, true); - } - - public List markRead( - final Conversation conversation, String upToUuid, boolean dismiss) { - if (dismiss) { - mNotificationService.clear(conversation); - } - final List readMessages = conversation.markRead(upToUuid); - if (readMessages.size() > 0) { - Runnable runnable = - () -> { - for (Message message : readMessages) { - databaseBackend.updateMessage(message, false); - } - }; - mDatabaseWriterExecutor.execute(runnable); - updateConversationUi(); - updateUnreadCountBadge(); - return readMessages; - } else { - return readMessages; - } - } - - public synchronized void updateUnreadCountBadge() { - final Runnable runnable = () -> { - int count = unreadCount(); - if (unreadCount != count) { - Log.d(Config.LOGTAG, "update unread count to " + count); - if (count > 0) { - ShortcutBadger.applyCount(getApplicationContext(), count); - } else { - ShortcutBadger.removeCount(getApplicationContext()); - } - unreadCount = count; - } - }; - mDatabaseWriterExecutor.execute(runnable); - } - - public void sendReadMarker(final Conversation conversation, String upToUuid) { - final boolean isPrivateAndNonAnonymousMuc = - conversation.getMode() == Conversation.MODE_MULTI - && conversation.isPrivateAndNonAnonymous(); - final List readMessages = this.markRead(conversation, upToUuid, true); - if (readMessages.size() > 0) { - updateConversationUi(); - } - final Message markable = - Conversation.getLatestMarkableMessage(readMessages, isPrivateAndNonAnonymousMuc); - if (confirmMessages() - && markable != null - && (markable.trusted() || isPrivateAndNonAnonymousMuc) - && markable.getRemoteMsgId() != null) { - Log.d( - Config.LOGTAG, - conversation.getAccount().getJid().asBareJid() - + ": sending read marker to " - + markable.getCounterpart().toString()); - final Account account = conversation.getAccount(); - final MessagePacket packet = mMessageGenerator.confirm(markable); - this.sendMessagePacket(account, packet); - } - } - - public MemorizingTrustManager getMemorizingTrustManager() { - return this.mMemorizingTrustManager; - } - - public void setMemorizingTrustManager(MemorizingTrustManager trustManager) { - this.mMemorizingTrustManager = trustManager; - } - - public void updateMemorizingTrustmanager() { - final MemorizingTrustManager tm; - final boolean dontTrustSystemCAs = - getBooleanPreference("dont_trust_system_cas", R.bool.dont_trust_system_cas); - if (dontTrustSystemCAs) { - tm = new MemorizingTrustManager(getApplicationContext(), null); - } else { - tm = new MemorizingTrustManager(getApplicationContext()); - } - setMemorizingTrustManager(tm); - } - - public LruCache getBitmapCache() { - return this.mBitmapCache; - } - - public void syncRosterToDisk(final Account account) { - Runnable runnable = () -> databaseBackend.writeRoster(account.getRoster()); - mDatabaseWriterExecutor.execute(runnable); - } - - public Collection getKnownHosts() { - final Set hosts = new HashSet<>(); - for (final Account account : getAccounts()) { - hosts.add(account.getServer()); - for (final Contact contact : account.getRoster().getContacts()) { - if (contact.showInRoster()) { - final String server = contact.getServer(); - if (server != null) { - hosts.add(server); - } - } - } - } - if (Config.QUICKSY_DOMAIN != null) { - hosts.remove( - Config.QUICKSY_DOMAIN - .toEscapedString()); // we only want to show this when we type a e164 - // number - } - if (Config.DOMAIN_LOCK != null) { - hosts.add(Config.DOMAIN_LOCK); - } - if (Config.MAGIC_CREATE_DOMAIN != null) { - hosts.add(Config.MAGIC_CREATE_DOMAIN); - } - return hosts; - } - - public Collection getKnownConferenceHosts() { - final Set mucServers = new HashSet<>(); - for (final Account account : accounts) { - if (account.getXmppConnection() != null) { - mucServers.addAll(account.getXmppConnection().getMucServers()); - for (final Bookmark bookmark : account.getBookmarks()) { - final Jid jid = bookmark.getJid(); - final String s = jid == null ? null : jid.getDomain().toEscapedString(); - if (s != null) { - mucServers.add(s); - } - } - } - } - return mucServers; - } - - public void sendMessagePacket(Account account, MessagePacket packet) { - final XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - connection.sendMessagePacket(packet); - } - } - - public void sendPresencePacket(Account account, PresencePacket packet) { - XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - connection.sendPresencePacket(packet); - } - } - - public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) { - final XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - IqPacket request = mIqGenerator.generateCreateAccountWithCaptcha(account, id, data); - connection.sendUnmodifiedIqPacket( - request, connection.registrationResponseListener, true); - } - } - - public void sendIqPacket( - final Account account, final IqPacket packet, final OnIqPacketReceived callback) { - final XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - connection.sendIqPacket(packet, callback); - } else if (callback != null) { - callback.onIqPacketReceived(account, new IqPacket(IqPacket.TYPE.TIMEOUT)); - } - } - - public void sendPresence(final Account account) { - sendPresence(account, checkListeners() && broadcastLastActivity()); - } - - public void sendPresence(final Account account, final boolean includeIdleTimestamp) { - final Presence.Status status; - if (manuallyChangePresence()) { - status = account.getPresenceStatus(); - } else { - status = getTargetPresence(); - } - final PresencePacket packet = mPresenceGenerator.selfPresence(account, status); - if (mLastActivity > 0 && includeIdleTimestamp) { - long since = - Math.min(mLastActivity, System.currentTimeMillis()); // don't send future dates - packet.addChild("idle", Namespace.IDLE) - .setAttribute("since", AbstractGenerator.getTimestamp(since)); - } - sendPresencePacket(account, packet); - } - - private void deactivateGracePeriod() { - for (Account account : getAccounts()) { - account.deactivateGracePeriod(); - } - } - - public void refreshAllPresences() { - boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity(); - for (Account account : getAccounts()) { - if (account.isEnabled()) { - sendPresence(account, includeIdleTimestamp); - } - } - } - - private void refreshAllFcmTokens() { - for (Account account : getAccounts()) { - if (account.isOnlineAndConnected() && mPushManagementService.available(account)) { - mPushManagementService.registerPushTokenOnServer(account); - } - } - } - - - private void sendOfflinePresence(final Account account) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence"); - sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account)); - } - - public MessageGenerator getMessageGenerator() { - return this.mMessageGenerator; - } - - public PresenceGenerator getPresenceGenerator() { - return this.mPresenceGenerator; - } - - public IqGenerator getIqGenerator() { - return this.mIqGenerator; - } - - public IqParser getIqParser() { - return this.mIqParser; - } - - public JingleConnectionManager getJingleConnectionManager() { - return this.mJingleConnectionManager; - } - - public MessageArchiveService getMessageArchiveService() { - return this.mMessageArchiveService; - } - - public QuickConversationsService getQuickConversationsService() { - return this.mQuickConversationsService; - } - - public List findContacts(Jid jid, String accountJid) { - ArrayList contacts = new ArrayList<>(); - for (Account account : getAccounts()) { - if ((account.isEnabled() || accountJid != null) - && (accountJid == null - || accountJid.equals(account.getJid().asBareJid().toString()))) { - Contact contact = account.getRoster().getContactFromContactList(jid); - if (contact != null) { - contacts.add(contact); - } - } - } - return contacts; - } - - public Conversation findFirstMuc(Jid jid) { - for (Conversation conversation : getConversations()) { - if (conversation.getAccount().isEnabled() - && conversation.getJid().asBareJid().equals(jid.asBareJid()) - && conversation.getMode() == Conversation.MODE_MULTI) { - return conversation; - } - } - return null; - } - - public NotificationService getNotificationService() { - return this.mNotificationService; - } - - public HttpConnectionManager getHttpConnectionManager() { - return this.mHttpConnectionManager; - } - - public void resendFailedMessages(final Message message) { - final Collection messages = new ArrayList<>(); - Message current = message; - while (current.getStatus() == Message.STATUS_SEND_FAILED) { - messages.add(current); - if (current.mergeable(current.next())) { - current = current.next(); - } else { - break; - } - } - for (final Message msg : messages) { - msg.setTime(System.currentTimeMillis()); - markMessage(msg, Message.STATUS_WAITING); - this.resendMessage(msg, false); - } - if (message.getConversation() instanceof Conversation) { - ((Conversation) message.getConversation()).sort(); - } - updateConversationUi(); - } - - public void deleteMessage(final Conversation conversation, final Message message) { - conversation.deleteMessage(message); - message.setMessageDeleted(true); - final Runnable runnable = () -> { - databaseBackend.deleteMessageInConversation(message); - databaseBackend.updateConversation(conversation); - }; - mDatabaseWriterExecutor.execute(runnable); - } - - public void clearConversationHistory(final Conversation conversation) { - try { - final long clearDate; - final String reference; - if (conversation.countMessages() > 0) { - final Message latestMessage = conversation.getLatestMessage(); - clearDate = latestMessage.getTimeSent() + 1000; - reference = latestMessage.getServerMsgId(); - } else { - clearDate = System.currentTimeMillis(); - reference = null; - } - conversation.clearMessages(); - conversation.setHasMessagesLeftOnServer(false); // avoid messages getting loaded through mam - conversation.setLastClearHistory(clearDate, reference); - Runnable runnable = - () -> { - databaseBackend.deleteMessagesInConversation(conversation); - databaseBackend.updateConversation(conversation); - }; - mDatabaseWriterExecutor.execute(runnable); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public boolean sendBlockRequest(final Blockable blockable, boolean reportSpam) { - if (blockable != null && blockable.getBlockedJid() != null) { - final Jid jid = blockable.getBlockedJid(); - this.sendIqPacket( - blockable.getAccount(), - getIqGenerator().generateSetBlockRequest(jid, reportSpam), - (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - a.getBlocklist().add(jid); - updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); - } - }); - if (blockable.getBlockedJid().isFullJid()) { - return false; - } else if (removeBlockedConversations(blockable.getAccount(), jid)) { - updateConversationUi(); - return true; - } else { - return false; - } - } else { - return false; - } - } - - public boolean removeBlockedConversations(final Account account, final Jid blockedJid) { - boolean removed = false; - synchronized (this.conversations) { - boolean domainJid = blockedJid.getLocal() == null; - for (Conversation conversation : this.conversations) { - boolean jidMatches = - (domainJid - && blockedJid - .getDomain() - .equals(conversation.getJid().getDomain())) - || blockedJid.equals(conversation.getJid().asBareJid()); - if (conversation.getAccount() == account - && conversation.getMode() == Conversation.MODE_SINGLE - && jidMatches) { - this.conversations.remove(conversation); - markRead(conversation); - conversation.setStatus(Conversation.STATUS_ARCHIVED); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": archiving conversation " - + conversation.getJid().asBareJid() - + " because jid was blocked"); - updateConversation(conversation); - removed = true; - } - } - } - return removed; - } - - public void sendUnblockRequest(final Blockable blockable) { - if (blockable != null && blockable.getJid() != null) { - final Jid jid = blockable.getBlockedJid(); - this.sendIqPacket( - blockable.getAccount(), - getIqGenerator().generateSetUnblockRequest(jid), - new OnIqPacketReceived() { - @Override - public void onIqPacketReceived( - final Account account, final IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.getBlocklist().remove(jid); - updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); - } - } - }); - } - } - - public void publishDisplayName(Account account) { - String displayName = account.getDisplayName(); - final IqPacket request; - if (TextUtils.isEmpty(displayName)) { - request = mIqGenerator.deleteNode(Namespace.NICK); - } else { - request = mIqGenerator.publishNick(displayName); - } - mAvatarService.clear(account); - sendIqPacket( - account, - request, - (account1, packet) -> { - if (packet.getType() == IqPacket.TYPE.ERROR) { - Log.d( - Config.LOGTAG, - account1.getJid().asBareJid() - + ": unable to modify nick name " - + packet); - } - }); - } - - public ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair key) { - ServiceDiscoveryResult result = discoCache.get(key); - if (result != null) { - return result; - } else { - result = databaseBackend.findDiscoveryResult(key.first, key.second); - if (result != null) { - discoCache.put(key, result); - } - return result; - } - } - - public void fetchCaps(Account account, final Jid jid, final Presence presence) { - final Pair key = new Pair<>(presence.getHash(), presence.getVer()); - final ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key); - if (disco != null) { - presence.setServiceDiscoveryResult(disco); - final Contact contact = account.getRoster().getContact(jid); - if (contact.refreshRtpCapability()) { - syncRoster(account); - } - } else { - final IqPacket request = new IqPacket(IqPacket.TYPE.GET); - request.setTo(jid); - final String node = presence.getNode(); - final String ver = presence.getVer(); - final Element query = request.query(Namespace.DISCO_INFO); - if (node != null && ver != null) { - query.setAttribute("node", node + "#" + ver); - } - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": making disco request for " - + key.second - + " to " - + jid); - sendIqPacket( - account, - request, - (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - final ServiceDiscoveryResult discoveryResult = - new ServiceDiscoveryResult(response); - if (presence.getVer().equals(discoveryResult.getVer())) { - databaseBackend.insertDiscoveryResult(discoveryResult); - injectServiceDiscoveryResult( - a.getRoster(), - presence.getHash(), - presence.getVer(), - discoveryResult); - } else { - Log.d( - Config.LOGTAG, - a.getJid().asBareJid() - + ": mismatch in caps for contact " - + jid - + " " - + presence.getVer() - + " vs " - + discoveryResult.getVer()); - } - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": unable to fetch caps from " - + jid); - } - }); - } - } - - private void injectServiceDiscoveryResult( - Roster roster, String hash, String ver, ServiceDiscoveryResult disco) { - boolean rosterNeedsSync = false; - for (final Contact contact : roster.getContacts()) { - boolean serviceDiscoverySet = false; - for (final Presence presence : contact.getPresences().getPresences()) { - if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) { - presence.setServiceDiscoveryResult(disco); - serviceDiscoverySet = true; - } - } - if (serviceDiscoverySet) { - rosterNeedsSync |= contact.refreshRtpCapability(); - } - } - if (rosterNeedsSync) { - syncRoster(roster.getAccount()); - } - } - - public void fetchMamPreferences(Account account, final OnMamPreferencesFetched callback) { - final MessageArchiveService.Version version = MessageArchiveService.Version.get(account); - IqPacket request = new IqPacket(IqPacket.TYPE.GET); - request.addChild("prefs", version.namespace); - sendIqPacket( - account, - request, - (account1, packet) -> { - Element prefs = packet.findChild("prefs", version.namespace); - if (packet.getType() == IqPacket.TYPE.RESULT && prefs != null) { - callback.onPreferencesFetched(prefs); - } else { - callback.onPreferencesFetchFailed(); - } - }); - } - - public PushManagementService getPushManagementService() { - return mPushManagementService; - } - - public void changeStatus(Account account, PresenceTemplate template, String signature) { - if (!template.getStatusMessage().isEmpty()) { - databaseBackend.insertPresenceTemplate(template); - } - account.setPgpSignature(signature); - account.setPresenceStatus(template.getStatus()); - account.setPresenceStatusMessage(template.getStatusMessage()); - databaseBackend.updateAccount(account); - sendPresence(account); - } - - public List getPresenceTemplates(Account account) { - List templates = databaseBackend.getPresenceTemplates(); - for (PresenceTemplate template : account.getSelfContact().getPresences().asTemplates()) { - if (!templates.contains(template)) { - templates.add(0, template); - } - } - return templates; - } - - public void saveConversationAsBookmark(Conversation conversation, String name) { - final Account account = conversation.getAccount(); - final Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid()); - final String nick = conversation.getJid().getResource(); - if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) { - bookmark.setNick(nick); - } - if (!TextUtils.isEmpty(name)) { - bookmark.setBookmarkName(name); - } - bookmark.setAutojoin( - getPreferences() - .getBoolean("autojoin", getResources().getBoolean(R.bool.autojoin))); - createBookmark(account, bookmark); - bookmark.setConversation(conversation); - } - - public boolean verifyFingerprints(Contact contact, List fingerprints) { - boolean needsRosterWrite = false; - boolean performedVerification = false; - final AxolotlService axolotlService = contact.getAccount().getAxolotlService(); - for (XmppUri.Fingerprint fp : fingerprints) { - if (fp.type == XmppUri.FingerprintType.OTR) { - performedVerification |= contact.addOtrFingerprint(fp.fingerprint); - needsRosterWrite |= performedVerification; - } else if (fp.type == XmppUri.FingerprintType.OMEMO) { - String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", ""); - FingerprintStatus fingerprintStatus = - axolotlService.getFingerprintTrust(fingerprint); - if (fingerprintStatus != null) { - if (!fingerprintStatus.isVerified()) { - performedVerification = true; - axolotlService.setFingerprintTrust( - fingerprint, fingerprintStatus.toVerified()); - } - } else { - axolotlService.preVerifyFingerprint(contact, fingerprint); - } - } - } - if (needsRosterWrite) { - syncRosterToDisk(contact.getAccount()); - } - return performedVerification; - } - - public boolean verifyFingerprints(Account account, List fingerprints) { - final AxolotlService axolotlService = account.getAxolotlService(); - boolean verifiedSomething = false; - for (XmppUri.Fingerprint fp : fingerprints) { - if (fp.type == XmppUri.FingerprintType.OMEMO) { - String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", ""); - Log.d(Config.LOGTAG, "trying to verify own fp=" + fingerprint); - FingerprintStatus fingerprintStatus = - axolotlService.getFingerprintTrust(fingerprint); - if (fingerprintStatus != null) { - if (!fingerprintStatus.isVerified()) { - axolotlService.setFingerprintTrust( - fingerprint, fingerprintStatus.toVerified()); - verifiedSomething = true; - } - } else { - axolotlService.preVerifyFingerprint(account, fingerprint); - verifiedSomething = true; - } - } - } - return verifiedSomething; - } - - public boolean blindTrustBeforeVerification() { - return getBooleanPreference(SettingsActivity.BLIND_TRUST_BEFORE_VERIFICATION, R.bool.btbv); - } - - public void ScheduleAutomaticExport() { - //start export log service every day at given time - if (Config.ExportLogs) { - if (Config.ExportLogs_Hour >= 0 && Config.ExportLogs_Hour <= 23 && Config.ExportLogs_Minute >= 0 && Config.ExportLogs_Minute <= 59) { - try { - Calendar now = Calendar.getInstance(); - now.setTimeInMillis(System.currentTimeMillis()); - now.setTimeZone(TimeZone.getDefault()); - Calendar timetoexport = Calendar.getInstance(); - timetoexport.setTimeInMillis(System.currentTimeMillis()); - timetoexport.setTimeZone(TimeZone.getDefault()); - Intent intent = new Intent(this, AlarmReceiver.class); - intent.setAction("exportlogs"); - Log.d(Config.LOGTAG, "Schedule automatic export logs at " + Config.ExportLogs_Hour + ":" + Config.ExportLogs_Minute + " (" + timetoexport.getTimeZone().getDisplayName() + ")"); - timetoexport.set(Calendar.HOUR_OF_DAY, Config.ExportLogs_Hour); - timetoexport.set(Calendar.MINUTE, Config.ExportLogs_Minute); - if (timetoexport.before(now)) { - SimpleDateFormat newDate = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US); - timetoexport.add(Calendar.DAY_OF_YEAR, 1); //DATE or DAY_OF_MONTH - Log.d(Config.LOGTAG, "Schedule automatic export logs, for today, the export time is in the past, scheduling first export run for the next day (" + newDate.format(timetoexport.getTimeInMillis()) + ")."); - } - final PendingIntent ScheduleExportIntent = PendingIntent.getBroadcast(this, AlarmReceiver.SCHEDULE_ALARM_REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT); - ((AlarmManager) getSystemService(ALARM_SERVICE)).setInexactRepeating(AlarmManager.RTC_WAKEUP, timetoexport.getTimeInMillis(), AlarmManager.INTERVAL_DAY, ScheduleExportIntent); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - } - - public void CancelAutomaticExport(boolean force) { - if (!Config.ExportLogs || force) { - Log.d(Config.LOGTAG, "Cancel scheduled automatic export"); - Intent intent = new Intent(this, AlarmReceiver.class); - final PendingIntent ScheduleExportIntent = PendingIntent.getBroadcast(this, AlarmReceiver.SCHEDULE_ALARM_REQUEST_CODE, intent, s() - ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - ((AlarmManager) this.getSystemService(ALARM_SERVICE)).cancel(ScheduleExportIntent); - } - } - - public String installedFrom() { - final PackageManager packageManager = this.getPackageManager(); - final String packageID = BuildConfig.APPLICATION_ID; - final String installedFrom = packageManager.getInstallerPackageName(packageID); - Log.d(Config.LOGTAG, "Messenger installed from " + installedFrom); - return installedFrom; - } - - public ShortcutService getShortcutService() { - return mShortcutService; - } - - public void pushMamPreferences(Account account, Element prefs) { - IqPacket set = new IqPacket(IqPacket.TYPE.SET); - set.addChild(prefs); - sendIqPacket(account, set, null); - } - - public void evictPreview(String uuid) { - if (mBitmapCache.remove(uuid) != null) { - Log.d(Config.LOGTAG, "deleted cached preview"); - } - } - - public void showInvitationNotification(final Conversation conversation, final Contact contact, final LocalizedContent id) { - final String messageId = "MUC_INVITATION_" + id; - final Message message = new Message( - conversation, - Message.STATUS_RECEIVED, - Message.TYPE_PRIVATE, - messageId - ); - String from; - if (contact == null) { - from = getString(R.string.a_user); - } else { - from = contact.getJid().asBareJid().toEscapedString(); - } - String to = conversation.getJid().toString(); - final String toDisplayName = conversation.getName().toString(); - if (toDisplayName != null && toDisplayName.length() > 0) { - to = toDisplayName + " (" + to + ")"; - } - message.setBody(String.format(getString(R.string.got_invitation_from), from, to)); - message.setServerMsgId(messageId); - message.setTime(System.currentTimeMillis()); - if (conversation instanceof Conversation) { - ((Conversation) conversation).add(message); - createMessageAsync(message); - message.markUnread(); - updateConversationUi(); - } else { - throw new IllegalStateException("Somehow the conversation in a message was a stub"); - } - } - - public interface OnMamPreferencesFetched { - void onPreferencesFetched(Element prefs); - - void onPreferencesFetchFailed(); - } - - public interface OnAccountCreated { - void onAccountCreated(Account account); - - void informUser(int r); - } - - public interface OnMoreMessagesLoaded { - void onMoreMessagesLoaded(int count, Conversation conversation); - - void informUser(int r); - } - - public interface OnAccountPasswordChanged { - void onPasswordChangeSucceeded(); - - void onPasswordChangeFailed(); - } - - public interface OnAffiliationChanged { - void onAffiliationChangedSuccessful(Jid jid); - - void onAffiliationChangeFailed(Jid jid, int resId); - } - - public interface OnRoomDestroy { - void onRoomDestroySucceeded(); - - void onRoomDestroyFailed(); - } - - public interface OnConversationUpdate { - void onConversationUpdate(); - } - - public interface OnJingleRtpConnectionUpdate { - void onJingleRtpConnectionUpdate( - final Account account, - final Jid with, - final String sessionId, - final RtpEndUserState state); - - void onAudioDeviceChanged( - AppRTCAudioManager.AudioDevice selectedAudioDevice, - Set availableAudioDevices); - } - - public interface OnAccountUpdate { - void onAccountUpdate(); - } - - public interface OnCaptchaRequested { - void onCaptchaRequested(Account account, String id, Data data, Bitmap captcha); - } - - public interface OnRosterUpdate { - void onRosterUpdate(); - } - - public interface OnMucRosterUpdate { - void onMucRosterUpdate(); - } - - public interface OnConferenceConfigurationFetched { - void onConferenceConfigurationFetched(Conversation conversation); - - void onFetchFailed(Conversation conversation, String errorCondition); - } - - public interface OnConferenceJoined { - void onConferenceJoined(Conversation conversation); - } - - public interface OnConfigurationPushed { - void onPushSucceeded(); - - void onPushFailed(); - } - - public interface OnShowErrorToast { - void onShowErrorToast(int resId); - } - - public class XmppConnectionBinder extends Binder { - public XmppConnectionService getService() { - return XmppConnectionService.this; - } - } - - private class InternalEventReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - onStartCommand(intent, 0, 0); - } - } - - public static class OngoingCall { - public final AbstractJingleConnection.Id id; - public final Set media; - public final boolean reconnecting; - - public OngoingCall( - AbstractJingleConnection.Id id, Set media, final boolean reconnecting) { - this.id = id; - this.media = media; - this.reconnecting = reconnecting; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - OngoingCall that = (OngoingCall) o; - return reconnecting == that.reconnecting - && Objects.equal(id, that.id) - && Objects.equal(media, that.media); - } - - @Override - public int hashCode() { - return Objects.hashCode(id, media, reconnecting); - } - } - - private void expireOldMessages(long timestamp, boolean stepped) { - if (stepped) { - final long expiredMessagesCount = databaseBackend.countExpireOldMessages(timestamp); - final long days = TimeUnit.MILLISECONDS.toDays(Calendar.getInstance().getTimeInMillis() - databaseBackend.getOldestMessages()); - final long day = (long) 24 * 60 * 60 * 1000; - int count = 0; - int messagesCount = 0; - while (count <= days) { - try { - messagesCount += databaseBackend.expireOldMessages(timestamp - ((days - count) * day)); - } catch (Exception e) { - e.printStackTrace(); - } - count++; - if (expiredMessagesCount == messagesCount) { - break; - } - } - } else { - databaseBackend.expireOldMessages(timestamp); - } - } - - public void expireOldFiles() { - mLastExpiryRun.set(SystemClock.elapsedRealtime()); - new Thread(() -> { - long timestamp = getAutomaticAttachmentDeletionDate(); - if (timestamp > 0) { - getFileBackend().expireOldFiles(new File(getAppMediaDirectory(this, AUDIOS)), timestamp); - getFileBackend().expireOldFiles(new File(getAppMediaDirectory(this, FILES)), timestamp); - getFileBackend().expireOldFiles(new File(getAppMediaDirectory(this, IMAGES)), timestamp); - getFileBackend().expireOldFiles(new File(getAppMediaDirectory(this, VIDEOS)), timestamp); - updateConversationUi(); - } - }).start(); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/AboutActivity.java b/src/main/java/eu/siacs/conversations/ui/AboutActivity.java deleted file mode 100644 index 8480f98a5..000000000 --- a/src/main/java/eu/siacs/conversations/ui/AboutActivity.java +++ /dev/null @@ -1,80 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.net.Uri; -import android.os.Bundle; -import android.text.SpannableStringBuilder; -import android.widget.Button; -import android.widget.TextView; - -import java.util.Calendar; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.ui.util.CustomTab; -import eu.siacs.conversations.ui.util.MyLinkify; -import eu.siacs.conversations.utils.ThemeHelper; -import me.drakeet.support.toast.ToastCompat; - -public class AboutActivity extends XmppActivity { - - private TextView aboutmessage; - private TextView libraries; - - @Override - protected void refreshUiReal() { - showText(); - } - - @Override - void onBackendConnected() { - showText(); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setTheme(ThemeHelper.find(this)); - setContentView(R.layout.activity_about); - setSupportActionBar(findViewById(R.id.toolbar)); - configureActionBar(getSupportActionBar()); - aboutmessage = findViewById(R.id.aboutmessage); - libraries = findViewById(R.id.libraries); - Button privacyButton = findViewById(R.id.show_privacy_policy); - privacyButton.setOnClickListener(view -> { - try { - final Uri uri = Uri.parse(Config.privacyURL); - CustomTab.openTab(this, uri, isDarkTheme()); - } catch (Exception e) { - ToastCompat.makeText(this, R.string.no_application_found_to_open_link, ToastCompat.LENGTH_SHORT).show(); - } - }); - Button termsOfUseButton = findViewById(R.id.show_terms_of_use); - termsOfUseButton.setOnClickListener(view -> { - try { - final Uri uri = Uri.parse(Config.termsOfUseURL); - CustomTab.openTab(this, uri, isDarkTheme()); - } catch (Exception e) { - ToastCompat.makeText(this, R.string.no_application_found_to_open_link, ToastCompat.LENGTH_SHORT).show(); - } - }); - } - - @Override - protected void onStart() { - super.onStart(); - showText(); - } - - private void showText() { - final String year = String.valueOf(Calendar.getInstance().get(Calendar.YEAR)); - SpannableStringBuilder aboutMessage = new SpannableStringBuilder(getString(R.string.pref_about_message, year)); - MyLinkify.addLinks(aboutMessage, false); - aboutmessage.setText(aboutMessage); - aboutmessage.setAutoLinkMask(0); - - SpannableStringBuilder libs = new SpannableStringBuilder(getString(R.string.pref_about_libraries)); - MyLinkify.addLinks(libs, false); - libraries.setText(libs); - libraries.setAutoLinkMask(0); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/AboutPreference.java b/src/main/java/eu/siacs/conversations/ui/AboutPreference.java deleted file mode 100644 index 82ffedcc3..000000000 --- a/src/main/java/eu/siacs/conversations/ui/AboutPreference.java +++ /dev/null @@ -1,33 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.Context; -import android.content.Intent; -import android.preference.Preference; -import android.util.AttributeSet; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.utils.PhoneHelper; - -public class AboutPreference extends Preference { - public AboutPreference(final Context context, final AttributeSet attrs, final int defStyle) { - super(context, attrs, defStyle); - setSummary(); - } - - public AboutPreference(final Context context, final AttributeSet attrs) { - super(context, attrs); - setSummary(); - } - - @Override - protected void onClick() { - super.onClick(); - final Intent intent = new Intent(getContext(), AboutActivity.class); - getContext().startActivity(intent); - } - - private void setSummary() { - setSummary(getContext().getString(R.string.app_name) + ' ' + PhoneHelper.getVersionName(getContext())); - } -} - diff --git a/src/main/java/eu/siacs/conversations/ui/AbstractSearchableListItemActivity.java b/src/main/java/eu/siacs/conversations/ui/AbstractSearchableListItemActivity.java deleted file mode 100644 index ffb41acff..000000000 --- a/src/main/java/eu/siacs/conversations/ui/AbstractSearchableListItemActivity.java +++ /dev/null @@ -1,136 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.Context; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.ArrayAdapter; -import android.widget.EditText; -import android.widget.ListView; -import android.widget.TextView; - -import androidx.appcompat.widget.Toolbar; -import androidx.databinding.DataBindingUtil; - -import java.util.ArrayList; -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ActivityChooseContactBinding; -import eu.siacs.conversations.entities.ListItem; -import eu.siacs.conversations.ui.adapter.ListItemAdapter; - -public abstract class AbstractSearchableListItemActivity extends XmppActivity implements TextView.OnEditorActionListener { - protected ActivityChooseContactBinding binding; - private final List listItems = new ArrayList<>(); - private ArrayAdapter mListItemsAdapter; - - private EditText mSearchEditText; - - private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() { - - @Override - public boolean onMenuItemActionExpand(final MenuItem item) { - mSearchEditText.post(() -> { - mSearchEditText.requestFocus(); - final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mSearchEditText, InputMethodManager.SHOW_IMPLICIT); - }); - - return true; - } - - @Override - public boolean onMenuItemActionCollapse(final MenuItem item) { - final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY); - mSearchEditText.setText(""); - filterContacts(); - return true; - } - }; - - private final TextWatcher mSearchTextWatcher = new TextWatcher() { - - @Override - public void afterTextChanged(final Editable editable) { - filterContacts(editable.toString()); - } - - @Override - public void beforeTextChanged(final CharSequence s, final int start, final int count, - final int after) { - } - - @Override - public void onTextChanged(final CharSequence s, final int start, final int before, - final int count) { - } - }; - - public ListView getListView() { - return binding.chooseContactList; - } - - public List getListItems() { - return listItems; - } - - public EditText getSearchEditText() { - return mSearchEditText; - } - - public ArrayAdapter getListItemAdapter() { - return mListItemsAdapter; - } - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_choose_contact); - setSupportActionBar((Toolbar) binding.toolbar); - configureActionBar(getSupportActionBar()); - this.binding.chooseContactList.setFastScrollEnabled(true); - mListItemsAdapter = new ListItemAdapter(this, listItems); - this.binding.chooseContactList.setAdapter(mListItemsAdapter); - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.choose_contact, menu); - final MenuItem menuSearchView = menu.findItem(R.id.action_search); - final View mSearchView = menuSearchView.getActionView(); - mSearchEditText = mSearchView.findViewById(R.id.search_field); - mSearchEditText.addTextChangedListener(mSearchTextWatcher); - mSearchEditText.setHint(R.string.search_contacts); - mSearchEditText.setOnEditorActionListener(this); - menuSearchView.setOnActionExpandListener(mOnActionExpandListener); - return true; - } - - protected void filterContacts() { - final String needle = mSearchEditText != null ? mSearchEditText.getText().toString() : null; - if (needle != null && !needle.isEmpty()) { - filterContacts(needle); - } else { - filterContacts(null); - } - } - - protected abstract void filterContacts(final String needle); - - @Override - void onBackendConnected() { - filterContacts(); - } - - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - return false; - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/ActionBarActivity.java b/src/main/java/eu/siacs/conversations/ui/ActionBarActivity.java deleted file mode 100644 index 151c5851b..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ActionBarActivity.java +++ /dev/null @@ -1,59 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.view.MenuItem; -import android.view.WindowManager; - -import androidx.annotation.BoolRes; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; - -import eu.siacs.conversations.R; - -public abstract class ActionBarActivity extends AppCompatActivity { - public static void configureActionBar(ActionBar actionBar) { - configureActionBar(actionBar, true); - } - - public static void configureActionBar(ActionBar actionBar, boolean upNavigation) { - if (actionBar != null) { - actionBar.setHomeButtonEnabled(upNavigation); - actionBar.setDisplayHomeAsUpEnabled(upNavigation); - } - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - finish(); - break; - } - return super.onOptionsItemSelected(item); - } - - void initializeScreenshotSecurity() { - try { - if (isScreenSecurityEnabled()) { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); - } else { - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - public boolean isScreenSecurityEnabled() { - return getBooleanPreference("screen_security", R.bool.screen_security); - } - - protected SharedPreferences getPreferences() { - return PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - } - - protected boolean getBooleanPreference(String name, @BoolRes int res) { - return getPreferences().getBoolean(name, getResources().getBoolean(res)); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java b/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java deleted file mode 100644 index a54abf1b1..000000000 --- a/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java +++ /dev/null @@ -1,62 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.view.View; - -import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; -import androidx.databinding.DataBindingUtil; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.DialogBlockContactBinding; -import eu.siacs.conversations.entities.Blockable; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.ui.util.JidDialog; -import me.drakeet.support.toast.ToastCompat; - -public final class BlockContactDialog { - public static void show(final XmppActivity xmppActivity, final Blockable blockable) { - final AlertDialog.Builder builder = new AlertDialog.Builder(xmppActivity); - final boolean isBlocked = blockable.isBlocked(); - builder.setNegativeButton(R.string.cancel, null); - DialogBlockContactBinding binding = DataBindingUtil.inflate(xmppActivity.getLayoutInflater(), R.layout.dialog_block_contact, null, false); - final boolean reporting = blockable.getAccount().getXmppConnection().getFeatures().spamReporting(); - binding.reportSpam.setVisibility(!isBlocked && reporting ? View.VISIBLE : View.GONE); - builder.setView(binding.getRoot()); - - final String value; - @StringRes int res; - if (blockable.getJid().isFullJid()) { - builder.setTitle(isBlocked ? R.string.action_unblock_participant : R.string.action_block_participant); - value = blockable.getJid().toEscapedString(); - res = isBlocked ? R.string.unblock_contact_text : R.string.block_contact_text; - } else if (blockable.getJid().getLocal() == null || blockable.getAccount().isBlocked(blockable.getJid().getDomain())) { - builder.setTitle(isBlocked ? R.string.action_unblock_domain : R.string.action_block_domain); - value =blockable.getJid().getDomain().toEscapedString(); - res = isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text; - } else { - int resBlockAction = blockable instanceof Conversation && ((Conversation) blockable).isWithStranger() ? R.string.block_stranger : R.string.action_block_contact; - builder.setTitle(isBlocked ? R.string.action_unblock_contact : resBlockAction); - value = blockable.getJid().asBareJid().toEscapedString(); - res = isBlocked ? R.string.unblock_contact_text : R.string.block_contact_text; - } - binding.text.setText(JidDialog.style(xmppActivity, res, value)); - builder.setPositiveButton(isBlocked ? R.string.unblock : R.string.block, (dialog, which) -> { - if (isBlocked) { - xmppActivity.xmppConnectionService.sendUnblockRequest(blockable); - } else { - boolean toastShown = false; - if (xmppActivity.xmppConnectionService.sendBlockRequest(blockable, binding.reportSpam.isChecked())) { - ToastCompat.makeText(xmppActivity, R.string.corresponding_conversations_closed, ToastCompat.LENGTH_SHORT).show(); - toastShown = true; - } - if (xmppActivity instanceof ContactDetailsActivity) { - if (!toastShown) { - ToastCompat.makeText(xmppActivity, R.string.contact_blocked_past_tense, ToastCompat.LENGTH_SHORT).show(); - } - xmppActivity.finish(); - } - } - }); - builder.create().show(); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java b/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java deleted file mode 100644 index f720b75d1..000000000 --- a/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java +++ /dev/null @@ -1,111 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.os.Bundle; -import android.text.Editable; - -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentTransaction; - -import java.util.Collections; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Blockable; -import eu.siacs.conversations.entities.ListItem; -import eu.siacs.conversations.entities.RawBlockable; -import eu.siacs.conversations.ui.interfaces.OnBackendConnected; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnUpdateBlocklist; -import me.drakeet.support.toast.ToastCompat; - -public class BlocklistActivity extends AbstractSearchableListItemActivity implements OnUpdateBlocklist { - private Account account = null; - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - getListView().setOnItemLongClickListener((parent, view, position, id) -> { - BlockContactDialog.show(BlocklistActivity.this, (Blockable) getListItems().get(position)); - return true; - }); - this.binding.fab.show(); - this.binding.fab.setOnClickListener((v) -> showEnterJidDialog()); - } - - @Override - public void onBackendConnected() { - for (final Account account : xmppConnectionService.getAccounts()) { - if (account.getJid().toEscapedString().equals(getIntent().getStringExtra(EXTRA_ACCOUNT))) { - this.account = account; - break; - } - } - filterContacts(); - Fragment fragment = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG); - if (fragment instanceof OnBackendConnected) { - ((OnBackendConnected) fragment).onBackendConnected(); - } - } - - @Override - protected void filterContacts(final String needle) { - getListItems().clear(); - if (account != null) { - for (final Jid jid : account.getBlocklist()) { - ListItem item; - if (jid.isFullJid()) { - item = new RawBlockable(account, jid); - } else { - item = account.getRoster().getContact(jid); - } - if (item.match(this, needle)) { - getListItems().add(item); - } - } - Collections.sort(getListItems()); - } - getListItemAdapter().notifyDataSetChanged(); - } - - protected void showEnterJidDialog() { - FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); - Fragment prev = getSupportFragmentManager().findFragmentByTag("dialog"); - if (prev != null) { - ft.remove(prev); - } - ft.addToBackStack(null); - EnterJidDialog dialog = EnterJidDialog.newInstance( - null, - getString(R.string.block_jabber_id), - getString(R.string.block), - null, - account.getJid().asBareJid().toEscapedString(), - true, - xmppConnectionService.multipleAccounts(), - false - ); - - dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid) -> { - Blockable blockable = new RawBlockable(account, contactJid); - if (xmppConnectionService.sendBlockRequest(blockable, false)) { - ToastCompat.makeText(BlocklistActivity.this, R.string.corresponding_conversations_closed, ToastCompat.LENGTH_SHORT).show(); - } - return true; - }); - dialog.show(ft, "dialog"); - } - - protected void refreshUiReal() { - final Editable editable = getSearchEditText().getText(); - if (editable != null) { - filterContacts(editable.toString()); - } else { - filterContacts(); - } - } - - @Override - public void OnUpdateBlocklist(final OnUpdateBlocklist.Status status) { - refreshUi(); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java b/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java deleted file mode 100644 index 0b493f2c9..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java +++ /dev/null @@ -1,125 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.Intent; -import android.os.Bundle; -import android.view.View; -import android.widget.Button; - -import com.google.android.material.textfield.TextInputLayout; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.widget.DisabledActionModeCallback; -import eu.siacs.conversations.ui.widget.TextInputEditText; -import me.drakeet.support.toast.ToastCompat; - -public class ChangePasswordActivity extends XmppActivity implements XmppConnectionService.OnAccountPasswordChanged { - - private Button mChangePasswordButton; - private View.OnClickListener mOnChangePasswordButtonClicked = new View.OnClickListener() { - @Override - public void onClick(View view) { - if (mAccount != null) { - final String currentPassword = mCurrentPassword.getText().toString(); - final String newPassword = mNewPassword.getText().toString(); - if (!mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) && !currentPassword.equals(mAccount.getPassword())) { - mCurrentPassword.requestFocus(); - mCurrentPasswordLayout.setError(getString(R.string.account_status_unauthorized)); - removeErrorsOnAllBut(mCurrentPasswordLayout); - } else if (newPassword.trim().isEmpty()) { - mNewPassword.requestFocus(); - mNewPasswordLayout.setError(getString(R.string.password_should_not_be_empty)); - removeErrorsOnAllBut(mNewPasswordLayout); - } else { - mCurrentPasswordLayout.setError(null); - mNewPasswordLayout.setError(null); - xmppConnectionService.updateAccountPasswordOnServer(mAccount, newPassword, ChangePasswordActivity.this); - mChangePasswordButton.setEnabled(false); - mChangePasswordButton.setText(R.string.updating); - } - } - } - }; - private TextInputEditText mCurrentPassword; - private TextInputEditText mNewPassword; - private TextInputLayout mNewPasswordLayout; - private TextInputLayout mCurrentPasswordLayout; - private Account mAccount; - - @Override - void onBackendConnected() { - this.mAccount = extractAccount(getIntent()); - if (this.mAccount != null && this.mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) { - this.mCurrentPasswordLayout.setVisibility(View.GONE); - } else { - this.mCurrentPassword.setVisibility(View.VISIBLE); - } - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_change_password); - setSupportActionBar(findViewById(R.id.toolbar)); - configureActionBar(getSupportActionBar()); - Button mCancelButton = findViewById(R.id.left_button); - mCancelButton.setOnClickListener(view -> finish()); - this.mChangePasswordButton = findViewById(R.id.right_button); - this.mChangePasswordButton.setOnClickListener(this.mOnChangePasswordButtonClicked); - this.mCurrentPassword = findViewById(R.id.current_password); - this.mCurrentPassword.setCustomSelectionActionModeCallback(new DisabledActionModeCallback()); - this.mNewPassword = findViewById(R.id.new_password); - this.mNewPassword.setCustomSelectionActionModeCallback(new DisabledActionModeCallback()); - this.mCurrentPasswordLayout = findViewById(R.id.current_password_layout); - this.mNewPasswordLayout = findViewById(R.id.new_password_layout); - } - - @Override - protected void onStart() { - super.onStart(); - Intent intent = getIntent(); - String password = intent != null ? intent.getStringExtra("password") : null; - if (password != null) { - this.mNewPassword.getEditableText().clear(); - this.mNewPassword.getEditableText().append(password); - } - } - - @Override - public void onPasswordChangeSucceeded() { - runOnUiThread(new Runnable() { - @Override - public void run() { - ToastCompat.makeText(ChangePasswordActivity.this, R.string.password_changed, ToastCompat.LENGTH_LONG).show(); - finish(); - } - }); - } - - @Override - public void onPasswordChangeFailed() { - runOnUiThread(() -> { - mNewPasswordLayout.setError(getString(R.string.could_not_change_password)); - mChangePasswordButton.setEnabled(true); - mChangePasswordButton.setText(R.string.change_password); - }); - - } - - private void removeErrorsOnAllBut(TextInputLayout exception) { - if (this.mCurrentPasswordLayout != exception) { - this.mCurrentPasswordLayout.setErrorEnabled(false); - this.mCurrentPasswordLayout.setError(null); - } - if (this.mNewPasswordLayout != exception) { - this.mNewPasswordLayout.setErrorEnabled(false); - this.mNewPasswordLayout.setError(null); - } - - } - - public void refreshUiReal() { - - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java deleted file mode 100644 index 271dda338..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java +++ /dev/null @@ -1,328 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.text.Html; -import android.text.method.LinkMovementMethod; -import android.util.Log; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; -import android.widget.TextView; - -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.Toolbar; -import androidx.databinding.DataBindingUtil; - -import java.util.Collections; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ActivityChannelDiscoveryBinding; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Bookmark; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Room; -import eu.siacs.conversations.services.ChannelDiscoveryService; -import eu.siacs.conversations.ui.adapter.ChannelSearchResultAdapter; -import eu.siacs.conversations.ui.util.PendingItem; -import eu.siacs.conversations.ui.util.SoftKeyboardUtils; -import eu.siacs.conversations.utils.AccountUtils; -import eu.siacs.conversations.xmpp.Jid; -import me.drakeet.support.toast.ToastCompat; - -public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.OnActionExpandListener, TextView.OnEditorActionListener, ChannelDiscoveryService.OnChannelSearchResultsFound, ChannelSearchResultAdapter.OnChannelSearchResultSelected { - - private static final String CHANNEL_DISCOVERY_OPT_IN = "channel_discovery_opt_in"; - - private final ChannelSearchResultAdapter adapter = new ChannelSearchResultAdapter(this); - private final PendingItem mInitialSearchValue = new PendingItem<>(); - private ActivityChannelDiscoveryBinding binding; - private MenuItem mJabberNetwork; - private MenuItem mLocalServer; - private MenuItem mMenuSearchView; - private EditText mSearchEditText; - private static String jabberNetwork = "JABBER_NETWORK"; - private static String localServer = "LOCAL_SERVER"; - - private ChannelDiscoveryService.Method method = ChannelDiscoveryService.Method.LOCAL_SERVER; - - private boolean optedIn = false; - - @Override - protected void refreshUiReal() { - - } - - @Override - void onBackendConnected() { - if (optedIn || method == ChannelDiscoveryService.Method.LOCAL_SERVER) { - final String query; - if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) { - query = mSearchEditText.getText().toString(); - } else { - query = mInitialSearchValue.peek(); - } - toggleLoadingScreen(); - xmppConnectionService.discoverChannels(query, this.method, this); - } - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = DataBindingUtil.setContentView(this, R.layout.activity_channel_discovery); - setSupportActionBar((Toolbar) binding.toolbar); - configureActionBar(getSupportActionBar(), true); - binding.list.setAdapter(this.adapter); - this.adapter.setOnChannelSearchResultSelectedListener(this); - this.optedIn = getPreferences().getBoolean(CHANNEL_DISCOVERY_OPT_IN, false); - - final String search = savedInstanceState == null ? null : savedInstanceState.getString("search"); - if (search != null) { - mInitialSearchValue.push(search); - } - } - - public static ChannelDiscoveryService.Method getMethod(final Context c) { - final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(c); - final String m = p.getString("channel_discovery_method", c.getString(R.string.default_channel_discovery)); - try { - return ChannelDiscoveryService.Method.valueOf(m); - } catch (IllegalArgumentException e) { - return ChannelDiscoveryService.Method.JABBER_NETWORK; - } - } - - private void handleMethodSelection(MenuItem item) { - final boolean updated; - switch (item.getItemId()) { - case R.id.jabber_network: - updated = getPreferences().edit().putString("channel_discovery_method", jabberNetwork).commit(); - item.setChecked(true); - break; - case R.id.local_server: - updated = getPreferences().edit().putString("channel_discovery_method", localServer).commit(); - item.setChecked(true); - break; - default: - updated = getPreferences().edit().putString("channel_discovery_method", getString(R.string.default_channel_discovery)).commit(); - item.setChecked(true); - break; - } - if (updated) { - Log.d(Config.LOGTAG, "Discovery method: " + getPreferences().getString("channel_discovery_method", getString(R.string.default_channel_discovery))); - } - recreate(); - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.channel_discovery_activity, menu); - mMenuSearchView = menu.findItem(R.id.action_search); - mJabberNetwork = menu.findItem(R.id.jabber_network); - mLocalServer = menu.findItem(R.id.local_server); - final View mSearchView = mMenuSearchView.getActionView(); - mSearchEditText = mSearchView.findViewById(R.id.search_field); - mSearchEditText.setHint(R.string.search_channels); - final String initialSearchValue = mInitialSearchValue.pop(); - if (initialSearchValue != null) { - mMenuSearchView.expandActionView(); - mSearchEditText.append(initialSearchValue); - mSearchEditText.requestFocus(); - if ((optedIn || method == ChannelDiscoveryService.Method.LOCAL_SERVER) && xmppConnectionService != null) { - xmppConnectionService.discoverChannels(initialSearchValue, this.method, this); - } - } - mSearchEditText.setOnEditorActionListener(this); - mMenuSearchView.setOnActionExpandListener(this); - switch (method) { - case JABBER_NETWORK: - mJabberNetwork.setChecked(true); - break; - case LOCAL_SERVER: - mLocalServer.setChecked(true); - break; - } - return true; - } - - @Override - public boolean onMenuItemActionExpand(MenuItem item) { - mSearchEditText.post(() -> { - mSearchEditText.requestFocus(); - final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mSearchEditText, InputMethodManager.SHOW_IMPLICIT); - }); - return true; - } - - @Override - public boolean onMenuItemActionCollapse(MenuItem item) { - final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY); - mSearchEditText.setText(""); - toggleLoadingScreen(); - if (optedIn || method == ChannelDiscoveryService.Method.LOCAL_SERVER) { - xmppConnectionService.discoverChannels(null, this.method, this); - } - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - finish(); - break; - case R.id.jabber_network: - case R.id.local_server: - handleMethodSelection(item); - break; - } - return super.onOptionsItemSelected(item); - } - - private void toggleLoadingScreen() { - adapter.submitList(Collections.emptyList()); - binding.progressBar.setVisibility(View.VISIBLE); - } - - @Override - public void onStart() { - super.onStart(); - this.method = getMethod(this); - if (!optedIn && method == ChannelDiscoveryService.Method.JABBER_NETWORK) { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.channel_discovery_opt_in_title); - builder.setMessage(Html.fromHtml(getString(R.string.channel_discover_opt_in_message))); - builder.setNegativeButton(R.string.cancel, (dialog, which) -> finish()); - builder.setPositiveButton(R.string.confirm, (dialog, which) -> optIn()); - builder.setOnCancelListener(dialog -> finish()); - final AlertDialog dialog = builder.create(); - dialog.setOnShowListener(d -> { - final TextView textView = dialog.findViewById(android.R.id.message); - if (textView == null) { - return; - } - textView.setMovementMethod(LinkMovementMethod.getInstance()); - }); - dialog.setCanceledOnTouchOutside(false); - dialog.show(); - holdLoading(); - } - } - - private void holdLoading() { - adapter.submitList(Collections.emptyList()); - binding.progressBar.setVisibility(View.GONE); - } - - @Override - public void onSaveInstanceState(Bundle savedInstanceState) { - if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) { - savedInstanceState.putString("search", mSearchEditText != null ? mSearchEditText.getText().toString() : null); - } - super.onSaveInstanceState(savedInstanceState); - } - - private void optIn() { - SharedPreferences preferences = getPreferences(); - preferences.edit().putBoolean(CHANNEL_DISCOVERY_OPT_IN, true).apply(); - optedIn = true; - toggleLoadingScreen(); - xmppConnectionService.discoverChannels(null, this.method, this); - } - - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (optedIn || method == ChannelDiscoveryService.Method.LOCAL_SERVER) { - toggleLoadingScreen(); - SoftKeyboardUtils.hideSoftKeyboard(this); - xmppConnectionService.discoverChannels(v.getText().toString(), this.method, this); - } - return true; - } - - @Override - public void onChannelSearchResultsFound(final List results) { - runOnUiThread(() -> { - adapter.submitList(results); - if (results.size() > 0) { - binding.list.setVisibility(View.VISIBLE); - binding.progressBar.setVisibility(View.GONE); - this.binding.noResults.setVisibility(View.GONE); - } else { - binding.list.setVisibility(View.GONE); - binding.progressBar.setVisibility(View.GONE); - this.binding.noResults.setVisibility(View.VISIBLE); - } - }); - } - - @Override - public void onChannelSearchResult(final Room result) { - final List accounts = AccountUtils.getEnabledAccounts(xmppConnectionService); - if (accounts.size() == 1) { - joinChannelSearchResult(accounts.get(0), result); - } else if (accounts.size() == 0) { - ToastCompat.makeText(this, R.string.please_enable_an_account, ToastCompat.LENGTH_LONG).show(); - } else { - final AtomicReference account = new AtomicReference<>(accounts.get(0)); - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.choose_account); - builder.setSingleChoiceItems(accounts.toArray(new CharSequence[0]), 0, (dialog, which) -> account.set(accounts.get(which))); - builder.setPositiveButton(R.string.join, (dialog, which) -> joinChannelSearchResult(account.get(), result)); - builder.setNegativeButton(R.string.cancel, null); - builder.create().show(); - } - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - final Room room = adapter.getCurrent(); - if (room != null) { - switch (item.getItemId()) { - case R.id.share_with: - StartConversationActivity.shareAsChannel(this, room.address); - return true; - case R.id.open_join_dialog: - final Intent intent = new Intent(this, StartConversationActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra("force_dialog", true); - intent.setData(Uri.parse(String.format("xmpp:%s?join", room.address))); - startActivity(intent); - return true; - } - } - return false; - } - - public void joinChannelSearchResult(String selectedAccount, Room result) { - final Jid jid = Config.DOMAIN_LOCK == null ? Jid.ofEscaped(selectedAccount) : Jid.ofLocalAndDomainEscaped(selectedAccount, Config.DOMAIN_LOCK); - final boolean syncAutoJoin = getBooleanPreference("autojoin", R.bool.autojoin); - final Account account = xmppConnectionService.findAccountByJid(jid); - final Conversation conversation = xmppConnectionService.findOrCreateConversation(account, result.getRoom(), true, true, true); - Bookmark bookmark = conversation.getBookmark(); - if (bookmark != null) { - if (!bookmark.autojoin() && syncAutoJoin) { - bookmark.setAutojoin(true); - xmppConnectionService.createBookmark(account, bookmark); - } - } else { - bookmark = new Bookmark(account, conversation.getJid().asBareJid()); - bookmark.setAutojoin(syncAutoJoin); - xmppConnectionService.createBookmark(account, bookmark); - } - switchToConversation(conversation); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java deleted file mode 100644 index fffe31dc3..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java +++ /dev/null @@ -1,88 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.widget.ListView; - -import java.util.ArrayList; -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.ui.adapter.AccountAdapter; -import me.drakeet.support.toast.ToastCompat; - -public class ChooseAccountForProfilePictureActivity extends XmppActivity { - - protected final List accountList = new ArrayList<>(); - protected ListView accountListView; - protected AccountAdapter mAccountAdapter; - - @Override - protected void refreshUiReal() { - loadEnabledAccounts(); - mAccountAdapter.notifyDataSetChanged(); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_manage_accounts); - setSupportActionBar(findViewById(R.id.toolbar)); - configureActionBar(getSupportActionBar(), false); - accountListView = findViewById(R.id.account_list); - this.mAccountAdapter = new AccountAdapter(this, accountList, false); - accountListView.setAdapter(this.mAccountAdapter); - accountListView.setOnItemClickListener((arg0, view, position, arg3) -> { - final Account account = accountList.get(position); - goToProfilePictureActivity(account); - }); - } - - @Override - protected void onStart() { - super.onStart(); - final int theme = findTheme(); - if (this.mTheme != theme) { - recreate(); - } - } - - @Override - void onBackendConnected() { - loadEnabledAccounts(); - if (accountList.size() == 1) { - goToProfilePictureActivity(accountList.get(0)); - return; - } - mAccountAdapter.notifyDataSetChanged(); - } - - private void loadEnabledAccounts() { - accountList.clear(); - for (Account account : xmppConnectionService.getAccounts()) { - if (account.isEnabled()) { - accountList.add(account); - } - } - } - - private void goToProfilePictureActivity(Account account) { - final Intent startIntent = getIntent(); - final Uri uri = startIntent == null ? null : startIntent.getData(); - if (uri != null) { - Intent intent = new Intent(this, PublishProfilePictureActivity.class); - intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); - intent.setData(uri); - intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - try { - startActivity(intent); - } catch (SecurityException e) { - ToastCompat.makeText(this, R.string.sharing_application_not_grant_permission, ToastCompat.LENGTH_SHORT).show(); - return; - } - } - finish(); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java deleted file mode 100644 index 2225d9403..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java +++ /dev/null @@ -1,414 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.view.ActionMode; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.AbsListView.MultiChoiceModeListener; -import android.widget.AdapterView; -import android.widget.ListView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.appcompat.app.ActionBar; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentTransaction; - -import com.google.common.base.Strings; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.ListItem; -import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.ui.interfaces.OnBackendConnected; -import eu.siacs.conversations.ui.util.ActivityResult; -import eu.siacs.conversations.ui.util.PendingItem; -import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.xmpp.Jid; - -public class ChooseContactActivity extends AbstractSearchableListItemActivity implements MultiChoiceModeListener, AdapterView.OnItemClickListener { - public static final String EXTRA_TITLE_RES_ID = "extra_title_res_id"; - public static final String EXTRA_GROUP_CHAT_NAME = "extra_group_chat_name"; - public static final String EXTRA_SELECT_MULTIPLE = "extra_select_multiple"; - public static final String EXTRA_SHOW_ENTER_JID = "extra_show_enter_jid"; - public static final String EXTRA_CONVERSATION = "extra_conversation"; - private static final String EXTRA_FILTERED_CONTACTS = "extra_filtered_contacts"; - private List mActivatedAccounts = new ArrayList<>(); - private Set selected = new HashSet<>(); - private Set filterContacts; - - private boolean showEnterJid = false; - private boolean startSearching = false; - private boolean multiple = false; - - private PendingItem postponedActivityResult = new PendingItem<>(); - - public static Intent create(Activity activity, Conversation conversation) { - final Intent intent = new Intent(activity, ChooseContactActivity.class); - List contacts = new ArrayList<>(); - if (conversation.getMode() == Conversation.MODE_MULTI) { - for (MucOptions.User user : conversation.getMucOptions().getUsers(false)) { - Jid jid = user.getRealJid(); - if (jid != null) { - contacts.add(jid.asBareJid().toString()); - } - } - } else { - contacts.add(conversation.getJid().asBareJid().toString()); - } - intent.putExtra(EXTRA_FILTERED_CONTACTS, contacts.toArray(new String[contacts.size()])); - intent.putExtra(EXTRA_CONVERSATION, conversation.getUuid()); - intent.putExtra(EXTRA_SELECT_MULTIPLE, true); - intent.putExtra(EXTRA_SHOW_ENTER_JID, true); - intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toEscapedString()); - return intent; - } - - public static List extractJabberIds(Intent result) { - List jabberIds = new ArrayList<>(); - try { - if (result.getBooleanExtra(EXTRA_SELECT_MULTIPLE, false)) { - String[] toAdd = result.getStringArrayExtra("contacts"); - for (String item : toAdd) { - jabberIds.add(Jid.of(item)); - } - } else { - jabberIds.add(Jid.of(result.getStringExtra("contact"))); - } - return jabberIds; - } catch (IllegalArgumentException e) { - return jabberIds; - } - } - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - filterContacts = new HashSet<>(); - if (savedInstanceState != null) { - String[] selectedContacts = savedInstanceState.getStringArray("selected_contacts"); - if (selectedContacts != null) { - selected.clear(); - selected.addAll(Arrays.asList(selectedContacts)); - } - } - - String[] contacts = getIntent().getStringArrayExtra(EXTRA_FILTERED_CONTACTS); - if (contacts != null) { - Collections.addAll(filterContacts, contacts); - } - - Intent intent = getIntent(); - - multiple = intent.getBooleanExtra(EXTRA_SELECT_MULTIPLE, false); - if (multiple) { - getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); - getListView().setMultiChoiceModeListener(this); - } - - getListView().setOnItemClickListener(this); - this.showEnterJid = intent.getBooleanExtra(EXTRA_SHOW_ENTER_JID, false); - this.binding.fab.setOnClickListener(this::onFabClicked); - if (this.showEnterJid) { - this.binding.fab.show(); - } else { - binding.fab.setImageResource(R.drawable.ic_forward_white_24dp); - } - final SharedPreferences preferences = getPreferences(); - this.startSearching = intent.getBooleanExtra("direct_search", false) && preferences.getBoolean("start_searching", getResources().getBoolean(R.bool.start_searching)); - } - - private void onFabClicked(View v) { - if (selected.size() == 0) { - showEnterJidDialog(null); - } else { - submitSelection(); - } - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - mode.setTitle(getTitleFromIntent()); - binding.fab.setImageResource(R.drawable.ic_forward_white_24dp); - binding.fab.show(); - final View view = getSearchEditText(); - final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - if (view != null && imm != null) { - imm.hideSoftInputFromWindow(getSearchEditText().getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY); - } - return true; - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - this.binding.fab.setImageResource(R.drawable.ic_person_add_white_24dp); - if (this.showEnterJid) { - this.binding.fab.show(); - } else { - this.binding.fab.hide(); - } - selected.clear(); - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - return false; - } - - private void submitSelection() { - final Intent request = getIntent(); - final Intent data = new Intent(); - data.putExtra("contacts", getSelectedContactJids()); - data.putExtra(EXTRA_SELECT_MULTIPLE, true); - data.putExtra(EXTRA_ACCOUNT, request.getStringExtra(EXTRA_ACCOUNT)); - copy(request, data); - setResult(RESULT_OK, data); - finish(); - } - - private static void copy(Intent from, Intent to) { - to.putExtra(EXTRA_CONVERSATION, from.getStringExtra(EXTRA_CONVERSATION)); - to.putExtra(EXTRA_GROUP_CHAT_NAME, from.getStringExtra(EXTRA_GROUP_CHAT_NAME)); - } - - @Override - public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { - if (selected.size() != 0) { - getListView().playSoundEffect(0); - } - Contact item = (Contact) getListItems().get(position); - if (checked) { - selected.add(item.getJid().toString()); - } else { - selected.remove(item.getJid().toString()); - } - } - - @Override - public void onStart() { - super.onStart(); - ActionBar bar = getSupportActionBar(); - if (bar != null) { - try { - bar.setTitle(getTitleFromIntent()); - } catch (Exception e) { - bar.setTitle(R.string.title_activity_choose_contact); - } - } - } - - public @StringRes - int getTitleFromIntent() { - final Intent intent = getIntent(); - boolean multiple = intent != null && intent.getBooleanExtra(EXTRA_SELECT_MULTIPLE, false); - @StringRes int fallback = multiple ? R.string.title_activity_choose_contacts : R.string.title_activity_choose_contact; - return intent != null ? intent.getIntExtra(EXTRA_TITLE_RES_ID, fallback) : fallback; - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - super.onCreateOptionsMenu(menu); - final Intent i = getIntent(); - boolean showEnterJid = i != null && i.getBooleanExtra(EXTRA_SHOW_ENTER_JID, false); - menu.findItem(R.id.action_scan_qr_code).setVisible(isCameraFeatureAvailable() && showEnterJid); - MenuItem mMenuSearchView = menu.findItem(R.id.action_search); - if (startSearching) { - mMenuSearchView.expandActionView(); - } - return true; - } - - @Override - public void onSaveInstanceState(Bundle savedInstanceState) { - savedInstanceState.putStringArray("selected_contacts", getSelectedContactJids()); - super.onSaveInstanceState(savedInstanceState); - } - - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (multiple) { - return false; - } else { - List items = getListItems(); - if (items.size() == 1) { - onListItemClicked(items.get(0)); - return true; - } - return false; - } - } - - protected void filterContacts(final String needle) { - getListItems().clear(); - if (xmppConnectionService == null) { - getListItemAdapter().notifyDataSetChanged(); - return; - } - for (final Account account : xmppConnectionService.getAccounts()) { - if (account.getStatus() != Account.State.DISABLED) { - for (final Contact contact : account.getRoster().getContacts()) { - if (contact.showInContactList() && - !filterContacts.contains(contact.getJid().asBareJid().toString()) - && contact.match(this, needle)) { - getListItems().add(contact); - } - } - } - } - Collections.sort(getListItems()); - getListItemAdapter().notifyDataSetChanged(); - } - - private String[] getSelectedContactJids() { - return selected.toArray(new String[0]); - } - - public void refreshUiReal() { - //nothing to do. This Activity doesn't implement any listeners - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_scan_qr_code: - ScanActivity.scan(this); - return true; - } - return super.onOptionsItemSelected(item); - } - - protected void showEnterJidDialog(XmppUri uri) { - FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); - Fragment prev = getSupportFragmentManager().findFragmentByTag("dialog"); - if (prev != null) { - ft.remove(prev); - } - ft.addToBackStack(null); - Jid jid = uri == null ? null : uri.getJid(); - EnterJidDialog dialog = EnterJidDialog.newInstance( - mActivatedAccounts, - getString(R.string.enter_contact), - getString(R.string.select), - jid == null ? null : jid.asBareJid().toString(), - getIntent().getStringExtra(EXTRA_ACCOUNT), - true, - true, - false - ); - - dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid) -> { - final Intent request = getIntent(); - final Intent data = new Intent(); - data.putExtra("contact", contactJid.toString()); - data.putExtra(EXTRA_ACCOUNT, accountJid.toEscapedString()); - data.putExtra(EXTRA_SELECT_MULTIPLE, false); - copy(request, data); - setResult(RESULT_OK, data); - finish(); - - return true; - }); - - dialog.show(ft, "dialog"); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent intent) { - super.onActivityResult(requestCode, requestCode, intent); - ActivityResult activityResult = ActivityResult.of(requestCode, resultCode, intent); - if (xmppConnectionService != null) { - handleActivityResult(activityResult); - } else { - this.postponedActivityResult.push(activityResult); - } - } - - private void handleActivityResult(ActivityResult activityResult) { - if (activityResult.resultCode == RESULT_OK && activityResult.requestCode == ScanActivity.REQUEST_SCAN_QR_CODE) { - String result = activityResult.data.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT); - XmppUri uri = new XmppUri(Strings.nullToEmpty(result)); - if (uri.isValidJid()) { - showEnterJidDialog(uri); - } - } - } - - @Override - void onBackendConnected() { - filterContacts(); - this.mActivatedAccounts.clear(); - for (Account account : xmppConnectionService.getAccounts()) { - if (account.getStatus() != Account.State.DISABLED) { - if (Config.DOMAIN_LOCK != null) { - this.mActivatedAccounts.add(account.getJid().getEscapedLocal()); - } else { - this.mActivatedAccounts.add(account.getJid().asBareJid().toEscapedString()); - } - } - } - ActivityResult activityResult = this.postponedActivityResult.pop(); - if (activityResult != null) { - handleActivityResult(activityResult); - } - final Fragment fragment = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG); - if (fragment instanceof OnBackendConnected) { - ((OnBackendConnected) fragment).onBackendConnected(); - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - ScanActivity.onRequestPermissionResult(this, requestCode, grantResults); - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - if (multiple) { - startActionMode(this); - getListView().setItemChecked(position, true); - return; - } - final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(getSearchEditText().getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY); - final ListItem mListItem = getListItems().get(position); - onListItemClicked(mListItem); - } - - private void onListItemClicked(ListItem item) { - final Intent request = getIntent(); - final Intent data = new Intent(); - data.putExtra("contact", item.getJid().toString()); - String account = request.getStringExtra(EXTRA_ACCOUNT); - if (account == null && item instanceof Contact) { - account = ((Contact) item).getAccount().getJid().asBareJid().toEscapedString(); - } - data.putExtra(EXTRA_ACCOUNT, account); - data.putExtra(EXTRA_SELECT_MULTIPLE, false); - copy(request, data); - setResult(RESULT_OK, data); - finish(); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceContactDetailsActivity.java deleted file mode 100644 index 6767eecf9..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceContactDetailsActivity.java +++ /dev/null @@ -1,113 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.os.Bundle; -import android.view.View; -import android.widget.TextView; - -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.widget.Toolbar; -import androidx.databinding.DataBindingUtil; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ActivityMucContactDetailsBinding; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.ui.util.AvatarWorkerTask; -import eu.siacs.conversations.utils.IrregularUnicodeDetector; -import eu.siacs.conversations.xmpp.Jid; - -public class ConferenceContactDetailsActivity extends XmppActivity { - public static final String ACTION_VIEW_CONTACT = "view_contact"; - - private Conversation mConversation; - ActivityMucContactDetailsBinding binding; - private Jid accountJid; - private Jid contactJid; - private MucOptions.User user = null; - - @Override - protected void refreshUiReal() { - invalidateOptionsMenu(); - populateView(); - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) { - try { - this.accountJid = Jid.ofEscaped(getIntent().getExtras().getString(EXTRA_ACCOUNT)); - } catch (final IllegalArgumentException ignored) { - } - try { - this.contactJid = Jid.ofEscaped(getIntent().getExtras().getString("user")); - } catch (final IllegalArgumentException ignored) { - } - } - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_muc_contact_details); - setSupportActionBar((Toolbar) binding.toolbar); - configureActionBar(getSupportActionBar()); - } - - @Override - public void onSaveInstanceState(final Bundle savedInstanceState) { - super.onSaveInstanceState(savedInstanceState); - } - - @Override - public void onStart() { - super.onStart(); - final int theme = findTheme(); - if (this.mTheme != theme) { - recreate(); - } - } - - private void populateView() { - if (getSupportActionBar() != null) { - final ActionBar ab = getSupportActionBar(); - if (ab != null) { - ab.setCustomView(R.layout.ab_title); - ab.setDisplayShowCustomEnabled(true); - TextView abtitle = findViewById(android.R.id.text1); - TextView absubtitle = findViewById(android.R.id.text2); - abtitle.setText(R.string.contact_details); - abtitle.setSelected(true); - abtitle.setClickable(false); - absubtitle.setVisibility(View.GONE); - absubtitle.setClickable(false); - } - } - if (user == null) { - return; - } - binding.contactDisplayName.setText(user.getName()); - binding.jid.setText(IrregularUnicodeDetector.style(this, contactJid)); - String account = accountJid.asBareJid().toEscapedString(); - binding.detailsAccount.setText(getString(R.string.using_account, account)); - AvatarWorkerTask.loadAvatar(user, binding.detailsContactBadge, R.dimen.avatar_on_details_screen_size); - binding.detailsContactBadge.setOnLongClickListener(v -> { - ShowAvatarPopup(ConferenceContactDetailsActivity.this, user); - return true; - }); - if (xmppConnectionService.multipleAccounts()) { - binding.detailsAccount.setVisibility(View.VISIBLE); - } else { - binding.detailsAccount.setVisibility(View.GONE); - } - } - - public void onBackendConnected() { - if (accountJid != null && contactJid != null) { - Account account = xmppConnectionService.findAccountByJid(accountJid); - if (account == null) { - return; - } - this.mConversation = xmppConnectionService.findConversation(account, contactJid, false); - final MucOptions mucOptions = ((Conversation) this.mConversation).getMucOptions(); - this.user = mucOptions.findUserByFullJid(contactJid); - populateView(); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java deleted file mode 100644 index 2927622e5..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ /dev/null @@ -1,827 +0,0 @@ -package eu.siacs.conversations.ui; - -import static eu.siacs.conversations.entities.Bookmark.printableValue; -import static eu.siacs.conversations.ui.util.IntroHelper.showIntro; -import static eu.siacs.conversations.utils.StringUtils.changed; - -import android.app.Activity; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.provider.Settings; -import android.text.Editable; -import android.text.SpannableStringBuilder; -import android.text.TextWatcher; -import android.text.method.LinkMovementMethod; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; - -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.Toolbar; -import androidx.databinding.DataBindingUtil; - -import java.util.Collections; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ActivityMucDetailsBinding; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Bookmark; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.entities.MucOptions.User; -import eu.siacs.conversations.services.NotificationService; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate; -import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate; -import eu.siacs.conversations.ui.adapter.MediaAdapter; -import eu.siacs.conversations.ui.adapter.UserPreviewAdapter; -import eu.siacs.conversations.ui.interfaces.OnMediaLoaded; -import eu.siacs.conversations.ui.util.Attachment; -import eu.siacs.conversations.ui.util.AvatarWorkerTask; -import eu.siacs.conversations.ui.util.GridManager; -import eu.siacs.conversations.ui.util.JidDialog; -import eu.siacs.conversations.ui.util.MucConfiguration; -import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper; -import eu.siacs.conversations.ui.util.MyLinkify; -import eu.siacs.conversations.ui.util.SoftKeyboardUtils; -import eu.siacs.conversations.utils.Compatibility; -import eu.siacs.conversations.utils.MenuDoubleTabUtil; -import eu.siacs.conversations.utils.StringUtils; -import eu.siacs.conversations.utils.StylingHelper; -import eu.siacs.conversations.utils.TimeFrameUtils; -import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.xmpp.Jid; -import me.drakeet.support.toast.ToastCompat; - -public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnConfigurationPushed, TextWatcher, OnMediaLoaded { - public static final String ACTION_VIEW_MUC = "view_muc"; - private Conversation mConversation; - private OnClickListener destroyListener = new OnClickListener() { - @Override - public void onClick(View v) { - final AlertDialog.Builder DestroyMucDialog = new AlertDialog.Builder(ConferenceDetailsActivity.this); - DestroyMucDialog.setNegativeButton(getString(R.string.cancel), null); - final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous(); - DestroyMucDialog.setTitle(groupChat ? R.string.destroy_room : R.string.destroy_channel); - DestroyMucDialog.setMessage(getString(groupChat ? R.string.destroy_room_dialog : R.string.destroy_channel_dialog, mConversation.getName())); - DestroyMucDialog.setPositiveButton(getString(R.string.delete), (dialogInterface, i) -> { - Intent intent = new Intent(xmppConnectionService, ConversationsActivity.class); - intent.setAction(ConversationsActivity.ACTION_DESTROY_MUC); - intent.putExtra("MUC_UUID", mConversation.getUuid()); - Log.d(Config.LOGTAG, "Sending DESTROY intent for " + mConversation.getName()); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - deleteBookmark(); - finish(); - }); - DestroyMucDialog.create().show(); - } - }; - private ActivityMucDetailsBinding binding; - private MediaAdapter mMediaAdapter; - private UserPreviewAdapter mUserPreviewAdapter; - private String uuid = null; - - private boolean mAdvancedMode = false; - private boolean mIndividualNotifications = false; - - private UiCallback renameCallback = new UiCallback() { - @Override - public void success(Conversation object) { - displayToast(getString(R.string.your_nick_has_been_changed)); - runOnUiThread(() -> { - updateView(); - }); - - } - - @Override - public void error(final int errorCode, Conversation object) { - displayToast(getString(errorCode)); - } - - @Override - public void userInputRequired(PendingIntent pi, Conversation object) { - - } - - @Override - public void progress(int progress) { - - } - }; - - 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); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - - private OnClickListener mNotifyStatusClickListener = new OnClickListener() { - @Override - public void onClick(View v) { - final AlertDialog.Builder builder = new AlertDialog.Builder(ConferenceDetailsActivity.this); - builder.setTitle(R.string.pref_notification_settings); - String[] choices = { - getString(R.string.notify_on_all_messages), - getString(R.string.notify_only_when_highlighted), - getString(R.string.notify_never) - }; - final AtomicInteger choice; - if (mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0) == Long.MAX_VALUE) { - choice = new AtomicInteger(2); - } else { - choice = new AtomicInteger(mConversation.alwaysNotify() ? 0 : 1); - } - builder.setSingleChoiceItems(choices, choice.get(), (dialog, which) -> choice.set(which)); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.ok, (dialog, which) -> { - if (choice.get() == 2) { - final AlertDialog.Builder builder1 = new AlertDialog.Builder(ConferenceDetailsActivity.this); - builder1.setTitle(R.string.disable_notifications); - final int[] durations = getResources().getIntArray(R.array.mute_options_durations); - final CharSequence[] labels = new CharSequence[durations.length]; - for (int i = 0; i < durations.length; ++i) { - if (durations[i] == -1) { - labels[i] = getString(R.string.until_further_notice); - } else { - labels[i] = TimeFrameUtils.resolve(ConferenceDetailsActivity.this, 1000L * durations[i]); - } - } - builder1.setItems(labels, (dialog1, which1) -> { - final long till; - if (durations[which1] == -1) { - till = Long.MAX_VALUE; - } else { - till = System.currentTimeMillis() + (durations[which1] * 1000); - } - mConversation.setMutedTill(till); - xmppConnectionService.updateConversation(mConversation); - updateView(); - }); - builder1.create().show(); - } else { - mConversation.setMutedTill(0); - mConversation.setAttribute(Conversation.ATTRIBUTE_ALWAYS_NOTIFY, String.valueOf(choice.get() == 0)); - } - xmppConnectionService.updateConversation(mConversation); - updateView(); - }); - builder.create().show(); - } - }; - - private OnClickListener mChangeConferenceSettings = new OnClickListener() { - @Override - public void onClick(View v) { - if (mConversation == null) { - return; - } - final MucOptions mucOptions = mConversation.getMucOptions(); - final AlertDialog.Builder builder = new AlertDialog.Builder(ConferenceDetailsActivity.this); - MucConfiguration configuration = MucConfiguration.get(ConferenceDetailsActivity.this, mAdvancedMode, mucOptions); - builder.setTitle(configuration.title); - final boolean[] values = configuration.values; - builder.setMultiChoiceItems(configuration.names, values, (dialog, which, isChecked) -> values[which] = isChecked); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.confirm, (dialog, which) -> { - final Bundle options = configuration.toBundle(values); - options.putString("muc#roomconfig_persistentroom", "1"); - xmppConnectionService.pushConferenceConfiguration(mConversation, - options, - ConferenceDetailsActivity.this); - }); - builder.create().show(); - } - }; - - @Override - public void onConversationUpdate() { - refreshUi(); - } - - @Override - public void onMucRosterUpdate() { - refreshUi(); - } - - @Override - protected void refreshUiReal() { - updateView(); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_muc_details); - this.binding.changeConferenceButton.setOnClickListener(this.mChangeConferenceSettings); - this.binding.destroy.setVisibility(View.GONE); - this.binding.destroy.setOnClickListener(destroyListener); - this.binding.leaveMuc.setVisibility(View.GONE); - this.binding.addContactButton.setVisibility(View.GONE); - setSupportActionBar((Toolbar) binding.toolbar); - configureActionBar(getSupportActionBar()); - this.binding.editNickButton.setOnClickListener(v -> { - try { - quickEdit(mConversation.getMucOptions().getActualNick(), - R.string.nickname, - value -> { - if (xmppConnectionService.renameInMuc(mConversation, value, renameCallback)) { - return null; - } else { - return getString(R.string.invalid_muc_nick); - } - }); - } catch (Exception e) { - ToastCompat.makeText(this, R.string.unable_to_perform_this_action, ToastCompat.LENGTH_SHORT).show(); - e.printStackTrace(); - } - }); - this.binding.detailsMucAvatar.setOnClickListener(v -> { - try { - final MucOptions mucOptions = mConversation.getMucOptions(); - if (!mucOptions.hasVCards()) { - ToastCompat.makeText(this, R.string.host_does_not_support_group_chat_avatars, ToastCompat.LENGTH_SHORT).show(); - return; - } - if (!mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)) { - ToastCompat.makeText(this, R.string.only_the_owner_can_change_group_chat_avatar, ToastCompat.LENGTH_SHORT).show(); - return; - } - final Intent intent = new Intent(this, PublishGroupChatProfilePictureActivity.class); - intent.putExtra("uuid", mConversation.getUuid()); - startActivity(intent); - } catch (Exception e) { - ToastCompat.makeText(this, R.string.unable_to_perform_this_action, ToastCompat.LENGTH_SHORT).show(); - e.printStackTrace(); - } - }); - this.binding.detailsMucAvatar.setOnLongClickListener(v -> { - ShowAvatarPopup(ConferenceDetailsActivity.this, mConversation); - return true; - }); - this.mAdvancedMode = getPreferences().getBoolean("advanced_muc_mode", false); - this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE); - this.binding.notificationStatusButton.setOnClickListener(this.mNotifyStatusClickListener); - - this.binding.editMucNameButton.setOnClickListener(this::onMucEditButtonClicked); - this.binding.mucEditTitle.addTextChangedListener(this); - this.binding.mucEditSubject.addTextChangedListener(this); - this.binding.mucEditSubject.addTextChangedListener(new StylingHelper.MessageEditorStyler(this.binding.mucEditSubject)); - this.binding.autojoinCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { - if (mConversation != null) { - final Bookmark bookmark = mConversation.getBookmark(); - if (bookmark != null) { - this.binding.autojoinCheckbox.setEnabled(!getBooleanPreference("autojoin", R.bool.autojoin)); - bookmark.setAutojoin(this.binding.autojoinCheckbox.isChecked()); - xmppConnectionService.pushBookmarks(mConversation.getAccount()); - updateView(); - } - } - }); - this.mMediaAdapter = new MediaAdapter(this, R.dimen.media_size); - this.mUserPreviewAdapter = new UserPreviewAdapter(); - this.binding.media.setAdapter(mMediaAdapter); - this.binding.users.setAdapter(mUserPreviewAdapter); - GridManager.setupLayoutManager(this, this.binding.media, R.dimen.media_size); - GridManager.setupLayoutManager(this, this.binding.users, R.dimen.media_size); - this.binding.invite.setOnClickListener(v -> inviteToConversation(mConversation)); - this.binding.showUsers.setOnClickListener(v -> { - Intent intent = new Intent(this, MucUsersActivity.class); - intent.putExtra("uuid", mConversation.getUuid()); - startActivity(intent); - }); - showIntro(this, true); - } - - @Override - protected void onStart() { - super.onStart(); - final int theme = findTheme(); - if (this.mTheme != theme) { - recreate(); - } - binding.mediaWrapper.setVisibility(Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE); - } - - private boolean canChangeMUCAvatar() { - final MucOptions mucOptions = mConversation.getMucOptions(); - if (!mucOptions.hasVCards()) { - return false; - } else if (!mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)) { - return false; - } else { - return true; - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem menuItem) { - if (MenuDoubleTabUtil.shouldIgnoreTap()) { - return false; - } - switch (menuItem.getItemId()) { - case android.R.id.home: - finish(); - break; - case R.id.action_share_http: - shareLink(true); - break; - case R.id.action_share_uri: - shareLink(false); - break; - case R.id.action_advanced_mode: - this.mAdvancedMode = !menuItem.isChecked(); - menuItem.setChecked(this.mAdvancedMode); - getPreferences().edit().putBoolean("advanced_muc_mode", mAdvancedMode).apply(); - invalidateOptionsMenu(); - updateView(); - break; - case R.id.action_activate_individual_notifications: - if (!menuItem.isChecked()) { - this.mIndividualNotifications = true; - } else { - if (Compatibility.runsTwentySix()) { - final AlertDialog.Builder removeIndividualNotificationDialog = new AlertDialog.Builder(ConferenceDetailsActivity.this); - removeIndividualNotificationDialog.setTitle(getString(R.string.remove_individual_notifications)); - removeIndividualNotificationDialog.setMessage(JidDialog.style(this, R.string.remove_individual_notifications_message, mConversation.getJid().asBareJid().toString())); - removeIndividualNotificationDialog.setPositiveButton(R.string.yes, (dialog, which) -> { - this.mIndividualNotifications = false; - try { - xmppConnectionService.getNotificationService().cleanNotificationChannels(this, mConversation.getUuid()); - } catch (Exception e) { - e.printStackTrace(); - } - menuItem.setChecked(this.mIndividualNotifications); - xmppConnectionService.setIndividualNotificationPreference(mConversation, !mIndividualNotifications); - xmppConnectionService.updateNotificationChannels(); - invalidateOptionsMenu(); - refreshUi(); - }); - removeIndividualNotificationDialog.setNegativeButton(R.string.no, (dialog, which) -> { - this.mIndividualNotifications = true; - }); - removeIndividualNotificationDialog.create().show(); - } - } - menuItem.setChecked(this.mIndividualNotifications); - xmppConnectionService.setIndividualNotificationPreference(mConversation, !mIndividualNotifications); - xmppConnectionService.updateNotificationChannels(); - invalidateOptionsMenu(); - refreshUi(); - break; - case R.id.action_message_notifications: - Intent messageNotificationIntent = null; - if (Compatibility.runsTwentySix()) { - final String time = String.valueOf(xmppConnectionService.getIndividualNotificationPreference(mConversation)); - messageNotificationIntent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) - .putExtra(Settings.EXTRA_APP_PACKAGE, this.getPackageName()) - .putExtra(Settings.EXTRA_CHANNEL_ID, NotificationService.INDIVIDUAL_NOTIFICATION_PREFIX + NotificationService.MESSAGES_CHANNEL_ID + "_" + mConversation.getUuid() + "_" + time); - } - startActivity(messageNotificationIntent); - break; - } - return super.onOptionsItemSelected(menuItem); - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - final User user = mUserPreviewAdapter.getSelectedUser(); - if (user == null) { - ToastCompat.makeText(this, R.string.unable_to_perform_this_action, ToastCompat.LENGTH_SHORT).show(); - return true; - } - if (!MucDetailsContextMenuHelper.onContextItemSelected(item, mUserPreviewAdapter.getSelectedUser(), this)) { - return super.onContextItemSelected(item); - } - return true; - } - - public void onMucEditButtonClicked(View v) { - if (this.binding.mucEditor.getVisibility() == View.GONE) { - final MucOptions mucOptions = mConversation.getMucOptions(); - this.binding.mucEditor.setVisibility(View.VISIBLE); - this.binding.mucDisplay.setVisibility(View.GONE); - this.binding.editMucNameButton.setImageResource(getThemeResource(R.attr.icon_cancel, R.drawable.ic_cancel_black_24dp)); - final String name = mucOptions.getName(); - this.binding.mucEditTitle.setText(""); - final boolean owner = mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER); - if (owner || printableValue(name)) { - this.binding.mucEditTitle.setVisibility(View.VISIBLE); - if (name != null) { - this.binding.mucEditTitle.append(name); - } - } else { - this.binding.mucEditTitle.setVisibility(View.GONE); - } - this.binding.mucEditTitle.setEnabled(owner); - final String subject = mucOptions.getSubject(); - this.binding.mucEditSubject.setText(""); - if (subject != null) { - this.binding.mucEditSubject.append(subject); - } - this.binding.mucEditSubject.setEnabled(mucOptions.canChangeSubject()); - if (!owner) { - this.binding.mucEditSubject.requestFocus(); - } - } else { - String subject = this.binding.mucEditSubject.isEnabled() ? this.binding.mucEditSubject.getEditableText().toString().trim() : null; - String name = this.binding.mucEditTitle.isEnabled() ? this.binding.mucEditTitle.getEditableText().toString().trim() : null; - onMucInfoUpdated(subject, name); - SoftKeyboardUtils.hideSoftKeyboard(this); - hideEditor(); - } - } - - private void hideEditor() { - this.binding.mucEditor.setVisibility(View.GONE); - this.binding.mucDisplay.setVisibility(View.VISIBLE); - this.binding.editMucNameButton.setImageResource(getThemeResource(R.attr.icon_edit_body, R.drawable.ic_edit_black_24dp)); - } - - private void onMucInfoUpdated(String subject, String name) { - final MucOptions mucOptions = mConversation.getMucOptions(); - if (mucOptions.canChangeSubject() && changed(mucOptions.getSubject(), subject)) { - xmppConnectionService.pushSubjectToConference(mConversation, subject); - } - if (mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER) && changed(mucOptions.getName(), name)) { - Bundle options = new Bundle(); - options.putString("muc#roomconfig_persistentroom", "1"); - options.putString("muc#roomconfig_roomname", StringUtils.nullOnEmpty(name)); - xmppConnectionService.pushConferenceConfiguration(mConversation, options, this); - } - } - - @Override - protected String getShareableUri(boolean http) { - if (mConversation != null) { - if (http) { - return Config.inviteMUCURL + XmppUri.lameUrlEncode(mConversation.getJid().asBareJid().toEscapedString()); - } else { - return "xmpp:" + mConversation.getJid().asBareJid().toEscapedString() + "?join"; - } - } else { - return null; - } - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - MenuItem menuItemAdvancedMode = menu.findItem(R.id.action_advanced_mode); - menuItemAdvancedMode.setChecked(mAdvancedMode); - MenuItem menuItemIndividualNotifications = menu.findItem(R.id.action_activate_individual_notifications); - menuItemIndividualNotifications.setChecked(mIndividualNotifications); - menuItemIndividualNotifications.setVisible(Compatibility.runsTwentySix()); - if (mConversation == null) { - return true; - } - return true; - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous(); - getMenuInflater().inflate(R.menu.muc_details, menu); - final MenuItem share = menu.findItem(R.id.action_share); - share.setVisible(!groupChat); - final MenuItem menuMessageNotification = menu.findItem(R.id.action_message_notifications); - if (Compatibility.runsTwentySix() && xmppConnectionServiceBound) { - menuMessageNotification.setVisible(xmppConnectionService.hasIndividualNotification(mConversation)); - } else { - menuMessageNotification.setVisible(false); - } - return super.onCreateOptionsMenu(menu); - } - - @Override - public void onMediaLoaded(List attachments) { - runOnUiThread(() -> { - int limit = GridManager.getCurrentColumnCount(binding.media); - mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit, attachments.size()))); - binding.mediaWrapper.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE); - }); - } - - protected void saveAsBookmark() { - xmppConnectionService.saveConversationAsBookmark(mConversation, mConversation.getMucOptions().getName()); - updateView(); - } - - protected void deleteBookmark() { - try { - final Account account = mConversation.getAccount(); - final Bookmark bookmark = mConversation.getBookmark(); - bookmark.setConversation(null); - xmppConnectionService.deleteBookmark(account, bookmark); - } catch (Exception e) { - e.printStackTrace(); - } finally { - updateView(); - } - } - - @Override - void onBackendConnected() { - if (mPendingConferenceInvite != null) { - mPendingConferenceInvite.execute(this); - mPendingConferenceInvite = null; - } - if (getIntent().getAction().equals(ACTION_VIEW_MUC)) { - this.uuid = getIntent().getExtras().getString("uuid"); - } - if (uuid != null) { - this.mConversation = xmppConnectionService.findConversationByUuid(uuid); - if (this.mConversation != null) { - if (Compatibility.hasStoragePermission(this)) { - final int limit = GridManager.getCurrentColumnCount(this.binding.media); - xmppConnectionService.getAttachments(this.mConversation, limit, this); - this.binding.showMedia.setOnClickListener((v) -> MediaBrowserActivity.launch(this, mConversation)); - final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous(); - this.binding.destroy.setText(groupChat ? R.string.destroy_room : R.string.destroy_channel); - this.binding.leaveMuc.setText(groupChat ? R.string.action_end_conversation_muc : R.string.action_end_conversation_channel); - this.binding.autojoinCheckbox.setText(groupChat ? R.string.autojoin_group_chat : R.string.autojoin_channel); - } - this.mIndividualNotifications = xmppConnectionService.hasIndividualNotification(mConversation); - updateView(); - } - } - } - - @Override - public void onBackPressed() { - if (this.binding.mucEditor.getVisibility() == View.VISIBLE) { - hideEditor(); - } else { - super.onBackPressed(); - } - } - - private void updateView() { - invalidateOptionsMenu(); - if (mConversation == null) { - return; - } - final MucOptions mucOptions = mConversation.getMucOptions(); - final Bookmark bookmark = mConversation.getBookmark(); - final User self = mucOptions.getSelf(); - String account; - if (Config.DOMAIN_LOCK != null) { - account = mConversation.getAccount().getJid().getEscapedLocal(); - } else { - account = mConversation.getAccount().getJid().asBareJid().toEscapedString(); - } - setTitle(mucOptions.isPrivateAndNonAnonymous() ? R.string.conference_details : R.string.channel_details); - this.binding.editMucNameButton.setVisibility((self.getAffiliation().ranks(MucOptions.Affiliation.OWNER) || mucOptions.canChangeSubject()) ? View.VISIBLE : View.INVISIBLE); - this.binding.detailsAccount.setText(getString(R.string.using_account, account)); - this.binding.jid.setText(mConversation.getJid().asBareJid().toEscapedString()); - if (xmppConnectionService.multipleAccounts()) { - this.binding.detailsAccount.setVisibility(View.VISIBLE); - } else { - this.binding.detailsAccount.setVisibility(View.GONE); - } - //todo add edit overlay to avatar and change layout - AvatarWorkerTask.loadAvatar(mConversation, binding.detailsMucAvatar, R.dimen.avatar_on_details_screen_size, canChangeMUCAvatar()); - AvatarWorkerTask.loadAvatar(mConversation.getAccount(), binding.yourPhoto, R.dimen.avatar_on_details_screen_size); - - String roomName = mucOptions.getName(); - String subject = mucOptions.getSubject(); - final boolean hasTitle; - if (printableValue(roomName)) { - this.binding.mucTitle.setText(roomName); - this.binding.mucTitle.setVisibility(View.VISIBLE); - hasTitle = true; - } else if (!printableValue(subject)) { - this.binding.mucTitle.setText(mConversation.getName()); - hasTitle = true; - this.binding.mucTitle.setVisibility(View.VISIBLE); - } else { - hasTitle = false; - this.binding.mucTitle.setVisibility(View.GONE); - } - if (printableValue(subject)) { - SpannableStringBuilder spannable = new SpannableStringBuilder(subject); - StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor(), true); - MyLinkify.addLinks(spannable, false); - this.binding.mucSubject.setText(spannable); - this.binding.mucSubject.setTextAppearance(this, subject.length() > (hasTitle ? 128 : 196) ? R.style.TextAppearance_Conversations_Body1_Linkified : R.style.TextAppearance_Conversations_Subhead); - this.binding.mucSubject.setAutoLinkMask(0); - this.binding.mucSubject.setVisibility(View.VISIBLE); - this.binding.mucSubject.setMovementMethod(LinkMovementMethod.getInstance()); - } else { - this.binding.mucSubject.setVisibility(View.GONE); - } - this.binding.mucYourNick.setText(mucOptions.getActualNick()); - if (mucOptions.online()) { - this.binding.usersWrapper.setVisibility(View.VISIBLE); - this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE); - this.binding.jid.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE); - this.binding.mucRole.setVisibility(View.VISIBLE); - this.binding.mucRole.setText(getStatus(self)); - if (mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)) { - this.binding.mucSettings.setVisibility(View.VISIBLE); - this.binding.mucConferenceType.setText(MucConfiguration.describe(this, mucOptions)); - } else if (!mucOptions.isPrivateAndNonAnonymous() && mucOptions.nonanonymous()) { - this.binding.mucSettings.setVisibility(View.VISIBLE); - this.binding.mucConferenceType.setText(R.string.group_chat_will_make_your_jabber_id_public); - } else { - this.binding.mucSettings.setVisibility(View.GONE); - } - if (mucOptions.mamSupport()) { - this.binding.mucInfoMam.setText(R.string.server_info_available); - } else { - this.binding.mucInfoMam.setText(R.string.server_info_unavailable); - } - if (bookmark != null) { - this.binding.autojoinCheckbox.setEnabled(!getBooleanPreference("autojoin", R.bool.autojoin)); - this.binding.autojoinCheckbox.setVisibility(View.VISIBLE); - this.binding.autojoinCheckbox.setChecked(bookmark.autojoin()); - } else { - this.binding.autojoinCheckbox.setVisibility(View.GONE); - } - if (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) { - if (mAdvancedMode) { - this.binding.destroy.getBackground().setTint(getWarningButtonColor()); - this.binding.destroy.setTextColor(getWarningTextColor()); - this.binding.destroy.setVisibility(View.VISIBLE); - } else { - this.binding.destroy.setVisibility(View.GONE); - } - this.binding.changeConferenceButton.setVisibility(View.VISIBLE); - } else { - this.binding.destroy.setVisibility(View.GONE); - this.binding.changeConferenceButton.setVisibility(View.INVISIBLE); - } - this.binding.leaveMuc.setVisibility(View.VISIBLE); - this.binding.leaveMuc.setOnClickListener(v1 -> { - final AlertDialog.Builder LeaveMucDialog = new AlertDialog.Builder(ConferenceDetailsActivity.this); - LeaveMucDialog.setTitle(getString(R.string.action_end_conversation_muc)); - LeaveMucDialog.setMessage(getString(R.string.leave_conference_warning)); - LeaveMucDialog.setNegativeButton(getString(R.string.cancel), null); - LeaveMucDialog.setPositiveButton(getString(R.string.action_end_conversation_muc), - (dialog, which) -> { - startActivity(new Intent(xmppConnectionService, ConversationsActivity.class)); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - this.xmppConnectionService.archiveConversation(mConversation); - finish(); - }); - LeaveMucDialog.create().show(); - }); - this.binding.leaveMuc.getBackground().setTint(getWarningButtonColor()); - this.binding.leaveMuc.setTextColor(getWarningTextColor()); - this.binding.addContactButton.setVisibility(View.VISIBLE); - if (mConversation.getBookmark() != null) { - this.binding.addContactButton.setText(R.string.delete_bookmark); - this.binding.addContactButton.getBackground().setTint(getWarningButtonColor()); - this.binding.addContactButton.setTextColor(getWarningTextColor()); - this.binding.addContactButton.setOnClickListener(v2 -> { - final AlertDialog.Builder deleteFromRosterDialog = new AlertDialog.Builder(ConferenceDetailsActivity.this); - deleteFromRosterDialog.setNegativeButton(getString(R.string.cancel), null); - deleteFromRosterDialog.setTitle(getString(R.string.action_delete_contact)); - deleteFromRosterDialog.setMessage(getString(R.string.remove_bookmark_text, mConversation.getJid().toString())); - deleteFromRosterDialog.setPositiveButton(getString(R.string.delete), - (dialog, which) -> { - deleteBookmark(); - recreate(); - }); - deleteFromRosterDialog.create().show(); - }); - } else { - this.binding.addContactButton.setText(R.string.save_as_bookmark); - this.binding.addContactButton.getBackground().clearColorFilter(); - this.binding.addContactButton.setTextColor(getDefaultButtonTextColor()); - this.binding.addContactButton.setOnClickListener(v2 -> { - saveAsBookmark(); - recreate(); - }); - } - } else { - this.binding.usersWrapper.setVisibility(View.GONE); - this.binding.mucInfoMore.setVisibility(View.GONE); - this.binding.mucSettings.setVisibility(View.GONE); - } - - int ic_notifications = getThemeResource(R.attr.icon_notifications, R.drawable.ic_notifications_black_24dp); - int ic_notifications_off = getThemeResource(R.attr.icon_notifications_off, R.drawable.ic_notifications_off_black_24dp); - int ic_notifications_paused = getThemeResource(R.attr.icon_notifications_paused, R.drawable.ic_notifications_paused_black_24dp); - int ic_notifications_none = getThemeResource(R.attr.icon_notifications_none, R.drawable.ic_notifications_none_black_24dp); - long mutedTill = mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0); - if (mutedTill == Long.MAX_VALUE) { - this.binding.notificationStatusText.setText(R.string.notify_never); - this.binding.notificationStatusButton.setImageResource(ic_notifications_off); - } else if (System.currentTimeMillis() < mutedTill) { - this.binding.notificationStatusText.setText(R.string.notify_paused); - this.binding.notificationStatusButton.setImageResource(ic_notifications_paused); - } else if (mConversation.alwaysNotify()) { - this.binding.notificationStatusText.setText(R.string.notify_on_all_messages); - this.binding.notificationStatusButton.setImageResource(ic_notifications); - } else { - this.binding.notificationStatusText.setText(R.string.notify_only_when_highlighted); - this.binding.notificationStatusButton.setImageResource(ic_notifications_none); - } - - final List users = mucOptions.getUsers(); - Collections.sort(users, (a, b) -> { - if (b.getAffiliation().outranks(a.getAffiliation())) { - return 1; - } else if (a.getAffiliation().outranks(b.getAffiliation())) { - return -1; - } else { - if (a.getAvatar() != null && b.getAvatar() == null) { - return -1; - } else if (a.getAvatar() == null && b.getAvatar() != null) { - return 1; - } else { - return a.getComparableName().compareToIgnoreCase(b.getComparableName()); - } - } - }); - this.mUserPreviewAdapter.submitList(MucOptions.sub(users, GridManager.getCurrentColumnCount(binding.users))); - this.binding.invite.setVisibility(mucOptions.canInvite() ? View.VISIBLE : View.GONE); - this.binding.showUsers.setVisibility(users.size() > 0 ? View.VISIBLE : View.GONE); - this.binding.showUsers.setText(getResources().getQuantityString(R.plurals.view_users, users.size(), users.size())); - this.binding.usersWrapper.setVisibility(users.size() > 0 || mucOptions.canInvite() ? View.VISIBLE : View.GONE); - if (users.size() == 0) { - this.binding.noUsersHints.setText(mucOptions.isPrivateAndNonAnonymous() ? R.string.no_users_hint_group_chat : R.string.no_users_hint_channel); - this.binding.noUsersHints.setVisibility(View.VISIBLE); - } else { - this.binding.noUsersHints.setVisibility(View.GONE); - } - } - - public static String getStatus(Context context, User user, final boolean advanced) { - if (advanced) { - return String.format("%s (%s)", context.getString(user.getAffiliation().getResId()), context.getString(user.getRole().getResId())); - } else { - return context.getString(user.getAffiliation().getResId()); - } - } - - private String getStatus(User user) { - return getStatus(this, user, mAdvancedMode); - } - - @Override - public void onAffiliationChangedSuccessful(Jid jid) { - refreshUi(); - } - - @Override - public void onAffiliationChangeFailed(Jid jid, int resId) { - displayToast(getString(resId, jid.asBareJid().toEscapedString())); - } - - @Override - public void onPushSucceeded() { - displayToast(getString(R.string.modified_conference_options)); - } - - @Override - public void onPushFailed() { - displayToast(getString(R.string.could_not_modify_conference_options)); - } - - private void displayToast(final String msg) { - runOnUiThread(() -> { - if (isFinishing()) { - return; - } - ToastCompat.makeText(this, msg, ToastCompat.LENGTH_SHORT).show(); - }); - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - - } - - @Override - public void afterTextChanged(Editable s) { - if (mConversation == null) { - return; - } - final MucOptions mucOptions = mConversation.getMucOptions(); - if (this.binding.mucEditor.getVisibility() == View.VISIBLE) { - boolean subjectChanged = changed(binding.mucEditSubject.getEditableText().toString(), mucOptions.getSubject()); - boolean nameChanged = changed(binding.mucEditTitle.getEditableText().toString(), mucOptions.getName()); - if (subjectChanged || nameChanged) { - this.binding.editMucNameButton.setImageResource(getThemeResource(R.attr.icon_save, R.drawable.ic_save_black_24dp)); - } else { - this.binding.editMucNameButton.setImageResource(getThemeResource(R.attr.icon_cancel, R.drawable.ic_cancel_black_24dp)); - } - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java deleted file mode 100644 index 71febba2e..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ /dev/null @@ -1,817 +0,0 @@ -package eu.siacs.conversations.ui; - -import static eu.siacs.conversations.ui.util.IntroHelper.showIntro; - -import android.Manifest; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.provider.ContactsContract.CommonDataKinds; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Intents; -import android.provider.Settings; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.style.RelativeSizeSpan; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.CompoundButton; -import android.widget.CompoundButton.OnCheckedChangeListener; -import android.widget.ImageButton; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.Toolbar; -import androidx.core.content.ContextCompat; -import androidx.databinding.DataBindingUtil; - -import com.google.common.base.Optional; - -import org.openintents.openpgp.util.OpenPgpUtils; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; -import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; -import eu.siacs.conversations.databinding.ActivityContactDetailsBinding; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.ListItem; -import eu.siacs.conversations.services.NotificationService; -import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; -import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; -import eu.siacs.conversations.ui.adapter.MediaAdapter; -import eu.siacs.conversations.ui.interfaces.OnMediaLoaded; -import eu.siacs.conversations.ui.util.Attachment; -import eu.siacs.conversations.ui.util.AvatarWorkerTask; -import eu.siacs.conversations.ui.util.CallManager; -import eu.siacs.conversations.ui.util.GridManager; -import eu.siacs.conversations.ui.util.JidDialog; -import eu.siacs.conversations.utils.Compatibility; -import eu.siacs.conversations.utils.Emoticons; -import eu.siacs.conversations.utils.IrregularUnicodeDetector; -import eu.siacs.conversations.utils.MenuDoubleTabUtil; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.utils.TimeFrameUtils; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; -import eu.siacs.conversations.xmpp.OnUpdateBlocklist; -import eu.siacs.conversations.xmpp.XmppConnection; -import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; -import eu.siacs.conversations.xmpp.jingle.RtpCapability; -import me.drakeet.support.toast.ToastCompat; - -public class ContactDetailsActivity extends OmemoActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated, OnMediaLoaded { - public static final String ACTION_VIEW_CONTACT = "view_contact"; - private final int REQUEST_SYNC_CONTACTS = 0x28cf; - - private Contact contact; - private Conversation mConversation; - private ConversationFragment mConversationFragment; - ActivityContactDetailsBinding binding; - private MediaAdapter mMediaAdapter; - private boolean mAdvancedMode = false; - private boolean mIndividualNotifications = false; - private DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - xmppConnectionService.deleteContactOnServer(contact); - recreate(); - } - }; - private OnCheckedChangeListener mOnSendCheckedChange = new OnCheckedChangeListener() { - - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (isChecked) { - if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { - xmppConnectionService.stopPresenceUpdatesTo(contact); - } else { - contact.setOption(Contact.Options.PREEMPTIVE_GRANT); - } - } else { - contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); - xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator().stopPresenceUpdatesTo(contact)); - } - } - }; - private OnCheckedChangeListener mOnReceiveCheckedChange = new OnCheckedChangeListener() { - - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (isChecked) { - xmppConnectionService.sendPresencePacket(contact.getAccount(), - xmppConnectionService.getPresenceGenerator() - .requestPresenceUpdatesFrom(contact)); - } else { - xmppConnectionService.sendPresencePacket(contact.getAccount(), - xmppConnectionService.getPresenceGenerator() - .stopPresenceUpdatesFrom(contact)); - } - } - }; - private Jid accountJid; - private Jid contactJid; - private boolean showDynamicTags = false; - private boolean showLastSeen = false; - private boolean showInactiveOmemo = false; - private String messageFingerprint; - private TextView mNotifyStatusText; - private ImageButton mNotifyStatusButton; - - private void checkContactPermissionAndShowAddDialog() { - if (hasContactsPermission()) { - showAddToPhoneBookDialog(); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS); - } - } - - private boolean hasContactsPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - return checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED; - } else { - return true; - } - } - - private void showAddToPhoneBookDialog() { - //TODO check if isQuicksy and contact is on quicksy.im domain - // store in final boolean. show different message. use phone number for add - 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.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); - //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); - } catch (ActivityNotFoundException e) { - ToastCompat.makeText(ContactDetailsActivity.this, R.string.no_application_found_to_view_contact, ToastCompat.LENGTH_SHORT).show(); - } - }); - builder.create().show(); - } - - private OnClickListener mNotifyStatusClickListener = new OnClickListener() { - @Override - public void onClick(View v) { - final AlertDialog.Builder builder = new AlertDialog.Builder(ContactDetailsActivity.this); - builder.setTitle(R.string.pref_notification_settings); - String[] choices = { - getString(R.string.notify_on_all_messages), - getString(R.string.notify_never) - }; - final AtomicInteger choice; - if (mConversation.alwaysNotify()) { - choice = new AtomicInteger(0); - } else { - choice = new AtomicInteger(1); - } - builder.setSingleChoiceItems(choices, choice.get(), (dialog, which) -> choice.set(which)); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.ok, (DialogInterface.OnClickListener) (dialog, which) -> { - if (choice.get() == 1) { - final AlertDialog.Builder builder1 = new AlertDialog.Builder(ContactDetailsActivity.this); - builder1.setTitle(R.string.disable_notifications); - final int[] durations = getResources().getIntArray(R.array.mute_options_durations); - final CharSequence[] labels = new CharSequence[durations.length]; - for (int i = 0; i < durations.length; ++i) { - if (durations[i] == -1) { - labels[i] = getString(R.string.until_further_notice); - } else { - labels[i] = TimeFrameUtils.resolve(ContactDetailsActivity.this, 1000L * durations[i]); - } - } - builder1.setItems(labels, (dialog1, which1) -> { - final long till; - if (durations[which1] == -1) { - till = Long.MAX_VALUE; - } else { - till = System.currentTimeMillis() + (durations[which1] * 1000); - } - mConversation.setMutedTill(till); - xmppConnectionService.updateConversation(mConversation); - populateView(); - }); - builder1.create().show(); - } else { - mConversation.setMutedTill(0); - mConversation.setAttribute(Conversation.ATTRIBUTE_ALWAYS_NOTIFY, String.valueOf(choice.get() == 0)); - } - xmppConnectionService.updateConversation(mConversation); - populateView(); - }); - builder.create().show(); - } - }; - - @Override - public void onRosterUpdate() { - refreshUi(); - } - - @Override - public void onAccountUpdate() { - refreshUi(); - } - - @Override - public void OnUpdateBlocklist(final Status status) { - refreshUi(); - } - - @Override - protected void refreshUiReal() { - invalidateOptionsMenu(); - populateView(); - } - - @Override - protected String getShareableUri(boolean http) { - if (http) { - return Config.inviteUserURL + XmppUri.lameUrlEncode(contact.getJid().asBareJid().toEscapedString()); - } else { - return "xmpp:" + contact.getJid().asBareJid().toEscapedString(); - } - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - this.mAdvancedMode = getPreferences().getBoolean("advanced_mode", false); - showInactiveOmemo = savedInstanceState != null && savedInstanceState.getBoolean("show_inactive_omemo", false); - if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) { - try { - this.accountJid = Jid.ofEscaped(getIntent().getExtras().getString(EXTRA_ACCOUNT)); - } catch (final IllegalArgumentException ignored) { - } - try { - this.contactJid = Jid.ofEscaped(getIntent().getExtras().getString("contact")); - } catch (final IllegalArgumentException ignored) { - } - } - this.messageFingerprint = getIntent().getStringExtra("fingerprint"); - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_contact_details); - setSupportActionBar((Toolbar) binding.toolbar); - configureActionBar(getSupportActionBar()); - binding.showInactiveDevices.setOnClickListener(v -> { - showInactiveOmemo = !showInactiveOmemo; - populateView(); - }); - binding.addContactButton.setOnClickListener(v -> showAddToRosterDialog(contact)); - this.mNotifyStatusButton = findViewById(R.id.notification_status_button); - this.mNotifyStatusButton.setOnClickListener(this.mNotifyStatusClickListener); - this.mNotifyStatusText = findViewById(R.id.notification_status_text); - mMediaAdapter = new MediaAdapter(this, R.dimen.media_size); - this.binding.media.setAdapter(mMediaAdapter); - GridManager.setupLayoutManager(this, this.binding.media, R.dimen.media_size); - showIntro(this, false); - } - - @Override - public void onSaveInstanceState(final Bundle savedInstanceState) { - savedInstanceState.putBoolean("show_inactive_omemo", showInactiveOmemo); - super.onSaveInstanceState(savedInstanceState); - } - - @Override - public void onStart() { - super.onStart(); - final int theme = findTheme(); - if (this.mTheme != theme) { - recreate(); - } else { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - this.showDynamicTags = preferences.getBoolean(SettingsActivity.SHOW_DYNAMIC_TAGS, getResources().getBoolean(R.bool.show_dynamic_tags)); - this.showLastSeen = preferences.getBoolean("last_activity", getResources().getBoolean(R.bool.last_activity)); - } - binding.mediaWrapper.setVisibility(Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE); - mMediaAdapter.setAttachments(Collections.emptyList()); - } - - @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) { - showAddToPhoneBookDialog(); - xmppConnectionService.loadPhoneContacts(); - xmppConnectionService.startContactObserver(); - } - } - } - - @Override - public boolean onOptionsItemSelected(final MenuItem menuItem) { - if (MenuDoubleTabUtil.shouldIgnoreTap()) { - return false; - } - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setNegativeButton(getString(R.string.cancel), null); - switch (menuItem.getItemId()) { - case android.R.id.home: - finish(); - break; - case R.id.action_audio_call: - CallManager.checkPermissionAndTriggerAudioCall(this, mConversation); - break; - case R.id.action_video_call: - CallManager.checkPermissionAndTriggerVideoCall(this, mConversation); - break; - case R.id.action_ongoing_call: - CallManager.returnToOngoingCall(this, mConversation); - break; - case R.id.action_share_http: - shareLink(true); - break; - case R.id.action_share_uri: - shareLink(false); - break; - case R.id.action_block: - BlockContactDialog.show(this, contact); - break; - case R.id.action_unblock: - BlockContactDialog.show(this, contact); - break; - case R.id.action_advanced_mode: - this.mAdvancedMode = !menuItem.isChecked(); - menuItem.setChecked(this.mAdvancedMode); - getPreferences().edit().putBoolean("advanced_mode", mAdvancedMode).apply(); - invalidateOptionsMenu(); - refreshUi(); - break; - case R.id.action_activate_individual_notifications: - if (!menuItem.isChecked()) { - this.mIndividualNotifications = true; - } else { - if (Compatibility.runsTwentySix()) { - final AlertDialog.Builder removeIndividualNotificationDialog = new AlertDialog.Builder(ContactDetailsActivity.this); - removeIndividualNotificationDialog.setTitle(getString(R.string.remove_individual_notifications)); - removeIndividualNotificationDialog.setMessage(JidDialog.style(this, R.string.remove_individual_notifications_message, contact.getJid().toEscapedString())); - removeIndividualNotificationDialog.setPositiveButton(R.string.yes, (dialog, which) -> { - this.mIndividualNotifications = false; - try { - xmppConnectionService.getNotificationService().cleanNotificationChannels(this, mConversation.getUuid()); - } catch (Exception e) { - e.printStackTrace(); - } - menuItem.setChecked(this.mIndividualNotifications); - xmppConnectionService.setIndividualNotificationPreference(mConversation, !mIndividualNotifications); - xmppConnectionService.updateNotificationChannels(); - invalidateOptionsMenu(); - refreshUi(); - }); - removeIndividualNotificationDialog.setNegativeButton(R.string.no, (dialog, which) -> { - this.mIndividualNotifications = true; - }); - removeIndividualNotificationDialog.create().show(); - } - } - menuItem.setChecked(this.mIndividualNotifications); - xmppConnectionService.setIndividualNotificationPreference(mConversation, !mIndividualNotifications); - xmppConnectionService.updateNotificationChannels(); - invalidateOptionsMenu(); - refreshUi(); - break; - case R.id.action_message_notifications: - Intent messageNotificationIntent = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - final String time = String.valueOf(xmppConnectionService.getIndividualNotificationPreference(mConversation)); - messageNotificationIntent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) - .putExtra(Settings.EXTRA_APP_PACKAGE, this.getPackageName()) - .putExtra(Settings.EXTRA_CHANNEL_ID, NotificationService.INDIVIDUAL_NOTIFICATION_PREFIX + NotificationService.MESSAGES_CHANNEL_ID + "_" + mConversation.getUuid() + "_" + time); - } - startActivity(messageNotificationIntent); - break; - case R.id.action_call_notifications: - Intent callNotificationIntent = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - final String time = String.valueOf(xmppConnectionService.getIndividualNotificationPreference(mConversation)); - callNotificationIntent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) - .putExtra(Settings.EXTRA_APP_PACKAGE, this.getPackageName()) - .putExtra(Settings.EXTRA_CHANNEL_ID, NotificationService.INDIVIDUAL_NOTIFICATION_PREFIX + NotificationService.INCOMING_CALLS_CHANNEL_ID + "_" + mConversation.getUuid() + "_" + time); - } - startActivity(callNotificationIntent); - break; - } - return super.onOptionsItemSelected(menuItem); - } - - private void editContact() { - Uri systemAccount = contact.getSystemAccount(); - if (systemAccount == null) { - quickEdit(contact.getServerName(), R.string.contact_name, value -> { - contact.setServerName(value); - ContactDetailsActivity.this.xmppConnectionService.pushContactToServer(contact); - populateView(); - return null; - }, true); - } else { - Intent intent = new Intent(Intent.ACTION_EDIT); - intent.setDataAndType(systemAccount, Contacts.CONTENT_ITEM_TYPE); - intent.putExtra("finishActivityOnSaveCompleted", true); - try { - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } catch (ActivityNotFoundException e) { - ToastCompat.makeText(ContactDetailsActivity.this, R.string.no_application_found_to_view_contact, ToastCompat.LENGTH_SHORT).show(); - } - } - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - MenuItem menuItemAdvancedMode = menu.findItem(R.id.action_advanced_mode); - menuItemAdvancedMode.setChecked(mAdvancedMode); - MenuItem menuItemIndividualNotifications = menu.findItem(R.id.action_activate_individual_notifications); - menuItemIndividualNotifications.setChecked(mIndividualNotifications); - menuItemIndividualNotifications.setVisible(Compatibility.runsTwentySix()); - if (mConversation == null) { - return true; - } - return true; - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.contact_details, menu); - final MenuItem block = menu.findItem(R.id.action_block); - final MenuItem unblock = menu.findItem(R.id.action_unblock); - final MenuItem menuCall = menu.findItem(R.id.action_call); - final MenuItem menuOngoingCall = menu.findItem(R.id.action_ongoing_call); - final MenuItem menuVideoCall = menu.findItem(R.id.action_video_call); - final MenuItem menuMessageNotification = menu.findItem(R.id.action_message_notifications); - final MenuItem menuCallNotification = menu.findItem(R.id.action_call_notifications); - if (contact == null) { - return true; - } - if (this.mConversation != null) { - if (xmppConnectionService.hasInternetConnection()) { - menuCall.setVisible(false); - menuOngoingCall.setVisible(false); - } else { - final Optional ongoingRtpSession = xmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection(this.mConversation.getContact()); - if (ongoingRtpSession.isPresent()) { - menuOngoingCall.setVisible(true); - menuCall.setVisible(false); - } else { - menuOngoingCall.setVisible(false); - final RtpCapability.Capability rtpCapability = RtpCapability.check(this.mConversation.getContact()); - menuCall.setVisible(rtpCapability != RtpCapability.Capability.NONE); - menuVideoCall.setVisible(rtpCapability == RtpCapability.Capability.VIDEO); - } - } - if (Compatibility.runsTwentySix()) { - menuCallNotification.setVisible(xmppConnectionService.hasIndividualNotification(mConversation)); - menuMessageNotification.setVisible(xmppConnectionService.hasIndividualNotification(mConversation)); - } else { - menuCallNotification.setVisible(false); - menuMessageNotification.setVisible(false); - } - } - final XmppConnection connection = contact.getAccount().getXmppConnection(); - if (connection != null && connection.getFeatures().blocking()) { - if (this.contact.isBlocked()) { - block.setVisible(false); - } else { - unblock.setVisible(false); - } - } else { - unblock.setVisible(false); - block.setVisible(false); - } - return super.onCreateOptionsMenu(menu); - } - - private void populateView() { - if (contact == null) { - return; - } - int ic_notifications = getThemeResource(R.attr.icon_notifications, R.drawable.ic_notifications_black_24dp); - int ic_notifications_off = getThemeResource(R.attr.icon_notifications_off, R.drawable.ic_notifications_off_black_24dp); - int ic_notifications_paused = getThemeResource(R.attr.icon_notifications_paused, R.drawable.ic_notifications_paused_black_24dp); - long mutedTill = mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0); - if (mutedTill == Long.MAX_VALUE) { - mNotifyStatusText.setText(R.string.notify_never); - mNotifyStatusButton.setImageResource(ic_notifications_off); - } else if (System.currentTimeMillis() < mutedTill) { - mNotifyStatusText.setText(R.string.notify_paused); - mNotifyStatusButton.setImageResource(ic_notifications_paused); - } else { - mNotifyStatusButton.setImageResource(ic_notifications); - mNotifyStatusText.setText(R.string.notify_on_all_messages); - } - if (getSupportActionBar() != null) { - final ActionBar ab = getSupportActionBar(); - if (ab != null) { - ab.setCustomView(R.layout.ab_title); - ab.setDisplayShowCustomEnabled(true); - TextView abtitle = findViewById(android.R.id.text1); - TextView absubtitle = findViewById(android.R.id.text2); - abtitle.setText(R.string.contact_details); - abtitle.setSelected(true); - abtitle.setClickable(false); - absubtitle.setVisibility(View.GONE); - absubtitle.setClickable(false); - } - } - invalidateOptionsMenu(); - binding.contactDisplayName.setText(contact.getDisplayName()); - this.binding.jid.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE); - if (contact.showInRoster()) { - binding.detailsSendPresence.setVisibility(View.VISIBLE); - binding.detailsSendPresence.setOnCheckedChangeListener(null); - binding.detailsReceivePresence.setVisibility(View.VISIBLE); - binding.detailsReceivePresence.setOnCheckedChangeListener(null); - binding.addContactButton.setVisibility(View.VISIBLE); - binding.addContactButton.setText(getString(R.string.action_delete_contact)); - binding.addContactButton.getBackground().setTint(getWarningButtonColor()); - binding.addContactButton.setTextColor(getWarningTextColor()); - binding.addContactButton.setOnClickListener(view -> { - final AlertDialog.Builder deleteFromRosterDialog = new AlertDialog.Builder(ContactDetailsActivity.this); - deleteFromRosterDialog.setNegativeButton(getString(R.string.cancel), null) - .setTitle(getString(R.string.action_delete_contact)) - .setMessage(JidDialog.style(this, R.string.remove_contact_text, contact.getJid().toEscapedString())) - .setPositiveButton(getString(R.string.delete), removeFromRoster).create().show(); - }); - binding.editContactNameButton.setVisibility(View.VISIBLE); - binding.editContactNameButton.setOnClickListener(view -> { - editContact(); - }); - List statusMessages = contact.getPresences().getStatusMessages(); - if (statusMessages.size() == 0) { - binding.statusMessage.setVisibility(View.GONE); - } else if (statusMessages.size() == 1) { - final String message = statusMessages.get(0); - binding.statusMessage.setVisibility(View.VISIBLE); - final Spannable span = new SpannableString(message); - if (Emoticons.isOnlyEmoji(message)) { - span.setSpan(new RelativeSizeSpan(2.0f), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - binding.statusMessage.setText(span); - } else { - StringBuilder builder = new StringBuilder(); - binding.statusMessage.setVisibility(View.VISIBLE); - int s = statusMessages.size(); - for (int i = 0; i < s; ++i) { - builder.append(statusMessages.get(i)); - if (i < s - 1) { - builder.append("\n"); - } - } - binding.statusMessage.setText(builder); - } - String resources = contact.getPresences().getMostAvailableResource(); - if (resources.length() == 0) { - binding.resource.setVisibility(View.GONE); - } else { - binding.resource.setVisibility(View.VISIBLE); - binding.resource.setText(resources); - } - if (contact.getOption(Contact.Options.FROM)) { - binding.detailsSendPresence.setText(R.string.send_presence_updates); - binding.detailsSendPresence.setChecked(true); - } else if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { - binding.detailsSendPresence.setChecked(false); - binding.detailsSendPresence.setText(R.string.send_presence_updates); - } else { - binding.detailsSendPresence.setText(R.string.preemptively_grant); - if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) { - binding.detailsSendPresence.setChecked(true); - } else { - binding.detailsSendPresence.setChecked(false); - } - } - if (contact.getOption(Contact.Options.TO)) { - binding.detailsReceivePresence.setText(R.string.receive_presence_updates); - binding.detailsReceivePresence.setChecked(true); - } else { - binding.detailsReceivePresence.setText(R.string.ask_for_presence_updates); - if (contact.getOption(Contact.Options.ASKING)) { - binding.detailsReceivePresence.setChecked(true); - } else { - binding.detailsReceivePresence.setChecked(false); - } - } - if (contact.getAccount().isOnlineAndConnected()) { - binding.detailsReceivePresence.setEnabled(true); - binding.detailsSendPresence.setEnabled(true); - } else { - binding.detailsReceivePresence.setEnabled(false); - binding.detailsSendPresence.setEnabled(false); - } - binding.detailsSendPresence.setOnCheckedChangeListener(this.mOnSendCheckedChange); - binding.detailsReceivePresence.setOnCheckedChangeListener(this.mOnReceiveCheckedChange); - } else { - binding.editContactNameButton.setVisibility(View.GONE); - binding.addContactButton.setVisibility(View.VISIBLE); - binding.addContactButton.setText(getString(R.string.add_contact)); - binding.addContactButton.getBackground().clearColorFilter(); - binding.addContactButton.setTextColor(getDefaultButtonTextColor()); - binding.addContactButton.setOnClickListener(view -> showAddToRosterDialog(contact)); - binding.detailsSendPresence.setVisibility(View.GONE); - binding.detailsReceivePresence.setVisibility(View.GONE); - binding.statusMessage.setVisibility(View.GONE); - } - - if (contact.isBlocked() && !this.showDynamicTags) { - binding.detailsLastseen.setVisibility(View.VISIBLE); - binding.detailsLastseen.setText(R.string.contact_blocked); - } else { - if (showLastSeen && contact.getLastseen() > 0 && contact.getPresences().allOrNonSupport(Namespace.IDLE)) { - binding.detailsLastseen.setVisibility(View.VISIBLE); - binding.detailsLastseen.setText(UIHelper.lastseen(getApplicationContext(), contact.isActive(), contact.getLastseen())); - } else { - binding.detailsLastseen.setVisibility(View.GONE); - } - } - - binding.jid.setText(IrregularUnicodeDetector.style(this, contact.getJid())); - String account; - if (Config.DOMAIN_LOCK != null) { - account = contact.getAccount().getJid().getEscapedLocal(); - } else { - account = contact.getAccount().getJid().asBareJid().toEscapedString(); - } - binding.detailsAccount.setText(getString(R.string.using_account, account)); - AvatarWorkerTask.loadAvatar(contact, binding.detailsContactBadge, R.dimen.avatar_on_details_screen_size); - binding.detailsContactBadge.setOnClickListener(this::onBadgeClick); - binding.detailsContactBadge.setOnLongClickListener(v -> { - ShowAvatarPopup(ContactDetailsActivity.this, contact); - return true; - }); - if (xmppConnectionService.multipleAccounts()) { - binding.detailsAccount.setVisibility(View.VISIBLE); - } else { - binding.detailsAccount.setVisibility(View.GONE); - } - - binding.detailsContactKeys.removeAllViews(); - boolean hasKeys = false; - LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); - final AxolotlService axolotlService = contact.getAccount().getAxolotlService(); - if (Config.supportOmemo() && axolotlService != null) { - final Collection sessions = axolotlService.findSessionsForContact(contact); - boolean anyActive = false; - for (XmppAxolotlSession session : sessions) { - anyActive = session.getTrust().isActive(); - if (anyActive) { - break; - } - } - boolean skippedInactive = false; - boolean showsInactive = false; - for (final XmppAxolotlSession session : sessions) { - final FingerprintStatus trust = session.getTrust(); - hasKeys |= !trust.isCompromised(); - if (!trust.isActive() && anyActive) { - if (showInactiveOmemo) { - showsInactive = true; - } else { - skippedInactive = true; - continue; - } - } - if (!trust.isCompromised()) { - boolean highlight = session.getFingerprint().equals(messageFingerprint); - addFingerprintRow(binding.detailsContactKeys, session, highlight); - } - } - if (showsInactive || skippedInactive) { - binding.showInactiveDevices.setText(showsInactive ? R.string.hide_inactive_devices : R.string.show_inactive_devices); - binding.showInactiveDevices.setVisibility(View.VISIBLE); - } else { - binding.showInactiveDevices.setVisibility(View.GONE); - } - } else { - binding.showInactiveDevices.setVisibility(View.GONE); - } - binding.scanButton.setVisibility(hasKeys && isCameraFeatureAvailable() ? View.VISIBLE : View.GONE); - if (hasKeys) { - binding.scanButton.setOnClickListener((v) -> ScanActivity.scan(this)); - } - if (Config.supportOpenPgp() && contact.getPgpKeyId() != 0) { - hasKeys = true; - View view = inflater.inflate(R.layout.contact_key, binding.detailsContactKeys, false); - TextView key = view.findViewById(R.id.key); - TextView keyType = view.findViewById(R.id.key_type); - keyType.setText(R.string.openpgp_key_id); - if ("pgp".equals(messageFingerprint)) { - keyType.setTextColor(ContextCompat.getColor(this, R.color.accent)); - } - key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId())); - final OnClickListener openKey = v -> launchOpenKeyChain(contact.getPgpKeyId()); - view.setOnClickListener(openKey); - key.setOnClickListener(openKey); - keyType.setOnClickListener(openKey); - binding.detailsContactKeys.addView(view); - } - binding.keysWrapper.setVisibility(hasKeys ? View.VISIBLE : View.GONE); - - List tagList = contact.getTags(this); - if (tagList.size() == 0 || !this.showDynamicTags) { - binding.tags.setVisibility(View.GONE); - } else { - binding.tags.setVisibility(View.VISIBLE); - binding.tags.removeAllViewsInLayout(); - for (final ListItem.Tag tag : tagList) { - final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag, binding.tags, false); - tv.setText(tag.getName()); - tv.setBackgroundColor(tag.getColor()); - binding.tags.addView(tv); - } - } - } - - private void onBadgeClick(View view) { - final Uri systemAccount = contact.getSystemAccount(); - if (systemAccount == null) { - checkContactPermissionAndShowAddDialog(); - } else { - final Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(systemAccount); - try { - startActivity(intent); - } catch (final ActivityNotFoundException e) { - ToastCompat.makeText(this, R.string.no_application_found_to_view_contact, ToastCompat.LENGTH_SHORT).show(); - } - } - } - - public void onBackendConnected() { - if (accountJid != null && contactJid != null) { - Account account = xmppConnectionService.findAccountByJid(accountJid); - if (account == null) { - return; - } - this.mConversation = xmppConnectionService.findConversation(account, contactJid, false); - this.contact = account.getRoster().getContact(contactJid); - if (mPendingFingerprintVerificationUri != null) { - processFingerprintVerification(mPendingFingerprintVerificationUri); - mPendingFingerprintVerificationUri = null; - } - if (Compatibility.hasStoragePermission(this)) { - final int limit = GridManager.getCurrentColumnCount(this.binding.media); - xmppConnectionService.getAttachments(account, contact.getJid().asBareJid(), limit, this); - this.binding.showMedia.setOnClickListener((v) -> MediaBrowserActivity.launch(this, contact)); - } - this.mIndividualNotifications = xmppConnectionService.hasIndividualNotification(mConversation); - populateView(); - } - } - - @Override - public void onKeyStatusUpdated(AxolotlService.FetchStatus report) { - refreshUi(); - } - - @Override - protected void processFingerprintVerification(XmppUri uri) { - if (contact != null && contact.getJid().asBareJid().equals(uri.getJid()) && uri.hasFingerprints()) { - if (xmppConnectionService.verifyFingerprints(contact, uri.getFingerprints())) { - ToastCompat.makeText(this, R.string.verified_fingerprints, ToastCompat.LENGTH_SHORT).show(); - } - } else { - ToastCompat.makeText(this, R.string.invalid_barcode, ToastCompat.LENGTH_SHORT).show(); - } - } - - @Override - public void onMediaLoaded(List attachments) { - runOnUiThread(() -> { - int limit = GridManager.getCurrentColumnCount(binding.media); - mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit, attachments.size()))); - binding.mediaWrapper.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE); - }); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/ContextMenuRecyclerView.java b/src/main/java/eu/siacs/conversations/ui/ContextMenuRecyclerView.java deleted file mode 100644 index dd16dfa39..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ContextMenuRecyclerView.java +++ /dev/null @@ -1,38 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.ContextMenu; -import android.view.View; -import android.widget.AdapterView.AdapterContextMenuInfo; - -public class ContextMenuRecyclerView extends androidx.recyclerview.widget.RecyclerView { - protected AdapterContextMenuInfo mAdapterContextMenuInfo = null; - - public ContextMenuRecyclerView(Context context) { - super(context); - } - - public ContextMenuRecyclerView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public ContextMenuRecyclerView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - protected ContextMenu.ContextMenuInfo getContextMenuInfo() { - return mAdapterContextMenuInfo; - } - - @Override - public boolean showContextMenuForChild(View originalView) { - mAdapterContextMenuInfo = new AdapterContextMenuInfo( - originalView, - getChildAdapterPosition(originalView), - getChildItemId(originalView) - ); - return super.showContextMenuForChild(originalView); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java deleted file mode 100644 index 4330097cc..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ /dev/null @@ -1,3900 +0,0 @@ -package eu.siacs.conversations.ui; - -import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; -import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; -import static eu.siacs.conversations.ui.SettingsActivity.HIDE_YOU_ARE_NOT_PARTICIPATING; -import static eu.siacs.conversations.ui.SettingsActivity.WARN_UNENCRYPTED_CHAT; -import static eu.siacs.conversations.ui.XmppActivity.EXTRA_ACCOUNT; -import static eu.siacs.conversations.ui.XmppActivity.REQUEST_INVITE_TO_CONVERSATION; -import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard; -import static eu.siacs.conversations.utils.PermissionUtils.allGranted; -import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied; -import static eu.siacs.conversations.utils.PermissionUtils.readGranted; -import static eu.siacs.conversations.utils.StorageHelper.getConversationsDirectory; -import static eu.siacs.conversations.xmpp.Patches.ENCRYPTION_EXCEPTIONS; -import com.google.common.collect.ImmutableList; -import static eu.siacs.conversations.utils.CameraUtils.getCameraApp; -import static eu.siacs.conversations.utils.CameraUtils.showCameraChooser; -import eu.siacs.conversations.utils.PermissionUtils; -import android.Manifest; -import android.animation.Animator; -import android.animation.AnimatorInflater; -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.Fragment; -import android.app.FragmentManager; -import android.app.PendingIntent; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentSender.SendIntentException; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.graphics.Typeface; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.SystemClock; -import android.preference.PreferenceManager; -import android.provider.MediaStore; -import android.text.Editable; -import android.text.Spannable; -import android.text.SpannableStringBuilder; -import android.text.TextUtils; -import android.text.style.StyleSpan; -import android.util.Log; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; -import android.widget.AbsListView; -import android.widget.AbsListView.OnScrollListener; -import android.widget.AdapterView; -import android.widget.AdapterView.AdapterContextMenuInfo; -import android.widget.CheckBox; -import android.widget.LinearLayout; -import android.widget.ListView; -import android.widget.TextView.OnEditorActionListener; -import android.widget.Toast; - -import androidx.annotation.IdRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.view.menu.MenuBuilder; -import androidx.appcompat.view.menu.MenuPopupHelper; -import androidx.appcompat.widget.PopupMenu; -import androidx.core.content.ContextCompat; -import androidx.core.view.inputmethod.InputConnectionCompat; -import androidx.core.view.inputmethod.InputContentInfoCompat; -import androidx.databinding.DataBindingUtil; - -import com.google.common.base.Optional; - -import org.jetbrains.annotations.NotNull; - -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicBoolean; - -import eu.siacs.conversations.utils.TimeFrameUtils; -import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; -import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; -import eu.siacs.conversations.xmpp.jingle.Media; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; -import eu.siacs.conversations.databinding.FragmentConversationBinding; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Blockable; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Conversational; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.entities.Edit; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.entities.MucOptions.User; -import eu.siacs.conversations.entities.Presence; -import eu.siacs.conversations.entities.ReadByMarker; -import eu.siacs.conversations.entities.Transferable; -import eu.siacs.conversations.entities.TransferablePlaceholder; -import eu.siacs.conversations.http.HttpDownloadConnection; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.services.AttachFileToConversationRunnable; -import eu.siacs.conversations.services.MessageArchiveService; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.adapter.MediaPreviewAdapter; -import eu.siacs.conversations.ui.adapter.MessageAdapter; -import eu.siacs.conversations.ui.adapter.MessageLogAdapter; -import eu.siacs.conversations.ui.adapter.model.MessageLogModel; -import eu.siacs.conversations.ui.util.ActivityResult; -import eu.siacs.conversations.ui.util.Attachment; -import eu.siacs.conversations.ui.util.CallManager; -import eu.siacs.conversations.ui.util.ConversationMenuConfigurator; -import eu.siacs.conversations.ui.util.DateSeparator; -import eu.siacs.conversations.ui.util.EditMessageActionModeCallback; -import eu.siacs.conversations.ui.util.KeyboardUtils; -import eu.siacs.conversations.ui.util.ListViewUtils; -import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper; -import eu.siacs.conversations.ui.util.PendingItem; -import eu.siacs.conversations.ui.util.PresenceSelector; -import eu.siacs.conversations.ui.util.QuoteHelper; -import eu.siacs.conversations.ui.util.ScrollState; -import eu.siacs.conversations.ui.util.SendButtonAction; -import eu.siacs.conversations.ui.util.SendButtonTool; -import eu.siacs.conversations.ui.util.ShareUtil; -import eu.siacs.conversations.ui.util.StyledAttributes; -import eu.siacs.conversations.ui.util.ViewUtil; -import eu.siacs.conversations.ui.widget.EditMessage; -import eu.siacs.conversations.utils.CameraUtils; -import eu.siacs.conversations.utils.Compatibility; -import eu.siacs.conversations.utils.GeoHelper; -import eu.siacs.conversations.utils.MenuDoubleTabUtil; -import eu.siacs.conversations.utils.MessageUtils; -import eu.siacs.conversations.utils.MimeUtils; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.utils.NickValidityChecker; -import eu.siacs.conversations.utils.Patterns; -import eu.siacs.conversations.utils.QuickLoader; -import eu.siacs.conversations.utils.StylingHelper; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.XmppConnection; -import eu.siacs.conversations.xmpp.chatstate.ChatState; -import eu.siacs.conversations.xmpp.jingle.JingleFileTransferConnection; -import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; -import eu.siacs.conversations.xmpp.jingle.RtpCapability; -import me.drakeet.support.toast.ToastCompat; -import net.java.otr4j.session.SessionStatus; - -public class ConversationFragment extends XmppFragment implements EditMessage.KeyboardListener, MessageAdapter.OnContactPictureLongClicked, MessageAdapter.OnContactPictureClicked { - - public static final int REQUEST_SEND_MESSAGE = 0x0201; - public static final int REQUEST_DECRYPT_PGP = 0x0202; - public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207; - public static final int REQUEST_TRUST_KEYS_TEXT = 0x0208; - public static final int REQUEST_TRUST_KEYS_ATTACHMENTS = 0x0209; - public static final int REQUEST_START_DOWNLOAD = 0x0210; - public static final int REQUEST_ADD_EDITOR_CONTENT = 0x0211; - public static final int REQUEST_COMMIT_ATTACHMENTS = 0x0212; - public static final int ATTACHMENT_CHOICE = 0x0300; - public static final int REQUEST_START_AUDIO_CALL = 0x213; - public static final int REQUEST_START_VIDEO_CALL = 0x214; - public static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301; - public static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302; - public static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303; - public static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304; - public static final int ATTACHMENT_CHOICE_LOCATION = 0x0305; - public static final int ATTACHMENT_CHOICE_CHOOSE_VIDEO = 0x0306; - public static final int ATTACHMENT_CHOICE_RECORD_VIDEO = 0x0307; - public static final int ATTACHMENT_CHOICE_INVALID = 0x0399; - - public static final String RECENTLY_USED_QUICK_ACTION = "recently_used_quick_action"; - public static final String STATE_CONVERSATION_UUID = ConversationFragment.class.getName() + ".uuid"; - public static final String STATE_SCROLL_POSITION = ConversationFragment.class.getName() + ".scroll_position"; - public static final String STATE_PHOTO_URI = ConversationFragment.class.getName() + ".media_previews"; - public static final String STATE_MEDIA_PREVIEWS = ConversationFragment.class.getName() + ".take_photo_uri"; - - private static final String STATE_LAST_MESSAGE_UUID = "state_last_message_uuid"; - - private final List messageList = new ArrayList<>(); - private final PendingItem postponedActivityResult = new PendingItem<>(); - private final PendingItem pendingConversationsUuid = new PendingItem<>(); - private final PendingItem> pendingMediaPreviews = new PendingItem<>(); - private final PendingItem pendingExtras = new PendingItem<>(); - private final PendingItem pendingTakePhotoUri = new PendingItem<>(); - private final PendingItem pendingTakeVideoUri = new PendingItem<>(); - private final PendingItem pendingScrollState = new PendingItem<>(); - private final PendingItem pendingLastMessageUuid = new PendingItem<>(); - private final PendingItem pendingMessage = new PendingItem<>(); - public Uri mPendingEditorContent = null; - public FragmentConversationBinding binding; - protected MessageAdapter messageListAdapter; - private String lastMessageUuid = null; - private Conversation conversation; - private Toast messageLoaderToast; - private ConversationsActivity activity; - private Menu mOptionsMenu; - protected OnClickListener clickToVerify = new OnClickListener() { - @Override - public void onClick(View v) { - activity.verifyOtrSessionDialog(conversation, v); - } - }; - - private final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd hh:mm (z)", Locale.US); - - private boolean reInitRequiredOnStart = true; - private MediaPreviewAdapter mediaPreviewAdapter; - private final OnClickListener clickToMuc = new OnClickListener() { - - @Override - public void onClick(View v) { - ConferenceDetailsActivity.open(getActivity(), conversation); - } - }; - private final OnClickListener leaveMuc = new OnClickListener() { - - @Override - public void onClick(View v) { - activity.xmppConnectionService.archiveConversation(conversation); - } - }; - private final OnClickListener joinMuc = new OnClickListener() { - - @Override - public void onClick(View v) { - activity.xmppConnectionService.joinMuc(conversation); - } - }; - - private final OnClickListener acceptJoin = new OnClickListener() { - @Override - public void onClick(View v) { - conversation.setAttribute("accept_non_anonymous", true); - activity.xmppConnectionService.updateConversation(conversation); - activity.xmppConnectionService.joinMuc(conversation); - } - }; - - private final OnClickListener enterPassword = new OnClickListener() { - - @Override - public void onClick(View v) { - MucOptions muc = conversation.getMucOptions(); - String password = muc.getPassword(); - if (password == null) { - password = ""; - } - activity.quickPasswordEdit(password, value -> { - activity.xmppConnectionService.providePasswordForMuc(conversation, value); - return null; - }); - } - }; - - private final OnClickListener meCommand = v -> Objects.requireNonNull(binding.textinput.getText()).insert(0, Message.ME_COMMAND + " "); - private final OnClickListener quote = v -> insertQuote(); - private final OnClickListener boldText = v -> insertFormatting("bold"); - private final OnClickListener italicText = v -> insertFormatting("italic"); - private final OnClickListener monospaceText = v -> insertFormatting("monospace"); - private final OnClickListener strikethroughText = v -> insertFormatting("strikethrough"); - private final OnClickListener help = v -> openHelp(); - private final OnClickListener close = v -> closeFormatting(); - - private void openHelp() { - final AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(R.string.format_text); - builder.setMessage(R.string.help_format_text); - builder.setNeutralButton(getString(R.string.ok), null); - builder.create().show(); - } - - private void closeFormatting() { - final AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(R.string.close); - builder.setMessage(R.string.close_format_text); - builder.setPositiveButton(getString(R.string.close), - (dialog, which) -> { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); - preferences.edit().putBoolean("showtextformatting", false).apply(); - updateSendButton(); - }); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.create().show(); - } - - private void insertFormatting(String format) { - final String BOLD = "*"; - final String ITALIC = "_"; - final String MONOSPACE = "`"; - final String STRIKETHROUGH = "~"; - - int selStart = this.binding.textinput.getSelectionStart(); - int selEnd = this.binding.textinput.getSelectionEnd(); - int min = 0; - int max = this.binding.textinput.getText().length(); - if (this.binding.textinput.isFocused()) { - selStart = this.binding.textinput.getSelectionStart(); - selEnd = this.binding.textinput.getSelectionEnd(); - min = Math.max(0, Math.min(selStart, selEnd)); - max = Math.max(0, Math.max(selStart, selEnd)); - } - final CharSequence selectedText = this.binding.textinput.getText().subSequence(min, max); - - switch (format) { - case "bold": - if (selectedText.length() != 0) { - this.binding.textinput.getText().replace(Math.min(selStart, selEnd), Math.max(selStart, selEnd), - BOLD + selectedText + BOLD, 0, selectedText.length() + 2); - } else { - this.binding.textinput.getText().insert(this.binding.textinput.getSelectionStart(), (BOLD)); - } - return; - case "italic": - if (selectedText.length() != 0) { - this.binding.textinput.getText().replace(Math.min(selStart, selEnd), Math.max(selStart, selEnd), - ITALIC + selectedText + ITALIC, 0, selectedText.length() + 2); - } else { - this.binding.textinput.getText().insert(this.binding.textinput.getSelectionStart(), (ITALIC)); - } - return; - case "monospace": - if (selectedText.length() != 0) { - this.binding.textinput.getText().replace(Math.min(selStart, selEnd), Math.max(selStart, selEnd), - MONOSPACE + selectedText + MONOSPACE, 0, selectedText.length() + 2); - } else { - this.binding.textinput.getText().insert(this.binding.textinput.getSelectionStart(), (MONOSPACE)); - } - return; - case "strikethrough": - if (selectedText.length() != 0) { - this.binding.textinput.getText().replace(Math.min(selStart, selEnd), Math.max(selStart, selEnd), - STRIKETHROUGH + selectedText + STRIKETHROUGH, 0, selectedText.length() + 2); - } else { - this.binding.textinput.getText().insert(this.binding.textinput.getSelectionStart(), (STRIKETHROUGH)); - } - return; - } - } - private void insertQuote() { - int pos = 0; - if (this.binding.textinput.getSelectionStart() == this.binding.textinput.getSelectionEnd()) { - pos = this.binding.textinput.getSelectionStart(); - } - if (pos == 0) { - Objects.requireNonNull(binding.textinput.getText()).insert(0, QuoteHelper.QUOTE_CHAR + " "); - } else { - Objects.requireNonNull(binding.textinput.getText()).insert(pos, System.getProperty("line.separator") + QuoteHelper.QUOTE_CHAR + " "); - } - } - - private final OnScrollListener mOnScrollListener = new OnScrollListener() { - - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - if (AbsListView.OnScrollListener.SCROLL_STATE_IDLE == scrollState) { - fireReadEvent(); - } - } - - @Override - public void onScroll(final AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { - toggleScrollDownButton(view); - synchronized (ConversationFragment.this.messageList) { - if (firstVisibleItem < 5 && conversation != null && conversation.messagesLoaded.compareAndSet(true, false) && messageList.size() > 0) { - long timestamp; - if (messageList.get(0).getType() == Message.TYPE_STATUS && messageList.size() >= 2) { - timestamp = messageList.get(1).getTimeSent(); - } else { - timestamp = messageList.get(0).getTimeSent(); - } - activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() { - @Override - public void onMoreMessagesLoaded(final int c, final Conversation conversation) { - if (ConversationFragment.this.conversation != conversation) { - conversation.messagesLoaded.set(true); - return; - } - runOnUiThread(() -> { - synchronized (messageList) { - final int oldPosition = binding.messagesView.getFirstVisiblePosition(); - Message message = null; - int childPos; - for (childPos = 0; childPos + oldPosition < messageList.size(); ++childPos) { - message = messageList.get(oldPosition + childPos); - if (message.getType() != Message.TYPE_STATUS) { - break; - } - } - final String uuid = message != null ? message.getUuid() : null; - View v = binding.messagesView.getChildAt(childPos); - final int pxOffset = (v == null) ? 0 : v.getTop(); - ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList); - try { - updateStatusMessages(); - } catch (IllegalStateException e) { - Log.d(Config.LOGTAG, "caught illegal state exception while updating status messages"); - } - messageListAdapter.notifyDataSetChanged(); - int pos = Math.max(getIndexOf(uuid, messageList), 0); - binding.messagesView.setSelectionFromTop(pos, pxOffset); - if (messageLoaderToast != null) { - messageLoaderToast.cancel(); - } - conversation.messagesLoaded.set(true); - } - }); - } - - @Override - public void informUser(final int resId) { - - runOnUiThread(() -> { - if (messageLoaderToast != null) { - messageLoaderToast.cancel(); - } - if (ConversationFragment.this.conversation != conversation) { - return; - } - messageLoaderToast = ToastCompat.makeText(view.getContext(), resId, ToastCompat.LENGTH_LONG); - messageLoaderToast.show(); - }); - } - }); - } - } - } - }; - private final EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() { - @Override - public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] contentMimeTypes) { - // try to get permission to read the image, if applicable - if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) { - try { - inputContentInfo.requestPermission(); - } catch (Exception e) { - Log.e(Config.LOGTAG, "InputContentInfoCompat#requestPermission() failed.", e); - ToastCompat.makeText(getActivity(), activity.getString(R.string.no_permission_to_access_x, inputContentInfo.getDescription()), ToastCompat.LENGTH_LONG - ).show(); - return false; - } - } - if (hasPermissions(REQUEST_ADD_EDITOR_CONTENT, Manifest.permission.WRITE_EXTERNAL_STORAGE) && hasPermissions(REQUEST_ADD_EDITOR_CONTENT, Manifest.permission.READ_EXTERNAL_STORAGE)) { - attachEditorContentToConversation(inputContentInfo.getContentUri()); - } else { - mPendingEditorContent = inputContentInfo.getContentUri(); - } - return true; - } - }; - private Message selectedMessage; - private final OnClickListener mEnableAccountListener = new OnClickListener() { - @Override - public void onClick(View v) { - final Account account = conversation == null ? null : conversation.getAccount(); - if (account != null) { - account.setOption(Account.OPTION_DISABLED, false); - activity.xmppConnectionService.updateAccount(account); - } - } - }; - private final OnClickListener mUnblockClickListener = new OnClickListener() { - @Override - public void onClick(final View v) { - v.post(() -> v.setVisibility(View.INVISIBLE)); - if (conversation.isDomainBlocked()) { - BlockContactDialog.show(activity, conversation); - } else { - unblockConversation(conversation); - } - } - }; - private final OnClickListener mBlockClickListener = this::showBlockSubmenu; - - private final OnClickListener mAddBackClickListener = new OnClickListener() { - - @Override - public void onClick(View v) { - final Contact contact = conversation == null ? null : conversation.getContact(); - if (contact != null) { - activity.xmppConnectionService.createContact(contact, true); - activity.switchToContactDetails(contact); - } - } - }; - - private final View.OnLongClickListener mLongPressBlockListener = this::showBlockSubmenu; - - private final OnClickListener mHideUnencryptionHint = v -> enableMessageEncryption(); - - private void enableMessageEncryption() { - if (Config.supportOmemo() && Conversation.suitableForOmemoByDefault(conversation) && conversation.isSingleOrPrivateAndNonAnonymous()) { - conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL); - activity.xmppConnectionService.updateConversation(conversation); - activity.refreshUi(); - } - hideSnackbar(); - } - private void disableMessageEncryption() { - if (conversation.isSingleOrPrivateAndNonAnonymous()) { - conversation.setNextEncryption(Message.ENCRYPTION_NONE); - activity.xmppConnectionService.updateConversation(conversation); - activity.refreshUi(); - } - hideSnackbar(); - } - - - - private final OnClickListener mAllowPresenceSubscription = new OnClickListener() { - @Override - public void onClick(View v) { - final Contact contact = conversation == null ? null : conversation.getContact(); - if (contact != null) { - activity.xmppConnectionService.sendPresencePacket(contact.getAccount(), - activity.xmppConnectionService.getPresenceGenerator() - .sendPresenceUpdatesTo(contact)); - hideSnackbar(); - } - } - }; - private OnClickListener mAnswerSmpClickListener = new OnClickListener() { - @Override - public void onClick(View view) { - Intent intent = new Intent(activity, VerifyOTRActivity.class); - intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT); - intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString()); - intent.putExtra(VerifyOTRActivity.EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString()); - intent.putExtra("mode", VerifyOTRActivity.MODE_ANSWER_QUESTION); - startActivity(intent); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - }; - - protected OnClickListener clickToDecryptListener = new OnClickListener() { - - @Override - public void onClick(View v) { - PendingIntent pendingIntent = conversation.getAccount().getPgpDecryptionService().getPendingIntent(); - if (pendingIntent != null) { - try { - getActivity().startIntentSenderForResult(pendingIntent.getIntentSender(), - REQUEST_DECRYPT_PGP, - null, - 0, - 0, - 0); - } catch (SendIntentException e) { - ToastCompat.makeText(getActivity(), R.string.unable_to_connect_to_keychain, ToastCompat.LENGTH_SHORT).show(); - conversation.getAccount().getPgpDecryptionService().continueDecryption(true); - } - } - updateSnackBar(conversation); - } - }; - private final AtomicBoolean mSendingPgpMessage = new AtomicBoolean(false); - private final OnEditorActionListener mEditorActionListener = (v, actionId, event) -> { - if (actionId == EditorInfo.IME_ACTION_SEND) { - InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null && imm.isFullscreenMode()) { - imm.hideSoftInputFromWindow(v.getWindowToken(), 0); - } - sendMessage(); - return true; - } else { - return false; - } - }; - private final OnClickListener mScrollButtonListener = new OnClickListener() { - - @Override - public void onClick(View v) { - stopScrolling(); - setSelection(binding.messagesView.getCount() - 1, true); - } - }; - - private final OnClickListener mRecordVoiceButtonListener = v -> attachFile(ATTACHMENT_CHOICE_RECORD_VOICE); - - private final OnClickListener mSendButtonListener = new OnClickListener() { - - @Override - public void onClick(View v) { - Object tag = v.getTag(); - if (tag instanceof SendButtonAction) { - SendButtonAction action = (SendButtonAction) tag; - switch (action) { - case CHOOSE_ATTACHMENT: - choose_attachment(v); - case TAKE_PHOTO: - case RECORD_VIDEO: - case SEND_LOCATION: - case RECORD_VOICE: - case CHOOSE_PICTURE: - attachFile(action.toChoice()); - break; - case CANCEL: - if (conversation != null) { - if (conversation.setCorrectingMessage(null)) { - binding.textinput.setText(""); - binding.textinput.append(conversation.getDraftMessage()); - conversation.setDraftMessage(null); - } else if (conversation.getMode() == Conversation.MODE_MULTI) { - conversation.setNextCounterpart(null); - binding.textinput.setText(""); - } else { - binding.textinput.setText(""); - } - updateChatMsgHint(); - updateSendButton(); - updateEditablity(); - } - break; - default: - sendMessage(); - } - } else { - sendMessage(); - } - } - }; - - private View.OnLongClickListener mSendButtonLongListener = new View.OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - final String body = binding.textinput.getText().toString(); - if (body.length() == 0) { - binding.textinput.getText().insert(0, Message.ME_COMMAND + " "); - } - return true; - } - }; - - private int completionIndex = 0; - private int lastCompletionLength = 0; - private String incomplete; - private int lastCompletionCursor; - private boolean firstWord = false; - private Message mPendingDownloadableMessage; - - private static ConversationFragment findConversationFragment(Activity activity) { - Fragment fragment = activity.getFragmentManager().findFragmentById(R.id.main_fragment); - if (fragment instanceof ConversationFragment) { - return (ConversationFragment) fragment; - } - fragment = activity.getFragmentManager().findFragmentById(R.id.secondary_fragment); - if (fragment instanceof ConversationFragment) { - return (ConversationFragment) fragment; - } - return null; - } - - public static void startStopPending(Activity activity) { - ConversationFragment fragment = findConversationFragment(activity); - if (fragment != null) { - fragment.messageListAdapter.startStopPending(); - } - } - - public static void downloadFile(Activity activity, Message message) { - ConversationFragment fragment = findConversationFragment(activity); - if (fragment != null) { - fragment.startDownloadable(message); - } - } - - public static void registerPendingMessage(Activity activity, Message message) { - ConversationFragment fragment = findConversationFragment(activity); - if (fragment != null) { - fragment.pendingMessage.push(message); - } - } - - public static void openPendingMessage(Activity activity) { - ConversationFragment fragment = findConversationFragment(activity); - if (fragment != null) { - Message message = fragment.pendingMessage.pop(); - if (message != null) { - fragment.messageListAdapter.openDownloadable(message); - } - } - } - - public static Conversation getConversation(Activity activity) { - return getConversation(activity, R.id.secondary_fragment); - } - - private static Conversation getConversation(Activity activity, @IdRes int res) { - final Fragment fragment = activity.getFragmentManager().findFragmentById(res); - if (fragment instanceof ConversationFragment) { - return ((ConversationFragment) fragment).getConversation(); - } else { - return null; - } - } - - public static ConversationFragment get(Activity activity) { - FragmentManager fragmentManager = activity.getFragmentManager(); - Fragment fragment = fragmentManager.findFragmentById(R.id.main_fragment); - if (fragment instanceof ConversationFragment) { - return (ConversationFragment) fragment; - } else { - fragment = fragmentManager.findFragmentById(R.id.secondary_fragment); - return fragment instanceof ConversationFragment ? (ConversationFragment) fragment : null; - } - } - - public static Conversation getConversationReliable(Activity activity) { - final Conversation conversation = getConversation(activity, R.id.secondary_fragment); - if (conversation != null) { - return conversation; - } - return getConversation(activity, R.id.main_fragment); - } - - private static boolean scrolledToBottom(AbsListView listView) { - final int count = listView.getCount(); - if (count == 0) { - return true; - } else if (listView.getLastVisiblePosition() == count - 1) { - final View lastChild = listView.getChildAt(listView.getChildCount() - 1); - return lastChild != null && lastChild.getBottom() <= listView.getHeight(); - } else { - return false; - } - } - - @SuppressLint("RestrictedApi") - private void choose_attachment(View v) { - SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity); - final boolean hideVoice = p.getBoolean("show_record_voice_btn", activity.getResources().getBoolean(R.bool.show_record_voice_btn)); - PopupMenu popup = new PopupMenu(activity, v); - popup.inflate(R.menu.choose_attachment); - final Menu menu = popup.getMenu(); - ConversationMenuConfigurator.configureQuickShareAttachmentMenu(conversation, menu, hideVoice); - popup.setOnMenuItemClickListener(attachmentItem -> { - switch (attachmentItem.getItemId()) { - case R.id.attach_choose_picture: - case R.id.attach_choose_video: - case R.id.attach_take_picture: - case R.id.attach_record_video: - case R.id.attach_choose_file: - case R.id.attach_record_voice: - case R.id.attach_location: - handleAttachmentSelection(attachmentItem); - default: - return false; - } - }); - MenuPopupHelper menuHelper = new MenuPopupHelper(getActivity(), (MenuBuilder) menu, v); - menuHelper.setForceShowIcon(true); - menuHelper.show(); - } - - private void toggleScrollDownButton() { - toggleScrollDownButton(binding.messagesView); - } - - private void toggleScrollDownButton(AbsListView listView) { - if (conversation == null) { - return; - } - if (scrolledToBottom(listView)) { - lastMessageUuid = null; - hideUnreadMessagesCount(); - } else { - binding.scrollToBottomButton.setEnabled(true); - binding.scrollToBottomButton.show(); - if (lastMessageUuid == null) { - lastMessageUuid = conversation.getLatestMessage().getUuid(); - } - if (conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid) > 0) { - binding.unreadCountCustomView.setVisibility(View.VISIBLE); - } - } - } - - private int getIndexOf(String uuid, List messages) { - if (uuid == null) { - return messages.size() - 1; - } - for (int i = 0; i < messages.size(); ++i) { - if (uuid.equals(messages.get(i).getUuid())) { - return i; - } else { - Message next = messages.get(i); - while (next != null && next.wasMergedIntoPrevious()) { - if (uuid.equals(next.getUuid())) { - return i; - } - next = next.next(); - } - - } - } - return -1; - } - - private ScrollState getScrollPosition() { - final ListView listView = this.binding == null ? null : this.binding.messagesView; - if (listView == null || listView.getCount() == 0 || listView.getLastVisiblePosition() == listView.getCount() - 1) { - return null; - } else { - final int pos = listView.getFirstVisiblePosition(); - final View view = listView.getChildAt(0); - if (view == null) { - return null; - } else { - return new ScrollState(pos, view.getTop()); - } - } - } - - private void setScrollPosition(ScrollState scrollPosition, String lastMessageUuid) { - if (scrollPosition != null) { - this.lastMessageUuid = lastMessageUuid; - if (lastMessageUuid != null) { - binding.unreadCountCustomView.setUnreadCount(conversation.getReceivedMessagesCountSinceUuid(lastMessageUuid)); - } - //TODO maybe this needs a 'post' - this.binding.messagesView.setSelectionFromTop(scrollPosition.position, scrollPosition.offset); - toggleScrollDownButton(); - } - } - - private void attachLocationToConversation(Conversation conversation, Uri uri) { - if (conversation == null) { - return; - } - activity.xmppConnectionService.attachLocationToConversation(conversation, uri, new UiCallback() { - - @Override - public void success(Message message) { - - } - - @Override - public void error(int errorCode, Message object) { - //TODO show possible pgp error - } - - @Override - public void userInputRequired(PendingIntent pi, Message object) { - - } - - @Override - public void progress(int progress) { - - } - }); - } - - private void attachFileToConversation(Conversation conversation, Uri uri, String type) { - if (conversation == null) { - return; - } - final Toast prepareFileToast = ToastCompat.makeText(getActivity(), getText(R.string.preparing_file), ToastCompat.LENGTH_SHORT); - prepareFileToast.show(); - activity.delegateUriPermissionsToService(uri); - activity.xmppConnectionService.attachFileToConversation(conversation, uri, type, new UiInformableCallback() { - @Override - public void inform(final String text) { - hidePrepareFileToast(prepareFileToast); - runOnUiThread(() -> activity.replaceToast(text)); - } - - @Override - public void success(Message message) { - runOnUiThread(() -> activity.hideToast()); - hidePrepareFileToast(prepareFileToast); - } - - @Override - public void error(final int errorCode, Message message) { - hidePrepareFileToast(prepareFileToast); - runOnUiThread(() -> activity.replaceToast(getString(errorCode))); - - } - - @Override - public void userInputRequired(PendingIntent pi, Message message) { - hidePrepareFileToast(prepareFileToast); - } - - @Override - public void progress(int progress) { - hidePrepareFileToast(prepareFileToast); - updateSnackBar(conversation); - } - }); - } - - public void attachEditorContentToConversation(Uri uri) { - mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), uri, Attachment.Type.FILE)); - toggleInputMethod(); - } - - private void attachImageToConversation(Conversation conversation, Uri uri, String type) { - if (conversation == null) { - return; - } - final Toast prepareFileToast = ToastCompat.makeText(getActivity(), getText(R.string.preparing_image), ToastCompat.LENGTH_LONG); - prepareFileToast.show(); - activity.delegateUriPermissionsToService(uri); - activity.xmppConnectionService.attachImageToConversation(conversation, uri, type, - new UiCallback() { - @Override - public void userInputRequired(PendingIntent pi, Message object) { - hidePrepareFileToast(prepareFileToast); - } - - @Override - public void progress(int progress) { - - } - - @Override - public void success(Message message) { - hidePrepareFileToast(prepareFileToast); - } - - @Override - public void error(final int error, final Message message) { - hidePrepareFileToast(prepareFileToast); - final ConversationsActivity activity = ConversationFragment.this.activity; - if (activity == null) { - return; - } - activity.runOnUiThread(() -> activity.replaceToast(getString(error))); - } - }); - } - - private void hidePrepareFileToast(final Toast prepareFileToast) { - if (prepareFileToast != null && activity != null) { - activity.runOnUiThread(prepareFileToast::cancel); - } - } - - private void sendMessage() { - if (mediaPreviewAdapter.hasAttachments()) { - commitAttachments(); - return; - } - final Editable text = this.binding.textinput.getText(); - final String body = text == null ? "" : text.toString(); - final Conversation conversation = this.conversation; - if (body.length() == 0 || conversation == null) { - return; - } - if (trustKeysIfNeeded(conversation, REQUEST_TRUST_KEYS_TEXT)) { - return; - } - final Message message; - if (conversation.getCorrectingMessage() == null) { - message = new Message(conversation, body, conversation.getNextEncryption()); - Message.configurePrivateMessage(message); - } else { - message = conversation.getCorrectingMessage(); - message.putEdited(message.getUuid(), message.getServerMsgId(), message.getBody(), message.getTimeSent()); - message.setBody(body); - message.setServerMsgId(null); - message.setUuid(UUID.randomUUID().toString()); - } - switch (conversation.getNextEncryption()) { - case Message.ENCRYPTION_OTR: - sendOtrMessage(message); - break; - case Message.ENCRYPTION_PGP: - sendPgpMessage(message); - break; - default: - sendMessage(message); - } - } - - private boolean trustKeysIfNeeded(final Conversation conversation, final int requestCode) { - return conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && trustKeysIfNeeded(requestCode); - } - - protected boolean trustKeysIfNeeded(int requestCode) { - AxolotlService axolotlService = conversation.getAccount().getAxolotlService(); - final List targets = axolotlService.getCryptoTargets(conversation); - boolean hasUnaccepted = !conversation.getAcceptedCryptoTargets().containsAll(targets); - boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided()).isEmpty(); - boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), targets).isEmpty(); - boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(conversation).isEmpty(); - boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets); - boolean downloadInProgress = axolotlService.hasPendingKeyFetches(targets); - if (hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys || hasUnaccepted || downloadInProgress) { - axolotlService.createSessionsIfNeeded(conversation); - Intent intent = new Intent(getActivity(), TrustKeysActivity.class); - String[] contacts = new String[targets.size()]; - for (int i = 0; i < contacts.length; ++i) { - contacts[i] = targets.get(i).toString(); - } - intent.putExtra("contacts", contacts); - intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toEscapedString()); - intent.putExtra("conversation", conversation.getUuid()); - startActivityForResult(intent, requestCode); - return true; - } else { - return false; - } - } - - public void updateChatMsgHint() { - final boolean multi = conversation.getMode() == Conversation.MODE_MULTI; - if (conversation.getCorrectingMessage() != null) { - this.binding.textInputHint.setVisibility(View.VISIBLE); - this.binding.textInputHint.setText(R.string.send_corrected_message); - this.binding.textinput.setHint(R.string.send_corrected_message); - } else if (isPrivateMessage()) { - this.binding.textinput.setHint(R.string.send_unencrypted_message); - this.binding.textInputHint.setVisibility(View.VISIBLE); - SpannableStringBuilder hint = new SpannableStringBuilder(getString(R.string.send_private_message_to, conversation.getNextCounterpart().getResource())); - hint.setSpan(new StyleSpan(Typeface.BOLD), 0, hint.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - this.binding.textInputHint.setText(hint); - } else if (multi && !conversation.getMucOptions().participating()) { - this.binding.textInputHint.setVisibility(View.VISIBLE); - this.binding.textInputHint.setText(R.string.ask_for_writeaccess); - this.binding.textinput.setHint(R.string.you_are_not_participating); - } else { - this.binding.textInputHint.setVisibility(View.GONE); - if (getActivity() != null) { - this.binding.textinput.setHint(UIHelper.getMessageHint(getActivity(), conversation)); - getActivity().invalidateOptionsMenu(); - } - } - } - - private boolean isPrivateMessage() { - return conversation != null && conversation.getMode() == Conversation.MODE_MULTI && conversation.getNextCounterpart() != null; - } - - public void setupIme() { - this.binding.textinput.refreshIme(); - } - - private void handleActivityResult(ActivityResult activityResult) { - if (activityResult.resultCode == Activity.RESULT_OK) { - handlePositiveActivityResult(activityResult.requestCode, activityResult.data); - } else { - handleNegativeActivityResult(activityResult.requestCode); - } - } - - private void handlePositiveActivityResult(int requestCode, final Intent data) { - switch (requestCode) { - case REQUEST_TRUST_KEYS_TEXT: - sendMessage(); - break; - case REQUEST_TRUST_KEYS_ATTACHMENTS: - commitAttachments(); - break; - case REQUEST_START_AUDIO_CALL: - triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VOICE_CALL); - break; - case REQUEST_START_VIDEO_CALL: - triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); - break; - case ATTACHMENT_CHOICE_CHOOSE_IMAGE: - final List imageUris = Attachment.extractAttachments(getActivity(), data, Attachment.Type.IMAGE); - mediaPreviewAdapter.addMediaPreviews(imageUris); - toggleInputMethod(); - break; - case ATTACHMENT_CHOICE_TAKE_PHOTO: - final Uri takePhotoUri = pendingTakePhotoUri.pop(); - if (takePhotoUri != null) { - mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), takePhotoUri, Attachment.Type.IMAGE)); - activity.xmppConnectionService.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, takePhotoUri)); - toggleInputMethod(); - } else { - Log.d(Config.LOGTAG, "lost take photo uri. unable to to attach"); - } - break; - case ATTACHMENT_CHOICE_CHOOSE_FILE: - case ATTACHMENT_CHOICE_RECORD_VIDEO: - case ATTACHMENT_CHOICE_RECORD_VOICE: - case ATTACHMENT_CHOICE_CHOOSE_VIDEO: - final Attachment.Type type = requestCode == ATTACHMENT_CHOICE_RECORD_VOICE ? Attachment.Type.RECORDING : Attachment.Type.FILE; - final List fileUris = Attachment.extractAttachments(getActivity(), data, type); - mediaPreviewAdapter.addMediaPreviews(fileUris); - toggleInputMethod(); - break; - case ATTACHMENT_CHOICE_LOCATION: - 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; - case REQUEST_INVITE_TO_CONVERSATION: - XmppActivity.ConferenceInvite invite = XmppActivity.ConferenceInvite.parse(data); - if (invite != null && activity != null) { - if (invite.execute(activity)) { - activity.mToast = ToastCompat.makeText(activity, R.string.creating_conference, ToastCompat.LENGTH_LONG); - activity.mToast.show(); - } - } - break; - } - } - - private void commitAttachments() { - final List attachments = mediaPreviewAdapter.getAttachments(); - if (anyNeedsExternalStoragePermission(attachments) && !hasPermissions(REQUEST_COMMIT_ATTACHMENTS, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - return; - } - if (trustKeysIfNeeded(conversation, REQUEST_TRUST_KEYS_ATTACHMENTS)) { - return; - } - final PresenceSelector.OnPresenceSelected callback = () -> { - for (Iterator i = attachments.iterator(); i.hasNext(); i.remove()) { - final Attachment attachment = i.next(); - if (attachment.getType() == Attachment.Type.LOCATION) { - 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(), 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()); - } - } - mediaPreviewAdapter.notifyDataSetChanged(); - toggleInputMethod(); - }; - if (conversation == null - || conversation.getMode() == Conversation.MODE_MULTI - || Attachment.canBeSendInband(attachments) - || (conversation.getAccount().httpUploadAvailable() && FileBackend.allFilesUnderSize(getActivity(), attachments, getMaxHttpUploadSize(conversation)))) { - callback.onPresenceSelected(); - } else { - activity.selectPresence(conversation, callback); - } - } - - - private static boolean anyNeedsExternalStoragePermission(final Collection attachments) { - for (final Attachment attachment : attachments) { - if (attachment.getType() != Attachment.Type.LOCATION) { - return true; - } - } - return false; - } - - public void toggleInputMethod() { - boolean hasAttachments = mediaPreviewAdapter.hasAttachments(); - binding.textinput.setVisibility(hasAttachments ? View.GONE : View.VISIBLE); - binding.mediaPreview.setVisibility(hasAttachments ? View.VISIBLE : View.GONE); - if (mOptionsMenu != null) { - ConversationMenuConfigurator.configureAttachmentMenu(conversation, mOptionsMenu, activity.getAttachmentChoicePreference(), hasAttachments); - } - updateSendButton(); - } - - private boolean canSendMeCommand() { - if (conversation != null) { - final String body = binding.textinput.getText().toString(); - return body.length() == 0; - } - return false; - } - - private void handleNegativeActivityResult(int requestCode) { - switch (requestCode) { - case ATTACHMENT_CHOICE_TAKE_PHOTO: - if (pendingTakePhotoUri.clear()) { - Log.d(Config.LOGTAG, "cleared pending photo uri after negative activity result"); - } - break; - } - } - - @Override - public void onActivityResult(int requestCode, int resultCode, final Intent data) { - super.onActivityResult(requestCode, resultCode, data); - ActivityResult activityResult = ActivityResult.of(requestCode, resultCode, data); - if (activity != null && activity.xmppConnectionService != null) { - handleActivityResult(activityResult); - } else { - this.postponedActivityResult.push(activityResult); - } - } - - public void unblockConversation(final Blockable conversation) { - activity.xmppConnectionService.sendUnblockRequest(conversation); - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - Log.d(Config.LOGTAG, "ConversationFragment.onAttach()"); - if (activity instanceof ConversationsActivity) { - this.activity = (ConversationsActivity) activity; - } else { - throw new IllegalStateException("Trying to attach fragment to activity that is not the ConversationsActivity"); - } - } - - @Override - public void onDetach() { - super.onDetach(); - this.activity = null; //TODO maybe not a good idea since some callbacks really need it - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { - mOptionsMenu = menu; - boolean hasAttachments = mediaPreviewAdapter != null && mediaPreviewAdapter.hasAttachments(); - menuInflater.inflate(R.menu.fragment_conversation, menu); - final MenuItem menuInviteContact = menu.findItem(R.id.action_invite); - final MenuItem menuNeedHelp = menu.findItem(R.id.action_create_issue); - final MenuItem menuSearchUpdates = menu.findItem(R.id.action_check_updates); - final MenuItem menuArchiveChat = menu.findItem(R.id.action_archive_chat); - final MenuItem menuGroupDetails = menu.findItem(R.id.action_group_details); - final MenuItem menuParticipants = menu.findItem(R.id.action_participants); - final MenuItem menuContactDetails = menu.findItem(R.id.action_contact_details); - final MenuItem menuCall = menu.findItem(R.id.action_call); - final MenuItem menuOngoingCall = menu.findItem(R.id.action_ongoing_call); - final MenuItem menuVideoCall = menu.findItem(R.id.action_video_call); - final MenuItem menuMediaBrowser = menu.findItem(R.id.action_mediabrowser); - final MenuItem menuTogglePinned = menu.findItem(R.id.action_toggle_pinned); - - if (conversation != null) { - if (conversation.getMode() == Conversation.MODE_MULTI || (activity.xmppConnectionService != null && !activity.xmppConnectionService.hasInternetConnection())) { - menuInviteContact.setVisible(conversation.getMucOptions().canInvite()); - menuArchiveChat.setTitle(R.string.action_end_conversation_muc); - menuCall.setVisible(false); - menuOngoingCall.setVisible(false); - menuParticipants.setVisible(true); - } else { - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; - final Optional ongoingRtpSession = service == null ? Optional.absent() : service.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); - if (ongoingRtpSession.isPresent()) { - menuOngoingCall.setVisible(true); - menuCall.setVisible(false); - } else { - menuOngoingCall.setVisible(false); - final RtpCapability.Capability rtpCapability = RtpCapability.check(conversation.getContact()); - final boolean cameraAvailable = activity != null && activity.isCameraFeatureAvailable(); - menuCall.setVisible(rtpCapability != RtpCapability.Capability.NONE); - menuVideoCall.setVisible(rtpCapability == RtpCapability.Capability.VIDEO && cameraAvailable); - } - menuParticipants.setVisible(false); - menuInviteContact.setVisible(false); - menuArchiveChat.setTitle(R.string.action_end_conversation); - } - try { - Fragment secondaryFragment = activity.getFragmentManager().findFragmentById(R.id.secondary_fragment); - if (secondaryFragment instanceof ConversationFragment) { - if (conversation.getMode() == Conversation.MODE_MULTI) { - menuGroupDetails.setTitle(conversation.getMucOptions().isPrivateAndNonAnonymous() ? R.string.action_group_details : R.string.channel_details); - menuGroupDetails.setVisible(true); - menuContactDetails.setVisible(false); - } else { - menuGroupDetails.setVisible(false); - menuContactDetails.setVisible(!this.conversation.withSelf()); - } - menuSearchUpdates.setVisible(true); - } else { - menuGroupDetails.setVisible(false); - menuContactDetails.setVisible(false); - menuSearchUpdates.setVisible(false); - } - } catch (Exception e) { - e.printStackTrace(); - menuGroupDetails.setVisible(false); - menuContactDetails.setVisible(false); - menuSearchUpdates.setVisible(false); - } - menuMediaBrowser.setVisible(true); - menuNeedHelp.setVisible(false); - ConversationMenuConfigurator.configureAttachmentMenu(conversation, menu, activity.getAttachmentChoicePreference(), hasAttachments); - ConversationMenuConfigurator.configureEncryptionMenu(conversation, menu, activity); - if (conversation.getBooleanAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP, false)) { - menuTogglePinned.setTitle(R.string.remove_from_favorites); - } else { - menuTogglePinned.setTitle(R.string.add_to_favorites); - } - } else { - menuNeedHelp.setVisible(true); - menuSearchUpdates.setVisible(true); - menuInviteContact.setVisible(false); - menuGroupDetails.setVisible(false); - menuContactDetails.setVisible(false); - menuMediaBrowser.setVisible(false); - } - super.onCreateOptionsMenu(menu, menuInflater); - } - - @Override - public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - this.binding = DataBindingUtil.inflate(inflater, R.layout.fragment_conversation, container, false); - binding.getRoot().setOnClickListener(null); //TODO why the fuck did we do this? - - binding.textinput.addTextChangedListener(new StylingHelper.MessageEditorStyler(binding.textinput)); - binding.textinput.setOnEditorActionListener(mEditorActionListener); - binding.textinput.setRichContentListener(new String[]{"image/*"}, mEditorContentListener); - - binding.textSendButton.setOnClickListener(this.mSendButtonListener); - binding.textSendButton.setOnLongClickListener(this.mSendButtonLongListener); - binding.scrollToBottomButton.setOnClickListener(this.mScrollButtonListener); - binding.recordVoiceButton.setOnClickListener(this.mRecordVoiceButtonListener); - - binding.messagesView.setOnScrollListener(mOnScrollListener); - binding.messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL); - mediaPreviewAdapter = new MediaPreviewAdapter(this); - binding.mediaPreview.setAdapter(mediaPreviewAdapter); - messageListAdapter = new MessageAdapter((XmppActivity) getActivity(), this.messageList); - messageListAdapter.setOnContactPictureClicked(this); - messageListAdapter.setOnContactPictureLongClicked(this); - binding.messagesView.setAdapter(messageListAdapter); - registerForContextMenu(binding.messagesView); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - this.binding.textinput.setCustomInsertionActionModeCallback(new EditMessageActionModeCallback(this.binding.textinput)); - } - return binding.getRoot(); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - Log.d(Config.LOGTAG, "ConversationFragment.onDestroyView()"); - messageListAdapter.setOnContactPictureClicked(null); - messageListAdapter.setOnContactPictureLongClicked(null); - } - - @Override - public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { - int animator = enter ? R.animator.fade_right_in : R.animator.fade_right_out; - return AnimatorInflater.loadAnimator(this.activity, animator); - - } - - private void quoteText(String text, String user) { - if (binding.textinput.isEnabled()) { - String username = ""; - if (user != null && user.length() > 0) { - if (user.equals(getString(R.string.me))) { - username = getString(R.string.me_quote) + System.getProperty("line.separator"); - } else { - username = getString(R.string.x_user_quote, user) + System.getProperty("line.separator"); - } - } - binding.textinput.insertAsQuote(username + text); - binding.textinput.requestFocus(); - InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); - if (inputMethodManager != null) { - inputMethodManager.showSoftInput(binding.textinput, InputMethodManager.SHOW_IMPLICIT); - } - } - } - - private void showRecordVoiceButton() { - SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity); - final boolean ShowRecordVoiceButton = p.getBoolean("show_record_voice_btn", activity.getResources().getBoolean(R.bool.show_record_voice_btn)); - Log.d(Config.LOGTAG, "Recorder " + ShowRecordVoiceButton); - if (ShowRecordVoiceButton) { - binding.recordVoiceButton.setVisibility(View.VISIBLE); - } else { - binding.recordVoiceButton.setVisibility(View.GONE); - } - binding.recordVoiceButton.setImageResource(activity.getThemeResource(R.attr.ic_send_voice_offline, R.drawable.ic_send_voice_offline)); - } - - private void quoteMedia(Message message, @Nullable String user) { - Message.FileParams params = message.getFileParams(); - String filesize = params.size != null ? UIHelper.filesizeToString(params.size) : null; - final StringBuilder stringBuilder = new StringBuilder(); - if (activity.showDateInQuotes()) { - stringBuilder.append(df.format(message.getTimeSent())).append(System.getProperty("line.separator")); - } - stringBuilder.append(MimeUtils.getMimeTypeEmoji(getActivity(), message.getMimeType())).append(" "); - stringBuilder.append(" \u00B7 "); - stringBuilder.append(filesize); - quoteText(stringBuilder.toString(), user); - } - - private void quoteGeoUri(Message message, @Nullable String user) { - final StringBuilder stringBuilder = new StringBuilder(); - if (activity.showDateInQuotes()) { - stringBuilder.append(df.format(message.getTimeSent())).append(System.getProperty("line.separator")); - } - stringBuilder.append("\uD83D\uDDFA"); // map - quoteText(stringBuilder.toString(), user); - } - - private void quoteMessage(Message message, @Nullable String user) { - if (message.isGeoUri()) { - quoteGeoUri(message, user); - } else if (message.isFileOrImage()) { - quoteMedia(message, user); - } else if (message.isTypeText()) { - final StringBuilder stringBuilder = new StringBuilder(); - if (activity.showDateInQuotes()) { - stringBuilder.append(df.format(message.getTimeSent())).append(System.getProperty("line.separator")); - } - stringBuilder.append(MessageUtils.prepareQuote(message)); - quoteText(stringBuilder.toString(), user); - } - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - //This should cancel any remaining click events that would otherwise trigger links - v.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)); - synchronized (this.messageList) { - super.onCreateContextMenu(menu, v, menuInfo); - AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; - this.selectedMessage = this.messageList.get(acmi.position); - populateContextMenu(menu); - } - } - - private void populateContextMenu(ContextMenu menu) { - final Message m = this.selectedMessage; - final Transferable t = m.getTransferable(); - Message relevantForCorrection = m; - while (relevantForCorrection.mergeable(relevantForCorrection.next())) { - relevantForCorrection = relevantForCorrection.next(); - } - if (m.getType() != Message.TYPE_STATUS && m.getType() != Message.TYPE_RTP_SESSION) { - - if (m.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || m.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) { - return; - } - if (m.getStatus() == Message.STATUS_RECEIVED && t != null && (t.getStatus() == Transferable.STATUS_CANCELLED || t.getStatus() == Transferable.STATUS_FAILED)) { - return; - } - final boolean fileDeleted = m.isFileDeleted(); - final boolean encrypted = m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED - || m.getEncryption() == Message.ENCRYPTION_PGP; - final boolean receiving = m.getStatus() == Message.STATUS_RECEIVED && (t instanceof JingleFileTransferConnection || t instanceof HttpDownloadConnection); - activity.getMenuInflater().inflate(R.menu.message_context, menu); - menu.setHeaderTitle(R.string.message_options); - MenuItem openWith = menu.findItem(R.id.open_with); - MenuItem copyMessage = menu.findItem(R.id.copy_message); - MenuItem quoteMessage = menu.findItem(R.id.quote_message); - MenuItem retryDecryption = menu.findItem(R.id.retry_decryption); - MenuItem correctMessage = menu.findItem(R.id.correct_message); - MenuItem deleteMessage = menu.findItem(R.id.delete_message); - MenuItem shareWith = menu.findItem(R.id.share_with); - MenuItem sendAgain = menu.findItem(R.id.send_again); - MenuItem copyUrl = menu.findItem(R.id.copy_url); - MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission); - MenuItem downloadFile = menu.findItem(R.id.download_file); - MenuItem deleteFile = menu.findItem(R.id.delete_file); - MenuItem showLog = menu.findItem(R.id.show_edit_log); - MenuItem showErrorMessage = menu.findItem(R.id.show_error_message); - MenuItem saveFile = menu.findItem(R.id.save_file); - final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(m); - final boolean showError = m.getStatus() == Message.STATUS_SEND_FAILED && m.getErrorMessage() != null && !Message.ERROR_MESSAGE_CANCELLED.equals(m.getErrorMessage()); - final boolean messageDeleted = m.isMessageDeleted(); - deleteMessage.setVisible(true); - if (!m.isFileOrImage() && !encrypted && !m.isGeoUri() && !m.treatAsDownloadable() && !unInitiatedButKnownSize && t == null && !m.isMessageDeleted()) { - copyMessage.setVisible(true); - } - if (!encrypted && !unInitiatedButKnownSize && t == null) { - quoteMessage.setVisible(!showError && QuoteHelper.isMessageQuoteable(m)); - } - if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED && !fileDeleted) { - retryDecryption.setVisible(true); - } - if (!showError - && relevantForCorrection.getType() == Message.TYPE_TEXT - && !m.isGeoUri() - && relevantForCorrection.isLastCorrectableMessage() - && m.getConversation() instanceof Conversation) { - correctMessage.setVisible(true); - } - if ((m.isFileOrImage() && !fileDeleted && !receiving) || (m.getType() == Message.TYPE_TEXT && !m.treatAsDownloadable()) && !unInitiatedButKnownSize && t == null && !messageDeleted) { - shareWith.setVisible(true); - - } - if (m.getStatus() == Message.STATUS_SEND_FAILED) { - sendAgain.setVisible(true); - } - if (m.hasFileOnRemoteHost() - || m.isGeoUri() - || m.isXmppUri() - || m.treatAsDownloadable() - || unInitiatedButKnownSize - || t instanceof HttpDownloadConnection) { - copyUrl.setVisible(true); - } - if (m.isFileOrImage() && fileDeleted && m.hasFileOnRemoteHost()) { - downloadFile.setVisible(true); - downloadFile.setTitle(activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, m))); - } - final boolean waitingOfferedSending = m.getStatus() == Message.STATUS_WAITING - || m.getStatus() == Message.STATUS_UNSEND - || m.getStatus() == Message.STATUS_OFFERED; - final boolean cancelable = (t != null && !fileDeleted) || waitingOfferedSending && m.needsUploading(); - if (cancelable) { - cancelTransmission.setVisible(true); - } - if (m.isFileOrImage() && !fileDeleted && !cancelable) { - final String path = m.getRelativeFilePath(); - Log.d(Config.LOGTAG, "Path = " + path); - if (path == null || !path.startsWith("/") || path.contains(getConversationsDirectory(this.activity, "null").getAbsolutePath())) { - deleteFile.setVisible(true); - deleteFile.setTitle(activity.getString(R.string.delete_x_file, UIHelper.getFileDescriptionString(activity, m))); - } - saveFile.setVisible(true); - saveFile.setTitle(activity.getString(R.string.save_x_file, UIHelper.getFileDescriptionString(activity, m))); - } - showErrorMessage.setVisible(showError); - final String mime = m.isFileOrImage() ? m.getMimeType() : null; - if ((m.isGeoUri() && GeoHelper.openInOsmAnd(getActivity(), m)) || (mime != null && mime.startsWith("audio/"))) { - openWith.setVisible(true); - } - if (m.edited() && m.getRetractId() == null) { - showLog.setVisible(true); - } - } - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - String user; - try { - final Contact contact = selectedMessage.getContact(); - if (conversation.getMode() == Conversation.MODE_MULTI) { - user = UIHelper.getDisplayedMucCounterpart(selectedMessage.getCounterpart()); - } else { - user = contact != null ? contact.getDisplayName() : null; - } - if (selectedMessage.getStatus() == Message.STATUS_SEND - || selectedMessage.getStatus() == Message.STATUS_SEND_FAILED - || selectedMessage.getStatus() == Message.STATUS_SEND_RECEIVED - || selectedMessage.getStatus() == Message.STATUS_SEND_DISPLAYED) { - user = getString(R.string.me); - } - } catch (Exception e) { - e.printStackTrace(); - user = null; - } - switch (item.getItemId()) { - case R.id.share_with: - ShareUtil.share(activity, selectedMessage, user); - return true; - case R.id.correct_message: - correctMessage(selectedMessage); - return true; - case R.id.copy_message: - ShareUtil.copyToClipboard(activity, selectedMessage); - return true; - case R.id.quote_message: - if (conversation.getMode() == Conversation.MODE_MULTI) { - quoteMessage(selectedMessage, user); - } else { - quoteMessage(selectedMessage, null); - } - return true; - case R.id.send_again: - resendMessage(selectedMessage); - return true; - case R.id.copy_url: - ShareUtil.copyUrlToClipboard(activity, selectedMessage); - return true; - case R.id.download_file: - startDownloadable(selectedMessage); - return true; - case R.id.cancel_transmission: - cancelTransmission(selectedMessage); - return true; - case R.id.retry_decryption: - retryDecryption(selectedMessage); - return true; - case R.id.delete_message: - deleteMessage(selectedMessage); - return true; - case R.id.delete_file: - deleteFile(selectedMessage); - return true; - case R.id.show_error_message: - showErrorMessage(selectedMessage); - return true; - case R.id.open_with: - openWith(selectedMessage); - return true; - case R.id.save_file: - activity.xmppConnectionService.getFileBackend().saveFile(selectedMessage, activity); - return true; - case R.id.show_edit_log: - openLog(selectedMessage); - return true; - default: - return super.onContextItemSelected(item); - } - } - - private void openLog(Message logMsg) { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(R.string.show_edit_log); - ArrayList dataModels = new ArrayList<>(); - for (Edit itm : logMsg.getEditedList()) { - dataModels.add(new MessageLogModel(itm.getBody(), itm.getTimeSent())); - } - dataModels.add(new MessageLogModel(logMsg.getBody(), logMsg.getTimeSent())); - - MessageLogAdapter adapter = new MessageLogAdapter(dataModels, getActivity()); - - LinearLayout layout = new LinearLayout(getActivity()); - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT); - layout.setOrientation(LinearLayout.VERTICAL); - layout.setLayoutParams(layoutParams); - - ListView listView = new ListView(getActivity()); - listView.setLayoutParams(layoutParams); - layout.addView(listView); - - builder.setView(layout); - - listView.setAdapter(adapter); - - builder.setPositiveButton(R.string.ok, (dialog, which) -> { - dialog.dismiss(); - }); - final AlertDialog dialog = builder.create(); - dialog.setCanceledOnTouchOutside(false); - dialog.show(); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - Activity mXmppActivity = getActivity(); - if (MenuDoubleTabUtil.shouldIgnoreTap()) { - return false; - } else if (conversation == null) { - return super.onOptionsItemSelected(item); - } - switch (item.getItemId()) { - case R.id.encryption_choice_axolotl: - case R.id.encryption_choice_otr: - case R.id.encryption_choice_pgp: - case R.id.encryption_choice_none: - handleEncryptionSelection(item); - break; - case R.id.attach_choose_picture: - case R.id.attach_choose_video: - case R.id.attach_take_picture: - case R.id.attach_record_video: - case R.id.attach_choose_file: - case R.id.attach_record_voice: - case R.id.attach_location: - handleAttachmentSelection(item); - break; - case R.id.action_search: - startSearch(); - break; - case R.id.action_archive_chat: - if (conversation.getMode() == Conversation.MODE_SINGLE) { - activity.xmppConnectionService.archiveConversation(conversation); - } else { - final AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(activity.getString(R.string.action_end_conversation_muc)); - builder.setMessage(activity.getString(R.string.leave_conference_warning)); - builder.setNegativeButton(activity.getString(R.string.cancel), null); - builder.setPositiveButton(activity.getString(R.string.action_end_conversation_muc), - (dialog, which) -> { - activity.xmppConnectionService.archiveConversation(conversation); - }); - builder.create().show(); - } - break; - case R.id.action_invite: - startActivityForResult(ChooseContactActivity.create(activity, conversation), REQUEST_INVITE_TO_CONVERSATION); - break; - case R.id.action_clear_history: - clearHistoryDialog(conversation); - break; - case R.id.action_group_details: - activity.switchToMUCDetails(conversation); - break; - case R.id.action_participants: - Intent intent1 = new Intent(activity, MucUsersActivity.class); - intent1.putExtra("uuid", conversation.getUuid()); - startActivity(intent1); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - break; - case R.id.action_contact_details: - activity.switchToContactDetails(conversation.getContact()); - break; - case R.id.action_mediabrowser: - MediaBrowserActivity.launch(activity, conversation); - break; - case R.id.action_block: - case R.id.action_unblock: - if (mXmppActivity instanceof XmppActivity) { - BlockContactDialog.show((XmppActivity) mXmppActivity, conversation); - } - break; - case R.id.action_audio_call: - if (mXmppActivity instanceof XmppActivity) { - CallManager.checkPermissionAndTriggerAudioCall((XmppActivity) mXmppActivity, conversation); - } - break; - case R.id.action_video_call: - if (mXmppActivity instanceof XmppActivity) { - CallManager.checkPermissionAndTriggerVideoCall((XmppActivity) mXmppActivity, conversation); - } - break; - case R.id.action_ongoing_call: - if (mXmppActivity instanceof XmppActivity) { - CallManager.returnToOngoingCall((XmppActivity) mXmppActivity, conversation); - } - break; - case R.id.action_toggle_pinned: - togglePinned(); - break; - case R.id.action_mute: - muteConversationDialog(conversation); - break; - case R.id.action_unmute: - unmuteConversation(conversation); - break; - default: - break; - } - return super.onOptionsItemSelected(item); - } - - private void startSearch() { - final Intent intent = new Intent(getActivity(), SearchActivity.class); - intent.putExtra(SearchActivity.EXTRA_CONVERSATION_UUID, conversation.getUuid()); - startActivity(intent); - } - private void returnToOngoingCall() { - final Optional ongoingRtpSession = activity.xmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); - if (ongoingRtpSession.isPresent()) { - final OngoingRtpSession id = ongoingRtpSession.get(); - final Intent intent = new Intent(activity, RtpSessionActivity.class); - intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.getAccount().getJid().asBareJid().toEscapedString()); - intent.putExtra(RtpSessionActivity.EXTRA_WITH, id.getWith().toEscapedString()); - if (id instanceof AbstractJingleConnection.Id) { - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.getSessionId()); - } else if (id instanceof JingleConnectionManager.RtpSessionProposal) { - if (((JingleConnectionManager.RtpSessionProposal) id).media.contains(Media.VIDEO)) { - intent.setAction(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); - } else { - intent.setAction(RtpSessionActivity.ACTION_MAKE_VOICE_CALL); - } - } - activity.startActivity(intent); - } - - } - - private void togglePinned() { - final boolean pinned = conversation.getBooleanAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP, false); - conversation.setAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP, !pinned); - activity.xmppConnectionService.updateConversation(conversation); - activity.invalidateOptionsMenu(); - } - - private void checkPermissionAndTriggerAudioCall() { - if (activity.mUseTor || conversation.getAccount().isOnion()) { - Toast.makeText(activity, R.string.disable_tor_to_make_call, Toast.LENGTH_SHORT).show(); - return; - } - if (activity.mUseI2P || conversation.getAccount().isI2P()) { - Toast.makeText(activity, R.string.no_i2p_calls, Toast.LENGTH_SHORT).show(); - return; - } - final List permissions; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - permissions = - Arrays.asList( - Manifest.permission.RECORD_AUDIO, - Manifest.permission.BLUETOOTH_CONNECT); - } else { - permissions = Collections.singletonList(Manifest.permission.RECORD_AUDIO); - } - if (hasPermissions(REQUEST_START_AUDIO_CALL, permissions)) { - triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VOICE_CALL); - } - } - - private void checkPermissionAndTriggerVideoCall() { - if (activity.mUseTor || conversation.getAccount().isOnion()) { - Toast.makeText(activity, R.string.disable_tor_to_make_call, Toast.LENGTH_SHORT).show(); - return; - } - if (activity.mUseI2P || conversation.getAccount().isI2P()) { - Toast.makeText(activity, R.string.no_i2p_calls, Toast.LENGTH_SHORT).show(); - return; - } - if (hasPermissions(REQUEST_START_VIDEO_CALL, Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)) { - triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); - } - } - - private void triggerRtpSession(final String action) { - if (activity.xmppConnectionService.getJingleConnectionManager().isBusy()) { - Toast.makeText(getActivity(), R.string.only_one_call_at_a_time, Toast.LENGTH_LONG).show(); - return; - } - final Contact contact = conversation.getContact(); - if (contact.getPresences().anySupport(Namespace.JINGLE_MESSAGE)) { - triggerRtpSession(contact.getAccount(), contact.getJid().asBareJid(), action); - } else { - final RtpCapability.Capability capability; - if (action.equals(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL)) { - capability = RtpCapability.Capability.VIDEO; - } else { - capability = RtpCapability.Capability.AUDIO; - } - PresenceSelector.selectFullJidForDirectRtpConnection(activity, contact, capability, fullJid -> { - triggerRtpSession(contact.getAccount(), fullJid, action); - }); - } - } - - private void triggerRtpSession(final Account account, final Jid with, final String action) { - final Intent intent = new Intent(activity, RtpSessionActivity.class); - intent.setAction(action); - intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString()); - intent.putExtra(RtpSessionActivity.EXTRA_WITH, with.toEscapedString()); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - startActivity(intent); - } - - private void handleAttachmentSelection(MenuItem item) { - switch (item.getItemId()) { - case R.id.attach_choose_picture: - attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE); - break; - case R.id.attach_choose_video: - attachFile(ATTACHMENT_CHOICE_CHOOSE_VIDEO); - break; - case R.id.attach_take_picture: - attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO); - break; - case R.id.attach_record_video: - attachFile(ATTACHMENT_CHOICE_RECORD_VIDEO); - break; - case R.id.attach_choose_file: - attachFile(ATTACHMENT_CHOICE_CHOOSE_FILE); - break; - case R.id.attach_record_voice: - attachFile(ATTACHMENT_CHOICE_RECORD_VOICE); - break; - case R.id.attach_location: - attachFile(ATTACHMENT_CHOICE_LOCATION); - break; - } - } - - private void handleEncryptionSelection(MenuItem item) { - if (conversation == null) { - return; - } - final boolean updated; - switch (item.getItemId()) { - case R.id.encryption_choice_none: - updated = conversation.setNextEncryption(Message.ENCRYPTION_NONE); - item.setChecked(true); - break; - case R.id.encryption_choice_otr: - updated = conversation.setNextEncryption(Message.ENCRYPTION_OTR); - item.setChecked(true); - break; - case R.id.encryption_choice_pgp: - if (activity.hasPgp()) { - if (conversation.getAccount().getPgpSignature() != null) { - updated = conversation.setNextEncryption(Message.ENCRYPTION_PGP); - item.setChecked(true); - } else { - updated = false; - activity.announcePgp(conversation.getAccount(), conversation, null, activity.onOpenPGPKeyPublished); - } - } else { - activity.showInstallPgpDialog(); - updated = false; - } - break; - case R.id.encryption_choice_axolotl: - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(conversation.getAccount()) - + "Enabled axolotl for Contact " + conversation.getContact().getJid()); - updated = conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL); - item.setChecked(true); - break; - default: - updated = conversation.setNextEncryption(Message.ENCRYPTION_NONE); - break; - } - if (updated) { - activity.xmppConnectionService.updateConversation(conversation); - } - updateChatMsgHint(); - getActivity().invalidateOptionsMenu(); - activity.refreshUi(); - } - - public void attachFile(final int attachmentChoice) { - attachFile(attachmentChoice, true); - } - - public void attachFile(final int attachmentChoice, final boolean updateRecentlyUsed) { - if (attachmentChoice == ATTACHMENT_CHOICE_RECORD_VOICE) { - if (!hasPermissions(attachmentChoice, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO)) { - return; - } - } else if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO || attachmentChoice == ATTACHMENT_CHOICE_RECORD_VIDEO) { - if (!hasPermissions(attachmentChoice, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA)) { - return; - } - } else if (attachmentChoice == ATTACHMENT_CHOICE_LOCATION) { - if (!hasPermissions(attachmentChoice, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)) { - return; - } - } else if (attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_FILE || attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_IMAGE || attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_VIDEO) { - if (!hasPermissions(attachmentChoice, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)) { - return; - } - } - if (updateRecentlyUsed) { - storeRecentlyUsedQuickAction(attachmentChoice); - } - final int encryption = conversation.getNextEncryption(); - final int mode = conversation.getMode(); - if (encryption == Message.ENCRYPTION_PGP) { - if (activity.hasPgp()) { - if (mode == Conversation.MODE_SINGLE && conversation.getContact().getPgpKeyId() != 0) { - activity.xmppConnectionService.getPgpEngine().hasKey( - conversation.getContact(), - new UiCallback() { - - @Override - public void userInputRequired(PendingIntent pi, Contact contact) { - startPendingIntent(pi, attachmentChoice); - } - - @Override - public void progress(int progress) { - - } - - @Override - public void success(Contact contact) { - invokeAttachFileIntent(attachmentChoice); - } - - @Override - public void error(int error, Contact contact) { - activity.replaceToast(getString(error)); - } - }); - } else if (mode == Conversation.MODE_MULTI && conversation.getMucOptions().pgpKeysInUse()) { - if (!conversation.getMucOptions().everybodyHasKeys()) { - getActivity().runOnUiThread(() -> { - Toast warning = ToastCompat.makeText(activity, R.string.missing_public_keys, ToastCompat.LENGTH_LONG); - warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0); - warning.show(); - }); - } - invokeAttachFileIntent(attachmentChoice); - } else { - showNoPGPKeyDialog(false, (dialog, which) -> { - conversation.setNextEncryption(Message.ENCRYPTION_NONE); - activity.xmppConnectionService.updateConversation(conversation); - invokeAttachFileIntent(attachmentChoice); - }); - } - } else { - activity.showInstallPgpDialog(); - } - } else { - invokeAttachFileIntent(attachmentChoice); - } - } - - private void storeRecentlyUsedQuickAction(final int attachmentChoice) { - try { - activity.getPreferences().edit() - .putString(RECENTLY_USED_QUICK_ACTION, SendButtonAction.of(attachmentChoice).toString()) - .apply(); - } catch (IllegalArgumentException e) { - //just do not save - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - final PermissionUtils.PermissionResult permissionResult = - PermissionUtils.removeBluetoothConnect(permissions, grantResults); - if (grantResults.length > 0) { - if (allGranted(permissionResult.grantResults)) { - Activity mXmppActivity = getActivity(); - switch (requestCode) { - case REQUEST_START_DOWNLOAD: - if (this.mPendingDownloadableMessage != null) { - startDownloadable(this.mPendingDownloadableMessage); - } - break; - case REQUEST_ADD_EDITOR_CONTENT: - if (this.mPendingEditorContent != null) { - attachEditorContentToConversation(this.mPendingEditorContent); - } - break; - case REQUEST_COMMIT_ATTACHMENTS: - commitAttachments(); - break; - case REQUEST_START_AUDIO_CALL: - if (mXmppActivity instanceof XmppActivity) { - CallManager.triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VOICE_CALL, (XmppActivity) mXmppActivity, conversation); - } - break; - case REQUEST_START_VIDEO_CALL: - if (mXmppActivity instanceof XmppActivity) { - CallManager.triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL, (XmppActivity) mXmppActivity, conversation); - } - break; - default: - attachFile(requestCode); - break; - } - } else { - @StringRes int res; - String firstDenied = getFirstDenied(grantResults, permissions); - if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) { - res = R.string.no_microphone_permission; - } else if (Manifest.permission.CAMERA.equals(firstDenied)) { - res = R.string.no_camera_permission; - } else if (Manifest.permission.ACCESS_COARSE_LOCATION.equals(firstDenied) - || Manifest.permission.ACCESS_FINE_LOCATION.equals(firstDenied)) { - res = R.string.no_location_permission; - } else { - res = R.string.no_storage_permission; - } - ToastCompat.makeText(getActivity(), res, ToastCompat.LENGTH_SHORT).show(); - } - } - if (readGranted(grantResults, permissions)) { - if (activity != null && activity.xmppConnectionService != null) { - activity.xmppConnectionService.getBitmapCache().evictAll(); - activity.xmppConnectionService.restartFileObserver(); - } - refresh(); - } - } - - private void updateChatBG() { - if (activity != null) { - if (activity.unicoloredBG()) { - binding.conversationsFragment.setBackgroundResource(0); - binding.conversationsFragment.setBackgroundColor(StyledAttributes.getColor(activity, R.attr.color_background_tertiary)); - } else { - binding.conversationsFragment.setBackground(ContextCompat.getDrawable(activity, R.drawable.chatbg)); - } - } - } - - public void startDownloadable(Message message) { - if (!hasPermissions(REQUEST_START_DOWNLOAD, Manifest.permission.WRITE_EXTERNAL_STORAGE) && !hasPermissions(REQUEST_START_DOWNLOAD, Manifest.permission.READ_EXTERNAL_STORAGE)) { - this.mPendingDownloadableMessage = message; - return; - } - Transferable transferable = message.getTransferable(); - if (transferable != null) { - if (transferable instanceof TransferablePlaceholder && message.hasFileOnRemoteHost()) { - createNewConnection(message); - return; - } - if (!transferable.start()) { - Log.d(Config.LOGTAG, "type: " + transferable.getClass().getName()); - ToastCompat.makeText(getActivity(), R.string.not_connected_try_again, ToastCompat.LENGTH_SHORT).show(); - } - } else if (message.treatAsDownloadable() || message.hasFileOnRemoteHost() || MessageUtils.unInitiatedButKnownSize(message)) { - createNewConnection(message); - } else { - Log.d(Config.LOGTAG, message.getConversation().getAccount() + ": unable to start downloadable"); - } - } - - private void createNewConnection(final Message message) { - if (!activity.xmppConnectionService.hasInternetConnection()) { - ToastCompat.makeText(getActivity(), R.string.not_connected_try_again, ToastCompat.LENGTH_SHORT).show(); - return; - } - activity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message, true); - } - - private OnClickListener OTRwarning = new OnClickListener() { - @Override - public void onClick(View v) { - try { - final Uri uri = Uri.parse("https://monocles.wiki/index.php?title=Monocles_Chat"); - Intent browserIntent = new Intent(Intent.ACTION_VIEW, uri); - startActivity(browserIntent); - } catch (Exception e) { - ToastCompat.makeText(activity, R.string.no_application_found_to_open_link, Toast.LENGTH_SHORT).show(); - } - } - }; - - - @SuppressLint("InflateParams") - protected void clearHistoryDialog(final Conversation conversation) { - final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()); - builder.setTitle(getString(R.string.clear_conversation_history)); - final View dialogView = requireActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null); - final CheckBox endConversationCheckBox = dialogView.findViewById(R.id.end_conversation_checkbox); - if (conversation.getMode() == Conversation.MODE_SINGLE) { - endConversationCheckBox.setVisibility(View.VISIBLE); - endConversationCheckBox.setChecked(true); - } - builder.setView(dialogView); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.confirm), (dialog, which) -> { - this.activity.xmppConnectionService.clearConversationHistory(conversation); - if (endConversationCheckBox.isChecked()) { - this.activity.xmppConnectionService.archiveConversation(conversation); - } else { - activity.onConversationsListItemUpdated(); - refresh(); - } - }); - builder.create().show(); - } - protected void muteConversationDialog(final Conversation conversation) { - final AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(R.string.disable_notifications); - final int[] durations = activity.getResources().getIntArray(R.array.mute_options_durations); - final CharSequence[] labels = new CharSequence[durations.length]; - for (int i = 0; i < durations.length; ++i) { - if (durations[i] == -1) { - labels[i] = activity.getString(R.string.until_further_notice); - } else { - labels[i] = TimeFrameUtils.resolve(activity, 1000L * durations[i]); - } - } - builder.setItems(labels, (dialog, which) -> { - final long till; - if (durations[which] == -1) { - till = Long.MAX_VALUE; - } else { - till = System.currentTimeMillis() + (durations[which] * 1000L); - } - conversation.setMutedTill(till); - activity.xmppConnectionService.updateConversation(conversation); - activity.onConversationsListItemUpdated(); - refresh(); - activity.invalidateOptionsMenu(); - }); - builder.create().show(); - } - - private boolean hasPermissions(int requestCode, List permissions) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - final List missingPermissions = new ArrayList<>(); - for (String permission : permissions) { - if (Config.ONLY_INTERNAL_STORAGE - && permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - continue; - } - if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { - missingPermissions.add(permission); - } - } - if (missingPermissions.size() == 0) { - return true; - } else { - requestPermissions( - missingPermissions.toArray(new String[0]), - requestCode); - return false; - } - } else { - return true; - } - } - private boolean hasPermissions(int requestCode, String... permissions) { - return hasPermissions(requestCode, ImmutableList.copyOf(permissions)); - } - public void unmuteConversation(final Conversation conversation) { - conversation.setMutedTill(0); - this.activity.xmppConnectionService.updateConversation(conversation); - this.activity.onConversationsListItemUpdated(); - refresh(); - this.activity.invalidateOptionsMenu(); - } - protected void invokeAttachFileIntent(final int attachmentChoice) { - Intent intent = new Intent(); - boolean chooser = false; - switch (attachmentChoice) { - case ATTACHMENT_CHOICE_CHOOSE_IMAGE: - intent.setAction(Intent.ACTION_GET_CONTENT); - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); - intent.setType("image/*"); - chooser = true; - break; - case ATTACHMENT_CHOICE_CHOOSE_VIDEO: - chooser = true; - intent.setType("video/*"); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setAction(Intent.ACTION_GET_CONTENT); - break; - case ATTACHMENT_CHOICE_RECORD_VIDEO: - if (Compatibility.runsThirty()) { - final List cameraApps = CameraUtils.getCameraApps(activity); - if (cameraApps.size() == 0) { - ToastCompat.makeText(activity, R.string.no_application_found, ToastCompat.LENGTH_LONG).show(); - } else if (cameraApps.size() == 1) { - getCameraApp(cameraApps.get(0)); - } else { - if (!activity.getPreferences().contains(SettingsActivity.CAMERA_CHOICE)) { - showCameraChooser(activity, cameraApps); - } else { - intent.setComponent(getCameraApp(cameraApps.get(activity.getPreferences().getInt(SettingsActivity.CAMERA_CHOICE, 0)))); - } - } - } - intent.setAction(MediaStore.ACTION_VIDEO_CAPTURE); - break; - case ATTACHMENT_CHOICE_TAKE_PHOTO: - final Uri photoUri = activity.xmppConnectionService.getFileBackend().getTakePhotoUri(); - pendingTakePhotoUri.push(photoUri); - if (Compatibility.runsThirty()) { - final List cameraApps = CameraUtils.getCameraApps(activity); - if (cameraApps.size() == 0) { - ToastCompat.makeText(activity, R.string.no_application_found, ToastCompat.LENGTH_LONG).show(); - } else if (cameraApps.size() == 1) { - getCameraApp(cameraApps.get(0)); - } else { - if (!activity.getPreferences().contains(SettingsActivity.CAMERA_CHOICE)) { - showCameraChooser(activity, cameraApps); - } else { - intent.setComponent(getCameraApp(cameraApps.get(activity.getPreferences().getInt(SettingsActivity.CAMERA_CHOICE, 0)))); - } - } - } - intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri); - intent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION & Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); - break; - case ATTACHMENT_CHOICE_CHOOSE_FILE: - chooser = true; - intent.setType("*/*"); - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setAction(Intent.ACTION_GET_CONTENT); - break; - case ATTACHMENT_CHOICE_RECORD_VOICE: - intent = new Intent(getActivity(), RecordingActivity.class); - intent.putExtra("ALTERNATIVE_CODEC", activity.xmppConnectionService.alternativeVoiceSettings()); - break; - case ATTACHMENT_CHOICE_LOCATION: - intent = GeoHelper.getFetchIntent(activity); - break; - } - final Context context = getActivity(); - if (context == null) { - return; - } - try { - Log.d(Config.LOGTAG, "Attachment: " + attachmentChoice); - if (chooser) { - startActivityForResult( - Intent.createChooser(intent, getString(R.string.perform_action_with)), - attachmentChoice); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } else { - startActivityForResult(intent, attachmentChoice); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - } catch (final ActivityNotFoundException e) { - - if (attachmentChoice == ATTACHMENT_CHOICE_RECORD_VIDEO - || attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO - || attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_FILE - || attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_IMAGE - || attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_VIDEO){ - ToastCompat.makeText(context, R.string.no_application_found, ToastCompat.LENGTH_LONG).show(); - } - } - } - - @Override - public void onResume() { - super.onResume(); - updateChatBG(); - disableEncrpytionForExceptions(); - binding.messagesView.post(this::fireReadEvent); - } - private void disableEncrpytionForExceptions() { - if (isEncryptionDisabledException()) { - disableMessageEncryption(); - } - } - - private boolean isEncryptionDisabledException() { - if (conversation != null) { - return ENCRYPTION_EXCEPTIONS.contains(conversation.getJid().toString()); - } - return false; - } - private void fireReadEvent() { - if (activity != null && this.conversation != null) { - String uuid = getLastVisibleMessageUuid(); - if (uuid != null) { - activity.onConversationRead(this.conversation, uuid); - } - } - } - - private String getLastVisibleMessageUuid() { - if (binding == null) { - return null; - } - synchronized (this.messageList) { - int pos = binding.messagesView.getLastVisiblePosition(); - if (pos >= 0) { - Message message = null; - for (int i = pos; i >= 0; --i) { - try { - message = (Message) binding.messagesView.getItemAtPosition(i); - } catch (IndexOutOfBoundsException e) { - //should not happen if we synchronize properly. however if that fails we just gonna try item -1 - continue; - } - if (message.getType() != Message.TYPE_STATUS) { - break; - } - } - if (message != null) { - while (message.next() != null && message.next().wasMergedIntoPrevious()) { - message = message.next(); - } - return message.getUuid(); - } - } - } - return null; - } - - private void openWith(final Message message) { - if (message.isGeoUri()) { - GeoHelper.view(getActivity(), message); - } else { - final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); - ViewUtil.view(activity, file); - } - } - - private void showErrorMessage(final Message message) { - final 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"); - final String displayError; - if (errorMessageParts.length == 2) { - displayError = errorMessageParts[1]; - } else { - displayError = errorMessage; - } - builder.setMessage(displayError); - builder.setNegativeButton(R.string.copy_to_clipboard, (dialog, which) -> { - activity.copyTextToClipboard(displayError, R.string.error_message); - ToastCompat.makeText(activity, R.string.error_message_copied_to_clipboard, ToastCompat.LENGTH_SHORT).show(); - }); - builder.setPositiveButton(R.string.ok, null); - builder.create().show(); - } - - private void deleteMessage(final Message message) { - - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setNegativeButton(R.string.cancel, null); - builder.setTitle(R.string.delete_message_dialog); - builder.setMessage(R.string.delete_message_dialog_msg); - - final Message finalMessage = message; - - builder.setPositiveButton(R.string.confirm, (dialog, which) -> { - - if (finalMessage.getType() == Message.TYPE_TEXT - && !finalMessage.isGeoUri() - && finalMessage.getConversation() instanceof Conversation) { - - Message retractedMessage = finalMessage; - retractedMessage.setMessageDeleted(true); - - long time = System.currentTimeMillis(); - Message retractmessage = new Message(conversation, - "This person attempted to retract a previous message, but it's unsupported by your client.", - Message.ENCRYPTION_NONE, - Message.STATUS_SEND); - if (retractedMessage.getEditedList().size() > 0) { - retractmessage.setRetractId(retractedMessage.getEditedList().get(0).getEditedId()); - } else { - retractmessage.setRetractId(retractedMessage.getRemoteMsgId() != null ? retractedMessage.getRemoteMsgId() : retractedMessage.getUuid()); - } - - retractedMessage.putEdited(retractedMessage.getUuid(), retractedMessage.getServerMsgId(), retractedMessage.getBody(), retractedMessage.getTimeSent()); - retractedMessage.setBody(Message.DELETED_MESSAGE_BODY); - retractedMessage.setServerMsgId(null); - retractedMessage.setRemoteMsgId(message.getRemoteMsgId()); - retractedMessage.setMessageDeleted(true); - - retractmessage.setType(Message.TYPE_TEXT); - retractmessage.setCounterpart(message.getCounterpart()); - retractmessage.setTrueCounterpart(message.getTrueCounterpart()); - retractmessage.setTime(time); - retractmessage.setUuid(UUID.randomUUID().toString()); - retractmessage.setCarbon(false); - retractmessage.setOob(false); - retractmessage.setRemoteMsgId(retractmessage.getUuid()); - retractmessage.setMessageDeleted(true); - retractedMessage.setTime(time); //set new time here to keep orginal timestamps - for (Edit itm : retractedMessage.getEditedList()) { - Message tmpRetractedMessage = conversation.findMessageWithUuidOrRemoteId(itm.getEditedId()); - if (tmpRetractedMessage != null) { - tmpRetractedMessage.setMessageDeleted(true); - activity.xmppConnectionService.updateMessage(tmpRetractedMessage, tmpRetractedMessage.getUuid()); - } - } - activity.xmppConnectionService.updateMessage(retractedMessage, retractedMessage.getUuid()); - if (finalMessage.getStatus() >= Message.STATUS_SEND) { - //only send retraction messages vor outgoing messages! - sendMessage(retractmessage); - } - activity.xmppConnectionService.deleteMessage(conversation, retractedMessage); - activity.xmppConnectionService.deleteMessage(conversation, retractmessage); - } - activity.xmppConnectionService.deleteMessage(conversation, message); - activity.onConversationsListItemUpdated(); - refresh(); - }); - builder.create().show(); - } - - private void deleteFile(final Message message) { - final 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); - builder.setPositiveButton(R.string.confirm, (dialog, which) -> { - if (activity.xmppConnectionService.getFileBackend().deleteFile(message)) { - message.setFileDeleted(true); - activity.xmppConnectionService.evictPreview(message.getUuid()); - activity.xmppConnectionService.updateMessage(message, false); - activity.onConversationsListItemUpdated(); - refresh(); - } - }); - builder.create().show(); - } - - public void resendMessage(final Message message) { - if (message != null && message.isFileOrImage()) { - if (!(message.getConversation() instanceof Conversation)) { - return; - } - final Conversation conversation = (Conversation) message.getConversation(); - final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); - if ((file.exists() && file.canRead()) || message.hasFileOnRemoteHost()) { - final XmppConnection xmppConnection = conversation.getAccount().getXmppConnection(); - if (!message.hasFileOnRemoteHost() - && xmppConnection != null - && conversation.getMode() == Conversational.MODE_SINGLE - && !xmppConnection.getFeatures().httpUpload(message.getFileParams().getSize())) { - activity.selectPresence(conversation, () -> { - message.setCounterpart(conversation.getNextCounterpart()); - activity.xmppConnectionService.resendFailedMessages(message); - new Handler().post(() -> { - int size = messageList.size(); - this.binding.messagesView.setSelection(size - 1); - }); - }); - return; - } - } else if (!Compatibility.hasStoragePermission(getActivity())) { - ToastCompat.makeText(activity, R.string.no_storage_permission, ToastCompat.LENGTH_SHORT).show(); - return; - } else { - ToastCompat.makeText(activity, R.string.file_deleted, ToastCompat.LENGTH_SHORT).show(); - message.setFileDeleted(true); - activity.xmppConnectionService.updateMessage(message, false); - activity.onConversationsListItemUpdated(); - refresh(); - return; - } - } - activity.xmppConnectionService.resendFailedMessages(message); - new Handler().post(() -> { - int size = messageList.size(); - this.binding.messagesView.setSelection(size - 1); - }); - } - - private void copyUrl(Message message) { - final String url; - final int resId; - if (message.isGeoUri()) { - resId = R.string.location; - url = message.getBody(); - } else if (message.isXmppUri()) { - resId = R.string.contact; - url = message.getBody(); - } else if (message.hasFileOnRemoteHost()) { - resId = R.string.file_url; - url = message.getFileParams().url.toString(); - } else { - url = message.getBody().trim(); - resId = R.string.file_url; - } - if (activity.copyTextToClipboard(url, resId)) { - ToastCompat.makeText(getActivity(), R.string.url_copied_to_clipboard, - ToastCompat.LENGTH_SHORT).show(); - } - } - - public void cancelTransmission(Message message) { - Transferable transferable = message.getTransferable(); - if (transferable != null) { - transferable.cancel(); - } else if (message.getStatus() != Message.STATUS_RECEIVED) { - activity.xmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, Message.ERROR_MESSAGE_CANCELLED); - } - } - - private void retryDecryption(Message message) { - message.setEncryption(Message.ENCRYPTION_PGP); - activity.onConversationsListItemUpdated(); - refresh(); - conversation.getAccount().getPgpDecryptionService().decrypt(message, false); - } - - public void privateMessageWith(final Jid counterpart) { - try { - final Jid tcp = conversation.getMucOptions().getTrueCounterpart(counterpart); - if (!getConversation().getMucOptions().isUserInRoom(counterpart) && getConversation().getMucOptions().findUserByRealJid(tcp == null ? null : tcp.asBareJid()) == null) { - ToastCompat.makeText(getActivity(), activity.getString(R.string.user_has_left_conference, counterpart.getResource()), ToastCompat.LENGTH_SHORT).show(); - return; - } - if (conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) { - activity.xmppConnectionService.sendChatState(conversation); - } - this.binding.textinput.setText(""); - this.conversation.setNextCounterpart(counterpart); - } catch (Exception e) { - e.printStackTrace(); - ToastCompat.makeText(getActivity(), activity.getString(R.string.user_has_left_conference, activity.getString(R.string.user)), ToastCompat.LENGTH_SHORT).show(); - } finally { - updateChatMsgHint(); - updateSendButton(); - updateEditablity(); - } - } - - private void correctMessage(Message message) { - while (message.mergeable(message.next())) { - message = message.next(); - } - this.conversation.setCorrectingMessage(message); - final Editable editable = binding.textinput.getText(); - this.conversation.setDraftMessage(editable.toString()); - this.binding.textinput.setText(""); - this.binding.textinput.append(message.getBody()); - - } - - private void highlightInConference(String nick) { - final Editable editable = this.binding.textinput.getText(); - String oldString = editable.toString().trim(); - final int pos = this.binding.textinput.getSelectionStart(); - if (oldString.isEmpty() || pos == 0) { - editable.insert(0, nick + ": "); - } else { - final char before = editable.charAt(pos - 1); - final char after = editable.length() > pos ? editable.charAt(pos) : '\0'; - if (before == '\n') { - editable.insert(pos, nick + ": "); - } else { - if (pos > 2 && editable.subSequence(pos - 2, pos).toString().equals(": ")) { - if (NickValidityChecker.check(conversation, Arrays.asList(editable.subSequence(0, pos - 2).toString().split(", ")))) { - editable.insert(pos - 2, ", " + nick); - return; - } - } - editable.insert(pos, (Character.isWhitespace(before) ? "" : " ") + nick + (Character.isWhitespace(after) ? "" : " ")); - if (Character.isWhitespace(after)) { - this.binding.textinput.setSelection(this.binding.textinput.getSelectionStart() + 1); - } - } - } - } - - @Override - public void startActivityForResult(Intent intent, int requestCode) { - final Activity activity = getActivity(); - if (activity instanceof ConversationsActivity) { - ((ConversationsActivity) activity).clearPendingViewIntent(); - } - super.startActivityForResult(intent, requestCode); - } - - @Override - public void onSaveInstanceState(@NotNull Bundle outState) { - super.onSaveInstanceState(outState); - if (conversation != null) { - outState.putString(STATE_CONVERSATION_UUID, conversation.getUuid()); - outState.putString(STATE_LAST_MESSAGE_UUID, lastMessageUuid); - final Uri uri = pendingTakePhotoUri.peek(); - if (uri != null) { - outState.putString(STATE_PHOTO_URI, uri.toString()); - } - final ScrollState scrollState = getScrollPosition(); - if (scrollState != null) { - outState.putParcelable(STATE_SCROLL_POSITION, scrollState); - } - final ArrayList attachments = mediaPreviewAdapter == null ? new ArrayList<>() : mediaPreviewAdapter.getAttachments(); - if (attachments.size() > 0) { - outState.putParcelableArrayList(STATE_MEDIA_PREVIEWS, attachments); - } - } - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - if (savedInstanceState == null) { - return; - } - String uuid = savedInstanceState.getString(STATE_CONVERSATION_UUID); - ArrayList attachments = savedInstanceState.getParcelableArrayList(STATE_MEDIA_PREVIEWS); - pendingLastMessageUuid.push(savedInstanceState.getString(STATE_LAST_MESSAGE_UUID, null)); - if (uuid != null) { - QuickLoader.set(uuid); - this.pendingConversationsUuid.push(uuid); - if (attachments != null && attachments.size() > 0) { - this.pendingMediaPreviews.push(attachments); - } - String takePhotoUri = savedInstanceState.getString(STATE_PHOTO_URI); - if (takePhotoUri != null) { - pendingTakePhotoUri.push(Uri.parse(takePhotoUri)); - } - pendingScrollState.push(savedInstanceState.getParcelable(STATE_SCROLL_POSITION)); - } - } - - @Override - public void onStart() { - super.onStart(); - updateChatBG(); - disableEncrpytionForExceptions(); - if (this.reInitRequiredOnStart && this.conversation != null) { - final Bundle extras = pendingExtras.pop(); - reInit(this.conversation, extras != null); - if (extras != null) { - processExtras(extras); - } - } else if (conversation == null && activity != null && activity.xmppConnectionService != null) { - final String uuid = pendingConversationsUuid.pop(); - Log.d(Config.LOGTAG, "ConversationFragment.onStart() - activity was bound but no conversation loaded. uuid=" + uuid); - if (uuid != null) { - findAndReInitByUuidOrArchive(uuid); - } - } - } - - @Override - public void onStop() { - super.onStop(); - final Activity activity = getActivity(); - messageListAdapter.unregisterListenerInAudioPlayer(); - if (activity == null || !activity.isChangingConfigurations()) { - hideSoftKeyboard(activity); - messageListAdapter.stopAudioPlayer(); - } - if (this.conversation != null) { - final String msg = this.binding.textinput.getText().toString(); - storeNextMessage(msg); - updateChatState(this.conversation, msg); - this.activity.xmppConnectionService.getNotificationService().setOpenConversation(null); - } - this.reInitRequiredOnStart = true; - } - - private void updateChatState(final Conversation conversation, final String msg) { - ChatState state = msg.length() == 0 ? Config.DEFAULT_CHAT_STATE : ChatState.PAUSED; - Account.State status = conversation.getAccount().getStatus(); - if (status == Account.State.ONLINE && conversation.setOutgoingChatState(state)) { - activity.xmppConnectionService.sendChatState(conversation); - } - } - - private void saveMessageDraftStopAudioPlayer() { - final Conversation previousConversation = this.conversation; - if (this.activity == null || this.binding == null || previousConversation == null) { - return; - } - Log.d(Config.LOGTAG, "ConversationFragment.saveMessageDraftStopAudioPlayer()"); - final String msg = this.binding.textinput.getText().toString(); - storeNextMessage(msg); - updateChatState(this.conversation, msg); - messageListAdapter.stopAudioPlayer(); - mediaPreviewAdapter.clearPreviews(); - toggleInputMethod(); - } - - public void reInit(final Conversation conversation, final Bundle extras) { - QuickLoader.set(conversation.getUuid()); - final boolean changedConversation = this.conversation != conversation; - if (changedConversation) { - this.saveMessageDraftStopAudioPlayer(); - } - this.clearPending(); - if (this.reInit(conversation, extras != null)) { - if (extras != null) { - processExtras(extras); - } - this.reInitRequiredOnStart = false; - } else { - this.reInitRequiredOnStart = true; - pendingExtras.push(extras); - } - resetUnreadMessagesCount(); - } - - private void reInit(Conversation conversation) { - reInit(conversation, false); - } - - private boolean reInit(final Conversation conversation, final boolean hasExtras) { - if (conversation == null) { - return false; - } - this.conversation = conversation; - //once we set the conversation all is good and it will automatically do the right thing in onStart() - if (this.activity == null || this.binding == null) { - return false; - } - - if (!activity.xmppConnectionService.isConversationStillOpen(this.conversation)) { - activity.onConversationArchived(this.conversation); - return false; - } - - stopScrolling(); - Log.d(Config.LOGTAG, "reInit(hasExtras=" + Boolean.toString(hasExtras) + ")"); - - if (this.conversation.isRead() && hasExtras) { - Log.d(Config.LOGTAG, "trimming conversation"); - this.conversation.trim(); - } - - setupIme(); - - final boolean scrolledToBottomAndNoPending = this.scrolledToBottom() && pendingScrollState.peek() == null; - - this.binding.textSendButton.setContentDescription(activity.getString(R.string.send_message_to_x, conversation.getName())); - this.binding.textinput.setKeyboardListener(null); - showRecordVoiceButton(); - final boolean participating = conversation.getMode() == Conversational.MODE_SINGLE || conversation.getMucOptions().participating(); - if (participating) { - this.binding.textinput.setText(this.conversation.getNextMessage()); - this.binding.textinput.setSelection(this.binding.textinput.length()); - } else { - this.binding.textinput.setText(MessageUtils.EMPTY_STRING); - } - this.binding.textinput.setKeyboardListener(this); - messageListAdapter.updatePreferences(); - refresh(false); - activity.invalidateOptionsMenu(); - this.conversation.messagesLoaded.set(true); - hasWriteAccessInMUC(); - Log.d(Config.LOGTAG, "scrolledToBottomAndNoPending=" + Boolean.toString(scrolledToBottomAndNoPending)); - - if (hasExtras || scrolledToBottomAndNoPending) { - resetUnreadMessagesCount(); - synchronized (this.messageList) { - Log.d(Config.LOGTAG, "jump to first unread message"); - final Message first = conversation.getFirstUnreadMessage(); - final int bottom = Math.max(0, this.messageList.size() - 1); - final int pos; - final boolean jumpToBottom; - if (first == null) { - pos = bottom; - jumpToBottom = true; - } else { - int i = getIndexOf(first.getUuid(), this.messageList); - pos = i < 0 ? bottom : i; - jumpToBottom = false; - } - setSelection(pos, jumpToBottom); - } - } - - this.binding.messagesView.post(this::fireReadEvent); - //TODO if we only do this when this fragment is running on main it won't *bing* in tablet layout which might be unnecessary since we can *see* it - activity.xmppConnectionService.getNotificationService().setOpenConversation(this.conversation); - return true; - } - - private void hasWriteAccessInMUC() { - if ((conversation.getMode() == Conversation.MODE_MULTI && !conversation.getMucOptions().participating()) && !activity.xmppConnectionService.hideYouAreNotParticipating()) { - activity.runOnUiThread(() -> { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(getString(R.string.you_are_not_participating)); - builder.setMessage(getString(R.string.no_write_access_in_public_muc)); - builder.setNegativeButton(getString(R.string.hide_warning), - (dialog, which) -> { - SharedPreferences preferences = activity.getPreferences(); - preferences.edit().putBoolean(HIDE_YOU_ARE_NOT_PARTICIPATING, true).apply(); - hideSnackbar(); - }); - builder.setPositiveButton(getString(R.string.ok), - (dialog, which) -> { - try { - Intent intent = new Intent(getActivity(), ConferenceDetailsActivity.class); - intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); - intent.putExtra("uuid", conversation.getUuid()); - startActivity(intent); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } catch (Exception e) { - e.printStackTrace(); - } - }); - builder.create().show(); - }); - showSnackbar(R.string.no_write_access_in_public_muc, R.string.ok, clickToMuc); - } - } - - private void resetUnreadMessagesCount() { - lastMessageUuid = null; - hideUnreadMessagesCount(); - } - - private void hideUnreadMessagesCount() { - if (this.binding == null) { - return; - } - this.binding.scrollToBottomButton.setEnabled(false); - this.binding.scrollToBottomButton.hide(); - this.binding.unreadCountCustomView.setVisibility(View.GONE); - } - - private void setSelection(int pos, boolean jumpToBottom) { - ListViewUtils.setSelection(this.binding.messagesView, pos, jumpToBottom); - this.binding.messagesView.post(() -> ListViewUtils.setSelection(this.binding.messagesView, pos, jumpToBottom)); - this.binding.messagesView.post(this::fireReadEvent); - } - - private boolean scrolledToBottom() { - return this.binding != null && scrolledToBottom(this.binding.messagesView); - } - - private void processExtras(final Bundle extras) { - final String downloadUuid = extras.getString(ConversationsActivity.EXTRA_DOWNLOAD_UUID); - final String text = extras.getString(Intent.EXTRA_TEXT); - final String nick = extras.getString(ConversationsActivity.EXTRA_NICK); - final String postInitAction = extras.getString(ConversationsActivity.EXTRA_POST_INIT_ACTION); - final boolean asQuote = extras.getBoolean(ConversationsActivity.EXTRA_AS_QUOTE); - final String user = extras.getString(ConversationsActivity.EXTRA_USER); - 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, type)); - } - toggleInputMethod(); - return; - } - if (nick != null) { - if (pm) { - Jid jid = conversation.getJid(); - try { - Jid next = Jid.of(jid.getLocal(), jid.getDomain(), nick); - privateMessageWith(next); - } catch (final IllegalArgumentException ignored) { - //do nothing - } - } else { - final MucOptions mucOptions = conversation.getMucOptions(); - if (mucOptions.participating() || conversation.getNextCounterpart() != null) { - highlightInConference(nick); - } - } - } else { - if (text != null && GeoHelper.GEO_URI.matcher(text).matches()) { - mediaPreviewAdapter.addMediaPreviews(Attachment.of(getActivity(), Uri.parse(text), Attachment.Type.LOCATION)); - toggleInputMethod(); - return; - } else if (text != null && asQuote) { - quoteText(text, user); - } else { - appendText(text, doNotAppend); - } - } - if (ConversationsActivity.POST_ACTION_RECORD_VOICE.equals(postInitAction)) { - attachFile(ATTACHMENT_CHOICE_RECORD_VOICE, false); - return; - } - final Message message = downloadUuid == null ? null : conversation.findMessageWithFileAndUuid(downloadUuid); - if (message != null) { - startDownloadable(message); - } - } - - private List extractUris(final Bundle extras) { - final List uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM); - if (uris != null) { - return uris; - } - final Uri uri = extras.getParcelable(Intent.EXTRA_STREAM); - if (uri != null) { - return Collections.singletonList(uri); - } else { - return null; - } - } - - private List cleanUris(final List uris) { - final Iterator iterator = uris.iterator(); - while (iterator.hasNext()) { - final Uri uri = iterator.next(); - if (FileBackend.weOwnFile(uri)) { - iterator.remove(); - ToastCompat.makeText(getActivity(), R.string.security_violation_not_attaching_file, ToastCompat.LENGTH_SHORT).show(); - } - } - return uris; - } - - private boolean showBlockSubmenu(View view) { - final Jid jid = conversation.getJid(); - final boolean showReject = conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); - PopupMenu popupMenu = new PopupMenu(getActivity(), view); - popupMenu.inflate(R.menu.block); - popupMenu.getMenu().findItem(R.id.block_contact).setVisible(jid.getLocal() != null); - popupMenu.getMenu().findItem(R.id.reject).setVisible(showReject); - popupMenu.setOnMenuItemClickListener(menuItem -> { - Blockable blockable; - switch (menuItem.getItemId()) { - case R.id.reject: - activity.xmppConnectionService.stopPresenceUpdatesTo(conversation.getContact()); - updateSnackBar(conversation); - return true; - case R.id.block_domain: - blockable = conversation.getAccount().getRoster().getContact(jid.getDomain()); - break; - default: - blockable = conversation; - } - BlockContactDialog.show(activity, blockable); - return true; - }); - popupMenu.show(); - return true; - } - - private void updateSnackBar(final Conversation conversation) { - if (conversation == null) { - return; - } - final Account account = conversation.getAccount(); - final XmppConnection connection = account.getXmppConnection(); - final int mode = conversation.getMode(); - final Contact contact = mode == Conversation.MODE_SINGLE ? conversation.getContact() : null; - if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) { - return; - } - if (account.getStatus() == Account.State.DISABLED) { - showSnackbar(R.string.this_account_is_disabled, R.string.enable, this.mEnableAccountListener); - } else if (conversation.isBlocked()) { - showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener); - } else if (contact != null && !contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { - showSnackbar(R.string.contact_added_you, R.string.add_back, this.mAddBackClickListener, this.mLongPressBlockListener); - } else if (contact != null && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { - showSnackbar(R.string.contact_asks_for_presence_subscription, R.string.allow, this.mAllowPresenceSubscription, this.mLongPressBlockListener); - } else if (mode == Conversation.MODE_MULTI - && !conversation.getMucOptions().online() - && account.getStatus() == Account.State.ONLINE) { - switch (conversation.getMucOptions().getError()) { - case NICK_IN_USE: - showSnackbar(R.string.nick_in_use, R.string.edit, clickToMuc); - break; - case NO_RESPONSE: - showSnackbar(R.string.joining_conference, 0, null); - break; - case SERVER_NOT_FOUND: - if (conversation.receivedMessagesCount() > 0) { - showSnackbar(R.string.remote_server_not_found, R.string.try_again, joinMuc); - } else { - showSnackbar(R.string.remote_server_not_found, R.string.leave, leaveMuc); - } - break; - case REMOTE_SERVER_TIMEOUT: - if (conversation.receivedMessagesCount() > 0) { - showSnackbar(R.string.remote_server_timeout, R.string.try_again, joinMuc); - } else { - showSnackbar(R.string.remote_server_timeout, R.string.leave, leaveMuc); - } - break; - case PASSWORD_REQUIRED: - showSnackbar(R.string.conference_requires_password, R.string.enter_password, enterPassword); - break; - case BANNED: - showSnackbar(R.string.conference_banned, R.string.leave, leaveMuc); - break; - case MEMBERS_ONLY: - showSnackbar(R.string.conference_members_only, R.string.leave, leaveMuc); - break; - case RESOURCE_CONSTRAINT: - showSnackbar(R.string.conference_resource_constraint, R.string.try_again, joinMuc); - break; - case KICKED: - showSnackbar(R.string.conference_kicked, R.string.join, joinMuc); - break; - case TECHNICAL_PROBLEMS: - showSnackbar(R.string.conference_technical_problems, R.string.try_again, joinMuc); - break; - case UNKNOWN: - showSnackbar(R.string.conference_unknown_error, R.string.join, joinMuc); - break; - case INVALID_NICK: - showSnackbar(R.string.invalid_muc_nick, R.string.edit, clickToMuc); - case SHUTDOWN: - showSnackbar(R.string.conference_shutdown, R.string.try_again, joinMuc); - break; - case DESTROYED: - showSnackbar(R.string.conference_destroyed, R.string.leave, leaveMuc); - break; - case NON_ANONYMOUS: - showSnackbar(R.string.group_chat_will_make_your_jabber_id_public, R.string.join, acceptJoin); - break; - default: - hideSnackbar(); - break; - } - } else if (account.hasPendingPgpIntent(conversation)) { - showSnackbar(R.string.openpgp_messages_found, R.string.decrypt, clickToDecryptListener); - } else if (mode == Conversation.MODE_SINGLE - && conversation.smpRequested()) { - showSnackbar(R.string.smp_requested, R.string.verify, this.mAnswerSmpClickListener); - } else if (mode == Conversation.MODE_SINGLE - && conversation.hasValidOtrSession() - && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) - && (!conversation.isOtrFingerprintVerified())) { - showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, clickToVerify); - } else if (connection != null - && connection.getFeatures().blocking() - && conversation.countMessages() != 0 - && !conversation.isBlocked() - && conversation.isWithStranger()) { - showSnackbar(R.string.received_message_from_stranger, R.string.block, mBlockClickListener); - } else if (activity != null && activity.warnUnecryptedChat()) { - if (conversation.getNextEncryption() == Message.ENCRYPTION_NONE && conversation.isSingleOrPrivateAndNonAnonymous() && ((Config.supportOmemo() && Conversation.suitableForOmemoByDefault(conversation)) || - (Config.supportOpenPgp() && account.isPgpDecryptionServiceConnected()) || ( - mode == Conversation.MODE_SINGLE && Config.supportOtr()))) { - if (isEncryptionDisabledException() || conversation.getJid().toString().equals(account.getJid().getDomain())) { - hideSnackbar(); - } else { - showSnackbar(R.string.conversation_unencrypted_hint, R.string.ok, showUnencryptionHintDialog); - } - } else { - hideSnackbar(); - } - } else if (conversation.getUuid().equalsIgnoreCase(AttachFileToConversationRunnable.isCompressingVideo[0])) { - Activity activity = getActivity(); - if (activity != null) { - showSnackbar(getString(R.string.transcoding_video_x, AttachFileToConversationRunnable.isCompressingVideo[1]), 0, null); - } - } else { - hideSnackbar(); - } - } - - private OnClickListener showUnencryptionHintDialog = new OnClickListener() { - @Override - public void onClick(View v) { - activity.runOnUiThread(() -> { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(getString(R.string.message_encryption)); - builder.setMessage(getString(R.string.enable_message_encryption)); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.enable), - (dialog, which) -> { - enableMessageEncryption(); - }); - builder.setNeutralButton(getString(R.string.hide_warning), - (dialog, which) -> { - SharedPreferences preferences = activity.getPreferences(); - preferences.edit().putBoolean(WARN_UNENCRYPTED_CHAT, false).apply(); - hideSnackbar(); - }); - builder.create().show(); - }); - } - }; - - @Override - public void refresh() { - if (this.binding == null) { - Log.d(Config.LOGTAG, "ConversationFragment.refresh() skipped updated because view binding was null"); - return; - } - updateChatBG(); - disableEncrpytionForExceptions(); - if (this.conversation != null && this.activity != null && this.activity.xmppConnectionService != null) { - if (!activity.xmppConnectionService.isConversationStillOpen(this.conversation)) { - activity.onConversationArchived(this.conversation); - return; - } - } - this.refresh(true); - } - - - private void refresh(boolean notifyConversationRead) { - synchronized (this.messageList) { - if (this.conversation != null) { - conversation.populateWithMessages(ConversationFragment.this.messageList); - updateStatusMessages(); - if (conversation.unreadCount() > 0) { - binding.unreadCountCustomView.setVisibility(View.VISIBLE); - binding.unreadCountCustomView.setUnreadCount(conversation.unreadCount()); - } - this.messageListAdapter.notifyDataSetChanged(); - updateChatMsgHint(); - if (notifyConversationRead && activity != null) { - binding.messagesView.post(this::fireReadEvent); - } - updateSendButton(); - updateEditablity(); - } - } - } - - protected void messageSent() { - mSendingPgpMessage.set(false); - this.binding.textinput.setText(""); - if (conversation.setCorrectingMessage(null)) { - this.binding.textinput.append(conversation.getDraftMessage()); - conversation.setDraftMessage(null); - } - storeNextMessage(); - updateChatMsgHint(); - SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity); - final boolean prefScrollToBottom = p.getBoolean("scroll_to_bottom", activity.getResources().getBoolean(R.bool.scroll_to_bottom)); - if (prefScrollToBottom || scrolledToBottom()) { - new Handler().post(() -> { - int size = messageList.size(); - this.binding.messagesView.setSelection(size - 1); - }); - } - } - - private boolean storeNextMessage() { - return storeNextMessage(this.binding.textinput.getText().toString()); - } - - private boolean storeNextMessage(String msg) { - final boolean participating = conversation.getMode() == Conversational.MODE_SINGLE || conversation.getMucOptions().participating(); - if (this.conversation.getStatus() != Conversation.STATUS_ARCHIVED && participating && this.conversation.setNextMessage(msg)) { - this.activity.xmppConnectionService.updateConversation(this.conversation); - return true; - } - return false; - } - - public void doneSendingPgpMessage() { - mSendingPgpMessage.set(false); - } - - public long getMaxHttpUploadSize(Conversation conversation) { - final XmppConnection connection = conversation.getAccount().getXmppConnection(); - return connection == null ? -1 : connection.getFeatures().getMaxHttpUploadSize(); - } - - private void updateTextFormat(final boolean me) { - KeyboardUtils.addKeyboardToggleListener(activity, isVisible -> { - Log.d(Config.LOGTAG, "keyboard visible: " + isVisible); - if (isVisible && activity != null && activity.xmppConnectionService != null && activity.xmppConnectionService.showTextFormatting()) { - showTextFormat(me); - } else { - hideTextFormat(); - } - }); - } - - private void updateEditablity() { - boolean canWrite = this.conversation.getMode() == Conversation.MODE_SINGLE || this.conversation.getMucOptions().participating() || this.conversation.getNextCounterpart() != null; - this.binding.textinput.setFocusable(canWrite); - this.binding.textinput.setFocusableInTouchMode(canWrite); - this.binding.textSendButton.setEnabled(canWrite); - this.binding.textinput.setCursorVisible(canWrite); - this.binding.textinput.setEnabled(canWrite); - } - - public void updateSendButton() { - messageListAdapter.setBubbleBackgroundColor(binding.messageInputBox, 0, isPrivateMessage(), true); - boolean hasAttachments = mediaPreviewAdapter != null && mediaPreviewAdapter.hasAttachments(); - boolean useSendButtonToIndicateStatus = activity != null && PreferenceManager.getDefaultSharedPreferences(activity).getBoolean("send_button_status", getResources().getBoolean(R.bool.send_button_status)); - final Conversation c = this.conversation; - final Presence.Status status; - final String text = this.binding.textinput == null ? "" : this.binding.textinput.getText().toString(); - final SendButtonAction action; - if (hasAttachments) { - action = SendButtonAction.TEXT; - } else { - action = SendButtonTool.getAction(getActivity(), c, text); - } - if (useSendButtonToIndicateStatus && c.getAccount().getStatus() == Account.State.ONLINE) { - if (activity != null && activity.xmppConnectionService != null && activity.xmppConnectionService.getMessageArchiveService().isCatchingUp(c)) { - status = Presence.Status.OFFLINE; - } else if (c.getMode() == Conversation.MODE_SINGLE) { - status = c.getContact().getShownStatus(); - } else { - status = c.getMucOptions().online() ? Presence.Status.ONLINE : Presence.Status.OFFLINE; - } - } else { - status = Presence.Status.OFFLINE; - } - this.binding.textSendButton.setTag(action); - final Activity activity = getActivity(); - if (activity != null) { - this.binding.textSendButton.setImageResource(SendButtonTool.getSendButtonImageResource(activity, action, status)); - } - updateSnackBar(conversation); - updateChatMsgHint(); - updateTextFormat(canSendMeCommand()); - } - - protected void updateStatusMessages() { - DateSeparator.addAll(this.messageList); - if (showLoadMoreMessages(conversation)) { - this.messageList.add(0, Message.createLoadMoreMessage(conversation)); - } - if (conversation.getMode() == Conversation.MODE_MULTI) { - final MucOptions mucOptions = conversation.getMucOptions(); - final List allUsers = mucOptions.getUsers(); - final Set addedMarkers = new HashSet<>(); - if (mucOptions.isPrivateAndNonAnonymous()) { - for (int i = this.messageList.size() - 1; i >= 0; --i) { - final Set markersForMessage = messageList.get(i).getReadByMarkers(); - final List shownMarkers = new ArrayList<>(); - for (ReadByMarker marker : markersForMessage) { - if (!ReadByMarker.contains(marker, addedMarkers)) { - addedMarkers.add(marker); //may be put outside this condition. set should do dedup anyway - MucOptions.User user = mucOptions.findUser(marker); - if (user != null) { - shownMarkers.add(user); - } - } - } - final ReadByMarker markerForSender = ReadByMarker.from(messageList.get(i)); - final Message statusMessage; - final int size = shownMarkers.size(); - if (size > 1) { - final String body; - if (size <= 4) { - body = getString(R.string.contacts_have_read_up_to_this_point, UIHelper.concatNames(shownMarkers)); - } else if (ReadByMarker.allUsersRepresented(allUsers, markersForMessage, markerForSender)) { - body = getString(R.string.everyone_has_read_up_to_this_point); - } else { - body = getString(R.string.contacts_and_n_more_have_read_up_to_this_point, UIHelper.concatNames(shownMarkers, 3), size - 3); - } - statusMessage = Message.createStatusMessage(conversation, body); - statusMessage.setCounterparts(shownMarkers); - } else if (size == 1) { - statusMessage = Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, UIHelper.getDisplayName(shownMarkers.get(0)))); - statusMessage.setCounterpart(shownMarkers.get(0).getFullJid()); - statusMessage.setTrueCounterpart(shownMarkers.get(0).getRealJid()); - } else { - statusMessage = null; - } - if (statusMessage != null) { - this.messageList.add(i + 1, statusMessage); - } - addedMarkers.add(markerForSender); - if (ReadByMarker.allUsersRepresented(allUsers, addedMarkers)) { - break; - } - } - } - } - } - - - private void stopScrolling() { - long now = SystemClock.uptimeMillis(); - MotionEvent cancel = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); - binding.messagesView.dispatchTouchEvent(cancel); - } - - private boolean showLoadMoreMessages(final Conversation c) { - if (activity == null || activity.xmppConnectionService == null) { - return false; - } - final boolean mam = hasMamSupport(c) && !c.getContact().isBlocked(); - final MessageArchiveService service = activity.xmppConnectionService.getMessageArchiveService(); - return mam && (c.getLastClearHistory().getTimestamp() != 0 || (c.countMessages() == 0 && c.messagesLoaded.get() && c.hasMessagesLeftOnServer() && !service.queryInProgress(c))); - } - - private boolean hasMamSupport(final Conversation c) { - if (c.getMode() == Conversation.MODE_SINGLE) { - final XmppConnection connection = c.getAccount().getXmppConnection(); - return connection != null && connection.getFeatures().mam(); - } else { - return c.getMucOptions().mamSupport(); - } - } - - protected void showSnackbar(final String message, final int action, final OnClickListener clickListener) { - this.binding.snackbar.setVisibility(View.VISIBLE); - this.binding.snackbar.setOnClickListener(null); - this.binding.snackbarMessage.setText(message); - this.binding.snackbarMessage.setOnClickListener(null); - this.binding.snackbarAction.setVisibility(clickListener == null ? View.GONE : View.VISIBLE); - if (action != 0) { - this.binding.snackbarAction.setText(action); - } - this.binding.snackbarAction.setOnClickListener(clickListener); - } - - protected void showSnackbar(final int message, final int action, final OnClickListener clickListener) { - showSnackbar(message, action, clickListener, null); - } - - protected void showSnackbar(final int message, final int action, final OnClickListener clickListener, final View.OnLongClickListener longClickListener) { - this.binding.snackbar.setVisibility(View.VISIBLE); - this.binding.snackbar.setOnClickListener(null); - this.binding.snackbarMessage.setText(message); - this.binding.snackbarMessage.setOnClickListener(null); - this.binding.snackbarAction.setVisibility(clickListener == null ? View.GONE : View.VISIBLE); - if (action != 0) { - this.binding.snackbarAction.setText(action); - } - this.binding.snackbarAction.setOnClickListener(clickListener); - this.binding.snackbarAction.setOnLongClickListener(longClickListener); - } - - protected void hideSnackbar() { - this.binding.snackbar.setVisibility(View.GONE); - } - - protected void sendMessage(Message message) { - activity.xmppConnectionService.sendMessage(message); - messageSent(); - } - - protected void sendPgpMessage(final Message message) { - final XmppConnectionService xmppService = activity.xmppConnectionService; - final Contact contact = message.getConversation().getContact(); - if (!activity.hasPgp()) { - activity.showInstallPgpDialog(); - return; - } - if (conversation.getAccount().getPgpSignature() == null) { - activity.announcePgp(conversation.getAccount(), conversation, null, activity.onOpenPGPKeyPublished); - return; - } - if (!mSendingPgpMessage.compareAndSet(false, true)) { - Log.d(Config.LOGTAG, "sending pgp message already in progress"); - } - if (conversation.getMode() == Conversation.MODE_SINGLE) { - if (contact.getPgpKeyId() != 0) { - xmppService.getPgpEngine().hasKey(contact, - new UiCallback() { - - @Override - public void userInputRequired(PendingIntent pi, Contact contact) { - startPendingIntent(pi, REQUEST_ENCRYPT_MESSAGE); - } - - @Override - public void progress(int progress) { - - } - - @Override - public void success(Contact contact) { - encryptTextMessage(message); - } - - @Override - public void error(int error, Contact contact) { - activity.runOnUiThread(() -> ToastCompat.makeText(activity, - R.string.unable_to_connect_to_keychain, - ToastCompat.LENGTH_SHORT - ).show()); - mSendingPgpMessage.set(false); - } - }); - - } else { - showNoPGPKeyDialog(false, - (dialog, which) -> { - conversation.setNextEncryption(Message.ENCRYPTION_NONE); - xmppService.updateConversation(conversation); - message.setEncryption(Message.ENCRYPTION_NONE); - xmppService.sendMessage(message); - messageSent(); - }); - } - } else { - if (conversation.getMucOptions().pgpKeysInUse()) { - if (!conversation.getMucOptions().everybodyHasKeys()) { - Toast warning = ToastCompat - .makeText(getActivity(), - R.string.missing_public_keys, - ToastCompat.LENGTH_LONG); - warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0); - warning.show(); - } - encryptTextMessage(message); - } else { - showNoPGPKeyDialog(true, - (dialog, which) -> { - conversation.setNextEncryption(Message.ENCRYPTION_NONE); - message.setEncryption(Message.ENCRYPTION_NONE); - xmppService.updateConversation(conversation); - xmppService.sendMessage(message); - messageSent(); - }); - } - } - } - - public void encryptTextMessage(Message message) { - activity.xmppConnectionService.getPgpEngine().encrypt(message, - new UiCallback() { - - @Override - public void userInputRequired(PendingIntent pi, Message message) { - startPendingIntent(pi, REQUEST_SEND_MESSAGE); - } - - @Override - public void progress(int progress) { - - } - - @Override - public void success(Message message) { - //TODO the following two call can be made before the callback - getActivity().runOnUiThread(() -> messageSent()); - } - - @Override - public void error(final int error, Message message) { - getActivity().runOnUiThread(() -> { - doneSendingPgpMessage(); - ToastCompat.makeText(getActivity(), error == 0 ? R.string.unable_to_connect_to_keychain : error, ToastCompat.LENGTH_SHORT).show(); - }); - - } - }); - } - - public void showNoPGPKeyDialog(boolean plural, DialogInterface.OnClickListener listener) { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setIconAttribute(android.R.attr.alertDialogIcon); - if (plural) { - builder.setTitle(getString(R.string.no_pgp_keys)); - builder.setMessage(getText(R.string.contacts_have_no_pgp_keys)); - } else { - builder.setTitle(getString(R.string.no_pgp_key)); - builder.setMessage(getText(R.string.contact_has_no_pgp_key)); - } - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.send_unencrypted), listener); - builder.create().show(); - } - protected void sendOtrMessage(final Message message) { - final ConversationsActivity activity = (ConversationsActivity) getActivity(); - final XmppConnectionService xmppService = activity.xmppConnectionService; - activity.selectPresence(conversation, - () -> { - message.setCounterpart(conversation.getNextCounterpart()); - xmppService.sendMessage(message); - messageSent(); - }); - } - public void appendText(String text, final boolean doNotAppend) { - if (text == null) { - return; - } - final Editable editable = this.binding.textinput.getText(); - String previous = editable == null ? "" : editable.toString(); - if (doNotAppend && !TextUtils.isEmpty(previous)) { - ToastCompat.makeText(getActivity(), R.string.already_drafting_message, ToastCompat.LENGTH_LONG).show(); - return; - } - if (UIHelper.isLastLineQuote(previous)) { - text = '\n' + text; - } else if (previous.length() != 0 && !Character.isWhitespace(previous.charAt(previous.length() - 1))) { - text = " " + text; - } - this.binding.textinput.append(text); - } - - @Override - public boolean onEnterPressed(final boolean isCtrlPressed) { - if (isCtrlPressed || enterIsSend()) { - sendMessage(); - return true; - } - return false; - } - - private boolean enterIsSend() { - final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(getActivity()); - return p.getBoolean("enter_is_send", getResources().getBoolean(R.bool.enter_is_send)); - } - - public boolean onArrowUpCtrlPressed() { - final Message lastEditableMessage = conversation == null ? null : conversation.getLastEditableMessage(); - if (lastEditableMessage != null) { - correctMessage(lastEditableMessage); - return true; - } else { - ToastCompat.makeText(getActivity(), R.string.could_not_correct_message, ToastCompat.LENGTH_LONG).show(); - return false; - } - } - - @Override - public void onTypingStarted() { - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; - if (service == null) { - return; - } - final Account.State status = conversation.getAccount().getStatus(); - if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.COMPOSING)) { - service.sendChatState(conversation); - } - runOnUiThread(this::updateSendButton); - - } - - @Override - public void onTypingStopped() { - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; - if (service == null) { - return; - } - final Account.State status = conversation.getAccount().getStatus(); - if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.PAUSED)) { - service.sendChatState(conversation); - } - } - - @Override - public void onTextDeleted() { - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; - if (service == null) { - return; - } - final Account.State status = conversation.getAccount().getStatus(); - if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) { - service.sendChatState(conversation); - } - runOnUiThread(() -> { - if (activity == null) { - return; - } - activity.onConversationsListItemUpdated(); - }); - runOnUiThread(this::updateSendButton); - } - - @Override - public void onTextChanged() { - if (conversation != null && conversation.getCorrectingMessage() != null) { - runOnUiThread(this::updateSendButton); - } - } - - @Override - public boolean onTabPressed(boolean repeated) { - if (conversation == null || conversation.getMode() == Conversation.MODE_SINGLE) { - return false; - } - if (repeated) { - completionIndex++; - } else { - lastCompletionLength = 0; - completionIndex = 0; - final String content = this.binding.textinput.getText().toString(); - lastCompletionCursor = this.binding.textinput.getSelectionEnd(); - int start = lastCompletionCursor > 0 ? content.lastIndexOf(" ", lastCompletionCursor - 1) + 1 : 0; - firstWord = start == 0; - incomplete = content.substring(start, lastCompletionCursor); - } - List completions = new ArrayList<>(); - for (MucOptions.User user : conversation.getMucOptions().getUsers()) { - String name = user.getName(); - if (name != null && name.startsWith(incomplete)) { - completions.add(name + (firstWord ? ": " : " ")); - } - } - Collections.sort(completions); - if (completions.size() > completionIndex) { - String completion = completions.get(completionIndex).substring(incomplete.length()); - this.binding.textinput.getEditableText().delete(lastCompletionCursor, lastCompletionCursor + lastCompletionLength); - this.binding.textinput.getEditableText().insert(lastCompletionCursor, completion); - lastCompletionLength = completion.length(); - } else { - completionIndex = -1; - this.binding.textinput.getEditableText().delete(lastCompletionCursor, lastCompletionCursor + lastCompletionLength); - lastCompletionLength = 0; - } - return true; - } - - private boolean messageContainsQuery(Message m, String q) { - return m != null && m.getMergedBody().toString().toLowerCase().contains(q.toLowerCase()); - } - - private void startPendingIntent(PendingIntent pendingIntent, int requestCode) { - try { - getActivity().startIntentSenderForResult(pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0); - } catch (final SendIntentException ignored) { - } - } - - @Override - public void onBackendConnected() { - Log.d(Config.LOGTAG, "ConversationFragment.onBackendConnected()"); - String uuid = pendingConversationsUuid.pop(); - if (uuid != null) { - if (!findAndReInitByUuidOrArchive(uuid)) { - return; - } - } else { - if (!activity.xmppConnectionService.isConversationStillOpen(conversation)) { - clearPending(); - activity.onConversationArchived(conversation); - return; - } - } - ActivityResult activityResult = postponedActivityResult.pop(); - if (activityResult != null) { - handleActivityResult(activityResult); - } - clearPending(); - } - - private boolean findAndReInitByUuidOrArchive(@NonNull final String uuid) { - Conversation conversation = activity.xmppConnectionService.findConversationByUuid(uuid); - if (conversation == null) { - clearPending(); - activity.onConversationArchived(null); - return false; - } - reInit(conversation); - ScrollState scrollState = pendingScrollState.pop(); - String lastMessageUuid = pendingLastMessageUuid.pop(); - List attachments = pendingMediaPreviews.pop(); - if (scrollState != null) { - setScrollPosition(scrollState, lastMessageUuid); - } - if (attachments != null && attachments.size() > 0) { - Log.d(Config.LOGTAG, "had attachments on restore"); - mediaPreviewAdapter.addMediaPreviews(attachments); - toggleInputMethod(); - } - return true; - } - - private void clearPending() { - if (postponedActivityResult.clear()) { - Log.e(Config.LOGTAG, "cleared pending intent with unhandled result left"); - if (pendingTakePhotoUri.clear()) { - Log.e(Config.LOGTAG, "cleared pending photo uri"); - } - } - if (pendingScrollState.clear()) { - Log.e(Config.LOGTAG, "cleared scroll state"); - } - if (pendingConversationsUuid.clear()) { - Log.e(Config.LOGTAG, "cleared pending conversations uuid"); - } - if (pendingMediaPreviews.clear()) { - Log.e(Config.LOGTAG, "cleared pending media previews"); - } - } - - public Conversation getConversation() { - return conversation; - } - - @Override - public void onContactPictureLongClicked(View v, final Message message) { - final String fingerprint; - if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - fingerprint = "pgp"; - } else { - fingerprint = message.getFingerprint(); - } - final PopupMenu popupMenu = new PopupMenu(getActivity(), v); - final Contact contact = message.getContact(); - if (message.getStatus() <= Message.STATUS_RECEIVED && (contact == null || !contact.isSelf())) { - if (message.getConversation().getMode() == Conversation.MODE_MULTI) { - final Jid cp = message.getCounterpart(); - if (cp == null || cp.isBareJid()) { - return; - } - final Jid tcp = message.getTrueCounterpart(); - final User userByRealJid = tcp != null ? conversation.getMucOptions().findOrCreateUserByRealJid(tcp, cp) : null; - final User user = userByRealJid != null ? userByRealJid : conversation.getMucOptions().findUserByFullJid(cp); - popupMenu.inflate(R.menu.muc_details_context); - final Menu menu = popupMenu.getMenu(); - MucDetailsContextMenuHelper.configureMucDetailsContextMenu(activity, menu, conversation, user, true, getUsername(message)); - popupMenu.setOnMenuItemClickListener(menuItem -> MucDetailsContextMenuHelper.onContextItemSelected(menuItem, user, activity, fingerprint)); - } else { - popupMenu.inflate(R.menu.one_on_one_context); - popupMenu.setOnMenuItemClickListener(item -> { - switch (item.getItemId()) { - case R.id.action_show_avatar: - activity.ShowAvatarPopup(activity, contact); - break; - case R.id.action_contact_details: - activity.switchToContactDetails(message.getContact(), fingerprint); - break; - case R.id.action_show_qr_code: - activity.showQrCode("xmpp:" + message.getContact().getJid().asBareJid().toEscapedString()); - break; - } - return true; - }); - } - } else { - popupMenu.inflate(R.menu.account_context); - final Menu menu = popupMenu.getMenu(); - popupMenu.setOnMenuItemClickListener(item -> { - final XmppActivity activity = this.activity; - if (activity == null) { - Log.e(Config.LOGTAG, "Unable to perform action. no context provided"); - return true; - } - switch (item.getItemId()) { - - - case R.id.action_show_qr_code: - activity.showQrCode(conversation.getAccount().getShareableUri()); - break; - case R.id.action_account_details: - activity.switchToAccount(message.getConversation().getAccount(), fingerprint); - break; - } - return true; - }); - } - popupMenu.show(); - } - - public String getUsername(Message message) { - if (message == null) { - return null; - } - String user; - try { - final Contact contact = message.getContact(); - if (conversation.getMode() == Conversation.MODE_MULTI) { - if (contact != null) { - user = contact.getDisplayName(); - } else { - user = UIHelper.getDisplayedMucCounterpart(message.getCounterpart()); - } - } else { - user = contact != null ? contact.getDisplayName() : null; - } - if (message.getStatus() == Message.STATUS_SEND - || message.getStatus() == Message.STATUS_SEND_FAILED - || message.getStatus() == Message.STATUS_SEND_RECEIVED - || message.getStatus() == Message.STATUS_SEND_DISPLAYED) { - user = getString(R.string.me); - } - } catch (Exception e) { - e.printStackTrace(); - user = null; - } - return user; - } - - @Override - public void onContactPictureClicked(Message message) { - String fingerprint; - if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - fingerprint = "pgp"; - } else { - fingerprint = message.getFingerprint(); - } - final boolean received = message.getStatus() <= Message.STATUS_RECEIVED; - if (received) { - if (message.getConversation() instanceof Conversation && message.getConversation().getMode() == Conversation.MODE_MULTI) { - Jid tcp = message.getTrueCounterpart(); - Jid user = message.getCounterpart(); - if (user != null && !user.isBareJid()) { - final MucOptions mucOptions = ((Conversation) message.getConversation()).getMucOptions(); - if (mucOptions.participating() || ((Conversation) message.getConversation()).getNextCounterpart() != null) { - if (!mucOptions.isUserInRoom(user) && mucOptions.findUserByRealJid(tcp == null ? null : tcp.asBareJid()) == null) { - ToastCompat.makeText(getActivity(), activity.getString(R.string.user_has_left_conference, user.getResource()), ToastCompat.LENGTH_SHORT).show(); - } - highlightInConference(user.getResource()); - } else { - ToastCompat.makeText(getActivity(), R.string.you_are_not_participating, ToastCompat.LENGTH_SHORT).show(); - } - } - return; - } else { - if (!message.getContact().isSelf()) { - activity.switchToContactDetails(message.getContact(), fingerprint); - return; - } - } - } - activity.switchToAccount(message.getConversation().getAccount(), fingerprint); - } - - private Activity requireActivity() { - final Activity activity = getActivity(); - if (activity == null) { - throw new IllegalStateException("Activity not attached"); - } - return activity; - } - - private void showTextFormat(final boolean me) { - this.binding.textformat.setVisibility(View.VISIBLE); - this.binding.me.setEnabled(me); - this.binding.me.setOnClickListener(meCommand); - this.binding.quote.setOnClickListener(quote); - this.binding.bold.setOnClickListener(boldText); - this.binding.italic.setOnClickListener(italicText); - this.binding.monospace.setOnClickListener(monospaceText); - this.binding.strikethrough.setOnClickListener(strikethroughText); - this.binding.help.setOnClickListener(help); - this.binding.close.setOnClickListener(close); - if (Compatibility.runsTwentyEight()) { - this.binding.me.setTooltipText(activity.getString(R.string.me)); - this.binding.quote.setTooltipText(activity.getString(R.string.quote)); - this.binding.bold.setTooltipText(activity.getString(R.string.bold)); - this.binding.italic.setTooltipText(activity.getString(R.string.italic)); - this.binding.monospace.setTooltipText(activity.getString(R.string.monospace)); - this.binding.monospace.setTooltipText(activity.getString(R.string.monospace)); - this.binding.strikethrough.setTooltipText(activity.getString(R.string.strikethrough)); - this.binding.help.setTooltipText(activity.getString(R.string.help)); - this.binding.close.setTooltipText(activity.getString(R.string.close)); - } - } - - private void hideTextFormat() { - this.binding.textformat.setVisibility(View.GONE); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java deleted file mode 100644 index b8c785949..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ /dev/null @@ -1,1109 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui; - -import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP; -import static eu.siacs.conversations.ui.SettingsActivity.HIDE_MEMORY_WARNING; -import static eu.siacs.conversations.ui.SettingsActivity.MIN_ANDROID_SDK21_SHOWN; -import static eu.siacs.conversations.utils.StorageHelper.getAppMediaDirectory; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.Fragment; -import android.app.FragmentManager; -import android.app.FragmentTransaction; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.graphics.Typeface; -import android.graphics.drawable.ColorDrawable; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.provider.Settings; -import android.util.Log; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.TextView; -import android.widget.Toast; - -import net.java.otr4j.session.SessionStatus; -import androidx.appcompat.widget.PopupMenu; - -import androidx.annotation.IdRes; -import androidx.annotation.NonNull; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.Toolbar; -import androidx.databinding.DataBindingUtil; - -import org.openintents.openpgp.util.OpenPgpApi; - -import java.io.File; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.atomic.AtomicBoolean; - -import eu.siacs.conversations.Config; -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.entities.MucOptions; -import eu.siacs.conversations.persistance.FileBackend; -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.IntroHelper; -import eu.siacs.conversations.ui.util.PendingItem; -import eu.siacs.conversations.ui.util.StyledAttributes; -import eu.siacs.conversations.ui.util.UpdateHelper; -import eu.siacs.conversations.utils.ExceptionHelper; -import eu.siacs.conversations.utils.MenuDoubleTabUtil; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.utils.SignupUtils; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnUpdateBlocklist; -import eu.siacs.conversations.xmpp.chatstate.ChatState; -import me.drakeet.support.toast.ToastCompat; - -public class ConversationsActivity extends XmppActivity implements OnConversationSelected, OnConversationArchived, OnConversationsListItemUpdated, OnConversationRead, XmppConnectionService.OnAccountUpdate, XmppConnectionService.OnConversationUpdate, XmppConnectionService.OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoomDestroy { - - public static final String ACTION_VIEW_CONVERSATION = "eu.siacs.conversations.VIEW"; - public static final String EXTRA_CONVERSATION = "conversationUuid"; - public static final String EXTRA_DOWNLOAD_UUID = "eu.siacs.conversations.download_uuid"; - public static final String EXTRA_AS_QUOTE = "eu.siacs.conversations.as_quote"; - public static final String EXTRA_NICK = "nick"; - public static final String EXTRA_USER = "user"; - public static final String EXTRA_IS_PRIVATE_MESSAGE = "pm"; - 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 ACTION_DESTROY_MUC = "eu.siacs.conversations.DESTROY_MUC"; - public static final int REQUEST_OPEN_MESSAGE = 0x9876; - public static final int REQUEST_PLAY_PAUSE = 0x5432; - public static final String EXTRA_TYPE = "type"; - - private static final List VIEW_AND_SHARE_ACTIONS = Arrays.asList( - ACTION_VIEW_CONVERSATION, - Intent.ACTION_SEND, - Intent.ACTION_SEND_MULTIPLE - ); - - private boolean showLastSeen; - - AlertDialog memoryWarningDialog; - - long FirstStartTime = -1; - String PREF_FIRST_START = "FirstStart"; - - //secondary fragment (when holding the conversation, must be initialized before refreshing the overview fragment - private static final @IdRes - int[] FRAGMENT_ID_NOTIFICATION_ORDER = {R.id.secondary_fragment, R.id.main_fragment}; - private final PendingItem pendingViewIntent = new PendingItem<>(); - private final PendingItem postponedActivityResult = new PendingItem<>(); - private ActivityConversationsBinding binding; - private boolean mActivityPaused = true; - private AtomicBoolean mRedirectInProcess = new AtomicBoolean(false); - - private static boolean isViewOrShareIntent(Intent i) { - Log.d(Config.LOGTAG, "action: " + (i == null ? null : i.getAction())); - return i != null && VIEW_AND_SHARE_ACTIONS.contains(i.getAction()) && i.hasExtra(EXTRA_CONVERSATION); - } - - private static Intent createLauncherIntent(Context context) { - final Intent intent = new Intent(context, ConversationsActivity.class); - intent.setAction(Intent.ACTION_MAIN); - intent.addCategory(Intent.CATEGORY_LAUNCHER); - return intent; - } - - @Override - protected void refreshUiReal() { - invalidateActionBarTitle(); - invalidateOptionsMenu(); - for (@IdRes int id : FRAGMENT_ID_NOTIFICATION_ORDER) { - refreshFragment(id); - } - } - - @Override - void onBackendConnected() { - if (performRedirectIfNecessary(true)) { - return; - } - Log.d(Config.LOGTAG, "ConversationsActivity onBackendConnected(): setIsInForeground = true"); - xmppConnectionService.getNotificationService().setIsInForeground(true); - - final Intent FirstStartIntent = getIntent(); - final Bundle extras = FirstStartIntent.getExtras(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (extras != null && extras.containsKey(PREF_FIRST_START)) { - FirstStartTime = extras.getLong(PREF_FIRST_START); - Log.d(Config.LOGTAG, "Get first start time from StartUI: " + FirstStartTime); - } - } else { - FirstStartTime = System.currentTimeMillis(); - Log.d(Config.LOGTAG, "Device is running Android < SDK 23, no restart required: " + FirstStartTime); - } - - final Intent intent = pendingViewIntent.pop(); - if (intent != null) { - if (processViewIntent(intent)) { - if (binding.secondaryFragment != null) { - notifyFragmentOfBackendConnected(R.id.main_fragment); - } - return; - } - } - - if (FirstStartTime == 0) { - Log.d(Config.LOGTAG, "First start time: " + FirstStartTime + ", restarting App"); - //write first start timestamp to file - FirstStartTime = System.currentTimeMillis(); - SharedPreferences FirstStart = getApplicationContext().getSharedPreferences(PREF_FIRST_START, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = FirstStart.edit(); - editor.putLong(PREF_FIRST_START, FirstStartTime); - editor.commit(); - // restart if storage not accessable - if (FileBackend.getDiskSize() > 0) { - return; - } else { - Intent restartintent = getBaseContext().getPackageManager().getLaunchIntentForPackage(getBaseContext().getPackageName()); - restartintent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - restartintent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(restartintent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - System.exit(0); - } - } - - if (useInternalUpdater()) { - if (xmppConnectionService.getAccounts().size() != 0) { - if (xmppConnectionService.hasInternetConnection()) { - if (xmppConnectionService.isWIFI() || (xmppConnectionService.isMobile() && !xmppConnectionService.isMobileRoaming())) { - AppUpdate(xmppConnectionService.installedFrom()); - } - } - } - } - - for (@IdRes int id : FRAGMENT_ID_NOTIFICATION_ORDER) { - notifyFragmentOfBackendConnected(id); - } - - final ActivityResult activityResult = postponedActivityResult.pop(); - if (activityResult != null) { - handleActivityResult(activityResult); - } - - if (binding.secondaryFragment != null && ConversationFragment.getConversation(this) == null) { - Conversation conversation = ConversationsOverviewFragment.getSuggestion(this); - if (conversation != null) { - openConversation(conversation, null); - } - } - invalidateActionBarTitle(); - showDialogsIfMainIsOverview(); - } - - private boolean performRedirectIfNecessary(boolean noAnimation) { - return performRedirectIfNecessary(null, noAnimation); - } - - private boolean performRedirectIfNecessary(final Conversation ignore, final boolean noAnimation) { - if (xmppConnectionService == null) { - return false; - } - boolean isConversationsListEmpty = xmppConnectionService.isConversationsListEmpty(ignore); - if (isConversationsListEmpty && mRedirectInProcess.compareAndSet(false, true)) { - final Intent intent = SignupUtils.getRedirectionIntent(this); - if (noAnimation) { - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - } - runOnUiThread(() -> { - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - if (noAnimation) { - overridePendingTransition(0, 0); - } - }); - } - return mRedirectInProcess.get(); - } - - private void showDialogsIfMainIsOverview() { - if (xmppConnectionService == null) { - return; - } - final Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment); - if (fragment instanceof ConversationsOverviewFragment) { - - if (ExceptionHelper.checkForCrash(this)) { - return; - } - openBatteryOptimizationDialogIfNeeded(); - new showMemoryWarning(this).execute(); - showOutdatedVersionWarning(); - } - } - - private void showOutdatedVersionWarning() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP || getPreferences().getBoolean(MIN_ANDROID_SDK21_SHOWN, false)) { - Log.d(Config.LOGTAG, "Device is running Android >= SDK 21"); - return; - } - Log.d(Config.LOGTAG, "Device is running Android < SDK 21"); - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.oldAndroidVersion); - builder.setMessage(R.string.oldAndroidVersionMessage); - builder.setPositiveButton(R.string.ok, (dialog, which) -> { - getPreferences().edit().putBoolean(MIN_ANDROID_SDK21_SHOWN, true).apply(); - }); - final AlertDialog dialog = builder.create(); - dialog.setCanceledOnTouchOutside(false); - dialog.show(); - } - - private String getBatteryOptimizationPreferenceKey() { - @SuppressLint("HardwareIds") - String device = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID); - return "show_battery_optimization" + (device == null ? "" : device); - } - - private void setNeverAskForBatteryOptimizationsAgain() { - getPreferences().edit().putBoolean(getBatteryOptimizationPreferenceKey(), false).apply(); - } - - public boolean getAttachmentChoicePreference() { - return getBooleanPreference(SettingsActivity.QUICK_SHARE_ATTACHMENT_CHOICE, R.bool.quick_share_attachment_choice); - } - - public boolean warnUnecryptedChat() { - return getBooleanPreference(SettingsActivity.WARN_UNENCRYPTED_CHAT, R.bool.warn_unencrypted_chat); - } - - private void openBatteryOptimizationDialogIfNeeded() { - if (isOptimizingBattery() - && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M - && getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true)) { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.battery_optimizations_enabled); - builder.setMessage(R.string.battery_optimizations_enabled_dialog); - builder.setPositiveButton(R.string.next, (dialog, which) -> { - final Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); - final Uri uri = Uri.parse("package:" + getPackageName()); - intent.setData(uri); - try { - startActivityForResult(intent, REQUEST_BATTERY_OP); - } catch (ActivityNotFoundException e) { - ToastCompat.makeText(this, R.string.device_does_not_support_battery_op, ToastCompat.LENGTH_SHORT).show(); - } - }); - builder.setOnDismissListener(dialog -> setNeverAskForBatteryOptimizationsAgain()); - final AlertDialog dialog = builder.create(); - dialog.setCanceledOnTouchOutside(false); - dialog.show(); - } - } - - public class showMemoryWarning extends AsyncTask { - - ConversationsActivity activity; - long totalMemory = 0; - long mediaUsage = 0; - double relativeUsage = 0; - String percentUsage = "0%"; - boolean force = false; - // normal warning: more or equals 20 % or 10 GiB and automatic file deletion is disabled - double normalWarningRelative = 0.2f; // 20% - double normalWarningAbsolute = 10f * 1024 * 1024 * 1024; // 10 GiB - // force warning: usage is more than 50% - double forceWarningRelative = 0.5f; // 50% - - public showMemoryWarning(ConversationsActivity conversationsActivity) { - activity = conversationsActivity; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - } - - @Override - protected Void doInBackground(Void... params) { - try { - totalMemory = FileBackend.getDiskSize(); - mediaUsage = FileBackend.getDirectorySize(new File(getAppMediaDirectory(activity, null))); - relativeUsage = ((double) mediaUsage / (double) totalMemory); - try { - percentUsage = String.format(Locale.getDefault(),"%.2f", relativeUsage * 100) + " %"; - } catch (Exception e) { - e.printStackTrace(); - percentUsage = String.format(Locale.ENGLISH,"%.2f", relativeUsage * 100) + " %"; - } - force = relativeUsage > forceWarningRelative; - } catch (Exception e) { - e.printStackTrace(); - relativeUsage = 0; - } - return null; - } - - private boolean showWarning(boolean force) { - if (force) { - SharedPreferences preferences = getPreferences(); - preferences.edit().putBoolean(HIDE_MEMORY_WARNING, false).apply(); - return true; - } - return !xmppConnectionService.hideMemoryWarning() && (relativeUsage > normalWarningRelative || mediaUsage >= normalWarningAbsolute) & xmppConnectionService.getAutomaticAttachmentDeletionDate() == 0; - } - - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - Log.d(Config.LOGTAG, "Memory management: using " + UIHelper.filesizeToString(mediaUsage) + " from " + UIHelper.filesizeToString(totalMemory) + " (" + percentUsage + ")"); - if (showWarning(force) && activity != null) { - final AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setPositiveButton(R.string.open_settings, (dialog, which) -> { - try { - final Intent intent = new Intent(activity, SettingsActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.addCategory("android.intent.category.PREFERENCE"); - intent.putExtra("page", "security"); - startActivity(intent); - } catch (ActivityNotFoundException e) { - e.printStackTrace(); - } - }); - if (!force) { - builder.setNeutralButton(R.string.hide_warning, (dialog, which) -> { - SharedPreferences preferences = getPreferences(); - preferences.edit().putBoolean(HIDE_MEMORY_WARNING, true).apply(); - }); - } - memoryWarningDialog = builder.create(); - memoryWarningDialog.setTitle(R.string.title_memory_management); - if (force) { - memoryWarningDialog.setMessage(getResources().getString(R.string.memory_warning_force, UIHelper.filesizeToString(mediaUsage), percentUsage)); - } else { - memoryWarningDialog.setMessage(getResources().getString(R.string.memory_warning, UIHelper.filesizeToString(mediaUsage), percentUsage)); - } - memoryWarningDialog.setCanceledOnTouchOutside(false); - memoryWarningDialog.show(); - } - } - } - - private void notifyFragmentOfBackendConnected(@IdRes int id) { - final Fragment fragment = getFragmentManager().findFragmentById(id); - if (fragment instanceof OnBackendConnected) { - ((OnBackendConnected) fragment).onBackendConnected(); - } - } - - private void refreshFragment(@IdRes int id) { - final Fragment fragment = getFragmentManager().findFragmentById(id); - if (fragment instanceof XmppFragment) { - ((XmppFragment) fragment).refresh(); - } - } - - private boolean processViewIntent(Intent intent) { - Log.d(Config.LOGTAG, "process view intent"); - final String uuid = intent.getStringExtra(EXTRA_CONVERSATION); - final Conversation conversation = uuid != null ? xmppConnectionService.findConversationByUuid(uuid) : null; - if (conversation == null) { - Log.d(Config.LOGTAG, "unable to view conversation with uuid:" + uuid); - return false; - } - openConversation(conversation, intent.getExtras()); - return true; - } - - @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) { - switch (requestCode) { - case REQUEST_OPEN_MESSAGE: - refreshUiReal(); - ConversationFragment.openPendingMessage(this); - break; - case REQUEST_PLAY_PAUSE: - ConversationFragment.startStopPending(this); - break; - } - } - } - } - - @Override - public void onActivityResult(int requestCode, int resultCode, final Intent data) { - super.onActivityResult(requestCode, resultCode, data); - ActivityResult activityResult = ActivityResult.of(requestCode, resultCode, data); - if (xmppConnectionService != null) { - handleActivityResult(activityResult); - } else { - this.postponedActivityResult.push(activityResult); - } - } - - private void handleActivityResult(ActivityResult activityResult) { - if (activityResult.resultCode == Activity.RESULT_OK) { - handlePositiveActivityResult(activityResult.requestCode, activityResult.data); - } else { - handleNegativeActivityResult(activityResult.requestCode); - } - } - - private void handleNegativeActivityResult(int requestCode) { - Conversation conversation = ConversationFragment.getConversationReliable(this); - switch (requestCode) { - case REQUEST_DECRYPT_PGP: - if (conversation == null) { - break; - } - conversation.getAccount().getPgpDecryptionService().giveUpCurrentDecryption(); - break; - case REQUEST_BATTERY_OP: - setNeverAskForBatteryOptimizationsAgain(); - break; - } - } - - private void handlePositiveActivityResult(int requestCode, final Intent data) { - Log.d(Config.LOGTAG, "positive activity result"); - Conversation conversation = ConversationFragment.getConversationReliable(this); - if (conversation == null) { - Log.d(Config.LOGTAG, "conversation not found"); - return; - } - switch (requestCode) { - case REQUEST_DECRYPT_PGP: - conversation.getAccount().getPgpDecryptionService().continueDecryption(data); - break; - case REQUEST_CHOOSE_PGP_ID: - long id = data.getLongExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, 0); - if (id != 0) { - conversation.getAccount().setPgpSignId(id); - announcePgp(conversation.getAccount(), null, null, onOpenPGPKeyPublished); - } else { - choosePgpSignId(conversation.getAccount()); - } - break; - case REQUEST_ANNOUNCE_PGP: - announcePgp(conversation.getAccount(), conversation, data, onOpenPGPKeyPublished); - break; - } - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ConversationMenuConfigurator.reloadFeatures(this); - OmemoSetting.load(this); - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_conversations); - setSupportActionBar((Toolbar) binding.toolbar); - configureActionBar(getSupportActionBar()); - this.getFragmentManager().addOnBackStackChangedListener(this::invalidateActionBarTitle); - this.getFragmentManager().addOnBackStackChangedListener(this::showDialogsIfMainIsOverview); - this.initializeFragments(); - this.invalidateActionBarTitle(); - final Intent intent; - if (savedInstanceState == null) { - intent = getIntent(); - } else { - intent = savedInstanceState.getParcelable("intent"); - } - if (isViewOrShareIntent(intent)) { - pendingViewIntent.push(intent); - setIntent(createLauncherIntent(this)); - } - UpdateHelper.showPopup(this); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.activity_conversations, menu); - final MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code); - final MenuItem menuEditProfiles = menu.findItem(R.id.action_accounts); - final MenuItem inviteUser = menu.findItem(R.id.action_invite_user); - if (qrCodeScanMenuItem != null) { - if (isCameraFeatureAvailable()) { - Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment); - boolean visible = getResources().getBoolean(R.bool.show_qr_code_scan) - && fragment instanceof ConversationsOverviewFragment; - qrCodeScanMenuItem.setVisible(visible); - } else { - qrCodeScanMenuItem.setVisible(false); - } - } - if (xmppConnectionServiceBound && xmppConnectionService.getAccounts().size() == 1 && !xmppConnectionService.multipleAccounts()) { - menuEditProfiles.setTitle(R.string.action_account); - } else { - menuEditProfiles.setTitle(R.string.action_accounts); - } - if (xmppConnectionServiceBound && xmppConnectionService.getAccounts().size() > 0) { - inviteUser.setVisible(true); - } else { - inviteUser.setVisible(false); - } - return super.onCreateOptionsMenu(menu); - } - - @Override - public void onConversationSelected(Conversation conversation) { - clearPendingViewIntent(); - if (ConversationFragment.getConversation(this) == conversation) { - Log.d(Config.LOGTAG, "ignore onConversationSelected() because conversation is already open"); - return; - } - openConversation(conversation, null); - } - - public void clearPendingViewIntent() { - if (pendingViewIntent.clear()) { - Log.e(Config.LOGTAG, "cleared pending view intent"); - } - } - - private void displayToast(final String msg) { - runOnUiThread(() -> ToastCompat.makeText(ConversationsActivity.this, msg, ToastCompat.LENGTH_SHORT).show()); - } - - @Override - public void onAffiliationChangedSuccessful(Jid jid) { - } - - @Override - public void onAffiliationChangeFailed(Jid jid, int resId) { - displayToast(getString(resId, jid.asBareJid().toEscapedString())); - } - - private void openConversation(Conversation conversation, Bundle extras) { - final FragmentManager fragmentManager = getFragmentManager(); - executePendingTransactions(fragmentManager); - ConversationFragment conversationFragment = (ConversationFragment) fragmentManager.findFragmentById(R.id.secondary_fragment); - xmppConnectionService.updateNotificationChannels(); - final boolean mainNeedsRefresh; - if (conversationFragment == null) { - mainNeedsRefresh = false; - Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment); - if (mainFragment instanceof ConversationFragment) { - conversationFragment = (ConversationFragment) mainFragment; - } else { - conversationFragment = new ConversationFragment(); - FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction(); - fragmentTransaction.setCustomAnimations( - R.animator.fade_right_in, R.animator.fade_right_out, - R.animator.fade_right_in, R.animator.fade_right_out - ); - fragmentTransaction.replace(R.id.main_fragment, conversationFragment); - fragmentTransaction.addToBackStack(null); - try { - fragmentTransaction.commit(); - } catch (IllegalStateException e) { - Log.w(Config.LOGTAG, "sate loss while opening conversation", e); - //allowing state loss is probably fine since view intents et all are already stored and a click can probably be 'ignored' - return; - } - } - } else { - mainNeedsRefresh = true; - } - conversationFragment.reInit(conversation, extras == null ? new Bundle() : extras); - if (mainNeedsRefresh) { - refreshFragment(R.id.main_fragment); - } else { - invalidateActionBarTitle(); - } - IntroHelper.showIntro(this, conversation.getMode() == Conversational.MODE_MULTI); - } - - 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()) { - final Conversation conversation = xmppConnectionService.findUniqueConversationByJid(xmppUri); - if (conversation != null) { - openConversation(conversation, null); - return true; - } - } - return false; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (MenuDoubleTabUtil.shouldIgnoreTap()) { - return false; - } - switch (item.getItemId()) { - case android.R.id.home: - FragmentManager fm = getFragmentManager(); - if (fm.getBackStackEntryCount() > 0) { - try { - fm.popBackStack(); - } catch (IllegalStateException e) { - Log.w(Config.LOGTAG, "Unable to pop back stack after pressing home button"); - } - return true; - } - break; - case R.id.action_scan_qr_code: - UriHandlerActivity.scan(this); - return true; - case R.id.action_search_all_conversations: - startActivity(new Intent(this, SearchActivity.class)); - return true; - case R.id.action_search_this_conversation: - final Conversation conversation = ConversationFragment.getConversation(this); - if (conversation == null) { - return true; - } - final Intent intent = new Intent(this, SearchActivity.class); - intent.putExtra(SearchActivity.EXTRA_CONVERSATION_UUID, conversation.getUuid()); - startActivity(intent); - return true; - case R.id.action_check_updates: - if (xmppConnectionService.hasInternetConnection()) { - openInstallFromUnknownSourcesDialogIfNeeded(true); - } else { - ToastCompat.makeText(this, R.string.account_status_no_internet, ToastCompat.LENGTH_LONG).show(); - } - break; - case R.id.action_invite_user: - inviteUser(); - break; - } - return super.onOptionsItemSelected(item); - } - - @Override - public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) { - if (keyCode == KeyEvent.KEYCODE_DPAD_UP && keyEvent.isCtrlPressed()) { - final ConversationFragment conversationFragment = ConversationFragment.get(this); - if (conversationFragment != null && conversationFragment.onArrowUpCtrlPressed()) { - return true; - } - } - return super.onKeyDown(keyCode, keyEvent); - } - - @Override - public void onSaveInstanceState(final Bundle savedInstanceState) { - final Intent pendingIntent = pendingViewIntent.peek(); - savedInstanceState.putParcelable("intent", pendingIntent != null ? pendingIntent : getIntent()); - super.onSaveInstanceState(savedInstanceState); - } - - @Override - protected void onStart() { - super.onStart(); - final int theme = findTheme(); - if (this.mTheme != theme) { - this.mSkipBackgroundBinding = true; - recreate(); - } else { - this.mSkipBackgroundBinding = false; - } - mRedirectInProcess.set(false); - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - this.showLastSeen = preferences.getBoolean("last_activity", getResources().getBoolean(R.bool.last_activity)); - super.onStart(); - } - - @Override - protected void onNewIntent(final Intent intent) { - super.onNewIntent(intent); - if (isViewOrShareIntent(intent)) { - if (xmppConnectionService != null) { - clearPendingViewIntent(); - processViewIntent(intent); - } else { - pendingViewIntent.push(intent); - } - } else if (intent != null && ACTION_DESTROY_MUC.equals(intent.getAction())) { - try { - final Bundle extras = intent.getExtras(); - if (extras != null && extras.containsKey("MUC_UUID")) { - Log.d(Config.LOGTAG, "Get " + intent.getAction() + " intent for " + extras.getString("MUC_UUID")); - Conversation conversation = xmppConnectionService.findConversationByUuid(extras.getString("MUC_UUID")); - ConversationsActivity.this.xmppConnectionService.clearConversationHistory(conversation); - xmppConnectionService.destroyRoom(conversation, ConversationsActivity.this); - endConversation(conversation); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - setIntent(createLauncherIntent(this)); - } - - public void endConversation(Conversation conversation) { - xmppConnectionService.archiveConversation(conversation); - onConversationArchived(conversation); - } - - @Override - public void onPause() { - this.mActivityPaused = true; - super.onPause(); - hideMemoryWarningDialog(); - } - - private void hideMemoryWarningDialog() { - if (memoryWarningDialog != null && memoryWarningDialog.isShowing()) { - memoryWarningDialog.cancel(); - } - } - - @Override - public void onResume() { - super.onResume(); - invalidateActionBarTitle(); - this.mActivityPaused = false; - } - - private void initializeFragments() { - FragmentTransaction transaction = getFragmentManager().beginTransaction(); - Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment); - Fragment secondaryFragment = getFragmentManager().findFragmentById(R.id.secondary_fragment); - if (mainFragment != null) { - Log.d(Config.LOGTAG, "initializeFragment(). main fragment exists"); - if (binding.secondaryFragment != null) { - if (mainFragment instanceof ConversationFragment) { - Log.d(Config.LOGTAG, "gained secondary fragment. moving..."); - getFragmentManager().popBackStack(); - transaction.remove(mainFragment); - transaction.commit(); - getFragmentManager().executePendingTransactions(); - transaction = getFragmentManager().beginTransaction(); - transaction.replace(R.id.secondary_fragment, mainFragment); - transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment()); - transaction.commit(); - return; - } - } else { - if (secondaryFragment instanceof ConversationFragment) { - Log.d(Config.LOGTAG, "lost secondary fragment. moving..."); - transaction.remove(secondaryFragment); - transaction.commit(); - getFragmentManager().executePendingTransactions(); - transaction = getFragmentManager().beginTransaction(); - transaction.replace(R.id.main_fragment, secondaryFragment); - transaction.addToBackStack(null); - transaction.commit(); - return; - } - } - } else { - transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment()); - } - - if (binding.secondaryFragment != null && secondaryFragment == null) { - transaction.replace(R.id.secondary_fragment, new ConversationFragment()); - } - transaction.commit(); - } - - private void invalidateActionBarTitle() { - final ActionBar actionBar = getSupportActionBar(); - 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.setDisplayHomeAsUpEnabled(true); - final View view = getLayoutInflater().inflate(R.layout.ab_title, null); - getSupportActionBar().setCustomView(view); - actionBar.setIcon(null); - actionBar.setBackgroundDrawable(new ColorDrawable(StyledAttributes.getColor(this, R.attr.colorPrimary))); - actionBar.setDisplayShowTitleEnabled(false); - actionBar.setDisplayShowCustomEnabled(true); - TextView abtitle = findViewById(android.R.id.text1); - TextView absubtitle = findViewById(android.R.id.text2); - abtitle.setText(conversation.getName()); - abtitle.setSelected(true); - if (conversation.getMode() == Conversation.MODE_SINGLE) { - if (!conversation.withSelf()) { - ChatState state = conversation.getIncomingChatState(); - if (state == ChatState.COMPOSING) { - absubtitle.setText(getString(R.string.is_typing)); - absubtitle.setVisibility(View.VISIBLE); - absubtitle.setTypeface(null, Typeface.BOLD_ITALIC); - absubtitle.setSelected(true); - } else { - if (showLastSeen && conversation.getContact().getLastseen() > 0 && conversation.getContact().getPresences().allOrNonSupport(Namespace.IDLE)) { - absubtitle.setText(UIHelper.lastseen(getApplicationContext(), conversation.getContact().isActive(), conversation.getContact().getLastseen())); - absubtitle.setVisibility(View.VISIBLE); - } else { - absubtitle.setText(null); - absubtitle.setVisibility(View.GONE); - } - absubtitle.setSelected(true); - } - } else { - absubtitle.setText(null); - absubtitle.setVisibility(View.GONE); - } - } else { - ChatState state = ChatState.COMPOSING; - List userWithChatStates = conversation.getMucOptions().getUsersWithChatState(state, 5); - if (userWithChatStates.size() == 0) { - state = ChatState.PAUSED; - userWithChatStates = conversation.getMucOptions().getUsersWithChatState(state, 5); - } - List users = conversation.getMucOptions().getUsers(true); - if (state == ChatState.COMPOSING) { - if (userWithChatStates.size() > 0) { - if (userWithChatStates.size() == 1) { - MucOptions.User user = userWithChatStates.get(0); - absubtitle.setText(getString(R.string.contact_is_typing, UIHelper.getDisplayName(user))); - absubtitle.setVisibility(View.VISIBLE); - } else { - StringBuilder builder = new StringBuilder(); - for (MucOptions.User user : userWithChatStates) { - if (builder.length() != 0) { - builder.append(", "); - } - builder.append(UIHelper.getDisplayName(user)); - } - absubtitle.setText(getString(R.string.contacts_are_typing, builder.toString())); - absubtitle.setVisibility(View.VISIBLE); - } - } - } else { - if (users.size() == 0) { - absubtitle.setText(getString(R.string.one_participant)); - absubtitle.setVisibility(View.VISIBLE); - } else { - int size = users.size() + 1; - absubtitle.setText(getString(R.string.more_participants, size)); - absubtitle.setVisibility(View.VISIBLE); - } - } - absubtitle.setSelected(true); - } - ActionBarUtil.setCustomActionBarOnClickListener( - binding.toolbar, - (v) -> openConversationDetails(conversation) - ); - return; - } - } - actionBar.setDisplayShowTitleEnabled(true); - actionBar.setDisplayShowCustomEnabled(false); - actionBar.setTitle(null); - actionBar.setIcon(R.drawable.logo_toolbar_white); - //actionBar.setBackgroundDrawable(new ColorDrawable(getResources().getColor(R.color.header_background))); - actionBar.setSubtitle(null); - actionBar.setDisplayHomeAsUpEnabled(false); - ActionBarUtil.resetCustomActionBarOnClickListeners(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); - } - } - } - public void verifyOtrSessionDialog(final Conversation conversation, View view) { - if (!conversation.hasValidOtrSession() || conversation.getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) { - ToastCompat.makeText(this, R.string.otr_session_not_started, Toast.LENGTH_LONG).show(); - return; - } - if (view == null) { - return; - } - PopupMenu popup = new PopupMenu(this, view); - popup.inflate(R.menu.verification_choices); - popup.setOnMenuItemClickListener(menuItem -> { - Intent intent = new Intent(ConversationsActivity.this, VerifyOTRActivity.class); - intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT); - intent.putExtra("contact", conversation.getContact().getJid().asBareJid().toString()); - intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString()); - switch (menuItem.getItemId()) { - case R.id.ask_question: - intent.putExtra("mode", VerifyOTRActivity.MODE_ASK_QUESTION); - break; - } - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - return true; - }); - popup.show(); - } - @Override - public void onConversationArchived(Conversation conversation) { - if (performRedirectIfNecessary(conversation, false)) { - return; - } - final FragmentManager fragmentManager = getFragmentManager(); - final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment); - if (mainFragment instanceof ConversationFragment) { - try { - 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; - } - final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment); - if (secondaryFragment instanceof ConversationFragment) { - if (((ConversationFragment) secondaryFragment).getConversation() == conversation) { - Conversation suggestion = ConversationsOverviewFragment.getSuggestion(this, conversation); - if (suggestion != null) { - openConversation(suggestion, null); - } - } - } - } - - @Override - public void onConversationsListItemUpdated() { - Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment); - if (fragment instanceof ConversationsOverviewFragment) { - ((ConversationsOverviewFragment) fragment).refresh(); - } - } - - @Override - public void switchToConversation(Conversation conversation) { - Log.d(Config.LOGTAG, "override"); - openConversation(conversation, null); - } - - @Override - public void onConversationRead(Conversation conversation, String upToUuid) { - if (!mActivityPaused && pendingViewIntent.peek() == null) { - xmppConnectionService.sendReadMarker(conversation, upToUuid); - } else { - Log.d(Config.LOGTAG, "ignoring read callback. mActivityPaused=" + Boolean.toString(mActivityPaused)); - } - } - - @Override - public void onAccountUpdate() { - this.refreshUi(); - } - - @Override - public void onConversationUpdate() { - if (performRedirectIfNecessary(false)) { - return; - } - this.refreshUi(); - } - - @Override - public void onRosterUpdate() { - this.refreshUi(); - } - - @Override - public void OnUpdateBlocklist(OnUpdateBlocklist.Status status) { - this.refreshUi(); - } - - @Override - public void onShowErrorToast(int resId) { - runOnUiThread(() -> ToastCompat.makeText(this, resId, ToastCompat.LENGTH_SHORT).show()); - } - - protected void AppUpdate(String Store) { - String PREFS_NAME = "UpdateTimeStamp"; - SharedPreferences UpdateTimeStamp = getApplicationContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - long lastUpdateTime = UpdateTimeStamp.getLong("lastUpdateTime", 0); - Log.d(Config.LOGTAG, "AppUpdater: LastUpdateTime: " + lastUpdateTime); - if ((lastUpdateTime + (Config.UPDATE_CHECK_TIMER * 1000)) < System.currentTimeMillis()) { - lastUpdateTime = System.currentTimeMillis(); - SharedPreferences.Editor editor = UpdateTimeStamp.edit(); - editor.putLong("lastUpdateTime", lastUpdateTime); - editor.apply(); - Log.d(Config.LOGTAG, "AppUpdater: CurrentTime: " + lastUpdateTime); - openInstallFromUnknownSourcesDialogIfNeeded(false); - } else { - Log.d(Config.LOGTAG, "AppUpdater stopped"); - } - } - - @Override - public void onRoomDestroySucceeded() { - Conversation conversation = ConversationFragment.getConversationReliable(this); - final boolean groupChat = conversation != null && conversation.isPrivateAndNonAnonymous(); - displayToast(getString(groupChat ? R.string.destroy_room_succeed : R.string.destroy_channel_succeed)); - } - - @Override - public void onRoomDestroyFailed() { - Conversation conversation = ConversationFragment.getConversationReliable(this); - final boolean groupChat = conversation != null && conversation.isPrivateAndNonAnonymous(); - displayToast(getString(groupChat ? R.string.destroy_room_failed : R.string.destroy_channel_failed)); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java deleted file mode 100644 index f861d213b..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java +++ /dev/null @@ -1,515 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui; - -import android.animation.Animator; -import android.animation.AnimatorInflater; -import android.app.Activity; -import android.app.Fragment; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import static androidx.recyclerview.widget.ItemTouchHelper.RIGHT; -import android.app.AlertDialog; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.widget.Toast; -import androidx.recyclerview.widget.ItemTouchHelper; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.snackbar.Snackbar; -import com.google.common.collect.Collections2; -import java.util.concurrent.atomic.AtomicReference; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Conversational; -import eu.siacs.conversations.ui.interfaces.OnConversationArchived; -import eu.siacs.conversations.ui.util.StyledAttributes; -import eu.siacs.conversations.utils.EasyOnboardingInvite; -import eu.siacs.conversations.utils.ThemeHelper; -import android.view.ContextMenu; -import android.widget.AdapterView.AdapterContextMenuInfo; -import com.google.common.base.Optional; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; -import androidx.databinding.DataBindingUtil; -import androidx.recyclerview.widget.LinearLayoutManager; - -import java.util.ArrayList; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.FragmentConversationsOverviewBinding; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.ui.adapter.ConversationAdapter; -import eu.siacs.conversations.ui.interfaces.OnConversationSelected; -import eu.siacs.conversations.ui.util.PendingActionHelper; -import eu.siacs.conversations.ui.util.PendingItem; -import eu.siacs.conversations.ui.util.ScrollState; -import eu.siacs.conversations.utils.MenuDoubleTabUtil; - -public class ConversationsOverviewFragment extends XmppFragment { - - private static final String STATE_SCROLL_POSITION = ConversationsOverviewFragment.class.getName() + ".scroll_state"; - - private final List conversations = new ArrayList<>(); - private final PendingItem swipedConversation = new PendingItem<>(); - private final PendingItem pendingScrollState = new PendingItem<>(); - private FragmentConversationsOverviewBinding binding; - private ConversationAdapter conversationsAdapter; - private XmppActivity activity; - private float mSwipeEscapeVelocity = 0f; - private final PendingActionHelper pendingActionHelper = new PendingActionHelper(); - - private final ItemTouchHelper.SimpleCallback callback = new ItemTouchHelper.SimpleCallback(0, 0) { - @Override - public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { - //todo maybe we can manually changing the position of the conversation - return false; - } - - @Override - public float getSwipeEscapeVelocity(float defaultValue) { - return mSwipeEscapeVelocity; - } - - @Override - public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, - float dX, float dY, int actionState, boolean isCurrentlyActive) { - super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); - if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { - Paint paint = new Paint(); - paint.setColor(StyledAttributes.getColor(activity, R.attr.color_warning)); - paint.setStyle(Paint.Style.FILL); - c.drawRect(viewHolder.itemView.getLeft(), viewHolder.itemView.getTop() - , viewHolder.itemView.getRight(), viewHolder.itemView.getBottom(), paint); - } - } - - @Override - public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { - super.clearView(recyclerView, viewHolder); - viewHolder.itemView.setAlpha(1f); - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { - pendingActionHelper.execute(); - int position = viewHolder.getLayoutPosition(); - try { - swipedConversation.push(conversations.get(position)); - } catch (IndexOutOfBoundsException e) { - return; - } - conversationsAdapter.remove(swipedConversation.peek(), position); - activity.xmppConnectionService.markRead(swipedConversation.peek()); - if (position == 0 && conversationsAdapter.getItemCount() == 0) { - final Conversation c = swipedConversation.pop(); - activity.xmppConnectionService.archiveConversation(c); - return; - } - final boolean formerlySelected = ConversationFragment.getConversation(getActivity()) == swipedConversation.peek(); - if (activity instanceof OnConversationArchived) { - ((OnConversationArchived) activity).onConversationArchived(swipedConversation.peek()); - } - final Conversation c = swipedConversation.peek(); - final int title; - if (c.getMode() == Conversational.MODE_MULTI) { - if (c.getMucOptions().isPrivateAndNonAnonymous()) { - title = R.string.title_undo_swipe_out_group_chat; - } else { - title = R.string.title_undo_swipe_out_channel; - } - } else { - title = R.string.title_undo_swipe_out_conversation; - } - final Snackbar snackbar = Snackbar.make(binding.list, title, 5000) - .setAction(R.string.undo, v -> { - pendingActionHelper.undo(); - Conversation conversation = swipedConversation.pop(); - conversationsAdapter.insert(conversation, position); - if (formerlySelected) { - if (activity instanceof OnConversationSelected) { - ((OnConversationSelected) activity).onConversationSelected(c); - } - } - LinearLayoutManager layoutManager = (LinearLayoutManager) binding.list.getLayoutManager(); - if (position > layoutManager.findLastVisibleItemPosition()) { - binding.list.smoothScrollToPosition(position); - } - }) - .addCallback(new Snackbar.Callback() { - @Override - public void onDismissed(Snackbar transientBottomBar, int event) { - switch (event) { - case DISMISS_EVENT_SWIPE: - case DISMISS_EVENT_TIMEOUT: - pendingActionHelper.execute(); - break; - } - } - }); - - pendingActionHelper.push(() -> { - if (snackbar.isShownOrQueued()) { - snackbar.dismiss(); - } - final Conversation conversation = swipedConversation.pop(); - if (conversation != null) { - if (!conversation.isRead() && conversation.getMode() == Conversation.MODE_SINGLE) { - return; - } - activity.xmppConnectionService.archiveConversation(c); - } - }); - ThemeHelper.fix(snackbar); - snackbar.show(); - } - - @Override - public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { - int dragFlags = 0; - int swipeFlags = conversations.get(viewHolder.getLayoutPosition()).getMode() == Conversational.MODE_SINGLE ? RIGHT : 0; - return makeMovementFlags(dragFlags, swipeFlags); - } - }; - - private ItemTouchHelper touchHelper; - - public static Conversation getSuggestion(Activity activity) { - final Conversation exception; - Fragment fragment = activity.getFragmentManager().findFragmentById(R.id.main_fragment); - if (fragment instanceof ConversationsOverviewFragment) { - exception = ((ConversationsOverviewFragment) fragment).swipedConversation.peek(); - } else { - exception = null; - } - return getSuggestion(activity, exception); - } - - public static Conversation getSuggestion(Activity activity, Conversation exception) { - Fragment fragment = activity.getFragmentManager().findFragmentById(R.id.main_fragment); - if (fragment instanceof ConversationsOverviewFragment) { - List conversations = ((ConversationsOverviewFragment) fragment).conversations; - if (conversations.size() > 0) { - Conversation suggestion = conversations.get(0); - if (suggestion == exception) { - if (conversations.size() > 1) { - return conversations.get(1); - } - } else { - return suggestion; - } - } - } - return null; - - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - if (savedInstanceState == null) { - return; - } - pendingScrollState.push(savedInstanceState.getParcelable(STATE_SCROLL_POSITION)); - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - if (activity instanceof XmppActivity) { - this.activity = (XmppActivity) activity; - } else { - throw new IllegalStateException("Trying to attach fragment to activity that is not an XmppActivity"); - } - } - - @Override - public void onDestroyView() { - Log.d(Config.LOGTAG, "ConversationsOverviewFragment.onDestroyView()"); - super.onDestroyView(); - this.binding = null; - this.conversationsAdapter = null; - this.touchHelper = null; - } - - @Override - public void onDestroy() { - Log.d(Config.LOGTAG, "ConversationsOverviewFragment.onDestroy()"); - super.onDestroy(); - } - - @Override - public void onPause() { - Log.d(Config.LOGTAG, "ConversationsOverviewFragment.onPause()"); - pendingActionHelper.execute(); - super.onPause(); - } - - @Override - public void onDetach() { - super.onDetach(); - this.activity = null; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setHasOptionsMenu(true); - } - - @Override - public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - this.mSwipeEscapeVelocity = getResources().getDimension(R.dimen.swipe_escape_velocity); - this.binding = DataBindingUtil.inflate(inflater, R.layout.fragment_conversations_overview, container, false); - this.binding.fab.setOnClickListener((view) -> StartConversationActivity.launch(getActivity())); - - this.conversationsAdapter = new ConversationAdapter(this.activity, this.conversations); - if (this.conversations.size() > 0) { - this.activity.xmppConnectionService.updateNotificationChannels(); - } - this.conversationsAdapter.setConversationClickListener((view, conversation) -> { - if (activity instanceof OnConversationSelected) { - ((OnConversationSelected) activity).onConversationSelected(conversation); - } else { - Log.w(ConversationsOverviewFragment.class.getCanonicalName(), "Activity does not implement OnConversationSelected"); - } - }); - this.binding.list.setAdapter(this.conversationsAdapter); - this.binding.list.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false)); - registerForContextMenu(this.binding.list); - this.touchHelper = new ItemTouchHelper(this.callback); - this.touchHelper.attachToRecyclerView(this.binding.list); - return binding.getRoot(); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { - menuInflater.inflate(R.menu.fragment_conversations_overview, menu); - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) { - activity.getMenuInflater().inflate(R.menu.conversations, menu); - final XmppActivity activity = XmppActivity.find(view); - final Object tag = view.getTag(); - Conversation conversation = conversations.get(((AdapterContextMenuInfo) menuInfo).position); - String name; - if (tag instanceof MucOptions.User && activity != null) { - activity.getMenuInflater().inflate(R.menu.muc_details_context, menu); - final MucOptions.User user = (MucOptions.User) tag; - final Contact contact = user.getContact(); - if (contact != null && contact.showInContactList()) { - name = contact.getDisplayName(); - } else if (user.getRealJid() != null) { - name = user.getRealJid().asBareJid().toEscapedString(); - } else { - name = user.getName(); - } - } else { - name = conversation.getAvatarName(); - } - menu.setHeaderTitle(name); - final MenuItem menuMucDetails = menu.findItem(R.id.action_group_details); - final MenuItem menuContactDetails = menu.findItem(R.id.action_contact_details); - final MenuItem menuMute = menu.findItem(R.id.action_mute); - final MenuItem menuUnmute = menu.findItem(R.id.action_unmute); - final MenuItem menuOngoingCall = menu.findItem(R.id.action_ongoing_call); - final MenuItem menuTogglePinned = menu.findItem(R.id.action_toggle_pinned); - - if (conversation != null) { - if (conversation.getMode() == Conversation.MODE_MULTI) { - menuContactDetails.setVisible(false); - menuMucDetails.setTitle(conversation.getMucOptions().isPrivateAndNonAnonymous() ? R.string.conference_details : R.string.channel_details); - menuOngoingCall.setVisible(false); - } else { - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; - final Optional ongoingRtpSession = service == null ? Optional.absent() : service.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); - if (ongoingRtpSession.isPresent()) { - menuOngoingCall.setVisible(true); - } else { - menuOngoingCall.setVisible(false); - } - menuContactDetails.setVisible(!conversation.withSelf()); - menuMucDetails.setVisible(false); - } - if (conversation.isMuted()) { - menuMute.setVisible(false); - } else { - menuUnmute.setVisible(false); - } - if (conversation.getBooleanAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP, false)) { - menuTogglePinned.setTitle(R.string.remove_from_favorites); - } else { - menuTogglePinned.setTitle(R.string.add_to_favorites); - } - } - super.onCreateContextMenu(menu, view, menuInfo); - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - Conversation conversation = conversations.get(((AdapterContextMenuInfo) item.getMenuInfo()).position); - ConversationFragment fragment = new ConversationFragment(); - fragment.setHasOptionsMenu(false); - fragment.onAttach(activity); - fragment.reInit(conversation, null); - boolean r = fragment.onOptionsItemSelected(item); - refresh(); - return r; - } - - @Override - public void onBackendConnected() { - refresh(); - } - - @Override - public void onSaveInstanceState(Bundle bundle) { - super.onSaveInstanceState(bundle); - ScrollState scrollState = getScrollState(); - if (scrollState != null) { - bundle.putParcelable(STATE_SCROLL_POSITION, scrollState); - } - } - - private ScrollState getScrollState() { - if (this.binding == null) { - return null; - } - LinearLayoutManager layoutManager = (LinearLayoutManager) this.binding.list.getLayoutManager(); - int position = layoutManager.findFirstVisibleItemPosition(); - final View view = this.binding.list.getChildAt(0); - if (view != null) { - return new ScrollState(position, view.getTop()); - } else { - return new ScrollState(position, 0); - } - } - - @Override - public void onStart() { - super.onStart(); - Log.d(Config.LOGTAG, "ConversationsOverviewFragment.onStart()"); - if (activity.xmppConnectionService != null) { - refresh(); - } - } - - @Override - public void onResume() { - super.onResume(); - Log.d(Config.LOGTAG, "ConversationsOverviewFragment.onResume()"); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - if (MenuDoubleTabUtil.shouldIgnoreTap()) { - return false; - } - switch (item.getItemId()) { - case R.id.action_search: - startActivity(new Intent(getActivity(), SearchActivity.class)); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { - int animator = enter ? R.animator.fade_left_in : R.animator.fade_left_out; - return AnimatorInflater.loadAnimator(getActivity(), animator); - } - - private void selectAccountToStartEasyInvite() { - final List accounts = EasyOnboardingInvite.getSupportingAccounts(activity.xmppConnectionService); - if (accounts.size() == 0) { - //This can technically happen if opening the menu item races with accounts reconnecting or something - Toast.makeText(getActivity(), R.string.no_active_accounts_support_this, Toast.LENGTH_LONG).show(); - } else if (accounts.size() == 1) { - openEasyInviteScreen(accounts.get(0)); - } else { - final AtomicReference selectedAccount = new AtomicReference<>(accounts.get(0)); - final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity); - alertDialogBuilder.setTitle(R.string.choose_account); - final String[] asStrings = Collections2.transform(accounts, a -> a.getJid().asBareJid().toEscapedString()).toArray(new String[0]); - alertDialogBuilder.setSingleChoiceItems(asStrings, 0, (dialog, which) -> selectedAccount.set(accounts.get(which))); - alertDialogBuilder.setNegativeButton(R.string.cancel, null); - alertDialogBuilder.setPositiveButton(R.string.ok, (dialog, which) -> openEasyInviteScreen(selectedAccount.get())); - alertDialogBuilder.create().show(); - } - } - - private void openEasyInviteScreen(final Account account) { - EasyOnboardingInviteActivity.launch(account, activity); - } - - @Override - void refresh() { - if (this.binding == null || this.activity == null) { - Log.d(Config.LOGTAG, "ConversationsOverviewFragment.refresh() skipped updated because view binding or activity was null"); - return; - } - this.activity.xmppConnectionService.populateWithOrderedConversations(this.conversations); - if (this.conversations.size() > 0) { - this.activity.xmppConnectionService.updateNotificationChannels(); - } - Conversation removed = this.swipedConversation.peek(); - if (removed != null) { - if (removed.isRead()) { - this.conversations.remove(removed); - } else { - pendingActionHelper.execute(); - } - } - this.conversationsAdapter.notifyDataSetChanged(); - ScrollState scrollState = pendingScrollState.pop(); - if (scrollState != null) { - setScrollPosition(scrollState); - } - } - - private void setScrollPosition(ScrollState scrollPosition) { - if (scrollPosition != null) { - LinearLayoutManager layoutManager = (LinearLayoutManager) binding.list.getLayoutManager(); - layoutManager.scrollToPositionWithOffset(scrollPosition.position, scrollPosition.offset); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/CreatePrivateGroupChatDialog.java b/src/main/java/eu/siacs/conversations/ui/CreatePrivateGroupChatDialog.java deleted file mode 100644 index 160a0f27c..000000000 --- a/src/main/java/eu/siacs/conversations/ui/CreatePrivateGroupChatDialog.java +++ /dev/null @@ -1,94 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.app.Dialog; -import android.content.Context; -import android.os.Bundle; -import android.view.View; -import android.widget.Spinner; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.databinding.DataBindingUtil; -import androidx.fragment.app.DialogFragment; - -import java.util.ArrayList; -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.CreateConferenceDialogBinding; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.util.DelayedHintHelper; - - -public class CreatePrivateGroupChatDialog extends DialogFragment { - - private static final String ACCOUNTS_LIST_KEY = "activated_accounts_list"; - private static final String MULTIPLE_ACCOUNTS = "multiple_accounts_enabled"; - public XmppConnectionService xmppConnectionService; - private CreateConferenceDialogListener mListener; - - public static CreatePrivateGroupChatDialog newInstance(List accounts, boolean multipleAccounts) { - CreatePrivateGroupChatDialog dialog = new CreatePrivateGroupChatDialog(); - Bundle bundle = new Bundle(); - bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList) accounts); - bundle.putBoolean(MULTIPLE_ACCOUNTS, multipleAccounts); - dialog.setArguments(bundle); - return dialog; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setRetainInstance(true); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(R.string.create_private_group_chat); - CreateConferenceDialogBinding binding = DataBindingUtil.inflate(getActivity().getLayoutInflater(), R.layout.create_conference_dialog, null, false); - if (getArguments().getBoolean(MULTIPLE_ACCOUNTS)) { - binding.yourAccount.setVisibility(View.VISIBLE); - binding.account.setVisibility(View.VISIBLE); - } else { - binding.yourAccount.setVisibility(View.GONE); - binding.account.setVisibility(View.GONE); - } - ArrayList mActivatedAccounts = getArguments().getStringArrayList(ACCOUNTS_LIST_KEY); - StartConversationActivity.populateAccountSpinner(getActivity(), mActivatedAccounts, binding.account); - builder.setView(binding.getRoot()); - builder.setPositiveButton(R.string.choose_participants, (dialog, which) -> mListener.onCreateDialogPositiveClick(binding.account, binding.groupChatName.getText().toString().trim())); - builder.setNegativeButton(R.string.cancel, null); - DelayedHintHelper.setHint(R.string.providing_a_name_is_optional, binding.groupChatName); - binding.groupChatName.setOnEditorActionListener((v, actionId, event) -> { - mListener.onCreateDialogPositiveClick(binding.account, binding.groupChatName.getText().toString().trim()); - return true; - }); - return builder.create(); - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - try { - mListener = (CreateConferenceDialogListener) context; - } catch (ClassCastException e) { - throw new ClassCastException(context.toString() - + " must implement CreateConferenceDialogListener"); - } - } - - @Override - public void onDestroyView() { - Dialog dialog = getDialog(); - if (dialog != null && getRetainInstance()) { - dialog.setDismissMessage(null); - } - super.onDestroyView(); - } - - public interface CreateConferenceDialogListener { - void onCreateDialogPositiveClick(Spinner spinner, String subject); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java b/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java deleted file mode 100644 index 6bf126c68..000000000 --- a/src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java +++ /dev/null @@ -1,303 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.app.Activity; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.view.View; -import android.widget.AdapterView; -import android.widget.Button; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.databinding.DataBindingUtil; -import androidx.fragment.app.DialogFragment; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.CreatePublicChannelDialogBinding; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; -import eu.siacs.conversations.ui.interfaces.OnBackendConnected; -import eu.siacs.conversations.ui.util.DelayedHintHelper; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.XmppConnection; - -public class CreatePublicChannelDialog extends DialogFragment implements OnBackendConnected { - - private static final char[] FORBIDDEN = new char[]{'\u0022', '&', '\'', '/', ':', '<', '>', '@'}; - - private static final String ACCOUNTS_LIST_KEY = "activated_accounts_list"; - private static final String MULTIPLE_ACCOUNTS = "multiple_accounts_enabled"; - private CreatePublicChannelDialogListener mListener; - private KnownHostsAdapter knownHostsAdapter; - private boolean jidWasModified = false; - private boolean nameEntered = false; - private boolean skipTetxWatcher = false; - - public static CreatePublicChannelDialog newInstance(List accounts, boolean multipleAccounts) { - CreatePublicChannelDialog dialog = new CreatePublicChannelDialog(); - Bundle bundle = new Bundle(); - bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList) accounts); - bundle.putBoolean(MULTIPLE_ACCOUNTS, multipleAccounts); - dialog.setArguments(bundle); - return dialog; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setRetainInstance(true); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - jidWasModified = savedInstanceState != null && savedInstanceState.getBoolean("jid_was_modified_false", false); - nameEntered = savedInstanceState != null && savedInstanceState.getBoolean("name_entered", false); - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(R.string.create_public_channel); - final CreatePublicChannelDialogBinding binding = DataBindingUtil.inflate(getActivity().getLayoutInflater(), R.layout.create_public_channel_dialog, null, false); - if (getArguments().getBoolean(MULTIPLE_ACCOUNTS)) { - binding.yourAccount.setVisibility(View.VISIBLE); - binding.account.setVisibility(View.VISIBLE); - } else { - binding.yourAccount.setVisibility(View.GONE); - binding.account.setVisibility(View.GONE); - } - binding.account.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - updateJidSuggestion(binding); - } - - @Override - public void onNothingSelected(AdapterView parent) { - - } - }); - binding.jid.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - - } - - @Override - public void afterTextChanged(Editable s) { - if (skipTetxWatcher) { - return; - } - if (jidWasModified) { - jidWasModified = !TextUtils.isEmpty(s); - } else { - jidWasModified = !s.toString().equals(getJidSuggestion(binding)); - } - } - }); - updateInputs(binding, false); - ArrayList mActivatedAccounts = getArguments().getStringArrayList(ACCOUNTS_LIST_KEY); - StartConversationActivity.populateAccountSpinner(getActivity(), mActivatedAccounts, binding.account); - builder.setView(binding.getRoot()); - builder.setPositiveButton(nameEntered ? R.string.create : R.string.next, null); - builder.setNegativeButton(nameEntered ? R.string.back : R.string.cancel, null); - DelayedHintHelper.setHint(R.string.channel_bare_jid_example, binding.jid); - this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.simple_list_item); - binding.jid.setAdapter(knownHostsAdapter); - final AlertDialog dialog = builder.create(); - binding.groupChatName.setOnEditorActionListener((v, actionId, event) -> { - submit(dialog, binding); - return true; - }); - dialog.setOnShowListener(dialogInterface -> { - dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(v -> goBack(dialog, binding)); - dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> submit(dialog, binding)); - }); - return dialog; - } - - private void updateJidSuggestion(CreatePublicChannelDialogBinding binding) { - if (jidWasModified) { - return; - } - String jid = getJidSuggestion(binding); - skipTetxWatcher = true; - binding.jid.setText(jid); - skipTetxWatcher = false; - } - - @Override - public void onSaveInstanceState(Bundle outState) { - outState.putBoolean("jid_was_modified", jidWasModified); - outState.putBoolean("name_entered", nameEntered); - super.onSaveInstanceState(outState); - } - - private static String getJidSuggestion(CreatePublicChannelDialogBinding binding) { - final Account account = StartConversationActivity.getSelectedAccount(binding.getRoot().getContext(), binding.account); - final XmppConnection connection = account == null ? null : account.getXmppConnection(); - if (connection == null) { - return ""; - } - final Editable nameText = binding.groupChatName.getText(); - final String name = nameText == null ? "" : nameText.toString().trim(); - final String domain = connection.getMucServer(); - if (domain == null) { - return ""; - } - final String localpart = clean(name); - if (TextUtils.isEmpty(localpart)) { - return ""; - } else { - try { - return Jid.of(localpart, domain, null).toEscapedString(); - } catch (IllegalArgumentException e) { - return Jid.of(CryptoHelper.pronounceable(), domain, null).toEscapedString(); - } - } - } - - private static String clean(String name) { - for (char c : FORBIDDEN) { - name = name.replace(String.valueOf(c), ""); - } - return name.replaceAll("\\s+", "-"); - } - - private void goBack(AlertDialog dialog, CreatePublicChannelDialogBinding binding) { - if (nameEntered) { - nameEntered = false; - updateInputs(binding, true); - updateButtons(dialog); - } else { - dialog.dismiss(); - } - } - - private void submit(AlertDialog dialog, CreatePublicChannelDialogBinding binding) { - final Context context = binding.getRoot().getContext(); - final Editable nameText = binding.groupChatName.getText(); - final String name = nameText == null ? "" : nameText.toString().trim(); - final Editable addressText = binding.jid.getText(); - final String address = addressText == null ? "" : addressText.toString().trim(); - if (nameEntered) { - binding.nameLayout.setError(null); - if (address.isEmpty()) { - binding.xmppAddressLayout.setError(context.getText(R.string.please_enter_xmpp_address)); - } else { - final Jid jid; - try { - jid = Jid.ofEscaped(address); - } catch (IllegalArgumentException e) { - binding.xmppAddressLayout.setError(context.getText(R.string.invalid_jid)); - return; - } - final Account account = StartConversationActivity.getSelectedAccount(context, binding.account); - if (account == null) { - return; - } - final XmppConnectionService service = ((XmppActivity) context).xmppConnectionService; - if (service != null && service.findFirstMuc(jid) != null) { - binding.xmppAddressLayout.setError(context.getString(R.string.channel_already_exists)); - return; - } - mListener.onCreatePublicChannel(account, name, jid); - dialog.dismiss(); - } - } else { - binding.xmppAddressLayout.setError(null); - if (name.isEmpty()) { - binding.nameLayout.setError(context.getText(R.string.please_enter_name)); - } else if (StartConversationActivity.isValidJid(name)) { - binding.nameLayout.setError(context.getText(R.string.this_is_an_xmpp_address)); - } else { - binding.nameLayout.setError(null); - nameEntered = true; - updateInputs(binding, true); - updateButtons(dialog); - binding.jid.setText(""); - binding.jid.append(getJidSuggestion(binding)); - } - } - } - - - private void updateInputs(CreatePublicChannelDialogBinding binding, boolean requestFocus) { - binding.xmppAddressLayout.setVisibility(nameEntered ? View.VISIBLE : View.GONE); - binding.nameLayout.setVisibility(nameEntered ? View.GONE : View.VISIBLE); - if (!requestFocus) { - return; - } - if (nameEntered) { - binding.xmppAddressLayout.requestFocus(); - } else { - binding.nameLayout.requestFocus(); - } - } - - private void updateButtons(AlertDialog dialog) { - final Button positive = dialog.getButton(DialogInterface.BUTTON_POSITIVE); - final Button negative = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); - positive.setText(nameEntered ? R.string.create : R.string.next); - negative.setText(nameEntered ? R.string.back : R.string.cancel); - } - - @Override - public void onBackendConnected() { - refreshKnownHosts(); - } - - private void refreshKnownHosts() { - Activity activity = getActivity(); - if (activity instanceof XmppActivity) { - Collection hosts = ((XmppActivity) activity).xmppConnectionService.getKnownConferenceHosts(); - this.knownHostsAdapter.refresh(hosts); - } - } - - public interface CreatePublicChannelDialogListener { - void onCreatePublicChannel(Account account, String name, Jid address); - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - try { - mListener = (CreatePublicChannelDialogListener) context; - } catch (ClassCastException e) { - throw new ClassCastException(context.toString() - + " must implement CreateConferenceDialogListener"); - } - } - - @Override - public void onStart() { - super.onStart(); - final Activity activity = getActivity(); - if (activity instanceof XmppActivity && ((XmppActivity) activity).xmppConnectionService != null) { - refreshKnownHosts(); - } - } - - @Override - public void onDestroyView() { - Dialog dialog = getDialog(); - if (dialog != null && getRetainInstance()) { - dialog.setDismissMessage(null); - } - super.onDestroyView(); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java b/src/main/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java deleted file mode 100644 index 2a2987aef..000000000 --- a/src/main/java/eu/siacs/conversations/ui/EasyOnboardingInviteActivity.java +++ /dev/null @@ -1,151 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.app.Activity; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.Point; -import android.os.Bundle; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; - -import androidx.appcompat.widget.Toolbar; -import androidx.databinding.DataBindingUtil; - -import com.google.common.base.Strings; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ActivityEasyInviteBinding; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.services.BarcodeProvider; -import eu.siacs.conversations.utils.EasyOnboardingInvite; -import eu.siacs.conversations.xmpp.Jid; -import me.drakeet.support.toast.ToastCompat; - -public class EasyOnboardingInviteActivity extends XmppActivity implements EasyOnboardingInvite.OnInviteRequested { - - private ActivityEasyInviteBinding binding; - - private EasyOnboardingInvite easyOnboardingInvite; - - - @Override - public void onCreate(final Bundle bundle) { - super.onCreate(bundle); - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_easy_invite); - setSupportActionBar((Toolbar) binding.toolbar); - configureActionBar(getSupportActionBar(), true); - if (bundle != null && bundle.containsKey("invite")) { - this.easyOnboardingInvite = bundle.getParcelable("invite"); - if (this.easyOnboardingInvite != null) { - showInvite(this.easyOnboardingInvite); - return; - } - } - this.showLoading(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.easy_onboarding_invite, menu); - final MenuItem share = menu.findItem(R.id.action_share); - share.setVisible(easyOnboardingInvite != null); - return super.onCreateOptionsMenu(menu); - } - - public boolean onOptionsItemSelected(MenuItem menuItem) { - if (menuItem.getItemId() == R.id.action_share) { - share(); - return true; - } else { - return super.onOptionsItemSelected(menuItem); - } - } - - private void share() { - final String shareText = getString( - R.string.easy_invite_share_text, - easyOnboardingInvite.getDomain(), - easyOnboardingInvite.getShareableLink() - ); - final Intent sendIntent = new Intent(); - sendIntent.setAction(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_TEXT, shareText); - sendIntent.setType("text/plain"); - startActivity(Intent.createChooser(sendIntent, getString(R.string.share_invite_with))); - } - - @Override - protected void refreshUiReal() { - invalidateOptionsMenu(); - if (easyOnboardingInvite != null) { - showInvite(easyOnboardingInvite); - } else { - showLoading(); - } - } - - private void showLoading() { - this.binding.inProgress.setVisibility(View.VISIBLE); - this.binding.invite.setVisibility(View.GONE); - } - - private void showInvite(final EasyOnboardingInvite invite) { - this.binding.inProgress.setVisibility(View.GONE); - this.binding.invite.setVisibility(View.VISIBLE); - this.binding.tapToShare.setText(getString(R.string.tap_share_button_send_invite, invite.getDomain())); - final Point size = new Point(); - getWindowManager().getDefaultDisplay().getSize(size); - final int width = Math.min(size.x, size.y); - final Bitmap bitmap = BarcodeProvider.create2dBarcodeBitmap(invite.getShareableUri(), width); - binding.qrCode.setImageBitmap(bitmap); - } - - @Override - public void onSaveInstanceState(Bundle bundle) { - super.onSaveInstanceState(bundle); - if (easyOnboardingInvite != null) { - bundle.putParcelable("invite", easyOnboardingInvite); - } - } - - @Override - void onBackendConnected() { - if (easyOnboardingInvite != null) { - return; - } - final Intent launchIntent = getIntent(); - final String accountExtra = launchIntent.getStringExtra(EXTRA_ACCOUNT); - final Jid jid = accountExtra == null ? null : Jid.ofEscaped(accountExtra); - if (jid == null) { - return; - } - final Account account = xmppConnectionService.findAccountByJid(jid); - xmppConnectionService.requestEasyOnboardingInvite(account, this); - } - - public static void launch(final Account account, final Activity context) { - final Intent intent = new Intent(context, EasyOnboardingInviteActivity.class); - intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); - context.startActivity(intent); - } - - @Override - public void inviteRequested(EasyOnboardingInvite invite) { - this.easyOnboardingInvite = invite; - Log.d(Config.LOGTAG, "invite requested"); - refreshUi(); - } - - @Override - public void inviteRequestFailed(final String message) { - runOnUiThread(() -> { - if (!Strings.isNullOrEmpty(message)) { - ToastCompat.makeText(this, message, ToastCompat.LENGTH_LONG).show(); - } - finish(); - }); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java deleted file mode 100644 index acdbfd209..000000000 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ /dev/null @@ -1,1645 +0,0 @@ -package eu.siacs.conversations.ui; - -import static eu.siacs.conversations.utils.PermissionUtils.allGranted; -import static eu.siacs.conversations.utils.PermissionUtils.readGranted; - -import android.app.Activity; -import android.app.PendingIntent; -import android.content.ActivityNotFoundException; -import android.content.Intent; -import android.content.IntentSender; -import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.provider.Settings; -import android.security.KeyChain; -import android.security.KeyChainAliasCallback; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.Log; -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.widget.Toolbar; -import androidx.databinding.DataBindingUtil; - -import com.google.android.material.textfield.TextInputLayout; -import com.google.common.base.CharMatcher; - -import org.openintents.openpgp.util.OpenPgpUtils; - -import java.util.Arrays; -import java.util.List; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; -import eu.siacs.conversations.databinding.ActivityEditAccountBinding; -import eu.siacs.conversations.databinding.DialogPresenceBinding; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Presence; -import eu.siacs.conversations.entities.PresenceTemplate; -import eu.siacs.conversations.services.BarcodeProvider; -import eu.siacs.conversations.services.QuickConversationsService; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; -import eu.siacs.conversations.services.XmppConnectionService.OnCaptchaRequested; -import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; -import eu.siacs.conversations.ui.adapter.PresenceTemplateAdapter; -import eu.siacs.conversations.ui.util.AvatarWorkerTask; -import eu.siacs.conversations.ui.util.CustomTab; -import eu.siacs.conversations.ui.util.PendingItem; -import eu.siacs.conversations.ui.util.SoftKeyboardUtils; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.EasyOnboardingInvite; -import eu.siacs.conversations.utils.MenuDoubleTabUtil; -import eu.siacs.conversations.utils.Resolver; -import eu.siacs.conversations.utils.SignupUtils; -import eu.siacs.conversations.utils.TorServiceUtils; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; -import eu.siacs.conversations.xmpp.OnUpdateBlocklist; -import eu.siacs.conversations.xmpp.XmppConnection; -import eu.siacs.conversations.xmpp.XmppConnection.Features; -import eu.siacs.conversations.xmpp.forms.Data; -import eu.siacs.conversations.xmpp.pep.Avatar; -import me.drakeet.support.toast.ToastCompat; -import okhttp3.HttpUrl; - -public class EditAccountActivity extends OmemoActivity implements OnAccountUpdate, OnUpdateBlocklist, - OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnMamPreferencesFetched { - - public static final String EXTRA_OPENED_FROM_NOTIFICATION = "opened_from_notification"; - public static final String EXTRA_FORCE_REGISTER = "force_register"; - - private static final int REQUEST_DATA_SAVER = 0xf244; - private static final int REQUEST_CHANGE_STATUS = 0xee11; - private static final int REQUEST_ORBOT = 0xff22; - private static final int REQUEST_IMPORT_BACKUP = 0x63fb; - private AlertDialog mCaptchaDialog = null; - private final AtomicBoolean mPendingReconnect = new AtomicBoolean(false); - private final AtomicBoolean redirectInProgress = new AtomicBoolean(false); - private Jid jidToEdit; - private boolean mInitMode = false; - private boolean mExisting = false; - private Boolean mForceRegister = null; - private boolean mUsernameMode = Config.DOMAIN_LOCK != null; - private boolean mShowOptions = false; - private boolean useOwnProvider = false; - private boolean register = false; - private Account mAccount; - private String messageFingerprint; - - private final PendingItem mPendingPresenceTemplate = new PendingItem<>(); - - private boolean mFetchingAvatar = false; - - private final OnClickListener mSaveButtonClickListener = new OnClickListener() { - - @Override - public void onClick(final View v) { - final String password = binding.accountPassword.getText().toString(); - final boolean wasDisabled = mAccount != null && mAccount.getStatus() == Account.State.DISABLED; - final boolean accountInfoEdited = accountInfoEdited(); - if (mInitMode && mAccount != null) { - mAccount.setOption(Account.OPTION_DISABLED, false); - } - if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !accountInfoEdited) { - mAccount.setOption(Account.OPTION_DISABLED, false); - if (!xmppConnectionService.updateAccount(mAccount)) { - ToastCompat.makeText(EditAccountActivity.this, R.string.unable_to_update_account, ToastCompat.LENGTH_SHORT).show(); - } - return; - } - final boolean registerNewAccount; - if (mForceRegister != null) { - registerNewAccount = mForceRegister; - } else { - registerNewAccount = binding.accountRegisterNew.isChecked() && !Config.DISALLOW_REGISTRATION_IN_UI; - } - if (mUsernameMode && binding.accountJid.getText().toString().contains("@")) { - binding.accountJidLayout.setError(getString(R.string.invalid_username)); - removeErrorsOnAllBut(binding.accountJidLayout); - binding.accountJid.requestFocus(); - return; - } - - XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection(); - final boolean startOrbot = mAccount != null && mAccount.getStatus() == Account.State.TOR_NOT_AVAILABLE; - final boolean startI2P = mAccount != null && mAccount.getStatus() == Account.State.I2P_NOT_AVAILABLE; - if (startOrbot) { - if (TorServiceUtils.isOrbotInstalled(EditAccountActivity.this)) { - TorServiceUtils.startOrbot(EditAccountActivity.this, REQUEST_ORBOT); - } else { - TorServiceUtils.downloadOrbot(EditAccountActivity.this, REQUEST_ORBOT); - } - return; - } - - if (startI2P) { - return; // just exit - } - - if (inNeedOfSaslAccept()) { - mAccount.resetPinnedMechanism(); - if (!xmppConnectionService.updateAccount(mAccount)) { - ToastCompat.makeText(EditAccountActivity.this, R.string.unable_to_update_account, ToastCompat.LENGTH_SHORT).show(); - } - return; - } - final boolean openRegistrationUrl = registerNewAccount && !accountInfoEdited && mAccount != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB; - final boolean openPaymentUrl = mAccount != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED; - final boolean redirectionWorthyStatus = openPaymentUrl || openRegistrationUrl; - final HttpUrl url = connection != null && redirectionWorthyStatus ? connection.getRedirectionUrl() : null; - if (url != null && !wasDisabled) { - try { - CustomTab.openTab(EditAccountActivity.this, Uri.parse(url.toString()), isDarkTheme()); - return; - } catch (ActivityNotFoundException e) { - ToastCompat.makeText(EditAccountActivity.this, R.string.application_found_to_open_website, ToastCompat.LENGTH_SHORT).show(); - return; - } - } - - final Jid jid; - try { - if (mUsernameMode) { - jid = Jid.ofEscaped(binding.accountJid.getText().toString(), getUserModeDomain(), null); - } else { - jid = Jid.ofEscaped(binding.accountJid.getText().toString()); - Resolver.checkDomain(jid); - } - } catch (final NullPointerException e) { - if (mUsernameMode) { - binding.accountJidLayout.setError(getString(R.string.invalid_username)); - } else { - binding.accountJidLayout.setError(getString(R.string.invalid_jid)); - } - binding.accountJid.requestFocus(); - removeErrorsOnAllBut(binding.accountJidLayout); - return; - } catch (final IllegalArgumentException e) { - if (mUsernameMode) { - binding.accountJidLayout.setError(getString(R.string.invalid_username)); - } else { - binding.accountJidLayout.setError(getString(R.string.invalid_jid)); - } - binding.accountJid.requestFocus(); - removeErrorsOnAllBut(binding.accountJidLayout); - return; - } - final String hostname; - int numericPort = 5222; - if (mShowOptions) { - hostname = CharMatcher.whitespace().removeFrom(binding.hostname.getText()); - final String port = CharMatcher.whitespace().removeFrom(binding.port.getText()); - if (Resolver.invalidHostname(hostname)) { - binding.hostnameLayout.setError(getString(R.string.not_valid_hostname)); - binding.hostname.requestFocus(); - removeErrorsOnAllBut(binding.hostnameLayout); - return; - } - if (!hostname.isEmpty()) { - try { - numericPort = Integer.parseInt(port); - if (numericPort < 0 || numericPort > 65535) { - binding.portLayout.setError(getString(R.string.not_a_valid_port)); - removeErrorsOnAllBut(binding.portLayout); - binding.port.requestFocus(); - return; - } - } catch (NumberFormatException e) { - binding.portLayout.setError(getString(R.string.not_a_valid_port)); - removeErrorsOnAllBut(binding.portLayout); - binding.port.requestFocus(); - return; - } - } - } else { - hostname = null; - } - - if (jid.getLocal() == null) { - if (mUsernameMode) { - binding.accountJidLayout.setError(getString(R.string.invalid_username)); - } else { - binding.accountJidLayout.setError(getString(R.string.invalid_jid)); - } - removeErrorsOnAllBut(binding.accountJidLayout); - binding.accountJid.requestFocus(); - return; - } - if (registerNewAccount) { - if (XmppConnection.errorMessage != null) { - ToastCompat.makeText(EditAccountActivity.this, XmppConnection.errorMessage, ToastCompat.LENGTH_LONG).show(); - } - } - if (mAccount != null) { - if (mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) { - mAccount.setOption(Account.OPTION_MAGIC_CREATE, mAccount.getPassword().contains(password)); - } - mAccount.setJid(jid); - mAccount.setPort(numericPort); - mAccount.setHostname(hostname); - if (XmppConnection.errorMessage != null) { - binding.accountJidLayout.setError(XmppConnection.errorMessage); - } else { - binding.accountJidLayout.setError(null); - } - mAccount.setPassword(password); - mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); - if (!xmppConnectionService.updateAccount(mAccount)) { - ToastCompat.makeText(EditAccountActivity.this, R.string.unable_to_update_account, ToastCompat.LENGTH_SHORT).show(); - return; - } - } else { - if (xmppConnectionService.findAccountByJid(jid) != null) { - binding.accountJidLayout.setError(getString(R.string.account_already_exists)); - removeErrorsOnAllBut(binding.accountJidLayout); - binding.accountJid.requestFocus(); - return; - } - mAccount = new Account(jid.asBareJid(), password); - mAccount.setPort(numericPort); - mAccount.setHostname(hostname); - mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); - xmppConnectionService.createAccount(mAccount); - } - binding.hostnameLayout.setError(null); - binding.portLayout.setError(null); - if (mAccount.isOnion()) { - ToastCompat.makeText(EditAccountActivity.this, R.string.audio_video_disabled_tor, ToastCompat.LENGTH_LONG).show(); - } - if (mAccount.isEnabled() - && !registerNewAccount - && !mInitMode) { - finish(); - } else { - updateSaveButton(); - updateAccountInformation(true); - } - - } - }; - private final OnClickListener mCancelButtonClickListener = v -> { - deleteAccountAndReturnIfNecessary(); - finish(); - }; - private Toast mFetchingMamPrefsToast; - private String mSavedInstanceAccount; - private boolean mSavedInstanceInit = false; - private XmppUri pendingUri = null; - private boolean mUseTor; - private boolean mUseI2P; - private ActivityEditAccountBinding binding; - - public void refreshUiReal() { - invalidateOptionsMenu(); - if (mAccount != null - && mAccount.getStatus() != Account.State.ONLINE - && mFetchingAvatar) { - Intent intent = new Intent(this, StartConversationActivity.class); - StartConversationActivity.addInviteUri(intent, getIntent()); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - finish(); - } else if (mInitMode && mAccount != null && mAccount.getStatus() == Account.State.ONLINE) { - runOnUiThread(this::next); - } - if (mAccount != null) { - updateAccountInformation(false); - } - updateSaveButton(); - } - - private void next() { - if (redirectInProgress.compareAndSet(false, true)) { - Intent intent = new Intent(this, EnterNameActivity.class); - intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toString()); - intent.putExtra("existing", mExisting); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - finish(); - } - } - - @Override - public boolean onNavigateUp() { - deleteAccountAndReturnIfNecessary(); - return super.onNavigateUp(); - } - - @Override - public void onBackPressed() { - deleteAccountAndReturnIfNecessary(); - super.onBackPressed(); - } - - private void deleteAccountAndReturnIfNecessary() { - if (mInitMode && mAccount != null && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)) { - xmppConnectionService.deleteAccount(mAccount); - } - - final boolean magicCreate = mAccount != null && mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY); - final Jid jid = mAccount == null ? null : mAccount.getJid(); - - if (SignupUtils.isSupportTokenRegistry() && jid != null && magicCreate && !jid.getDomain().equals(Config.MAGIC_CREATE_DOMAIN)) { - final Jid preset; - if (mAccount.isOptionSet(Account.OPTION_FIXED_USERNAME)) { - preset = jid.asBareJid(); - } else { - preset = jid.getDomain(); - } - final Intent intent = SignupUtils.getTokenRegistrationIntent(this, preset, mAccount.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN), register); - StartConversationActivity.addInviteUri(intent, getIntent()); - startActivity(intent); - return; - } - - final List accounts = xmppConnectionService == null ? null : xmppConnectionService.getAccounts(); - if (accounts != null && accounts.size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) { - Intent intent = SignupUtils.getSignUpIntent(this, mForceRegister != null && mForceRegister); - StartConversationActivity.addInviteUri(intent, getIntent()); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - } - - @Override - public void onAccountUpdate() { - refreshUi(); - } - - private final UiCallback mAvatarFetchCallback = new UiCallback() { - - @Override - public void userInputRequired(final PendingIntent pi, final Avatar avatar) { - finishInitialSetup(avatar); - } - - @Override - public void progress(int progress) { - - } - - @Override - public void success(final Avatar avatar) { - finishInitialSetup(avatar); - } - - @Override - public void error(final int errorCode, final Avatar avatar) { - finishInitialSetup(avatar); - } - }; - private final TextWatcher mTextWatcher = new TextWatcher() { - - @Override - public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { - updatePortLayout(); - updateSaveButton(); - updateInfoButtons(); - } - - @Override - public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { - } - - @Override - public void afterTextChanged(final Editable s) { - - } - }; - - private View.OnFocusChangeListener mEditTextFocusListener = new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean b) { - EditText et = (EditText) view; - if (b) { - int resId = mUsernameMode ? R.string.username : R.string.account_settings_example_jabber_id; - if (view.getId() == R.id.hostname) { - resId = mUseTor ? R.string.hostname_or_onion : R.string.hostname_example; - } - if (view.getId() == R.id.port) { - resId = R.string.port_example; - } - final int res = resId; - new Handler().postDelayed(() -> et.setHint(res), 500); - } else { - et.setHint(null); - } - } - }; - - - private final OnClickListener mAvatarClickListener = new OnClickListener() { - @Override - public void onClick(final View view) { - if (mAccount != null) { - final Intent intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class); - intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString()); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - } - }; - - protected void finishInitialSetup(final Avatar avatar) { - runOnUiThread(() -> { - SoftKeyboardUtils.hideSoftKeyboard(EditAccountActivity.this); - final Intent intent; - final XmppConnection connection = mAccount.getXmppConnection(); - final boolean wasFirstAccount = xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 1; - if (avatar != null || (connection != null && !connection.getFeatures().pep())) { - intent = new Intent(getApplicationContext(), StartConversationActivity.class); - if (wasFirstAccount) { - intent.putExtra("init", true); - } - intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString()); - } else { - intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class); - intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString()); - intent.putExtra("setup", true); - } - if (wasFirstAccount) { - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - } - StartConversationActivity.addInviteUri(intent, getIntent()); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - finish(); - }); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_BATTERY_OP || requestCode == REQUEST_DATA_SAVER) { - updateAccountInformation(mAccount == null); - } - if (requestCode == REQUEST_CHANGE_STATUS) { - PresenceTemplate template = mPendingPresenceTemplate.pop(); - if (template != null && resultCode == Activity.RESULT_OK) { - generateSignature(data, template); - } else { - Log.d(Config.LOGTAG, "pgp result not ok"); - } - } - } - - @Override - protected void processFingerprintVerification(XmppUri uri) { - processFingerprintVerification(uri, true); - } - - - protected void processFingerprintVerification(XmppUri uri, boolean showWarningToast) { - if (mAccount != null && mAccount.getJid().asBareJid().equals(uri.getJid()) && uri.hasFingerprints()) { - if (xmppConnectionService.verifyFingerprints(mAccount, uri.getFingerprints())) { - ToastCompat.makeText(this, R.string.verified_fingerprints, ToastCompat.LENGTH_SHORT).show(); - updateAccountInformation(false); - } - } else if (showWarningToast) { - ToastCompat.makeText(this, R.string.invalid_barcode, ToastCompat.LENGTH_SHORT).show(); - } - } - - protected void updateInfoButtons() { - if (this.binding.accountRegisterNew.isChecked() && this.binding.accountJid.getText().length() > 0 && !this.binding.accountJid.getText().toString().contains("@")) { - try { - final String jid = this.binding.accountJid.getText().toString(); - if (!mUsernameMode && Jid.of(jid).getDomain().toEscapedString().toLowerCase().equals("pix-art.de")) { - this.binding.showPrivacyPolicy.setVisibility(View.VISIBLE); - this.binding.showTermsOfUse.setVisibility(View.VISIBLE); - } - } catch (Exception e) { - e.printStackTrace(); - } - } else { - this.binding.showPrivacyPolicy.setVisibility(View.GONE); - this.binding.showTermsOfUse.setVisibility(View.GONE); - } - } - - private void updatePortLayout() { - final String hostname = this.binding.hostname.getText().toString(); - if (TextUtils.isEmpty(hostname)) { - this.binding.portLayout.setEnabled(false); - this.binding.portLayout.setError(null); - } else { - this.binding.portLayout.setEnabled(true); - } - } - - protected void updateSaveButton() { - boolean accountInfoEdited = accountInfoEdited(); - if (accountInfoEdited && !mInitMode) { - this.binding.saveButton.setText(R.string.save); - this.binding.saveButton.setEnabled(true); - } else if (mAccount != null - && (mAccount.getStatus() == Account.State.CONNECTING || mAccount.getStatus() == Account.State.REGISTRATION_SUCCESSFUL || mFetchingAvatar)) { - this.binding.saveButton.setEnabled(false); - this.binding.saveButton.setText(R.string.account_status_connecting); - } else if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !mInitMode) { - this.binding.saveButton.setEnabled(true); - this.binding.saveButton.setText(R.string.enable); - } else if (torNeedsInstall(mAccount)) { - this.binding.saveButton.setEnabled(true); - this.binding.saveButton.setText(R.string.install_orbot); - } else if (torNeedsStart(mAccount)) { - this.binding.saveButton.setEnabled(true); - this.binding.saveButton.setText(R.string.start_orbot); - } else { - this.binding.saveButton.setEnabled(true); - if (!mInitMode) { - if (mAccount != null && mAccount.isOnlineAndConnected()) { - this.binding.yourStatusBox.setVisibility(View.VISIBLE); - this.binding.saveButton.setText(R.string.save); - if (!accountInfoEdited) { - this.binding.saveButton.setEnabled(false); - } - if (!mUsernameMode && Jid.of(mAccount.getJid()).getDomain().toEscapedString().toLowerCase().equals("pix-art.de")) { - this.binding.showPrivacyPolicy.setVisibility(View.VISIBLE); - this.binding.showTermsOfUse.setVisibility(View.VISIBLE); - } - } else { - this.binding.yourStatusBox.setVisibility(View.GONE); - this.binding.showPrivacyPolicy.setVisibility(View.GONE); - this.binding.showTermsOfUse.setVisibility(View.GONE); - XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection(); - HttpUrl url = connection != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED ? connection.getRedirectionUrl() : null; - if (url != null) { - this.binding.saveButton.setText(R.string.open_website); - } else if (inNeedOfSaslAccept()) { - this.binding.saveButton.setText(R.string.accept); - } else { - this.binding.saveButton.setText(R.string.connect); - } - } - } else { - XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection(); - HttpUrl url = connection != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB ? connection.getRedirectionUrl() : null; - if (url != null && this.binding.accountRegisterNew.isChecked() && !accountInfoEdited) { - this.binding.saveButton.setText(R.string.open_website); - } else { - this.binding.saveButton.setText(R.string.next); - } - } - } - } - - private boolean torNeedsInstall(final Account account) { - return account != null && account.getStatus() == Account.State.TOR_NOT_AVAILABLE && !TorServiceUtils.isOrbotInstalled(this); - } - - private boolean torNeedsStart(final Account account) { - return account != null && account.getStatus() == Account.State.TOR_NOT_AVAILABLE; - } - - protected boolean accountInfoEdited() { - if (this.mAccount == null) { - return false; - } - return jidEdited() || - !this.mAccount.getPassword().equals(binding.accountPassword.getText().toString()) || - !this.mAccount.getHostname().equals(this.binding.hostname.getText().toString()) || - !String.valueOf(this.mAccount.getPort()).equals(this.binding.port.getText().toString()); - } - - protected boolean jidEdited() { - final String unmodified; - if (mUsernameMode) { - unmodified = this.mAccount.getJid().getEscapedLocal(); - } else { - unmodified = this.mAccount.getJid().asBareJid().toEscapedString(); - } - return !unmodified.equals(this.binding.accountJid.getText().toString()); - } - - @Override - protected String getShareableUri(boolean http) { - if (mAccount != null) { - return http ? mAccount.getShareableLink() : mAccount.getShareableUri(); - } else { - return null; - } - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - this.mSavedInstanceAccount = savedInstanceState.getString("account"); - this.mSavedInstanceInit = savedInstanceState.getBoolean("initMode", false); - } - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_edit_account); - setSupportActionBar((Toolbar) binding.toolbar); - configureActionBar(getSupportActionBar()); - this.binding.accountJid.addTextChangedListener(this.mTextWatcher); - this.binding.accountJid.setOnFocusChangeListener(this.mEditTextFocusListener); - this.binding.accountPassword.addTextChangedListener(this.mTextWatcher); - this.binding.avater.setOnClickListener(this.mAvatarClickListener); - this.binding.hostname.addTextChangedListener(mTextWatcher); - this.binding.hostname.setOnFocusChangeListener(mEditTextFocusListener); - this.binding.clearDevices.setOnClickListener(v -> showWipePepDialog()); - this.binding.port.setText(String.valueOf(Resolver.DEFAULT_PORT_XMPP)); - this.binding.port.setOnFocusChangeListener(mEditTextFocusListener); - this.binding.port.addTextChangedListener(mTextWatcher); - this.binding.saveButton.setOnClickListener(this.mSaveButtonClickListener); - this.binding.cancelButton.setOnClickListener(this.mCancelButtonClickListener); - this.binding.actionEditYourName.setOnClickListener(this::onEditYourNameClicked); - this.binding.actionEditYourStatus.setOnClickListener(this::onEditYourStatusClicked); - if (savedInstanceState != null && savedInstanceState.getBoolean("showMoreTable")) { - changeMoreTableVisibility(true); - } - final OnCheckedChangeListener OnCheckedShowConfirmPassword = (buttonView, isChecked) -> { - updatePortLayout(); - updateSaveButton(); - updateInfoButtons(); - }; - this.binding.accountRegisterNew.setOnCheckedChangeListener(OnCheckedShowConfirmPassword); - if (Config.DISALLOW_REGISTRATION_IN_UI) { - this.binding.accountRegisterNew.setVisibility(View.GONE); - } - this.binding.showPrivacyPolicy.setOnClickListener(view -> { - final Uri uri = Uri.parse("https://jabber.pix-art.de/privacy/"); - CustomTab.openTab(EditAccountActivity.this, uri, isDarkTheme()); - }); - this.binding.showTermsOfUse.setOnClickListener(view -> { - final Uri uri = Uri.parse("https://jabber.pix-art.de/termsofuse/"); - CustomTab.openTab(EditAccountActivity.this, uri, isDarkTheme()); - }); - } - - private void onEditYourNameClicked(View view) { - quickEdit(mAccount.getDisplayName(), R.string.your_name, value -> { - final String displayName = value.trim(); - updateDisplayName(displayName); - mAccount.setDisplayName(displayName); - xmppConnectionService.publishDisplayName(mAccount); - refreshAvatar(); - return null; - }, true); - } - - private void onEditYourStatusClicked(View view) { - changePresence(); - } - - private void refreshAvatar() { - AvatarWorkerTask.loadAvatar(mAccount, binding.avater, R.dimen.avatar_on_details_screen_size, true); - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - super.onCreateOptionsMenu(menu); - getMenuInflater().inflate(R.menu.editaccount, menu); - final MenuItem showBlocklist = menu.findItem(R.id.action_show_block_list); - final MenuItem reconnect = menu.findItem(R.id.mgmt_account_reconnect); - final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more); - final MenuItem changePassword = menu.findItem(R.id.action_change_password_on_server); - final MenuItem showPassword = menu.findItem(R.id.action_show_password); - final MenuItem renewCertificate = menu.findItem(R.id.action_renew_certificate); - final MenuItem mamPrefs = menu.findItem(R.id.action_mam_prefs); - final MenuItem actionShare = menu.findItem(R.id.action_share); - final MenuItem shareBarcode = menu.findItem(R.id.action_share_barcode); - final MenuItem shareQRCode = menu.findItem(R.id.action_show_qr_code); - final MenuItem announcePGP = menu.findItem(R.id.mgmt_account_announce_pgp); - final MenuItem forgotPassword = menu.findItem(R.id.mgmt_account_password_forgotten); - renewCertificate.setVisible(mAccount != null && mAccount.getPrivateKeyAlias() != null); - - if (mAccount != null && mAccount.isOnlineAndConnected()) { - if (!mAccount.getXmppConnection().getFeatures().blocking()) { - showBlocklist.setVisible(false); - } - if (!mAccount.getXmppConnection().getFeatures().register()) { - changePassword.setVisible(false); - } - reconnect.setVisible(true); - announcePGP.setVisible(true); - forgotPassword.setVisible(true); - mamPrefs.setVisible(mAccount.getXmppConnection().getFeatures().mam()); - } else { - announcePGP.setVisible(false); - forgotPassword.setVisible(false); - reconnect.setVisible(false); - showBlocklist.setVisible(false); - showMoreInfo.setVisible(false); - changePassword.setVisible(false); - mamPrefs.setVisible(false); - actionShare.setVisible(false); - shareBarcode.setVisible(false); - shareQRCode.setVisible(false); - } - - if (mAccount != null) { - showPassword.setVisible(mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) - && !mAccount.isOptionSet(Account.OPTION_REGISTER)); - } else { - showPassword.setVisible(false); - } - return super.onCreateOptionsMenu(menu); - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more); - if (showMoreInfo.isVisible()) { - showMoreInfo.setChecked(binding.serverInfoMore.getVisibility() == View.VISIBLE); - } - return super.onPrepareOptionsMenu(menu); - } - - @Override - protected void onStart() { - super.onStart(); - final Intent intent = getIntent(); - final int theme = findTheme(); - if (this.mTheme != theme) { - recreate(); - } else if (intent != null) { - try { - this.jidToEdit = Jid.ofEscaped(intent.getStringExtra("jid")); - } catch (final IllegalArgumentException ignored) { - this.jidToEdit = null; - } catch (final NullPointerException ignored) { - this.jidToEdit = null; - } - 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 { - displayVerificationWarningDialog(xmppUri); - } - } - boolean init = intent.getBooleanExtra("init", false); - boolean existing = intent.getBooleanExtra("existing", false); - useOwnProvider = intent.getBooleanExtra("useownprovider", false); - register = intent.getBooleanExtra("register", false); - boolean openedFromNotification = intent.getBooleanExtra(EXTRA_OPENED_FROM_NOTIFICATION, false); - Log.d(Config.LOGTAG, "extras " + intent.getExtras()); - this.mForceRegister = intent.hasExtra(EXTRA_FORCE_REGISTER) ? intent.getBooleanExtra(EXTRA_FORCE_REGISTER, false) : null; - Log.d(Config.LOGTAG, "force register=" + mForceRegister); - this.mInitMode = init || this.jidToEdit == null; - this.mExisting = existing; - this.messageFingerprint = intent.getStringExtra("fingerprint"); - if (mExisting) { - this.binding.accountRegisterNew.setVisibility(View.GONE); - } - if (!mInitMode) { - this.binding.accountRegisterNew.setVisibility(View.GONE); - setTitle(getString(R.string.account_details)); - configureActionBar(getSupportActionBar(), !openedFromNotification); - } else { - this.binding.yourNameBox.setVisibility(View.GONE); - this.binding.yourStatusBox.setVisibility(View.GONE); - this.binding.avater.setVisibility(View.GONE); - configureActionBar(getSupportActionBar(), !(init && Config.MAGIC_CREATE_DOMAIN == null)); - if (mForceRegister != null) { - if (mForceRegister) { - setTitle(R.string.action_add_new_account); - } else { - setTitle(R.string.action_add_existing_account); - } - } - } - } - SharedPreferences preferences = getPreferences(); - mUseTor = QuickConversationsService.isConversations() && preferences.getBoolean("use_tor", getResources().getBoolean(R.bool.use_tor)); - mUseI2P = QuickConversationsService.isConversations() && preferences.getBoolean("use_i2p", getResources().getBoolean(R.bool.use_i2p)); - this.mShowOptions = mUseTor || mUseI2P || (QuickConversationsService.isConversations() && preferences.getBoolean("show_connection_options", getResources().getBoolean(R.bool.show_connection_options))); - this.binding.namePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE); - if (mForceRegister != null) { - this.binding.accountRegisterNew.setVisibility(View.GONE); - } - } - - 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); - if (intent != null && intent.getData() != null) { - final XmppUri uri = new XmppUri(intent.getData()); - if (xmppConnectionServiceBound) { - processFingerprintVerification(uri, false); - } else { - this.pendingUri = uri; - } - } - } - - @Override - public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) { - if (mAccount != null) { - savedInstanceState.putString("account", mAccount.getJid().asBareJid().toEscapedString()); - savedInstanceState.putBoolean("existing", mExisting); - savedInstanceState.putBoolean("initMode", mInitMode); - savedInstanceState.putBoolean("showMoreTable", binding.serverInfoMore.getVisibility() == View.VISIBLE); - } - super.onSaveInstanceState(savedInstanceState); - } - - protected void onBackendConnected() { - boolean init = true; - if (mSavedInstanceAccount != null) { - try { - this.mAccount = xmppConnectionService.findAccountByJid(Jid.ofEscaped(mSavedInstanceAccount)); - this.mInitMode = mSavedInstanceInit; - init = false; - } catch (IllegalArgumentException e) { - this.mAccount = null; - } - - } else if (this.jidToEdit != null) { - this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit); - } - - if (mAccount != null) { - this.mInitMode |= this.mAccount.isOptionSet(Account.OPTION_REGISTER); - this.mUsernameMode |= mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) && mAccount.isOptionSet(Account.OPTION_REGISTER) && !useOwnProvider; - if (mPendingFingerprintVerificationUri != null) { - processFingerprintVerification(mPendingFingerprintVerificationUri, false); - mPendingFingerprintVerificationUri = null; - } - updateAccountInformation(init); - } - - - if (Config.MAGIC_CREATE_DOMAIN == null && this.xmppConnectionService.getAccounts().size() == 0) { - this.binding.cancelButton.setEnabled(false); - } - if (mUsernameMode) { - this.binding.accountJidLayout.setHint(getString(R.string.username_hint)); - } else { - final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this, - R.layout.simple_list_item, - xmppConnectionService.getKnownHosts()); - this.binding.accountJid.setAdapter(mKnownHostsAdapter); - } - if (pendingUri != null) { - processFingerprintVerification(pendingUri, false); - pendingUri = null; - } - updateSaveButton(); - invalidateOptionsMenu(); - } - - private String getUserModeDomain() { - if (mAccount != null && mAccount.getJid().getDomain() != null) { - return mAccount.getServer(); - } else { - return Config.DOMAIN_LOCK; - } - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - if (MenuDoubleTabUtil.shouldIgnoreTap()) { - return false; - } - switch (item.getItemId()) { - case android.R.id.home: - deleteAccountAndReturnIfNecessary(); - break; - case R.id.action_import_backup: - if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) { - startActivity(new Intent(this, ImportBackupActivity.class)); - } - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - break; - case R.id.mgmt_account_reconnect: - XmppConnection connection = mAccount.getXmppConnection(); - if (connection != null) { - connection.resetStreamId(); - } - xmppConnectionService.reconnectAccountInBackground(mAccount); - break; - case R.id.action_show_block_list: - final Intent showBlocklistIntent = new Intent(this, BlocklistActivity.class); - showBlocklistIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toEscapedString()); - startActivity(showBlocklistIntent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - break; - case R.id.action_server_info_show_more: - changeMoreTableVisibility(!item.isChecked()); - break; - case R.id.action_share_barcode: - shareBarcode(); - break; - case R.id.action_share_http: - shareLink(true); - break; - case R.id.action_share_uri: - shareLink(false); - break; - case R.id.action_change_password_on_server: - gotoChangePassword(null); - break; - case R.id.action_mam_prefs: - editMamPrefs(); - break; - case R.id.action_renew_certificate: - renewCertificate(); - break; - case R.id.action_show_password: - showPassword(); - break; - case R.id.mgmt_account_announce_pgp: - publishOpenPGPPublicKey(mAccount); - return true; - case R.id.mgmt_account_password_forgotten: - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.password_forgotten_title); - builder.setMessage(R.string.password_forgotten_text); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.confirm, (dialog, which) -> { - try { - Uri uri = Uri.parse(getSupportSite(mAccount.getJid().getDomain().toEscapedString())); - CustomTab.openTab(EditAccountActivity.this, uri, isDarkTheme()); - } catch (Exception e) { - e.printStackTrace(); - } - }); - builder.create().show(); - } - return super.onOptionsItemSelected(item); - } - - private String getSupportSite(String domain) { - int i = -1; - for (String domains : getResources().getStringArray(R.array.support_domains)) { - i++; - if (domains.equals(domain)) { - return getResources().getStringArray(R.array.support_site)[i]; - } - } - return domain; - } - - private boolean inNeedOfSaslAccept() { - return mAccount != null && mAccount.getLastErrorStatus() == Account.State.DOWNGRADE_ATTACK && mAccount.getPinnedMechanismPriority() >= 0 && !accountInfoEdited(); - } - - private void publishOpenPGPPublicKey(Account account) { - if (EditAccountActivity.this.hasPgp()) { - announcePgp(account, null, null, onOpenPGPKeyPublished); - } else { - this.showInstallPgpDialog(); - } - } - - private void shareBarcode() { - Intent intent = new Intent(Intent.ACTION_SEND); - intent.putExtra(Intent.EXTRA_STREAM, BarcodeProvider.getUriForAccount(this, mAccount)); - intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - intent.setType("image/png"); - startActivity(Intent.createChooser(intent, getText(R.string.share_with))); - } - - private void changeMoreTableVisibility(boolean visible) { - binding.serverInfoMore.setVisibility(visible ? View.VISIBLE : View.GONE); - } - - private void gotoChangePassword(String newPassword) { - final Intent changePasswordIntent = new Intent(this, ChangePasswordActivity.class); - changePasswordIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toEscapedString()); - if (newPassword != null) { - changePasswordIntent.putExtra("password", newPassword); - } - startActivity(changePasswordIntent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - - private void renewCertificate() { - KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null); - } - - private void changePresence() { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - boolean manualStatus = sharedPreferences.getBoolean(SettingsActivity.MANUALLY_CHANGE_PRESENCE, getResources().getBoolean(R.bool.manually_change_presence)); - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - final DialogPresenceBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_presence, null, false); - final String current = mAccount.getPresenceStatusMessage(); - if (current != null && !current.trim().isEmpty()) { - binding.statusMessage.append(current); - } - setAvailabilityRadioButton(mAccount.getPresenceStatus(), binding); - binding.show.setVisibility(manualStatus ? View.VISIBLE : View.GONE); - List templates = xmppConnectionService.getPresenceTemplates(mAccount); - PresenceTemplateAdapter presenceTemplateAdapter = new PresenceTemplateAdapter(this, R.layout.simple_list_item, templates); - binding.statusMessage.setAdapter(presenceTemplateAdapter); - binding.statusMessage.setOnItemClickListener((parent, view, position, id) -> { - PresenceTemplate template = (PresenceTemplate) parent.getItemAtPosition(position); - setAvailabilityRadioButton(template.getStatus(), binding); - }); - builder.setTitle(R.string.edit_status_message_title); - builder.setView(binding.getRoot()); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.confirm, (dialog, which) -> { - PresenceTemplate template = new PresenceTemplate(getAvailabilityRadioButton(binding), binding.statusMessage.getText().toString().trim()); - if (mAccount.getPgpId() != 0 && hasPgp()) { - generateSignature(null, template); - } else { - xmppConnectionService.changeStatus(mAccount, template, null); - } - updatePresenceStatus(getPresenceStatus(getAvailabilityRadioButton(binding)), binding.statusMessage.getText().toString().trim()); - }); - builder.create().show(); - } - - private void generateSignature(Intent intent, PresenceTemplate template) { - xmppConnectionService.getPgpEngine().generateSignature(intent, mAccount, template.getStatusMessage(), new UiCallback() { - @Override - public void success(String signature) { - xmppConnectionService.changeStatus(mAccount, template, signature); - } - - @Override - public void error(int errorCode, String object) { - Log.d(Config.LOGTAG, mAccount.getJid().asBareJid() + ": error generating signature. Code: " + errorCode + " Object: " + object); - xmppConnectionService.changeStatus(mAccount, template, null); - } - - @Override - public void userInputRequired(PendingIntent pi, String object) { - mPendingPresenceTemplate.push(template); - try { - startIntentSenderForResult(pi.getIntentSender(), REQUEST_CHANGE_STATUS, null, 0, 0, 0); - } catch (final IntentSender.SendIntentException ignored) { - } - } - - @Override - public void progress(int progress) { - - } - }); - } - - private static void setAvailabilityRadioButton(Presence.Status status, DialogPresenceBinding binding) { - if (status == null) { - binding.online.setChecked(true); - return; - } - switch (status) { - case DND: - binding.dnd.setChecked(true); - break; - case XA: - binding.xa.setChecked(true); - break; - case AWAY: - binding.away.setChecked(true); - break; - default: - binding.online.setChecked(true); - } - } - - private static Presence.Status getAvailabilityRadioButton(DialogPresenceBinding binding) { - if (binding.dnd.isChecked()) { - return Presence.Status.DND; - } else if (binding.xa.isChecked()) { - return Presence.Status.XA; - } else if (binding.away.isChecked()) { - return Presence.Status.AWAY; - } else { - return Presence.Status.ONLINE; - } - } - - private String getPresenceStatus(Presence.Status status) { - if (status == null) { - return getString(R.string.presence_online); - } - switch (status) { - case DND: - return getString(R.string.presence_dnd); - case XA: - return getString(R.string.presence_xa); - case AWAY: - return getString(R.string.presence_away); - default: - return getString(R.string.presence_online); - } - } - - @Override - public void alias(String alias) { - if (alias != null) { - xmppConnectionService.updateKeyInAccount(mAccount, alias); - } - } - - private void updateAccountInformation(boolean init) { - if (init) { - this.binding.accountJid.getEditableText().clear(); - if (mUsernameMode) { - this.binding.accountJid.getEditableText().append(this.mAccount.getJid().getEscapedLocal()); - } else { - this.binding.accountJid.getEditableText().append(this.mAccount.getJid().asBareJid().toEscapedString()); - } - binding.accountPassword.getEditableText().clear(); - binding.accountPassword.getEditableText().append(this.mAccount.getPassword()); - binding.accountPassword.setText(this.mAccount.getPassword()); - this.binding.hostname.setText(""); - if (this.mAccount.getHostname().length() > 0) { - this.binding.hostname.getEditableText().append(this.mAccount.getHostname()); - } else { - this.binding.hostname.getEditableText().append(this.mAccount.getDomain()); - } - this.binding.port.setText(""); - this.binding.port.getEditableText().append(String.valueOf(this.mAccount.getPort())); - this.binding.namePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE); - - } - - if (!mInitMode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - this.binding.accountPassword.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO); - } - - final boolean editable = !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY) && !mAccount.isOptionSet(Account.OPTION_FIXED_USERNAME) && QuickConversationsService.isConversations(); - this.binding.accountJid.setEnabled(editable); - this.binding.accountJid.setFocusable(editable); - this.binding.accountJid.setFocusableInTouchMode(editable); - this.binding.accountJid.setCursorVisible(editable); - - final String displayName = mAccount.getDisplayName(); - updateDisplayName(displayName); - final String presenceStatus = getPresenceStatus(mAccount.getPresenceStatus()); - final String presenceStatusMessage = mAccount.getPresenceStatusMessage(); - updatePresenceStatus(presenceStatus, presenceStatusMessage); - final boolean togglePassword = mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) || !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY); - final boolean editPassword = !mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) || (!mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY) && QuickConversationsService.isConversations()) || mAccount.getLastErrorStatus() == Account.State.UNAUTHORIZED; - this.binding.accountPasswordLayout.setPasswordVisibilityToggleEnabled(togglePassword); - this.binding.accountPassword.setFocusable(editPassword); - this.binding.accountPassword.setFocusableInTouchMode(editPassword); - this.binding.accountPassword.setCursorVisible(editPassword); - this.binding.accountPassword.setEnabled(editPassword); - - if (!mInitMode) { - binding.avater.setVisibility(View.VISIBLE); - refreshAvatar(); - this.binding.accountJid.setEnabled(false); - } else { - binding.avater.setVisibility(View.GONE); - } - this.binding.accountRegisterNew.setChecked(this.mAccount.isOptionSet(Account.OPTION_REGISTER)); - if (this.mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) { - if (this.mAccount.isOptionSet(Account.OPTION_REGISTER)) { - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - setTitle(R.string.action_add_new_account); - } - } - this.binding.accountRegisterNew.setVisibility(View.GONE); - } else if (this.mAccount.isOptionSet(Account.OPTION_REGISTER) && mForceRegister == null) { - this.binding.accountRegisterNew.setVisibility(View.VISIBLE); - } else if (mExisting) { - this.binding.accountRegisterNew.setVisibility(View.GONE); - } else { - if (mInitMode) { - this.binding.accountRegisterNew.setVisibility(View.VISIBLE); - } else { - this.binding.accountRegisterNew.setVisibility(View.GONE); - } - } - this.binding.yourNameBox.setVisibility(mInitMode ? View.GONE : View.VISIBLE); - this.binding.yourStatusBox.setVisibility(mInitMode ? View.GONE : View.VISIBLE); - if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) { - Features features = this.mAccount.getXmppConnection().getFeatures(); - this.binding.stats.setVisibility(View.VISIBLE); - boolean showBatteryWarning = isOptimizingBattery(); - boolean showDataSaverWarning = isAffectedByDataSaver(); - showOsOptimizationWarning(showBatteryWarning, showDataSaverWarning); - this.binding.sessionEst.setText(UIHelper.readableTimeDifferenceFull(this, this.mAccount.getXmppConnection().getLastSessionEstablished())); - if (features.rosterVersioning()) { - this.binding.serverInfoRosterVersion.setText(R.string.server_info_available); - } else { - this.binding.serverInfoRosterVersion.setText(R.string.server_info_unavailable); - } - if (features.carbons()) { - this.binding.serverInfoCarbons.setText(R.string.server_info_available); - } else { - this.binding.serverInfoCarbons.setText(R.string.server_info_unavailable); - } - if (features.mam()) { - this.binding.serverInfoMam.setText(R.string.server_info_available); - } else { - this.binding.serverInfoMam.setText(R.string.server_info_unavailable); - } - if (features.csi()) { - this.binding.serverInfoCsi.setText(R.string.server_info_available); - } else { - this.binding.serverInfoCsi.setText(R.string.server_info_unavailable); - } - if (features.blocking()) { - this.binding.serverInfoBlocking.setText(R.string.server_info_available); - } else { - this.binding.serverInfoBlocking.setText(R.string.server_info_unavailable); - } - if (features.sm()) { - this.binding.serverInfoSm.setText(R.string.server_info_available); - } else { - this.binding.serverInfoSm.setText(R.string.server_info_unavailable); - } - if (features.externalServiceDiscovery()) { - this.binding.serverInfoExternalService.setText(R.string.server_info_available); - } else { - this.binding.serverInfoExternalService.setText(R.string.server_info_unavailable); - } - if (EasyOnboardingInvite.hasAccountSupport(this.mAccount)) { - this.binding.serverInfoEasyInvite.setText(R.string.server_info_available); - } else { - this.binding.serverInfoEasyInvite.setText(R.string.server_info_unavailable); - } - if (features.pep()) { - AxolotlService axolotlService = this.mAccount.getAxolotlService(); - if (axolotlService != null && axolotlService.isPepBroken()) { - this.binding.serverInfoPep.setText(R.string.server_info_broken); - } else if (features.pepPublishOptions() || features.pepOmemoWhitelisted()) { - this.binding.serverInfoPep.setText(R.string.server_info_available); - } else { - this.binding.serverInfoPep.setText(R.string.server_info_partial); - } - } else { - this.binding.serverInfoPep.setText(R.string.server_info_unavailable); - } - if (features.httpUpload(0)) { - final long maxFileSize = features.getMaxHttpUploadSize(); - if (maxFileSize > 0) { - this.binding.serverInfoHttpUpload.setText(UIHelper.filesizeToString(maxFileSize)); - } else { - this.binding.serverInfoHttpUpload.setText(R.string.server_info_available); - } - } else { - this.binding.serverInfoHttpUpload.setText(R.string.server_info_unavailable); - } - - this.binding.pushRow.setVisibility(xmppConnectionService.getPushManagementService().isStub() ? View.GONE : View.VISIBLE); - - if (xmppConnectionService.getPushManagementService().available(mAccount)) { - this.binding.serverInfoPush.setText(R.string.server_info_available); - } else { - this.binding.serverInfoPush.setText(R.string.server_info_unavailable); - } - final long pgpKeyId = this.mAccount.getPgpId(); - if (pgpKeyId != 0 && Config.supportOpenPgp()) { - OnClickListener openPgp = view -> launchOpenKeyChain(pgpKeyId); - OnClickListener delete = view -> showDeletePgpDialog(); - this.binding.pgpFingerprintBox.setVisibility(View.VISIBLE); - this.binding.pgpFingerprint.setText(OpenPgpUtils.convertKeyIdToHex(pgpKeyId)); - this.binding.pgpFingerprint.setOnClickListener(openPgp); - if ("pgp".equals(messageFingerprint)) { - this.binding.pgpFingerprintDesc.setTextAppearance(this, R.style.TextAppearance_Conversations_Caption_Highlight); - } - this.binding.pgpFingerprintDesc.setOnClickListener(openPgp); - this.binding.actionDeletePgp.setOnClickListener(delete); - } else { - this.binding.pgpFingerprintBox.setVisibility(View.GONE); - } - final String ownAxolotlFingerprint = this.mAccount.getAxolotlService().getOwnFingerprint(); - if (ownAxolotlFingerprint != null && Config.supportOmemo()) { - this.binding.axolotlFingerprintBox.setVisibility(View.VISIBLE); - if (ownAxolotlFingerprint.equals(messageFingerprint)) { - this.binding.ownFingerprintDesc.setTextAppearance(this, R.style.TextAppearance_Conversations_Caption_Highlight); - this.binding.ownFingerprintDesc.setText(R.string.omemo_fingerprint_selected_message); - } else { - this.binding.ownFingerprintDesc.setTextAppearance(this, R.style.TextAppearance_Conversations_Caption); - this.binding.ownFingerprintDesc.setText(R.string.omemo_fingerprint); - } - this.binding.axolotlFingerprint.setText(CryptoHelper.prettifyFingerprint(ownAxolotlFingerprint.substring(2))); - this.binding.actionCopyAxolotlToClipboard.setVisibility(View.VISIBLE); - this.binding.actionCopyAxolotlToClipboard.setOnClickListener(v -> copyOmemoFingerprint(ownAxolotlFingerprint)); - } else { - this.binding.axolotlFingerprintBox.setVisibility(View.GONE); - } - boolean hasKeys = false; - binding.otherDeviceKeys.removeAllViews(); - for (XmppAxolotlSession session : mAccount.getAxolotlService().findOwnSessions()) { - if (!session.getTrust().isCompromised()) { - boolean highlight = session.getFingerprint().equals(messageFingerprint); - addFingerprintRow(binding.otherDeviceKeys, session, highlight); - hasKeys = true; - } - } - if (hasKeys && Config.supportOmemo()) { //TODO: either the button should be visible if we print an active device or the device list should be fed with reactived devices - this.binding.otherDeviceKeysCard.setVisibility(View.VISIBLE); - Set otherDevices = mAccount.getAxolotlService().getOwnDeviceIds(); - if (otherDevices == null || otherDevices.isEmpty()) { - binding.clearDevices.setVisibility(View.GONE); - } else { - binding.clearDevices.setVisibility(View.VISIBLE); - } - } else { - this.binding.otherDeviceKeysCard.setVisibility(View.GONE); - } - } else { - final TextInputLayout errorLayout; - if (this.mAccount.errorStatus()) { - if (this.mAccount.getStatus() == Account.State.UNAUTHORIZED || this.mAccount.getStatus() == Account.State.DOWNGRADE_ATTACK) { - errorLayout = this.binding.accountPasswordLayout; - } else if (mShowOptions - && this.mAccount.getStatus() == Account.State.SERVER_NOT_FOUND - && this.binding.hostname.getText().length() > 0) { - errorLayout = this.binding.hostnameLayout; - } else { - errorLayout = this.binding.accountJidLayout; - } - errorLayout.setError(getString(this.mAccount.getStatus().getReadableId())); - if (init || !accountInfoEdited()) { - errorLayout.requestFocus(); - } - } else { - errorLayout = null; - } - removeErrorsOnAllBut(errorLayout); - this.binding.stats.setVisibility(View.GONE); - this.binding.otherDeviceKeysCard.setVisibility(View.GONE); - } - } - - private void updateDisplayName(String displayName) { - if (TextUtils.isEmpty(displayName)) { - this.binding.yourName.setText(R.string.no_name_set_instructions); - this.binding.yourName.setTextAppearance(this, R.style.TextAppearance_Conversations_Body1_Tertiary); - } else { - this.binding.yourName.setText(displayName); - this.binding.yourName.setTextAppearance(this, R.style.TextAppearance_Conversations_Body1); - } - } - - private void updatePresenceStatus(String presenceStatus, String presenceStatusMessage) { - String status = presenceStatus; - if (!TextUtils.isEmpty(presenceStatusMessage)) { - status = presenceStatus + ": " + presenceStatusMessage; - } - this.binding.yourStatus.setText(status); - this.binding.yourStatus.setTextAppearance(this, R.style.TextAppearance_Conversations_Body1); - } - - private void removeErrorsOnAllBut(TextInputLayout exception) { - if (this.binding.accountJidLayout != exception) { - this.binding.accountJidLayout.setErrorEnabled(false); - this.binding.accountJidLayout.setError(null); - } - if (this.binding.accountPasswordLayout != exception) { - this.binding.accountPasswordLayout.setErrorEnabled(false); - this.binding.accountPasswordLayout.setError(null); - } - if (this.binding.hostnameLayout != exception) { - this.binding.hostnameLayout.setErrorEnabled(false); - this.binding.hostnameLayout.setError(null); - } - if (this.binding.portLayout != exception) { - this.binding.portLayout.setErrorEnabled(false); - this.binding.portLayout.setError(null); - } - } - - private void showDeletePgpDialog() { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.unpublish_pgp); - builder.setMessage(R.string.unpublish_pgp_message); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.confirm, (dialogInterface, i) -> { - mAccount.setPgpSignId(0); - mAccount.unsetPgpSignature(); - xmppConnectionService.databaseBackend.updateAccount(mAccount); - xmppConnectionService.sendPresence(mAccount); - refreshUiReal(); - }); - builder.create().show(); - } - - private void showOsOptimizationWarning(boolean showBatteryWarning, boolean showDataSaverWarning) { - this.binding.osOptimization.setVisibility(showBatteryWarning || showDataSaverWarning ? View.VISIBLE : View.GONE); - if (showDataSaverWarning && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { - this.binding.osOptimizationHeadline.setText(R.string.data_saver_enabled); - this.binding.osOptimizationBody.setText(R.string.data_saver_enabled_explained); - this.binding.osOptimizationDisable.setText(R.string.allow); - this.binding.osOptimizationDisable.setOnClickListener(v -> { - Intent intent = new Intent(Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS); - Uri uri = Uri.parse("package:" + getPackageName()); - intent.setData(uri); - try { - startActivityForResult(intent, REQUEST_DATA_SAVER); - } catch (ActivityNotFoundException e) { - ToastCompat.makeText(EditAccountActivity.this, R.string.device_does_not_support_data_saver, ToastCompat.LENGTH_SHORT).show(); - } - }); - } else if (showBatteryWarning && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - this.binding.osOptimizationDisable.setText(R.string.disable); - this.binding.osOptimizationHeadline.setText(R.string.battery_optimizations_enabled); - this.binding.osOptimizationBody.setText(R.string.battery_optimizations_enabled_explained); - this.binding.osOptimizationDisable.setOnClickListener(v -> { - Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); - Uri uri = Uri.parse("package:" + getPackageName()); - intent.setData(uri); - try { - startActivityForResult(intent, REQUEST_BATTERY_OP); - } catch (ActivityNotFoundException e) { - ToastCompat.makeText(EditAccountActivity.this, R.string.device_does_not_support_battery_op, ToastCompat.LENGTH_SHORT).show(); - } - }); - } - } - - public void showWipePepDialog() { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.clear_other_devices)); - builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setMessage(getString(R.string.clear_other_devices_desc)); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.accept), - (dialog, which) -> mAccount.getAxolotlService().wipeOtherPepDevices()); - builder.create().show(); - } - - private void editMamPrefs() { - this.mFetchingMamPrefsToast = ToastCompat.makeText(this, R.string.fetching_mam_prefs, ToastCompat.LENGTH_LONG); - this.mFetchingMamPrefsToast.show(); - xmppConnectionService.fetchMamPreferences(mAccount, this); - } - - private void showPassword() { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - final View view = getLayoutInflater().inflate(R.layout.dialog_show_password, null); - final TextView password = view.findViewById(R.id.password); - password.setText(mAccount.getPassword()); - builder.setTitle(R.string.password); - builder.setView(view); - builder.setPositiveButton(R.string.cancel, null); - builder.create().show(); - } - - @Override - public void onKeyStatusUpdated(AxolotlService.FetchStatus report) { - refreshUi(); - } - - @Override - public void onCaptchaRequested(final Account account, final String id, final Data data, final Bitmap captcha) { - runOnUiThread(() -> { - if ((mCaptchaDialog != null) && mCaptchaDialog.isShowing()) { - mCaptchaDialog.dismiss(); - } - final AlertDialog.Builder builder = new AlertDialog.Builder(EditAccountActivity.this); - final View view = getLayoutInflater().inflate(R.layout.captcha, null); - final ImageView imageView = view.findViewById(R.id.captcha); - final EditText input = view.findViewById(R.id.input); - imageView.setImageBitmap(captcha); - - builder.setTitle(getString(R.string.captcha_required)); - builder.setView(view); - - builder.setPositiveButton(getString(R.string.ok), - (dialog, which) -> { - String rc = input.getText().toString(); - data.put("username", account.getUsername()); - data.put("password", account.getPassword()); - data.put("ocr", rc); - data.submit(); - - if (xmppConnectionServiceBound) { - xmppConnectionService.sendCreateAccountWithCaptchaPacket( - account, id, data); - } - }); - builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> { - if (xmppConnectionService != null) { - xmppConnectionService.sendCreateAccountWithCaptchaPacket(account, null, null); - } - }); - - builder.setOnCancelListener(dialog -> { - if (xmppConnectionService != null) { - xmppConnectionService.sendCreateAccountWithCaptchaPacket(account, null, null); - } - }); - mCaptchaDialog = builder.create(); - mCaptchaDialog.show(); - input.requestFocus(); - }); - } - - public void onShowErrorToast(final int resId) { - runOnUiThread(new Runnable() { - @Override - public void run() { - ToastCompat.makeText(EditAccountActivity.this, resId, ToastCompat.LENGTH_SHORT).show(); - } - }); - } - - @Override - public void onPreferencesFetched(final Element prefs) { - runOnUiThread(() -> { - if (mFetchingMamPrefsToast != null) { - mFetchingMamPrefsToast.cancel(); - } - final AlertDialog.Builder builder = new AlertDialog.Builder(EditAccountActivity.this); - builder.setTitle(R.string.server_side_mam_prefs); - final String defaultAttr = prefs.getAttribute("default"); - final List defaults = Arrays.asList("never", "roster", "always"); - final AtomicInteger choice = new AtomicInteger(Math.max(0, defaults.indexOf(defaultAttr))); - builder.setSingleChoiceItems(R.array.mam_prefs, choice.get(), (dialog, which) -> choice.set(which)); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.ok, (dialog, which) -> { - prefs.setAttribute("default", defaults.get(choice.get())); - xmppConnectionService.pushMamPreferences(mAccount, prefs); - }); - builder.create().show(); - }); - } - - @Override - public void onPreferencesFetchFailed() { - runOnUiThread(() -> { - if (mFetchingMamPrefsToast != null) { - mFetchingMamPrefsToast.cancel(); - } - ToastCompat.makeText(EditAccountActivity.this, R.string.unable_to_fetch_mam_prefs, ToastCompat.LENGTH_LONG).show(); - }); - } - - @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) { - case REQUEST_IMPORT_BACKUP: - startActivity(new Intent(this, ImportBackupActivity.class)); - break; - } - } else { - ToastCompat.makeText(this, R.string.no_storage_permission, ToastCompat.LENGTH_SHORT).show(); - } - } - if (readGranted(grantResults, permissions)) { - if (xmppConnectionService != null) { - xmppConnectionService.restartFileObserver(); - } - } - } - - @Override - public void OnUpdateBlocklist(Status status) { - if (isFinishing()) { - return; - } - refreshUi(); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java deleted file mode 100644 index 763f2452e..000000000 --- a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java +++ /dev/null @@ -1,275 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.app.Activity; -import android.app.Dialog; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.View; -import android.widget.ArrayAdapter; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.databinding.DataBindingUtil; -import androidx.fragment.app.DialogFragment; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.EnterJidDialogBinding; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; -import eu.siacs.conversations.ui.interfaces.OnBackendConnected; -import eu.siacs.conversations.ui.util.DelayedHintHelper; -import eu.siacs.conversations.xmpp.Jid; - -public class EnterJidDialog extends DialogFragment implements OnBackendConnected, TextWatcher { - - - private static final List SUSPICIOUS_DOMAINS = Arrays.asList("conference", "muc", "room", "rooms", "chat"); - - private OnEnterJidDialogPositiveListener mListener = null; - - private static final String TITLE_KEY = "title"; - private static final String POSITIVE_BUTTON_KEY = "positive_button"; - private static final String PREFILLED_JID_KEY = "prefilled_jid"; - private static final String ACCOUNT_KEY = "account"; - private static final String ALLOW_EDIT_JID_KEY = "allow_edit_jid"; - private static final String MULTIPLE_ACCOUNTS = "multiple_accounts_enabled"; - private static final String ACCOUNTS_LIST_KEY = "activated_accounts_list"; - private static final String SANITY_CHECK_JID = "sanity_check_jid"; - - private KnownHostsAdapter knownHostsAdapter; - private Collection whitelistedDomains = Collections.emptyList(); - - private EnterJidDialogBinding binding; - private AlertDialog dialog; - private boolean sanityCheckJid = false; - - private boolean issuedWarning = false; - - public static EnterJidDialog newInstance(final List activatedAccounts, - final String title, final String positiveButton, - final String prefilledJid, final String account, boolean allowEditJid, boolean multipleAccounts, final boolean sanity_check_jid) { - EnterJidDialog dialog = new EnterJidDialog(); - Bundle bundle = new Bundle(); - bundle.putString(TITLE_KEY, title); - bundle.putString(POSITIVE_BUTTON_KEY, positiveButton); - bundle.putString(PREFILLED_JID_KEY, prefilledJid); - bundle.putString(ACCOUNT_KEY, account); - bundle.putBoolean(ALLOW_EDIT_JID_KEY, allowEditJid); - bundle.putBoolean(MULTIPLE_ACCOUNTS, multipleAccounts); - bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList) activatedAccounts); - bundle.putBoolean(SANITY_CHECK_JID, sanity_check_jid); - dialog.setArguments(bundle); - return dialog; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setRetainInstance(true); - } - - @Override - public void onStart() { - super.onStart(); - final Activity activity = getActivity(); - if (activity instanceof XmppActivity && ((XmppActivity) activity).xmppConnectionService != null) { - refreshKnownHosts(); - } - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(getArguments().getString(TITLE_KEY)); - binding = DataBindingUtil.inflate(getActivity().getLayoutInflater(), R.layout.enter_jid_dialog, null, false); - this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.simple_list_item); - binding.jid.setAdapter(this.knownHostsAdapter); - binding.jid.addTextChangedListener(this); - String prefilledJid = getArguments().getString(PREFILLED_JID_KEY); - if (prefilledJid != null) { - binding.jid.append(prefilledJid); - if (!getArguments().getBoolean(ALLOW_EDIT_JID_KEY)) { - binding.jid.setFocusable(false); - binding.jid.setFocusableInTouchMode(false); - binding.jid.setClickable(false); - binding.jid.setCursorVisible(false); - } - } - sanityCheckJid = getArguments().getBoolean(SANITY_CHECK_JID, false); - - DelayedHintHelper.setHint(R.string.account_settings_example_jabber_id, binding.jid); - - String account = getArguments().getString(ACCOUNT_KEY); - - if (getArguments().getBoolean(MULTIPLE_ACCOUNTS)) { - binding.yourAccount.setVisibility(View.VISIBLE); - binding.account.setVisibility(View.VISIBLE); - } else { - binding.yourAccount.setVisibility(View.GONE); - binding.account.setVisibility(View.GONE); - } - - if (account == null) { - StartConversationActivity.populateAccountSpinner(getActivity(), getArguments().getStringArrayList(ACCOUNTS_LIST_KEY), binding.account); - } else { - ArrayAdapter adapter = new ArrayAdapter<>( - getActivity(), R.layout.simple_list_item, - new String[]{account}); - binding.account.setEnabled(false); - adapter.setDropDownViewResource(R.layout.simple_list_item); - binding.account.setAdapter(adapter); - } - - builder.setView(binding.getRoot()); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(getArguments().getString(POSITIVE_BUTTON_KEY), null); - this.dialog = builder.create(); - - View.OnClickListener dialogOnClick = v -> { - handleEnter(binding, account); - }; - - binding.jid.setOnEditorActionListener((v, actionId, event) -> { - handleEnter(binding, account); - return true; - }); - - dialog.show(); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(dialogOnClick); - return dialog; - } - - private void handleEnter(EnterJidDialogBinding binding, String account) { - final Jid accountJid; - if (!binding.account.isEnabled() && account == null) { - return; - } - try { - if (Config.DOMAIN_LOCK != null) { - accountJid = Jid.ofEscaped((String) binding.account.getSelectedItem(), Config.DOMAIN_LOCK, null); - } else { - accountJid = Jid.ofEscaped((String) binding.account.getSelectedItem()); - } - } catch (final IllegalArgumentException e) { - return; - } - final Jid contactJid; - try { - contactJid = Jid.ofEscaped(binding.jid.getText().toString().trim()); - } catch (final IllegalArgumentException e) { - binding.jidLayout.setError(getActivity().getString(R.string.invalid_jid)); - return; - } - - if (!issuedWarning && sanityCheckJid) { - if (contactJid.isDomainJid()) { - binding.jidLayout.setError(getActivity().getString(R.string.this_looks_like_a_domain)); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anyway); - issuedWarning = true; - return; - } - if (suspiciousSubDomain(contactJid.getDomain().toEscapedString())) { - binding.jidLayout.setError(getActivity().getString(R.string.this_looks_like_channel)); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add_anyway); - issuedWarning = true; - return; - } - } - - if (mListener != null) { - try { - if (mListener.onEnterJidDialogPositive(accountJid, contactJid)) { - dialog.dismiss(); - } - } catch (JidError error) { - binding.jidLayout.setError(error.toString()); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add); - issuedWarning = false; - } - } - } - - public void setOnEnterJidDialogPositiveListener(OnEnterJidDialogPositiveListener listener) { - this.mListener = listener; - } - - @Override - public void onBackendConnected() { - refreshKnownHosts(); - } - - private void refreshKnownHosts() { - final Activity activity = getActivity(); - if (activity instanceof XmppActivity) { - final XmppConnectionService service = ((XmppActivity) activity).xmppConnectionService; - if (service == null) { - return; - } - final Collection hosts = service.getKnownHosts(); - this.knownHostsAdapter.refresh(hosts); - this.whitelistedDomains = hosts; - } - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - - } - - @Override - public void afterTextChanged(Editable s) { - if (issuedWarning) { - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.add); - binding.jidLayout.setError(null); - issuedWarning = false; - } - } - - public interface OnEnterJidDialogPositiveListener { - boolean onEnterJidDialogPositive(Jid account, Jid contact) throws EnterJidDialog.JidError; - } - - public static class JidError extends Exception { - final String msg; - - public JidError(final String msg) { - this.msg = msg; - } - - @NonNull - public String toString() { - return msg; - } - } - - @Override - public void onDestroyView() { - Dialog dialog = getDialog(); - if (dialog != null && getRetainInstance()) { - dialog.setDismissMessage(null); - } - super.onDestroyView(); - } - - private boolean suspiciousSubDomain(String domain) { - if (this.whitelistedDomains.contains(domain)) { - return false; - } - final String[] parts = domain.split("\\."); - return parts.length >= 3 && SUSPICIOUS_DOMAINS.contains(parts[0]); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/EnterNameActivity.java b/src/main/java/eu/siacs/conversations/ui/EnterNameActivity.java deleted file mode 100644 index bf1317662..000000000 --- a/src/main/java/eu/siacs/conversations/ui/EnterNameActivity.java +++ /dev/null @@ -1,131 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.Intent; -import android.os.Bundle; -import android.view.View; - -import androidx.appcompat.widget.Toolbar; -import androidx.databinding.DataBindingUtil; - -import java.util.concurrent.atomic.AtomicBoolean; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ActivityEnterNameBinding; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.FirstStartManager; - -public class EnterNameActivity extends XmppActivity implements XmppConnectionService.OnAccountUpdate { - - private ActivityEnterNameBinding binding; - private Account account; - private boolean mExisting = false; - private AtomicBoolean setNick = new AtomicBoolean(false); - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_enter_name); - setSupportActionBar((Toolbar) this.binding.toolbar); - this.binding.next.setOnClickListener(this::next); - this.binding.skip.setOnClickListener(this::skip); - updateNextButton(); - this.setNick.set(savedInstanceState != null && savedInstanceState.getBoolean("set_nick", false)); - } - - private void updateNextButton() { - if (account != null && (account.getStatus() == Account.State.CONNECTING || account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL)) { - this.binding.next.setEnabled(false); - this.binding.next.setText(R.string.account_status_connecting); - } else if (account != null && (account.getStatus() == Account.State.ONLINE)) { - this.binding.next.setEnabled(true); - this.binding.next.setText(R.string.next); - } - } - @Override - protected void onStart() { - super.onStart(); - final Intent intent = getIntent(); - final int theme = findTheme(); - if (this.mTheme != theme) { - recreate(); - } else if (intent != null) { - boolean existing = intent.getBooleanExtra("existing", false); - this.mExisting = existing; - } - } - - private void next(View view) { - FirstStartManager firstStartManager = new FirstStartManager(this); - if (account != null) { - String name = this.binding.name.getText().toString().trim(); - account.setDisplayName(name); - xmppConnectionService.publishDisplayName(account); - if (firstStartManager.isFirstTimeLaunch()) { - Intent intent = new Intent(this, SetSettingsActivity.class); - intent.putExtra("setup", true); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } else { - Intent intent = new Intent(this, PublishProfilePictureActivity.class); - intent.putExtra(PublishProfilePictureActivity.EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); - intent.putExtra("setup", true); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - } - finish(); - } - private void skip(View view) { - if (account != null) { - String name = this.binding.name.getText().toString().trim(); - account.setDisplayName(name); - xmppConnectionService.publishDisplayName(account); - final Intent intent = new Intent(getApplicationContext(), StartConversationActivity.class); - if (xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 1) { - intent.putExtra("init", true); - } - StartConversationActivity.addInviteUri(intent, getIntent()); - intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - finish(); - } - finish(); - } - - @Override - public void onSaveInstanceState(Bundle savedInstanceState) { - savedInstanceState.putBoolean("set_nick", this.setNick.get()); - super.onSaveInstanceState(savedInstanceState); - } - - @Override - protected void refreshUiReal() { - checkSuggestPreviousNick(); - updateNextButton(); - } - - @Override - void onBackendConnected() { - this.account = extractAccount(getIntent()); - if (this.account != null) { - checkSuggestPreviousNick(); - } - updateNextButton(); - } - - private void checkSuggestPreviousNick() { - String displayName = this.account == null ? null : this.account.getDisplayName(); - if (displayName != null) { - if (setNick.compareAndSet(false, true) && this.binding.name.getText().length() == 0) { - this.binding.name.getText().append(displayName); - } - } - } - - @Override - public void onAccountUpdate() { - refreshUi(); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/ImportBackupActivity.java b/src/main/java/eu/siacs/conversations/ui/ImportBackupActivity.java deleted file mode 100644 index 0be65b463..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ImportBackupActivity.java +++ /dev/null @@ -1,274 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.ComponentName; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.ServiceConnection; -import android.net.Uri; -import android.os.Bundle; -import android.os.IBinder; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import eu.siacs.conversations.utils.Compatibility; - -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.Toolbar; -import androidx.core.content.ContextCompat; -import androidx.databinding.DataBindingUtil; - -import com.google.android.material.snackbar.Snackbar; - -import java.io.IOException; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -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.utils.ThemeHelper; - -public class ImportBackupActivity extends XmppActivity implements ServiceConnection, ImportBackupService.OnBackupFilesLoaded, BackupFileAdapter.OnItemClickedListener, ImportBackupService.OnBackupProcessed { - - private ActivityImportBackupBinding binding; - - private BackupFileAdapter backupFileAdapter; - private ImportBackupService service; - private boolean mLoadingState = false; - - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = DataBindingUtil.setContentView(this, R.layout.activity_import_backup); - setSupportActionBar((Toolbar) binding.toolbar); - setLoadingState(savedInstanceState != null && savedInstanceState.getBoolean("loading_state", false)); - this.backupFileAdapter = new BackupFileAdapter(); - this.binding.list.setAdapter(this.backupFileAdapter); - this.backupFileAdapter.setOnItemClickedListener(this); - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.import_backup, menu); - final MenuItem openBackup = menu.findItem(R.id.action_open_backup_file); - openBackup.setVisible(!this.mLoadingState); - return true; - } - - @Override - protected void refreshUiReal() { - } - - @Override - public void onSaveInstanceState(Bundle bundle) { - bundle.putBoolean("loading_state", this.mLoadingState); - super.onSaveInstanceState(bundle); - } - - @Override - public void onStart() { - super.onStart(); - final int theme = ThemeHelper.find(this); - if (this.mTheme != theme) { - recreate(); - } else { - bindService(new Intent(this, ImportBackupService.class), this, Context.BIND_AUTO_CREATE); - } - final Intent intent = getIntent(); - if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction()) && !this.mLoadingState) { - Uri uri = intent.getData(); - if (uri != null) { - openBackupFileFromUri(uri, true); - } - } - } - - @Override - public void onStop() { - super.onStop(); - if (this.service != null) { - this.service.removeOnBackupProcessedListener(this); - } - unbindService(this); - } - - @Override - void onBackendConnected() { - } - - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - ImportBackupService.ImportBackupServiceBinder binder = (ImportBackupService.ImportBackupServiceBinder) service; - this.service = binder.getService(); - this.service.addOnBackupProcessedListener(this); - setLoadingState(this.service.getLoadingState()); - this.service.loadBackupFiles(this); - } - - @Override - public void onServiceDisconnected(ComponentName name) { - this.service = null; - } - - @Override - public void onBackupFilesLoaded(final List files) { - runOnUiThread(() -> { - if (files.size() >= 1) { - this.binding.hint.setVisibility(View.GONE); - this.binding.list.setVisibility(View.VISIBLE); - backupFileAdapter.setFiles(files); - } else { - this.binding.hint.setVisibility(View.VISIBLE); - if (Compatibility.runsThirty()) { - this.binding.hint.setText(getString(R.string.import_backup_description)); - } else { - this.binding.hint.setText(getString(R.string.no_backup_available)); - } - this.binding.list.setVisibility(View.GONE); - } - }); - } - - @Override - public void onClick(final ImportBackupService.BackupFile backupFile) { - showEnterPasswordDialog(backupFile, false); - } - - private void openBackupFileFromUri(final Uri uri, final boolean finishOnCancel) { - try { - final ImportBackupService.BackupFile backupFile = ImportBackupService.BackupFile.read(this, uri); - showEnterPasswordDialog(backupFile, finishOnCancel); - } catch (final IOException e) { - Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG).show(); - } catch (IllegalArgumentException e) { - Log.d(Config.LOGTAG, "unable to open backup file " + uri, e); - Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG).show(); - } - } - - private void showEnterPasswordDialog(final ImportBackupService.BackupFile backupFile, final boolean finishOnCancel) { - final DialogEnterPasswordBinding enterPasswordBinding = DataBindingUtil.inflate(LayoutInflater.from(this), R.layout.dialog_enter_password, null, false); - Log.d(Config.LOGTAG, "attempting to import " + backupFile.getUri()); - enterPasswordBinding.explain.setText(getString(R.string.enter_password_to_restore, backupFile.getHeader().getJid().toString())); - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setView(enterPasswordBinding.getRoot()); - builder.setTitle(R.string.enter_password); - builder.setNegativeButton(R.string.cancel, (dialog, which) -> { - if (finishOnCancel) { - finish(); - } - }); - builder.setPositiveButton(R.string.restore, null); - builder.setCancelable(false); - final AlertDialog dialog = builder.create(); - dialog.setOnShowListener((d) -> { - dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> { - final String password = enterPasswordBinding.accountPassword.getEditableText().toString(); - if (password.isEmpty()) { - enterPasswordBinding.accountPasswordLayout.setError(getString(R.string.please_enter_password)); - return; - } - final Uri uri = backupFile.getUri(); - Intent intent = new Intent(this, ImportBackupService.class); - intent.setAction(Intent.ACTION_SEND); - intent.putExtra("password", password); - if ("file".equals(uri.getScheme())) { - intent.putExtra("file", uri.getPath()); - } else { - intent.setData(uri); - intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - setLoadingState(true); - ContextCompat.startForegroundService(this, intent); - d.dismiss(); - }); - }); - dialog.show(); - } - - private void setLoadingState(final boolean loadingState) { - binding.coordinator.setVisibility(loadingState ? View.GONE : View.VISIBLE); - binding.inProgress.setVisibility(loadingState ? View.VISIBLE : View.GONE); - setTitle(loadingState ? R.string.restoring_backup : R.string.restore_backup); - configureActionBar(getSupportActionBar(), !loadingState); - this.mLoadingState = loadingState; - invalidateOptionsMenu(); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent intent) { - super.onActivityResult(requestCode, resultCode, intent); - if (resultCode == RESULT_OK) { - if (requestCode == 0xbac) { - openBackupFileFromUri(intent.getData(), false); - } - } - } - - @Override - public void onAccountAlreadySetup() { - runOnUiThread(() -> { - setLoadingState(false); - Snackbar.make(binding.coordinator, R.string.account_already_setup, Snackbar.LENGTH_LONG).show(); - }); - } - - @Override - public void onBackupRestored() { - runOnUiThread(this::restart); - } - - private void restart() { - Log.d(Config.LOGTAG, "Restarting " + getBaseContext().getPackageManager().getLaunchIntentForPackage(getBaseContext().getPackageName())); - Intent intent = getBaseContext().getPackageManager().getLaunchIntentForPackage(getBaseContext().getPackageName()); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - System.exit(0); - } - - @Override - public void onBackupDecryptionFailed() { - runOnUiThread(() -> { - setLoadingState(false); - Snackbar.make(binding.coordinator, R.string.unable_to_decrypt_backup, Snackbar.LENGTH_LONG).show(); - }); - } - - @Override - public void onBackupRestoreFailed() { - runOnUiThread(() -> { - setLoadingState(false); - Snackbar.make(binding.coordinator, R.string.unable_to_restore_backup, Snackbar.LENGTH_LONG).show(); - }); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == R.id.action_open_backup_file) { - openBackupFile(); - return true; - } - return super.onOptionsItemSelected(item); - } - - private void openBackupFile() { - Intent intent; - if (Compatibility.runsThirty()) { - intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - } else { - intent = new Intent(Intent.ACTION_GET_CONTENT); - } - intent.setType("*/*"); - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false); - intent.addCategory(Intent.CATEGORY_OPENABLE); - startActivityForResult(Intent.createChooser(intent, getString(R.string.open_backup)), 0xbac); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/IntroActivity.java b/src/main/java/eu/siacs/conversations/ui/IntroActivity.java deleted file mode 100644 index 751474fb5..000000000 --- a/src/main/java/eu/siacs/conversations/ui/IntroActivity.java +++ /dev/null @@ -1,195 +0,0 @@ -package eu.siacs.conversations.ui; - -import static eu.siacs.conversations.ui.util.IntroHelper.SaveIntroShown; - -import android.content.Intent; -import android.os.Bundle; - -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; - -import com.github.appintro.AppIntro2; -import com.github.appintro.AppIntroFragment; -import com.github.appintro.model.SliderPage; - -import eu.siacs.conversations.R; - -public class IntroActivity extends AppIntro2 { - public static final String ACTIVITY = "activity"; - public static final String MULTICHAT = "multi_chat"; - public static final String START_UI = "StartUI"; - public static final String WELCOME_ACTIVITY = "WelcomeActivity"; - public static final String START_CONVERSATION_ACTIVITY = "StartConversationActivity"; - public static final String CONVERSATIONS_ACTIVITY = "ConversationsActivity"; - public static final String CONTACT_DETAILS_ACTIVITY = "ContactDetailsActivity"; - public static final String CONFERENCE_DETAILS_ACTIVITY = "ConferenceDetailsActivity"; - String activity = null; - boolean mode_multi = false; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - final int backgroundColor = getResources().getColor(R.color.header_background); - final int barColor = getResources().getColor(R.color.accent_monocles); - final int indicatorColorActive = getResources().getColor(R.color.darkmonocles); - final int indicatorColorUsed = getResources().getColor(R.color.darkmonocles); - - setBarColor(barColor); - setIndicatorColor(indicatorColorActive, indicatorColorUsed); - setButtonsEnabled(true); - setImmersiveMode(); - setSystemBackButtonLocked(true); - //setProgressIndicator(); - - final Intent intent = getIntent(); - if (intent != null) { - activity = intent.getStringExtra(ACTIVITY); - mode_multi = intent.getBooleanExtra(MULTICHAT, false); - } - if (activity == null) { - finish(); - } - switch (activity) { - case START_UI: - SliderPage welcome = new SliderPage(); - welcome.setTitle(getString(R.string.welcome_header)); - welcome.setDescription(getString(R.string.intro_desc_main)); - welcome.setImageDrawable(R.drawable.logo_800); - welcome.setBackgroundColor(backgroundColor); - addSlide(AppIntroFragment.createInstance(welcome)); - - SliderPage privacy = new SliderPage(); - privacy.setTitle(getString(R.string.intro_privacy)); - privacy.setDescription(getString(R.string.intro_desc_privacy)); - privacy.setImageDrawable(R.drawable.intro_security_icon); - privacy.setBackgroundColor(backgroundColor); - addSlide(AppIntroFragment.createInstance(privacy)); - - - SliderPage xmpp = new SliderPage(); - xmpp.setTitle(getString(R.string.intro_whats_xmpp)); - xmpp.setDescription(getString(R.string.intro_desc_whats_xmpp)); - xmpp.setImageDrawable(R.drawable.intro_xmpp_icon); - xmpp.setBackgroundColor(backgroundColor); - addSlide(AppIntroFragment.createInstance(xmpp)); - - SliderPage permissions = new SliderPage(); - permissions.setTitle(getString(R.string.intro_required_permissions)); - permissions.setDescription(getString(R.string.intro_desc_required_permissions)); - permissions.setImageDrawable(R.drawable.intro_memory_icon); - permissions.setBackgroundColor(backgroundColor); - addSlide(AppIntroFragment.createInstance(permissions)); - - SliderPage permissions2 = new SliderPage(); - permissions2.setTitle(getString(R.string.intro_optional_permissions)); - permissions2.setDescription(getString(R.string.intro_desc_optional_permissions)); - permissions2.setImageDrawable(R.drawable.intro_contacts_icon); - permissions2.setBackgroundColor(backgroundColor); - addSlide(AppIntroFragment.createInstance(permissions2)); - - SliderPage permissions3 = new SliderPage(); - permissions3.setTitle(getString(R.string.intro_optional_permissions)); - permissions3.setDescription(getString(R.string.intro_desc_optional_permissions2)); - permissions3.setImageDrawable(R.drawable.intro_location_icon); - permissions3.setBackgroundColor(backgroundColor); - addSlide(AppIntroFragment.createInstance(permissions3)); - break; - case WELCOME_ACTIVITY: - SliderPage account = new SliderPage(); - account.setTitle(getString(R.string.intro_account)); - account.setDescription(getString(R.string.intro_desc_account)); - account.setImageDrawable(R.drawable.intro_account_icon); - account.setBackgroundColor(backgroundColor); - addSlide(AppIntroFragment.createInstance(account)); - - SliderPage account2 = new SliderPage(); - account2.setTitle(getString(R.string.intro_account)); - account2.setDescription(getString(R.string.intro_desc_account2)); - account2.setImageDrawable(R.drawable.intro_account_icon); - account2.setBackgroundColor(backgroundColor); - addSlide(AppIntroFragment.createInstance(account2)); - - SliderPage account3 = new SliderPage(); - account3.setTitle(getString(R.string.intro_account)); - account3.setDescription(getString(R.string.intro_desc_account3)); - account3.setImageDrawable(R.drawable.intro_account_icon); - account3.setBackgroundColor(backgroundColor); - addSlide(AppIntroFragment.createInstance(account3)); - break; - case START_CONVERSATION_ACTIVITY: - SliderPage startChatting = new SliderPage(); - startChatting.setTitle(getString(R.string.intro_start_chatting)); - startChatting.setDescription(getString(R.string.intro_desc_start_chatting)); - startChatting.setImageDrawable(R.drawable.intro_start_chat_icon); - startChatting.setBackgroundColor(backgroundColor); - addSlide(AppIntroFragment.createInstance(startChatting)); - - SliderPage startChatting2 = new SliderPage(); - startChatting2.setTitle(getString(R.string.intro_start_chatting)); - startChatting2.setDescription(getString(R.string.intro_desc_start_chatting2)); - startChatting2.setImageDrawable(R.drawable.intro_start_chat_icon); - startChatting2.setBackgroundColor(backgroundColor); - addSlide(AppIntroFragment.createInstance(startChatting2)); - - SliderPage startChatting3 = new SliderPage(); - startChatting3.setTitle(getString(R.string.intro_start_chatting)); - startChatting3.setDescription(getString(R.string.intro_desc_start_chatting3)); - startChatting3.setImageDrawable(R.drawable.intro_start_chat_icon); - startChatting3.setBackgroundColor(backgroundColor); - addSlide(AppIntroFragment.createInstance(startChatting3)); - break; - case CONVERSATIONS_ACTIVITY: - SliderPage openChat = new SliderPage(); - openChat.setTitle(getString(R.string.intro_start_chatting)); - openChat.setDescription(getString(R.string.intro_desc_open_chat)); - openChat.setImageDrawable(R.drawable.intro_start_chat_icon); - openChat.setBackgroundColor(backgroundColor); - addSlide(AppIntroFragment.createInstance(openChat)); - - SliderPage chatDetails = new SliderPage(); - chatDetails.setTitle(getString(R.string.intro_chat_details)); - chatDetails.setDescription(getString(R.string.intro_desc_chat_details)); - chatDetails.setImageDrawable(R.drawable.intro_account_details_icon); - chatDetails.setBackgroundColor(backgroundColor); - addSlide(AppIntroFragment.createInstance(chatDetails)); - - if (mode_multi) { - SliderPage highlightUser = new SliderPage(); - highlightUser.setTitle(getString(R.string.intro_highlight_user)); - highlightUser.setDescription(getString(R.string.intro_desc_highlight_user)); - highlightUser.setImageDrawable(R.drawable.intro_account_details_icon); - highlightUser.setBackgroundColor(backgroundColor); - addSlide(AppIntroFragment.createInstance(highlightUser)); - } - break; - case CONTACT_DETAILS_ACTIVITY: - case CONFERENCE_DETAILS_ACTIVITY: - SliderPage openChatDetails = new SliderPage(); - openChatDetails.setTitle(getString(R.string.intro_chat_details)); - openChatDetails.setDescription(getString(R.string.intro_desc_open_chat_details)); - openChatDetails.setImageDrawable(R.drawable.intro_account_details_icon); - openChatDetails.setBackgroundColor(backgroundColor); - addSlide(AppIntroFragment.createInstance(openChatDetails)); - } - } - - @Override - public void onSkipPressed(Fragment currentFragment) { - super.onSkipPressed(currentFragment); - SaveIntroShown(getBaseContext(), activity, mode_multi); - finish(); - } - - @Override - public void onDonePressed(Fragment currentFragment) { - super.onDonePressed(currentFragment); - SaveIntroShown(getBaseContext(), activity, mode_multi); - finish(); - } - - @Override - public void onSlideChanged(@Nullable Fragment oldFragment, @Nullable Fragment newFragment) { - super.onSlideChanged(oldFragment, newFragment); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/JoinConferenceDialog.java b/src/main/java/eu/siacs/conversations/ui/JoinConferenceDialog.java deleted file mode 100644 index 0fc1fe6fe..000000000 --- a/src/main/java/eu/siacs/conversations/ui/JoinConferenceDialog.java +++ /dev/null @@ -1,133 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.app.Activity; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.os.Bundle; -import android.view.View; -import android.widget.AutoCompleteTextView; -import android.widget.Spinner; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.databinding.DataBindingUtil; -import androidx.fragment.app.DialogFragment; - -import com.google.android.material.textfield.TextInputLayout; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.DialogJoinConferenceBinding; -import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; -import eu.siacs.conversations.ui.interfaces.OnBackendConnected; -import eu.siacs.conversations.ui.util.DelayedHintHelper; - -public class JoinConferenceDialog extends DialogFragment implements OnBackendConnected { - - private static final String PREFILLED_JID_KEY = "prefilled_jid"; - private static final String ACCOUNTS_LIST_KEY = "activated_accounts_list"; - private static final String MULTIPLE_ACCOUNTS = "multiple_accounts_enabled"; - private JoinConferenceDialogListener mListener; - private KnownHostsAdapter knownHostsAdapter; - - public static JoinConferenceDialog newInstance(String prefilledJid, List accounts, boolean multipleAccounts) { - JoinConferenceDialog dialog = new JoinConferenceDialog(); - Bundle bundle = new Bundle(); - bundle.putString(PREFILLED_JID_KEY, prefilledJid); - bundle.putBoolean(MULTIPLE_ACCOUNTS, multipleAccounts); - bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList) accounts); - - dialog.setArguments(bundle); - return dialog; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setRetainInstance(true); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(R.string.join_public_channel); - DialogJoinConferenceBinding binding = DataBindingUtil.inflate(getActivity().getLayoutInflater(), R.layout.dialog_join_conference, null, false); - DelayedHintHelper.setHint(R.string.channel_full_jid_example, binding.jid); - this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.simple_list_item); - binding.jid.setAdapter(knownHostsAdapter); - String prefilledJid = getArguments().getString(PREFILLED_JID_KEY); - if (prefilledJid != null) { - binding.jid.append(prefilledJid); - } - if (getArguments().getBoolean(MULTIPLE_ACCOUNTS)) { - binding.yourAccount.setVisibility(View.VISIBLE); - binding.account.setVisibility(View.VISIBLE); - } else { - binding.yourAccount.setVisibility(View.GONE); - binding.account.setVisibility(View.GONE); - } - StartConversationActivity.populateAccountSpinner(getActivity(), getArguments().getStringArrayList(ACCOUNTS_LIST_KEY), binding.account); - builder.setView(binding.getRoot()); - builder.setPositiveButton(R.string.join, null); - builder.setNegativeButton(R.string.cancel, null); - final AlertDialog dialog = builder.create(); - dialog.show(); - dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(view -> mListener.onJoinDialogPositiveClick(dialog, binding.account, binding.accountJidLayout, binding.jid, binding.bookmark.isChecked())); - binding.jid.setOnEditorActionListener((v, actionId, event) -> { - mListener.onJoinDialogPositiveClick(dialog, binding.account, binding.accountJidLayout, binding.jid, binding.bookmark.isChecked()); - return true; - }); - return dialog; - } - - @Override - public void onBackendConnected() { - refreshKnownHosts(); - } - - private void refreshKnownHosts() { - Activity activity = getActivity(); - if (activity instanceof XmppActivity) { - Collection hosts = ((XmppActivity) activity).xmppConnectionService.getKnownConferenceHosts(); - this.knownHostsAdapter.refresh(hosts); - } - } - - @Override - public void onAttach(Context context) { - super.onAttach(context); - try { - mListener = (JoinConferenceDialogListener) context; - } catch (ClassCastException e) { - throw new ClassCastException(context.toString() - + " must implement JoinConferenceDialogListener"); - } - } - - @Override - public void onDestroyView() { - Dialog dialog = getDialog(); - if (dialog != null && getRetainInstance()) { - dialog.setDismissMessage(null); - } - super.onDestroyView(); - } - - @Override - public void onStart() { - super.onStart(); - final Activity activity = getActivity(); - if (activity instanceof XmppActivity && ((XmppActivity) activity).xmppConnectionService != null) { - refreshKnownHosts(); - } - } - - public interface JoinConferenceDialogListener { - void onJoinDialogPositiveClick(Dialog dialog, Spinner spinner, TextInputLayout jidLayout, AutoCompleteTextView jid, boolean isBookmarkChecked); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/LocationActivity.java b/src/main/java/eu/siacs/conversations/ui/LocationActivity.java deleted file mode 100644 index 81eaec10a..000000000 --- a/src/main/java/eu/siacs/conversations/ui/LocationActivity.java +++ /dev/null @@ -1,291 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.Manifest; -import android.annotation.TargetApi; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.os.Build; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.provider.Settings; -import android.util.Log; -import android.view.MenuItem; - -import androidx.annotation.BoolRes; -import androidx.annotation.NonNull; - -import org.osmdroid.api.IGeoPoint; -import org.osmdroid.api.IMapController; -import org.osmdroid.config.Configuration; -import org.osmdroid.config.IConfigurationProvider; -import org.osmdroid.tileprovider.tilesource.TileSourceFactory; -import org.osmdroid.util.GeoPoint; -import org.osmdroid.views.CustomZoomButtonsController; -import org.osmdroid.views.MapView; -import org.osmdroid.views.overlay.CopyrightOverlay; -import org.osmdroid.views.overlay.Overlay; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.http.HttpConnectionManager; -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.utils.ThemeHelper; - -public abstract class LocationActivity extends XmppActivity implements LocationListener { - protected LocationManager locationManager; - protected boolean hasLocationFeature; - - public static final int REQUEST_CODE_CREATE = 0; - public static final int REQUEST_CODE_FAB_PRESSED = 1; - public static final int REQUEST_CODE_SNACKBAR_PRESSED = 2; - - protected static final String KEY_LOCATION = "loc"; - protected static final String KEY_ZOOM_LEVEL = "zoom"; - - protected Location myLoc = null; - private MapView map = null; - protected IMapController mapController = null; - - protected Bitmap marker_icon; - - protected void clearMarkers() { - synchronized (this.map.getOverlays()) { - for (final Overlay overlay : this.map.getOverlays()) { - if (overlay instanceof Marker || overlay instanceof MyLocation) { - this.map.getOverlays().remove(overlay); - } - } - } - } - - - protected void updateLocationMarkers() { - clearMarkers(); - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final Context ctx = getApplicationContext(); - setTheme(ThemeHelper.find(this)); - - final PackageManager packageManager = ctx.getPackageManager(); - hasLocationFeature = packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION) || - packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS) || - packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION_NETWORK); - this.locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE); - this.marker_icon = BitmapFactory.decodeResource(ctx.getResources(), R.drawable.marker); - - // Ask for location permissions if location services are enabled and we're - // just starting the activity (we don't want to keep pestering them on every - // screen rotation or if there's no point because it's disabled anyways). - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && savedInstanceState == null) { - requestPermissions(REQUEST_CODE_CREATE); - } - - final IConfigurationProvider config = Configuration.getInstance(); - config.load(ctx, getPreferences()); - //config.setUserAgentValue(BuildConfig.APPLICATION_ID + "/" + BuildConfig.VERSION_CODE); - final boolean useTor = QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor); - final boolean useI2P = QuickConversationsService.isConversations() && getBooleanPreference("use_i2p", R.bool.use_i2p); - if (useTor || useI2P) { - config.setHttpProxy(HttpConnectionManager.getProxy(useI2P)); - } - } - - @Override - protected void onSaveInstanceState(@NonNull final Bundle outState) { - super.onSaveInstanceState(outState); - - final IGeoPoint center = map.getMapCenter(); - outState.putParcelable(KEY_LOCATION, new GeoPoint( - center.getLatitude(), - center.getLongitude() - )); - outState.putDouble(KEY_ZOOM_LEVEL, map.getZoomLevelDouble()); - } - - @Override - protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - - if (savedInstanceState.containsKey(KEY_LOCATION)) { - mapController.setCenter(savedInstanceState.getParcelable(KEY_LOCATION)); - } - if (savedInstanceState.containsKey(KEY_ZOOM_LEVEL)) { - mapController.setZoom(savedInstanceState.getDouble(KEY_ZOOM_LEVEL)); - } - } - - protected void setupMapView(MapView mapView, final GeoPoint pos) { - map = mapView; - map.getOverlays().add(new CopyrightOverlay(this)); - map.setTileSource(TileSourceFactory.MAPNIK); - map.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER); - map.setMultiTouchControls(true); - map.setTilesScaledToDpi(false); - mapController = map.getController(); - mapController.setZoom(Config.Map.INITIAL_ZOOM_LEVEL); - mapController.setCenter(pos); - } - - protected void gotoLoc() { - gotoLoc(map.getZoomLevelDouble() == Config.Map.INITIAL_ZOOM_LEVEL); - } - - protected abstract void gotoLoc(final boolean setZoomLevel); - - protected abstract void setMyLoc(final Location location); - - protected void requestLocationUpdates() { - if (!hasLocationFeature || locationManager == null) { - return; - } - - Log.d(Config.LOGTAG, "Requesting location updates..."); - final Location lastKnownLocationGps; - final Location lastKnownLocationNetwork; - - try { - if (locationManager.getAllProviders().contains(LocationManager.GPS_PROVIDER)) { - lastKnownLocationGps = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); - - if (lastKnownLocationGps != null) { - setMyLoc(lastKnownLocationGps); - } - locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, Config.Map.LOCATION_FIX_TIME_DELTA, - Config.Map.LOCATION_FIX_SPACE_DELTA, this); - } else { - lastKnownLocationGps = null; - } - - if (locationManager.getAllProviders().contains(LocationManager.NETWORK_PROVIDER)) { - lastKnownLocationNetwork = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); - if (lastKnownLocationNetwork != null && LocationHelper.isBetterLocation(lastKnownLocationNetwork, - lastKnownLocationGps)) { - setMyLoc(lastKnownLocationNetwork); - } - locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, Config.Map.LOCATION_FIX_TIME_DELTA, - Config.Map.LOCATION_FIX_SPACE_DELTA, this); - } - - // If something else is also querying for location more frequently than we are, the battery is already being - // drained. Go ahead and use the existing locations as often as we can get them. - if (locationManager.getAllProviders().contains(LocationManager.PASSIVE_PROVIDER)) { - locationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 0, 0, this); - } - } catch (final SecurityException ignored) { - // Do nothing if the users device has no location providers. - } - } - - protected void pauseLocationUpdates() throws SecurityException { - if (locationManager != null) { - locationManager.removeUpdates(this); - } - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - finish(); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public void onPause() { - super.onPause(); - Configuration.getInstance().save(this, getPreferences()); - map.onPause(); - try { - pauseLocationUpdates(); - } catch (final SecurityException ignored) { - } - } - - protected abstract void updateUi(); - - protected boolean mapAtInitialLoc() { - return map.getZoomLevelDouble() == Config.Map.INITIAL_ZOOM_LEVEL; - } - - @Override - public void onResume() { - super.onResume(); - Configuration.getInstance().load(this, getPreferences()); - map.onResume(); - this.setMyLoc(null); - requestLocationUpdates(); - updateLocationMarkers(); - updateUi(); - map.setTileSource(TileSourceFactory.MAPNIK); - map.setTilesScaledToDpi(true); - - if (mapAtInitialLoc()) { - gotoLoc(); - } - } - - @TargetApi(Build.VERSION_CODES.M) - protected boolean hasLocationPermissions() { - return (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || - checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED); - } - - @TargetApi(Build.VERSION_CODES.M) - protected void requestPermissions(final int request_code) { - if (!hasLocationPermissions()) { - requestPermissions( - new String[]{ - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION, - }, - request_code - ); - } - } - - @Override - public void onRequestPermissionsResult(final int requestCode, - @NonNull final String[] permissions, - @NonNull final int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - for (int i = 0; i < grantResults.length; i++) { - if (Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[i]) || - Manifest.permission.ACCESS_COARSE_LOCATION.equals(permissions[i])) { - if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { - requestLocationUpdates(); - } - } - } - } - - public SharedPreferences getPreferences() { - return PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - } - - protected boolean getBooleanPreference(String name, @BoolRes int res) { - return getPreferences().getBoolean(name, getResources().getBoolean(res)); - } - - protected boolean isLocationEnabled() { - try { - final int locationMode = Settings.Secure.getInt(getContentResolver(), Settings.Secure.LOCATION_MODE); - return locationMode != Settings.Secure.LOCATION_MODE_OFF; - } catch (final Settings.SettingNotFoundException e) { - return false; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/MagicCreateActivity.java b/src/main/java/eu/siacs/conversations/ui/MagicCreateActivity.java deleted file mode 100644 index 9d7ebef96..000000000 --- a/src/main/java/eu/siacs/conversations/ui/MagicCreateActivity.java +++ /dev/null @@ -1,282 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.net.Uri; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.View; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.CompoundButton; - -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.Toolbar; -import androidx.databinding.DataBindingUtil; - -import java.security.SecureRandom; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.ExecutionException; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ActivityMagicCreateBinding; -import eu.siacs.conversations.entities.Account; -//import eu.siacs.conversations.services.ProviderService; -import eu.siacs.conversations.utils.CryptoHelper; -//import eu.siacs.conversations.utils.InstallReferrerUtils; -import eu.siacs.conversations.xmpp.Jid; - -public class MagicCreateActivity extends XmppActivity implements TextWatcher, AdapterView.OnItemSelectedListener, CompoundButton.OnCheckedChangeListener { - - private boolean useOwnProvider = false; - private boolean registerFromUri = false; - public static final String EXTRA_DOMAIN = "domain"; - public static final String EXTRA_PRE_AUTH = "pre_auth"; - public static final String EXTRA_USERNAME = "username"; - public static final String EXTRA_REGISTER = "register"; - - private ActivityMagicCreateBinding binding; - private String domain; - private String username; - private String preAuth; - - @Override - protected void refreshUiReal() { - - } - - @Override - void onBackendConnected() { - - } - - @Override - public void onStart() { - super.onStart(); - final int theme = findTheme(); - if (this.mTheme != theme) { - recreate(); - } - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - final Intent data = getIntent(); - if (data != null) { - this.domain = data.getStringExtra(EXTRA_DOMAIN); - this.preAuth = data.getStringExtra(EXTRA_PRE_AUTH); - this.username = data.getStringExtra(EXTRA_USERNAME); - this.registerFromUri = data.getBooleanExtra(EXTRA_REGISTER, false); - } else { - this.domain = null; - this.preAuth = null; - this.username = null; - this.registerFromUri = false; - } - if (getResources().getBoolean(R.bool.portrait_only)) { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - } - super.onCreate(savedInstanceState); - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_magic_create); - // final List domains = ProviderService.getProviders(); - // Collections.sort(domains, String::compareToIgnoreCase); - // final ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_selectable_list_item, domains); - //try { - // if (new ProviderService().execute().get()) { - // adapter.notifyDataSetChanged(); - // } - // } catch (ExecutionException | InterruptedException e) { - // e.printStackTrace(); - // } - // int defaultServer = adapter.getPosition(Config.DOMAIN.getRandomServer()); - // if (registerFromUri && !useOwnProvider && (this.preAuth != null || domain != null)) { - // binding.server.setEnabled(false); - // binding.server.setVisibility(View.GONE); - // binding.useOwn.setEnabled(false); - // binding.useOwn.setChecked(true); - // binding.useOwn.setVisibility(View.GONE); - // binding.servertitle.setText(R.string.your_server); - // binding.yourserver.setVisibility(View.VISIBLE); - // binding.yourserver.setText(domain); - // } else { - // binding.yourserver.setVisibility(View.GONE); - // } - // binding.useOwn.setOnCheckedChangeListener(this); - // binding.server.setAdapter(adapter); - // binding.server.setSelection(defaultServer); - // binding.server.setOnItemSelectedListener(this); - // adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - setSupportActionBar((Toolbar) this.binding.toolbar); - configureActionBar(getSupportActionBar(), this.domain == null); - if (username != null && domain != null) { - // binding.title.setText(R.string.your_server_invitation); - // binding.instructions.setText(getString(R.string.magic_create_text_fixed, domain)); - // binding.username.setEnabled(false); - // binding.username.setText(this.username); - updateFullJidInformation(this.username); - } else if (domain != null) { - // binding.instructions.setText(getString(R.string.magic_create_text_on_x, domain)); - } - binding.createAccount.setOnClickListener(v -> { - Uri uri = Uri.parse("https://ocean.monocles.eu/apps/registration/"); // missing 'http://' will cause crashed - Intent intent = new Intent(Intent.ACTION_VIEW, uri); - startActivity(intent); - /* try { - final String username = binding.username.getText().toString(); - final boolean fixedUsername; - final Jid jid; - if (this.domain != null && this.username != null) { - fixedUsername = true; - jid = Jid.ofLocalAndDomainEscaped(this.username, this.domain); - } else if (this.domain != null) { - fixedUsername = false; - jid = Jid.ofLocalAndDomainEscaped(username, this.domain); - } else { - fixedUsername = false; - domain = updateDomain(); - jid = Jid.ofLocalAndDomainEscaped(username, domain); - } - if (!jid.getEscapedLocal().equals(jid.getLocal()) || (this.username == null && username.length() < 3)) { - binding.username.setError(getString(R.string.invalid_username)); - binding.username.requestFocus(); - } else { - binding.username.setError(null); - Account account = xmppConnectionService.findAccountByJid(jid); - String password = CryptoHelper.createPassword(new SecureRandom()); - if (account == null) { - account = new Account(jid, password); - account.setOption(Account.OPTION_REGISTER, true); - account.setOption(Account.OPTION_DISABLED, true); - account.setOption(Account.OPTION_MAGIC_CREATE, true); - account.setOption(Account.OPTION_FIXED_USERNAME, fixedUsername); - if (this.preAuth != null) { - account.setKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN, this.preAuth); - } - xmppConnectionService.createAccount(account); - } - Intent intent = new Intent(MagicCreateActivity.this, EditAccountActivity.class); - intent.putExtra("jid", account.getJid().asBareJid().toString()); - intent.putExtra("init", true); - intent.putExtra("existing", false); - intent.putExtra("useownprovider", useOwnProvider); - intent.putExtra("register", registerFromUri); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.create_account)); - builder.setCancelable(false); - StringBuilder messasge = new StringBuilder(); - messasge.append(getString(R.string.secure_password_generated)); - messasge.append("\n\n"); - messasge.append(getString(R.string.password)); - messasge.append(": "); - messasge.append(password); - messasge.append("\n\n"); - messasge.append(getString(R.string.change_password_in_next_step)); - builder.setMessage(messasge); - builder.setPositiveButton(getString(R.string.copy_to_clipboard), (dialogInterface, i) -> { - if (copyTextToClipboard(password, R.string.create_account)) { - StartConversationActivity.addInviteUri(intent, getIntent()); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - finish(); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - }); - builder.create().show(); - } - } catch (IllegalArgumentException e) { - binding.username.setError(getString(R.string.invalid_username)); - binding.username.requestFocus(); - }*/ - }); - // binding.username.addTextChangedListener(this); - } - - private String updateDomain() { - String getUpdatedDomain = null; - if (domain == null && !useOwnProvider) { - getUpdatedDomain = Config.MAGIC_CREATE_DOMAIN; - } - if (useOwnProvider) { - getUpdatedDomain = "your-domain.com"; - } - return getUpdatedDomain; - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - - } - - @Override - public void afterTextChanged(Editable s) { - updateFullJidInformation(s.toString()); - } - - @Override - public void onItemSelected(AdapterView adapterView, View view, int i, long l) { - // updateFullJidInformation(binding.username.getText().toString()); - } - - @Override - public void onNothingSelected(AdapterView adapterView) { - // updateFullJidInformation(binding.username.getText().toString()); - } - - private void updateFullJidInformation(String username) { - if (useOwnProvider && !registerFromUri) { - this.domain = updateDomain(); - } else if (!registerFromUri) { - // this.domain = binding.server.getSelectedItem().toString(); - } - if (username.trim().isEmpty()) { - // binding.fullJid.setVisibility(View.INVISIBLE); - } else { - try { - // binding.fullJid.setVisibility(View.VISIBLE); - final Jid jid; - if (this.domain == null) { - jid = Jid.ofLocalAndDomainEscaped(username, Config.MAGIC_CREATE_DOMAIN); - } else { - jid = Jid.ofLocalAndDomainEscaped(username, this.domain); - } - // binding.fullJid.setText(getString(R.string.your_full_jid_will_be, jid.toEscapedString())); - } catch (IllegalArgumentException e) { - // binding.fullJid.setVisibility(View.INVISIBLE); - } - - } - } - - @Override - public void onDestroy() { - // InstallReferrerUtils.markInstallReferrerExecuted(this); - super.onDestroy(); - } - - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - // if (binding.useOwn.isChecked()) { - // binding.server.setEnabled(false); - // binding.fullJid.setVisibility(View.GONE); - useOwnProvider = true; - - // } else { - // binding.server.setEnabled(true); - // binding.fullJid.setVisibility(View.VISIBLE); - // useOwnProvider = false; - // } - registerFromUri = false; - // updateFullJidInformation(binding.username.getText().toString()); - } - -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java deleted file mode 100644 index 5e41b10cd..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java +++ /dev/null @@ -1,429 +0,0 @@ -package eu.siacs.conversations.ui; - -import static eu.siacs.conversations.utils.PermissionUtils.allGranted; -import static eu.siacs.conversations.utils.PermissionUtils.readGranted; - -import android.content.ActivityNotFoundException; -import android.content.Intent; -import android.os.Bundle; -import android.security.KeyChain; -import android.security.KeyChainAliasCallback; -import android.util.Pair; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.AdapterView.AdapterContextMenuInfo; -import android.widget.ListView; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; - -import org.openintents.openpgp.util.OpenPgpApi; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; -import eu.siacs.conversations.ui.adapter.AccountAdapter; -import eu.siacs.conversations.utils.MenuDoubleTabUtil; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.XmppConnection; -import me.drakeet.support.toast.ToastCompat; - -public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate, KeyChainAliasCallback, XmppConnectionService.OnAccountCreated, AccountAdapter.OnTglAccountState { - - private final String STATE_SELECTED_ACCOUNT = "selected_account"; - - private static final int REQUEST_IMPORT_BACKUP = 0x63fb; - - protected Account selectedAccount = null; - protected Jid selectedAccountJid = null; - - protected final List accountList = new ArrayList<>(); - protected ListView accountListView; - protected AccountAdapter mAccountAdapter; - protected AtomicBoolean mInvokedAddAccount = new AtomicBoolean(false); - - protected Pair mPostponedActivityResult = null; - - @Override - public void onAccountUpdate() { - refreshUi(); - } - - @Override - protected void refreshUiReal() { - synchronized (this.accountList) { - accountList.clear(); - accountList.addAll(xmppConnectionService.getAccounts()); - } - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setHomeButtonEnabled(this.accountList.size() > 0); - actionBar.setDisplayHomeAsUpEnabled(this.accountList.size() > 0); - } - invalidateOptionsMenu(); - mAccountAdapter.notifyDataSetChanged(); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_manage_accounts); - setSupportActionBar(findViewById(R.id.toolbar)); - configureActionBar(getSupportActionBar()); - if (savedInstanceState != null) { - String jid = savedInstanceState.getString(STATE_SELECTED_ACCOUNT); - if (jid != null) { - try { - this.selectedAccountJid = Jid.ofEscaped(jid); - } catch (IllegalArgumentException e) { - this.selectedAccountJid = null; - } - } - } - - accountListView = findViewById(R.id.account_list); - this.mAccountAdapter = new AccountAdapter(this, accountList); - accountListView.setAdapter(this.mAccountAdapter); - accountListView.setOnItemClickListener((arg0, view, position, arg3) -> switchToAccount(accountList.get(position))); - registerForContextMenu(accountListView); - } - - @Override - protected void onStart() { - super.onStart(); - final int theme = findTheme(); - if (this.mTheme != theme) { - recreate(); - } - } - - @Override - public void onSaveInstanceState(final Bundle savedInstanceState) { - if (selectedAccount != null) { - savedInstanceState.putString(STATE_SELECTED_ACCOUNT, selectedAccount.getJid().asBareJid().toEscapedString()); - } - super.onSaveInstanceState(savedInstanceState); - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - ManageAccountActivity.this.getMenuInflater().inflate( - R.menu.manageaccounts_context, menu); - AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; - this.selectedAccount = accountList.get(acmi.position); - if (this.selectedAccount.isEnabled()) { - menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(Config.supportOpenPgp()); - } else { - menu.findItem(R.id.mgmt_account_reconnect).setVisible(false); - menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(false); - menu.findItem(R.id.mgmt_account_publish_avatar).setVisible(false); - } - menu.setHeaderTitle(this.selectedAccount.getJid().asBareJid().toEscapedString()); - } - - @Override - void onBackendConnected() { - if (selectedAccountJid != null) { - this.selectedAccount = xmppConnectionService.findAccountByJid(selectedAccountJid); - } - refreshUiReal(); - if (this.mPostponedActivityResult != null) { - this.onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second); - } - if (Config.X509_VERIFICATION && this.accountList.size() == 0) { - if (mInvokedAddAccount.compareAndSet(false, true)) { - addAccountFromKey(); - } - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.manageaccounts, menu); - MenuItem addAccount = menu.findItem(R.id.action_add_account); - MenuItem addAccountWithCertificate = menu.findItem(R.id.action_add_account_with_cert); - - if (Config.X509_VERIFICATION) { - addAccount.setVisible(false); - addAccountWithCertificate.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - } - - return true; - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.mgmt_account_publish_avatar: - publishAvatar(selectedAccount); - return true; - case R.id.mgmt_account_reconnect: - disableAccount(selectedAccount); - enableAccount(selectedAccount); - return true; - case R.id.mgmt_account_delete: - deleteAccount(selectedAccount); - return true; - case R.id.mgmt_account_announce_pgp: - publishOpenPGPPublicKey(selectedAccount); - return true; - default: - return super.onContextItemSelected(item); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (MenuDoubleTabUtil.shouldIgnoreTap()) { - return false; - } - switch (item.getItemId()) { - case R.id.action_add_account: - startActivity(new Intent(this, EditAccountActivity.class)); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - break; - case R.id.action_import_backup: - if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) { - startActivity(new Intent(this, ImportBackupActivity.class)); - } - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - break; - case R.id.action_add_account_with_cert: - addAccountFromKey(); - break; - default: - break; - } - return super.onOptionsItemSelected(item); - } - - @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) { - case REQUEST_IMPORT_BACKUP: - startActivity(new Intent(this, ImportBackupActivity.class)); - break; - } - } else { - ToastCompat.makeText(this, R.string.no_storage_permission, ToastCompat.LENGTH_SHORT).show(); - } - } - if (readGranted(grantResults, permissions)) { - if (xmppConnectionService != null) { - xmppConnectionService.restartFileObserver(); - } - } - } - - @Override - public boolean onNavigateUp() { - if (xmppConnectionService.getConversations().size() == 0) { - Intent contactsIntent = new Intent(this, - StartConversationActivity.class); - contactsIntent.setFlags( - // if activity exists in stack, pop the stack and go back to it - Intent.FLAG_ACTIVITY_CLEAR_TOP | - // otherwise, make a new task for it - Intent.FLAG_ACTIVITY_NEW_TASK | - // don't use the new activity animation; finish - // animation runs instead - Intent.FLAG_ACTIVITY_NO_ANIMATION); - startActivity(contactsIntent); - finish(); - return true; - } else { - return super.onNavigateUp(); - } - } - - @Override - public void onClickTglAccountState(Account account, boolean enable) { - if (enable) { - enableAccount(account); - } else { - disableAccount(account); - } - } - - private void addAccountFromKey() { - try { - KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null); - } catch (ActivityNotFoundException e) { - ToastCompat.makeText(this, R.string.device_does_not_support_certificates, ToastCompat.LENGTH_LONG).show(); - } - } - - private void publishAvatar(Account account) { - Intent intent = new Intent(getApplicationContext(), - PublishProfilePictureActivity.class); - intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - - private void disableAllAccounts() { - List list = new ArrayList<>(); - synchronized (this.accountList) { - for (Account account : this.accountList) { - if (account.isEnabled()) { - list.add(account); - } - } - } - for (Account account : list) { - disableAccount(account); - } - } - - private boolean accountsLeftToDisable() { - synchronized (this.accountList) { - for (Account account : this.accountList) { - if (account.isEnabled()) { - return true; - } - } - return false; - } - } - - private boolean accountsLeftToEnable() { - synchronized (this.accountList) { - for (Account account : this.accountList) { - if (!account.isEnabled()) { - return true; - } - } - return false; - } - } - - private void enableAllAccounts() { - List list = new ArrayList<>(); - synchronized (this.accountList) { - for (Account account : this.accountList) { - if (!account.isEnabled()) { - list.add(account); - } - } - } - for (Account account : list) { - enableAccount(account); - } - } - - private void disableAccount(Account account) { - account.setOption(Account.OPTION_DISABLED, true); - if (!xmppConnectionService.updateAccount(account)) { - ToastCompat.makeText(this, R.string.unable_to_update_account, ToastCompat.LENGTH_SHORT).show(); - } - } - - private void enableAccount(Account account) { - account.setOption(Account.OPTION_DISABLED, false); - final XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - connection.resetEverything(); - } - if (!xmppConnectionService.updateAccount(account)) { - ToastCompat.makeText(this, R.string.unable_to_update_account, ToastCompat.LENGTH_SHORT).show(); - } - } - - private void publishOpenPGPPublicKey(Account account) { - if (ManageAccountActivity.this.hasPgp()) { - announcePgp(selectedAccount, null, null, onOpenPGPKeyPublished); - } else { - this.showInstallPgpDialog(); - } - } - - private void deleteAccount(final Account account) { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.mgmt_account_are_you_sure)); - builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setMessage(getString(R.string.mgmt_account_delete_confirm_message)); - builder.setPositiveButton(getString(R.string.delete), - (dialog, which) -> { - xmppConnectionService.deleteAccount(account); - selectedAccount = null; - if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) { - WelcomeActivity.launch(this); - } - }); - - builder.setNegativeButton(getString(R.string.delete_from_server), - (dialog, which) -> { - if (account.isOnlineAndConnected()) { - xmppConnectionService.deleteAccountFromServer(account); - selectedAccount = null; - if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) { - WelcomeActivity.launch(this); - } - } else { - informUser(R.string.go_online_to_delete); - } - }); - - builder.setNeutralButton(getString(R.string.cancel), null); - builder.create().show(); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (resultCode == RESULT_OK) { - if (xmppConnectionServiceBound) { - if (requestCode == REQUEST_CHOOSE_PGP_ID) { - if (data.getExtras().containsKey(OpenPgpApi.EXTRA_SIGN_KEY_ID)) { - selectedAccount.setPgpSignId(data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID)); - announcePgp(selectedAccount, null, null, onOpenPGPKeyPublished); - } else { - choosePgpSignId(selectedAccount); - } - } else if (requestCode == REQUEST_ANNOUNCE_PGP) { - announcePgp(selectedAccount, null, data, onOpenPGPKeyPublished); - } - this.mPostponedActivityResult = null; - } else { - this.mPostponedActivityResult = new Pair<>(requestCode, data); - } - } - } - - @Override - public void alias(final String alias) { - if (alias != null) { - xmppConnectionService.createAccountFromKey(alias, this); - } - } - - @Override - public void onAccountCreated(final Account account) { - final Intent intent = new Intent(this, EditAccountActivity.class); - intent.putExtra("jid", account.getJid().asBareJid().toString()); - intent.putExtra("init", true); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - - @Override - public void informUser(final int r) { - runOnUiThread(() -> ToastCompat.makeText(ManageAccountActivity.this, r, ToastCompat.LENGTH_LONG).show()); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/MediaBrowserActivity.java b/src/main/java/eu/siacs/conversations/ui/MediaBrowserActivity.java deleted file mode 100644 index 6cbf4f527..000000000 --- a/src/main/java/eu/siacs/conversations/ui/MediaBrowserActivity.java +++ /dev/null @@ -1,206 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; - -import androidx.appcompat.widget.Toolbar; -import androidx.databinding.DataBindingUtil; - -import java.util.ArrayList; -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ActivityMediaBrowserBinding; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.ui.adapter.MediaAdapter; -import eu.siacs.conversations.ui.interfaces.OnMediaLoaded; -import eu.siacs.conversations.ui.util.Attachment; -import eu.siacs.conversations.ui.util.GridManager; -import eu.siacs.conversations.utils.MenuDoubleTabUtil; -import eu.siacs.conversations.xmpp.Jid; - - -public class MediaBrowserActivity extends XmppActivity implements OnMediaLoaded { - - private ActivityMediaBrowserBinding binding; - private MediaAdapter mMediaAdapter; - private boolean OnlyImagesVideos = false; - ArrayList allAttachments = new ArrayList<>(); - ArrayList filteredAttachments = new ArrayList<>(); - private String mSavedInstanceAccount; - private String mSavedInstanceJid; - private String account; - private String jid; - - @Override - protected void onStart() { - super.onStart(); - filter(OnlyImagesVideos); - invalidateOptionsMenu(); - refreshUiReal(); - } - - public static void launch(Context context, Contact contact) { - launch(context, contact.getAccount(), contact.getJid().asBareJid().toEscapedString()); - } - - public static void launch(Context context, Conversation conversation) { - launch(context, conversation.getAccount(), conversation.getJid().asBareJid().toEscapedString()); - } - - private static void launch(Context context, Account account, String jid) { - final Intent intent = new Intent(context, MediaBrowserActivity.class); - intent.putExtra("account", account.getUuid()); - intent.putExtra("jid", jid); - context.startActivity(intent); - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - this.mSavedInstanceAccount = savedInstanceState.getString("account"); - this.mSavedInstanceJid = savedInstanceState.getString("jid"); - } - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_media_browser); - setSupportActionBar((Toolbar) binding.toolbar); - configureActionBar(getSupportActionBar()); - mMediaAdapter = new MediaAdapter(this, R.dimen.media_size); - this.binding.media.setAdapter(mMediaAdapter); - GridManager.setupLayoutManager(this, this.binding.media, R.dimen.browser_media_size); - this.binding.noMedia.setVisibility(View.GONE); - this.binding.progressbar.setVisibility(View.VISIBLE); - this.OnlyImagesVideos = getPreferences().getBoolean("show_videos_images_only", this.getResources().getBoolean(R.bool.show_videos_images_only)); - } - - @Override - public void onSaveInstanceState(final Bundle savedInstanceState) { - savedInstanceState.putString("account", account); - savedInstanceState.putString("jid", jid); - super.onSaveInstanceState(savedInstanceState); - } - - @Override - public void refreshUiReal() { - mMediaAdapter.notifyDataSetChanged(); - } - - @Override - public boolean onPrepareOptionsMenu(final Menu menu) { - MenuItem showImagesVideosOnly = menu.findItem(R.id.show_videos_images_only); - showImagesVideosOnly.setChecked(OnlyImagesVideos); - return true; - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.media_browser, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(final MenuItem menuItem) { - if (MenuDoubleTabUtil.shouldIgnoreTap()) { - return false; - } - switch (menuItem.getItemId()) { - case android.R.id.home: - finish(); - break; - case R.id.show_videos_images_only: - this.OnlyImagesVideos = !menuItem.isChecked(); - menuItem.setChecked(this.OnlyImagesVideos); - getPreferences().edit().putBoolean("show_videos_images_only", OnlyImagesVideos).apply(); - filter(OnlyImagesVideos); - invalidateOptionsMenu(); - refreshUiReal(); - break; - } - return super.onOptionsItemSelected(menuItem); - } - - @Override - void onBackendConnected() { - final Intent intent = getIntent(); - if (mSavedInstanceAccount != null) { - try { - account = mSavedInstanceAccount; - } catch (Exception e) { - account = intent == null ? null : intent.getStringExtra("account"); - } - } else { - account = intent == null ? null : intent.getStringExtra("account"); - } - if (mSavedInstanceJid != null) { - try { - jid = mSavedInstanceJid; - } catch (Exception e) { - jid = intent == null ? null : intent.getStringExtra("jid"); - } - } else { - jid = intent == null ? null : intent.getStringExtra("jid"); - } - if (account != null && jid != null) { - xmppConnectionService.getAttachments(account, Jid.ofEscaped(jid), 0, this); - } - } - - @Override - public void onMediaLoaded(List attachments) { - allAttachments.clear(); - allAttachments.addAll(attachments); - runOnUiThread(() -> { - filter(OnlyImagesVideos); - }); - } - - private void loadAttachments(List attachments) { - if (attachments.size() > 0) { - if (mMediaAdapter.getItemCount() != attachments.size()) { - mMediaAdapter.setAttachments(attachments); - } - this.binding.noMedia.setVisibility(View.GONE); - this.binding.progressbar.setVisibility(View.GONE); - } else { - this.binding.noMedia.setVisibility(View.VISIBLE); - this.binding.progressbar.setVisibility(View.GONE); - } - } - - protected void filter(boolean needle) { - if (xmppConnectionServiceBound) { - filterAttachments(needle); - } - } - - @Override - public void onResume() { - super.onResume(); - filter(OnlyImagesVideos); - } - - protected void filterAttachments(boolean needle) { - if (allAttachments.size() > 0) { - if (needle) { - final ArrayList attachments = new ArrayList<>(allAttachments); - filteredAttachments.clear(); - for (Attachment attachment : attachments) { - if (attachment.getMime() != null && (attachment.getMime().startsWith("image/") || attachment.getMime().startsWith("video/"))) { - filteredAttachments.add(attachment); - } - } - loadAttachments(filteredAttachments); - } else { - loadAttachments(allAttachments); - } - } else { - loadAttachments(allAttachments); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/MediaViewerActivity.java b/src/main/java/eu/siacs/conversations/ui/MediaViewerActivity.java deleted file mode 100644 index 897fe3193..000000000 --- a/src/main/java/eu/siacs/conversations/ui/MediaViewerActivity.java +++ /dev/null @@ -1,608 +0,0 @@ -package eu.siacs.conversations.ui; - -import static eu.siacs.conversations.utils.StorageHelper.getConversationsDirectory; - -import android.app.PictureInPictureParams; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.ColorStateList; -import android.content.res.Configuration; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.media.AudioManager; -import android.media.MediaMetadataRetriever; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.support.v4.media.session.MediaSessionCompat; -import android.util.Log; -import android.util.Rational; -import android.view.GestureDetector; -import android.view.MotionEvent; -import android.view.View; -import android.view.WindowManager; -import android.webkit.MimeTypeMap; - -import androidx.annotation.RequiresApi; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; -import androidx.core.content.ContextCompat; -import androidx.databinding.DataBindingUtil; - -import com.davemorrissey.labs.subscaleview.ImageSource; -import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.MediaItem; -import com.google.android.exoplayer2.PlaybackException; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; -import com.leinardi.android.speeddial.SpeedDialActionItem; - -import java.io.File; -import java.io.InputStream; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ActivityMediaViewerBinding; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.ui.util.Rationals; -import eu.siacs.conversations.utils.Compatibility; -import eu.siacs.conversations.utils.MimeUtils; -import me.drakeet.support.toast.ToastCompat; - -public class MediaViewerActivity extends XmppActivity implements AudioManager.OnAudioFocusChangeListener { - - Integer oldOrientation; - ExoPlayer player; - Uri mFileUri; - File mFile; - int height = 0; - int width = 0; - Rational aspect; - int rotation = 0; - boolean isImage = false; - boolean isVideo = false; - private ActivityMediaViewerBinding binding; - private GestureDetector gestureDetector; - - public static String getMimeType(String path) { - try { - String type = null; - String extension = path.substring(path.lastIndexOf(".") + 1, path.length()); - if (extension != null) { - type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - } - return type; - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_media_viewer); - this.mTheme = findTheme(); - setTheme(this.mTheme); - gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() { - - @Override - public boolean onDown(MotionEvent e) { - if (isImage) { - if (binding.speedDial.isShown()) { - hideFAB(); - } else { - showFAB(); - } - } - return super.onDown(e); - } - }); - - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null && actionBar.isShowing()) { - actionBar.hide(); - } - - oldOrientation = getRequestedOrientation(); - - WindowManager.LayoutParams layout = getWindow().getAttributes(); - if (useMaxBrightness()) { - layout.screenBrightness = 1; - } - getWindow().setAttributes(layout); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - //binding.speedDial.inflate(R.menu.media_viewer); - } - - private void share() { - Intent share = new Intent(Intent.ACTION_SEND); - share.setType(getMimeType(mFile.toString())); - share.putExtra(Intent.EXTRA_STREAM, FileBackend.getUriForFile(this, mFile)); - try { - startActivity(Intent.createChooser(share, getText(R.string.share_with))); - } catch (ActivityNotFoundException e) { - //This should happen only on faulty androids because normally chooser is always available - ToastCompat.makeText(this, R.string.no_application_found_to_open_file, ToastCompat.LENGTH_SHORT).show(); - } - } - - private void deleteFile() { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setNegativeButton(R.string.cancel, null); - builder.setTitle(R.string.delete_file_dialog); - builder.setMessage(R.string.delete_file_dialog_msg); - builder.setPositiveButton(R.string.confirm, (dialog, which) -> { - if (this.xmppConnectionService.getFileBackend().deleteFile(mFile)) { - finish(); - } - }); - builder.create().show(); - } - - private void open() { - Uri uri; - try { - uri = FileBackend.getUriForFile(this, mFile); - } catch (SecurityException e) { - Log.d(Config.LOGTAG, "No permission to access " + mFile.getAbsolutePath(), e); - ToastCompat.makeText(this, this.getString(R.string.no_permission_to_access_x, mFile.getAbsolutePath()), ToastCompat.LENGTH_SHORT).show(); - return; - } - String mime = MimeUtils.guessMimeTypeFromUri(this, uri); - Intent openIntent = new Intent(Intent.ACTION_VIEW); - openIntent.setDataAndType(uri, mime); - openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - PackageManager manager = this.getPackageManager(); - List info = manager.queryIntentActivities(openIntent, 0); - if (info.size() == 0) { - openIntent.setDataAndType(uri, "*/*"); - } - if (player != null && isVideo) { - openIntent.putExtra("position", player.getCurrentPosition()); - } - try { - this.startActivity(openIntent); - finish(); - } catch (ActivityNotFoundException e) { - ToastCompat.makeText(this, R.string.no_application_found_to_open_file, ToastCompat.LENGTH_SHORT).show(); - } - } - - @Override - protected void refreshUiReal() { - - } - - @Override - protected void onStart() { - super.onStart(); - Intent intent = getIntent(); - if (intent != null) { - if (intent.hasExtra("image")) { - mFileUri = intent.getParcelableExtra("image"); - mFile = new File(mFileUri.getPath()); - if (mFileUri != null && mFile.exists() && mFile.length() > 0) { - try { - isImage = true; - DisplayImage(mFile, mFileUri); - } catch (Exception e) { - isImage = false; - Log.d(Config.LOGTAG, "Illegal exeption :" + e); - ToastCompat.makeText(MediaViewerActivity.this, getString(R.string.error_file_corrupt), ToastCompat.LENGTH_SHORT).show(); - finish(); - } - } else { - ToastCompat.makeText(MediaViewerActivity.this, getString(R.string.file_deleted), ToastCompat.LENGTH_SHORT).show(); - } - } else if (intent.hasExtra("video")) { - mFileUri = intent.getParcelableExtra("video"); - mFile = new File(mFileUri.getPath()); - if (mFileUri != null && mFile.exists() && mFile.length() > 0) { - try { - isVideo = true; - DisplayVideo(mFileUri); - } catch (Exception e) { - isVideo = false; - Log.d(Config.LOGTAG, "Illegal exeption :" + e); - ToastCompat.makeText(MediaViewerActivity.this, getString(R.string.error_file_corrupt), ToastCompat.LENGTH_SHORT).show(); - finish(); - } - } else { - ToastCompat.makeText(MediaViewerActivity.this, getString(R.string.file_deleted), ToastCompat.LENGTH_SHORT).show(); - } - } - } - if (isDeletableFile(mFile)) { - binding.speedDial.addActionItem(new SpeedDialActionItem.Builder(R.id.action_delete, R.drawable.ic_delete_white_24dp) - .setLabel(R.string.action_delete) - .setFabImageTintColor(ContextCompat.getColor(this, R.color.white)) - .create() - ); - } - binding.speedDial.addActionItem(new SpeedDialActionItem.Builder(R.id.action_open, R.drawable.ic_open_in_new_white_24dp) - .setLabel(R.string.open_with) - .setFabImageTintColor(ContextCompat.getColor(this, R.color.white)) - .create() - ); - binding.speedDial.addActionItem(new SpeedDialActionItem.Builder(R.id.action_share, R.drawable.ic_share_white_24dp) - .setLabel(R.string.share) - .setFabImageTintColor(ContextCompat.getColor(this, R.color.white)) - .create() - ); - - if (isDeletableFile(mFile)) { - binding.speedDial.setOnActionSelectedListener(actionItem -> { - switch (actionItem.getId()) { - case R.id.action_share: - share(); - break; - case R.id.action_open: - open(); - break; - case R.id.action_delete: - deleteFile(); - break; - default: - return false; - } - return false; - }); - } else { - binding.speedDial.setOnActionSelectedListener(actionItem -> { - switch (actionItem.getId()) { - case R.id.action_share: - share(); - break; - case R.id.action_open: - open(); - break; - default: - return false; - } - return false; - }); - } - binding.speedDial.getMainFab().setSupportImageTintList(ColorStateList.valueOf(getResources().getColor(R.color.realwhite))); - } - - private void DisplayImage(final File file, final Uri uri) { - final boolean gif = "image/gif".equalsIgnoreCase(getMimeType(file.toString())); - final boolean bmp = "image/bmp".equalsIgnoreCase(getMimeType(file.toString())) || "image/x-ms-bmp".equalsIgnoreCase(getMimeType(file.toString())); - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(new File(file.getPath()).getAbsolutePath(), options); - height = options.outHeight; - width = options.outWidth; - aspect = new Rational(width, height); - rotation = getRotation(Uri.parse("file://" + file.getAbsolutePath())); - Log.d(Config.LOGTAG, "Image height: " + height + ", width: " + width + ", rotation: " + rotation + " aspect: " + aspect); - if (useAutoRotateScreen()) { - rotateScreen(width, height, rotation); - } - try { - if (gif) { - binding.messageGifView.setVisibility(View.VISIBLE); - binding.messageGifView.setImageURI(uri); - binding.messageGifView.setOnTouchListener((view, motionEvent) -> gestureDetector.onTouchEvent(motionEvent)); - } else { - binding.messageImageView.setVisibility(View.VISIBLE); - binding.messageImageView.setImage(ImageSource.uri(uri).tiling(!bmp)); - binding.messageImageView.setOrientation(SubsamplingScaleImageView.ORIENTATION_USE_EXIF); - binding.messageImageView.setOnTouchListener((view, motionEvent) -> gestureDetector.onTouchEvent(motionEvent)); - } - } catch (Exception e) { - ToastCompat.makeText(this, getString(R.string.error_file_corrupt), ToastCompat.LENGTH_LONG).show(); - e.printStackTrace(); - } - } - - private void DisplayVideo(final Uri uri) { - try { - MediaMetadataRetriever retriever = new MediaMetadataRetriever(); - retriever.setDataSource(uri.getPath()); - Bitmap bitmap = null; - try { - bitmap = retriever.getFrameAtTime(0); - height = bitmap.getHeight(); - width = bitmap.getWidth(); - } catch (Exception e) { - height = Integer.valueOf(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)); - width = Integer.valueOf(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)); - } finally { - if (bitmap != null) { - bitmap.recycle(); - } - } - try { - rotation = Integer.valueOf(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)); - } catch (Exception e) { - rotation = 0; - } - aspect = new Rational(width, height); - Log.d(Config.LOGTAG, "Video height: " + height + ", width: " + width + ", rotation: " + rotation + ", aspect: " + aspect); - if (useAutoRotateScreen()) { - rotateScreen(width, height, rotation); - } - binding.messageVideoView.setVisibility(View.VISIBLE); - player = new ExoPlayer.Builder(this).build(); - player.addListener(new Player.Listener() { - @Override - public void onIsPlayingChanged(boolean isPlaying) { - Player.Listener.super.onIsPlayingChanged(isPlaying); - if (isPlaying) { - hideFAB(); - } else { - if (Compatibility.runsTwentyFour() && isInPictureInPictureMode()) { - hideFAB(); - } else { - showFAB(); - } - } - } - - @Override - public void onPlayerError(PlaybackException error) { - open(); - } - }); - player.setRepeatMode(Player.REPEAT_MODE_OFF); - binding.messageVideoView.setPlayer(player); - player.setMediaItem(MediaItem.fromUri(uri)); - player.prepare(); - player.setPlayWhenReady(true); - final MediaSessionCompat session = new MediaSessionCompat(this, getPackageName()); - final MediaSessionConnector connector = new MediaSessionConnector(session); - connector.setPlayer(player); - session.setActive(true); - requestAudioFocus(); - setVolumeControlStream(AudioManager.STREAM_MUSIC); -// binding.messageVideoView.setOnTouchListener((view, motionEvent) -> gestureDetector.onTouchEvent(motionEvent)); - } catch (Exception e) { - e.printStackTrace(); - open(); - } - } - - @RequiresApi(api = Build.VERSION_CODES.N) - private void PIPVideo() { - try { - binding.messageVideoView.hideController(); - binding.speedDial.setVisibility(View.GONE); - if (supportsPIP()) { - if (Compatibility.runsTwentySix()) { - final Rational rational = new Rational(width, height); - final Rational clippedRational = Rationals.clip(rational); - final PictureInPictureParams params = new PictureInPictureParams.Builder() - .setAspectRatio(clippedRational) - .build(); - this.enterPictureInPictureMode(params); - } else { - this.enterPictureInPictureMode(); - } - } - } catch (final IllegalStateException e) { - // this sometimes happens on Samsung phones (possibly when Knox is enabled) - Log.w(Config.LOGTAG, "unable to enter picture in picture mode", e); - } - } - - @Override - public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) { - if (isInPictureInPictureMode) { - startPlayer(); - hideFAB(); - } else { - showFAB(); - } - } - - private void releaseAudiFocus() { - AudioManager am = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); - if (am != null) { - am.abandonAudioFocus(this); - } - } - - private void requestAudioFocus() { - AudioManager am = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); - if (am != null) { - am.requestAudioFocus(this, - AudioManager.STREAM_MUSIC, - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); - } - } - - @Override - public void onBackPressed() { - if (isVideo && isPlaying() && supportsPIP()) { - PIPVideo(); - } else { - super.onBackPressed(); - } - } - - @RequiresApi(api = Build.VERSION_CODES.N) - @Override - protected void onUserLeaveHint() { - super.onUserLeaveHint(); - if (isVideo) { - PIPVideo(); - } - } - - private int getRotation(Uri image) { - try (final InputStream is = this.getContentResolver().openInputStream(image)) { - return is == null ? 0 : FileBackend.getRotation(is); - } catch (final Exception e) { - return 0; - } - } - - private void rotateScreen(final int width, final int height, final int rotation) { - if (width > height) { - if (rotation == 0 || rotation == 180) { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); - } else { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); - } - } else { - if (rotation == 90 || rotation == 270) { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); - } else { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); - } - } - } - - private void pausePlayer() { - if (player != null && isVideo && isPlaying()) { - player.setPlayWhenReady(false); - player.getPlaybackState(); - if (Compatibility.runsTwentyFour() && isInPictureInPictureMode()) { - hideFAB(); - } else { - showFAB(); - } - } - } - - private void startPlayer() { - if (player != null && isVideo && !isPlaying()) { - player.setPlayWhenReady(true); - player.getPlaybackState(); - hideFAB(); - } - } - - private void stopPlayer() { - if (player != null && isVideo) { - if (supportsPIP()) { - finishAndRemoveTask(); - } - if (isPlaying()) { - player.stop(); - } - player.release(); - if (Compatibility.runsTwentyFour() && isInPictureInPictureMode()) { - hideFAB(); - } else { - showFAB(); - } - } - } - - private boolean isPlaying() { - return player != null - && player.getPlaybackState() != Player.STATE_ENDED - && player.getPlaybackState() != Player.STATE_IDLE - && player.getPlayWhenReady(); - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - } - - @Override - public void onResume() { - WindowManager.LayoutParams layout = getWindow().getAttributes(); - if (useMaxBrightness()) { - layout.screenBrightness = 1; - } - getWindow().setAttributes(layout); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - if (!isPlaying()) { - showFAB(); - } else { - hideFAB(); - } - super.onResume(); - } - - @Override - public void onPause() { - if (Compatibility.runsTwentyFour() && isInPictureInPictureMode()) { - startPlayer(); - } else { - pausePlayer(); - } - WindowManager.LayoutParams layout = getWindow().getAttributes(); - if (useMaxBrightness()) { - layout.screenBrightness = -1; - } - getWindow().setAttributes(layout); - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - setRequestedOrientation(oldOrientation); - super.onPause(); - } - - @Override - public void onStop() { - stopPlayer(); - releaseAudiFocus(); - WindowManager.LayoutParams layout = getWindow().getAttributes(); - if (useMaxBrightness()) { - layout.screenBrightness = -1; - } - getWindow().setAttributes(layout); - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - setRequestedOrientation(oldOrientation); - super.onStop(); - } - - @Override - void onBackendConnected() { - - } - - public boolean useMaxBrightness() { - return getPreferences().getBoolean("use_max_brightness", getResources().getBoolean(R.bool.use_max_brightness)); - } - - public boolean useAutoRotateScreen() { - return getPreferences().getBoolean("use_auto_rotate", getResources().getBoolean(R.bool.auto_rotate)); - } - - public SharedPreferences getPreferences() { - return PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - } - - @Override - public void onAudioFocusChange(int focusChange) { - if (focusChange == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - Log.i(Config.LOGTAG, "Audio focus granted."); - } else if (focusChange == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { - Log.i(Config.LOGTAG, "Audio focus failed."); - } - } - - private boolean isDeletableFile(File file) { - return (file == null || !file.toString().startsWith("/") || file.toString().contains(getConversationsDirectory(this, "null").getAbsolutePath())); - } - - private void showFAB() { - binding.speedDial.show(); - } - - private void hideFAB() { - binding.speedDial.hide(); - } - - private boolean supportsPIP() { - if (Compatibility.runsTwentyFour()) { - return this.getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE); - } else { - return false; - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/MemorizingActivity.java b/src/main/java/eu/siacs/conversations/ui/MemorizingActivity.java deleted file mode 100644 index 6dfc505a8..000000000 --- a/src/main/java/eu/siacs/conversations/ui/MemorizingActivity.java +++ /dev/null @@ -1,111 +0,0 @@ -/* MemorizingTrustManager - a TrustManager which asks the user about invalid - * certificates and memorizes their decision. - * - * Copyright (c) 2010 Georg Lukas - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package eu.siacs.conversations.ui; - -import android.content.DialogInterface; -import android.content.DialogInterface.OnCancelListener; -import android.content.DialogInterface.OnClickListener; -import android.content.Intent; -import android.os.Bundle; - -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; - -import java.util.logging.Level; -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.utils.ThemeHelper; - -public class MemorizingActivity extends AppCompatActivity implements OnClickListener,OnCancelListener { - - private final static Logger LOGGER = Logger.getLogger(MemorizingActivity.class.getName()); - - int decisionId; - - AlertDialog dialog; - - @Override - public void onCreate(Bundle savedInstanceState) { - LOGGER.log(Level.FINE, "onCreate"); - setTheme(ThemeHelper.find(this)); - super.onCreate(savedInstanceState); - getLayoutInflater().inflate(R.layout.toolbar, findViewById(android.R.id.content)); - setSupportActionBar(findViewById(R.id.toolbar)); - } - - @Override - public void onResume() { - super.onResume(); - 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); - String cert = i.getStringExtra(MemorizingTrustManager.DECISION_INTENT_CERT); - LOGGER.log(Level.FINE, "onResume with " + i.getExtras() + " decId=" + decisionId + " data: " + i.getData()); - dialog = new AlertDialog.Builder(this).setTitle(titleId) - .setMessage(cert) - .setPositiveButton(R.string.always, this) - .setNeutralButton(R.string.once, this) - .setNegativeButton(R.string.cancel, this) - .setOnCancelListener(this) - .create(); - dialog.show(); - } - - @Override - protected void onPause() { - if (dialog.isShowing()) - dialog.dismiss(); - super.onPause(); - } - - void sendDecision(int decision) { - LOGGER.log(Level.FINE, "Sending decision: " + decision); - MemorizingTrustManager.interactResult(decisionId, decision); - finish(); - } - - // react on AlertDialog button press - public void onClick(DialogInterface dialog, int btnId) { - int decision; - dialog.dismiss(); - switch (btnId) { - case DialogInterface.BUTTON_POSITIVE: - decision = MTMDecision.DECISION_ALWAYS; - break; - case DialogInterface.BUTTON_NEUTRAL: - decision = MTMDecision.DECISION_ONCE; - break; - default: - decision = MTMDecision.DECISION_ABORT; - } - sendDecision(decision); - } - - public void onCancel(DialogInterface dialog) { - sendDecision(MTMDecision.DECISION_ABORT); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/MemoryManagementActivity.java b/src/main/java/eu/siacs/conversations/ui/MemoryManagementActivity.java deleted file mode 100644 index 504b1db98..000000000 --- a/src/main/java/eu/siacs/conversations/ui/MemoryManagementActivity.java +++ /dev/null @@ -1,185 +0,0 @@ -package eu.siacs.conversations.ui; - -import static eu.siacs.conversations.persistance.FileBackend.AUDIOS; -import static eu.siacs.conversations.persistance.FileBackend.FILES; -import static eu.siacs.conversations.persistance.FileBackend.IMAGES; -import static eu.siacs.conversations.persistance.FileBackend.VIDEOS; -import static eu.siacs.conversations.utils.StorageHelper.getAppMediaDirectory; -import static eu.siacs.conversations.utils.StorageHelper.getConversationsDirectory; - -import android.app.Activity; -import android.content.Context; -import android.os.AsyncTask; -import android.os.Bundle; -import android.widget.ImageButton; -import android.widget.TextView; - -import androidx.appcompat.app.AlertDialog; - -import java.io.File; -import java.lang.ref.WeakReference; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.ThemeHelper; -import eu.siacs.conversations.utils.UIHelper; - -public class MemoryManagementActivity extends XmppActivity { - - private static TextView disk_storage; - private static TextView media_usage; - private ImageButton delete_media; - private static TextView pictures_usage; - private ImageButton delete_pictures; - private static TextView videos_usage; - private ImageButton delete_videos; - private static TextView files_usage; - private ImageButton delete_files; - private static TextView audios_usage; - private ImageButton delete_audios; - - static String totalMemory = "..."; - static String mediaUsage = "..."; - static String picturesUsage = "..."; - static String videosUsage = "..."; - static String filesUsage = "..."; - static String audiosUsage = "..."; - - @Override - protected void refreshUiReal() { - } - - @Override - void onBackendConnected() { - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setTheme(ThemeHelper.find(this)); - setContentView(R.layout.activity_memory_management); - setSupportActionBar(findViewById(R.id.toolbar)); - configureActionBar(getSupportActionBar()); - disk_storage = findViewById(R.id.disk_storage); - media_usage = findViewById(R.id.media_usage); - delete_media = findViewById(R.id.action_delete_media); - pictures_usage = findViewById(R.id.pictures_usage); - delete_pictures = findViewById(R.id.action_delete_pictures); - videos_usage = findViewById(R.id.videos_usage); - delete_videos = findViewById(R.id.action_delete_videos); - files_usage = findViewById(R.id.files_usage); - delete_files = findViewById(R.id.action_delete_files); - audios_usage = findViewById(R.id.audios_usage); - delete_audios = findViewById(R.id.action_delete_audios); - } - - @Override - protected void onStart() { - super.onStart(); - new getMemoryUsages(this).execute(); - delete_media.setOnClickListener(view -> { - deleteMedia(new File(getAppMediaDirectory(this, null))); - }); - delete_pictures.setOnClickListener(view -> { - deleteMedia(new File(getConversationsDirectory(this, IMAGES).getAbsolutePath())); - }); - delete_videos.setOnClickListener(view -> { - deleteMedia(new File(getConversationsDirectory(this, VIDEOS).getAbsolutePath())); - }); - delete_files.setOnClickListener(view -> { - deleteMedia(new File(getConversationsDirectory(this, FILES).getAbsolutePath())); - }); - delete_audios.setOnClickListener(view -> { - deleteMedia(new File(getConversationsDirectory(this, AUDIOS).getAbsolutePath())); - }); - } - - - private void deleteMedia(File dir) { - final String file; - if (dir.toString().endsWith(IMAGES)) { - file = getString(R.string.images); - } else if (dir.toString().endsWith(VIDEOS)) { - file = getString(R.string.videos); - } else if (dir.toString().endsWith(FILES)) { - file = getString(R.string.files); - } else if (dir.toString().endsWith(AUDIOS)) { - file = getString(R.string.audios); - } else { - file = getString(R.string.all_media_files); - } - final androidx.appcompat.app.AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setNegativeButton(R.string.cancel, null); - builder.setTitle(R.string.delete_files_dialog); - builder.setMessage(getResources().getString(R.string.delete_files_dialog_msg, file)); - builder.setPositiveButton(R.string.confirm, (dialog, which) -> { - new Thread(new deleteFilesInDirFinisher(dir, this, xmppConnectionService)).start(); - }); - builder.create().show(); - } - - private static class deleteFilesInDirFinisher implements Runnable { - - private final File dir; - private final WeakReference activityReference; - private final XmppConnectionService service; - - private deleteFilesInDirFinisher(File dir, Activity activity, XmppConnectionService service) { - this.dir = dir; - this.activityReference = new WeakReference<>(activity); - this.service = service; - } - - @Override - public void run() { - final Activity activity = activityReference.get(); - if (activity == null) { - return; - } - service.getFileBackend().deleteFilesInDir(dir); - activity.runOnUiThread(() -> new getMemoryUsages(activity).execute()); - } - } - - static class getMemoryUsages extends AsyncTask { - private Context mContext; - - public getMemoryUsages(Context context) { - mContext = context; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - disk_storage.setText(totalMemory); - media_usage.setText(mediaUsage); - pictures_usage.setText(picturesUsage); - videos_usage.setText(videosUsage); - files_usage.setText(filesUsage); - audios_usage.setText(audiosUsage); - } - - @Override - protected Void doInBackground(Void... params) { - totalMemory = UIHelper.filesizeToString(FileBackend.getDiskSize()); - mediaUsage = UIHelper.filesizeToString(FileBackend.getDirectorySize(new File(getAppMediaDirectory(mContext, null)))); - picturesUsage = UIHelper.filesizeToString(FileBackend.getDirectorySize(new File(getConversationsDirectory(mContext, IMAGES).getAbsolutePath()))); - videosUsage = UIHelper.filesizeToString(FileBackend.getDirectorySize(new File(getConversationsDirectory(mContext, VIDEOS).getAbsolutePath()))); - filesUsage = UIHelper.filesizeToString(FileBackend.getDirectorySize(new File(getConversationsDirectory(mContext, FILES).getAbsolutePath()))); - audiosUsage = UIHelper.filesizeToString(FileBackend.getDirectorySize(new File(getConversationsDirectory(mContext, AUDIOS).getAbsolutePath()))); - return null; - } - - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - disk_storage.setText(totalMemory); - media_usage.setText(mediaUsage); - pictures_usage.setText(picturesUsage); - videos_usage.setText(videosUsage); - files_usage.setText(filesUsage); - audios_usage.setText(audiosUsage); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/MemoryManagementPref.java b/src/main/java/eu/siacs/conversations/ui/MemoryManagementPref.java deleted file mode 100644 index 6f2c804ba..000000000 --- a/src/main/java/eu/siacs/conversations/ui/MemoryManagementPref.java +++ /dev/null @@ -1,71 +0,0 @@ -package eu.siacs.conversations.ui; - -import static eu.siacs.conversations.utils.StorageHelper.getAppMediaDirectory; - -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; -import android.preference.Preference; -import android.util.AttributeSet; - -import java.io.File; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.utils.UIHelper; - -public class MemoryManagementPref extends Preference { - - String mediaUsage = "..."; - String totalMemory = "..."; - - - public MemoryManagementPref(final Context context, final AttributeSet attrs, final int defStyle) { - super(context, attrs, defStyle); - setSummary(); - } - - public MemoryManagementPref(final Context context, final AttributeSet attrs) { - super(context, attrs); - setSummary(); - } - - @Override - protected void onClick() { - super.onClick(); - final Intent intent = new Intent(getContext(), MemoryManagementActivity.class); - getContext().startActivity(intent); - } - - private void setSummary() { - new getMemoryUsages(this.getContext()).execute(); - } - - public class getMemoryUsages extends AsyncTask { - private Context mContext; - - public getMemoryUsages(Context context) { - mContext = context; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - setSummary(getContext().getString(R.string.media_usage) + ": " + mediaUsage + "/" + totalMemory); - } - - @Override - protected Void doInBackground(Void... params) { - totalMemory = UIHelper.filesizeToString(FileBackend.getDiskSize()); - mediaUsage = UIHelper.filesizeToString(FileBackend.getDirectorySize(new File(getAppMediaDirectory(mContext, null)))); - return null; - } - - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - setSummary(getContext().getString(R.string.media_usage) + ": " + mediaUsage + "/" + totalMemory); - } - } -} - diff --git a/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java b/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java deleted file mode 100644 index 0862196e7..000000000 --- a/src/main/java/eu/siacs/conversations/ui/MucUsersActivity.java +++ /dev/null @@ -1,165 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; - -import androidx.appcompat.widget.Toolbar; -import androidx.databinding.DataBindingUtil; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Locale; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ActivityMucUsersBinding; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.adapter.UserAdapter; -import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper; -import eu.siacs.conversations.xmpp.Jid; -import me.drakeet.support.toast.ToastCompat; - -public class MucUsersActivity extends XmppActivity implements XmppConnectionService.OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, MenuItem.OnActionExpandListener, TextWatcher { - - private UserAdapter userAdapter; - - private Conversation mConversation = null; - - private EditText mSearchEditText; - - private ArrayList allUsers = new ArrayList<>(); - - @Override - protected void refreshUiReal() { - } - - @Override - void onBackendConnected() { - final Intent intent = getIntent(); - final String uuid = intent == null ? null : intent.getStringExtra("uuid"); - if (uuid != null) { - mConversation = xmppConnectionService.findConversationByUuid(uuid); - } - loadAndSubmitUsers(); - } - - private void loadAndSubmitUsers() { - if (mConversation != null) { - allUsers = mConversation.getMucOptions().getUsers(); - Collections.sort(allUsers); - submitFilteredList(mSearchEditText != null ? mSearchEditText.getText().toString() : null); - } - } - - private void submitFilteredList(String search) { - if (TextUtils.isEmpty(search)) { - userAdapter.submitList(allUsers); - } else { - final String needle = search.toLowerCase(Locale.getDefault()); - ArrayList filtered = new ArrayList<>(); - for (MucOptions.User user : allUsers) { - final String name = user.getName(); - final Contact contact = user.getContact(); - if (name != null && name.toLowerCase(Locale.getDefault()).contains(needle) || contact != null && contact.getDisplayName().toLowerCase(Locale.getDefault()).contains(needle)) { - filtered.add(user); - } - } - userAdapter.submitList(filtered); - } - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - if (!MucDetailsContextMenuHelper.onContextItemSelected(item, userAdapter.getSelectedUser(), this)) { - return super.onContextItemSelected(item); - } - return true; - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ActivityMucUsersBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_muc_users); - setSupportActionBar((Toolbar) binding.toolbar); - configureActionBar(getSupportActionBar(), true); - this.userAdapter = new UserAdapter(getPreferences().getBoolean("advanced_muc_mode", false)); - binding.list.setAdapter(this.userAdapter); - } - - - @Override - public void onMucRosterUpdate() { - loadAndSubmitUsers(); - } - - private void displayToast(final String msg) { - runOnUiThread(() -> ToastCompat.makeText(this, msg, ToastCompat.LENGTH_SHORT).show()); - } - - @Override - public void onAffiliationChangedSuccessful(Jid jid) { - - } - - @Override - public void onAffiliationChangeFailed(Jid jid, int resId) { - displayToast(getString(resId, jid.asBareJid().toString())); - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.muc_users_activity, menu); - final MenuItem menuSearchView = menu.findItem(R.id.action_search); - final View mSearchView = menuSearchView.getActionView(); - mSearchEditText = mSearchView.findViewById(R.id.search_field); - mSearchEditText.addTextChangedListener(this); - mSearchEditText.setHint(R.string.search_participants); - menuSearchView.setOnActionExpandListener(this); - return true; - } - - @Override - public boolean onMenuItemActionExpand(MenuItem item) { - mSearchEditText.post(() -> { - mSearchEditText.requestFocus(); - final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mSearchEditText, InputMethodManager.SHOW_IMPLICIT); - }); - return true; - } - - @Override - public boolean onMenuItemActionCollapse(MenuItem item) { - final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY); - mSearchEditText.setText(""); - submitFilteredList(""); - return true; - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - - } - - @Override - public void afterTextChanged(Editable s) { - submitFilteredList(s.toString()); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java deleted file mode 100644 index c5e8173f5..000000000 --- a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java +++ /dev/null @@ -1,211 +0,0 @@ -package eu.siacs.conversations.ui; - - -import android.content.Intent; -import android.view.ContextMenu; -import android.view.MenuItem; -import android.view.View; -import android.widget.CompoundButton; -import android.widget.LinearLayout; - -import androidx.appcompat.app.AlertDialog; -import androidx.databinding.DataBindingUtil; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; -import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; -import eu.siacs.conversations.databinding.ContactKeyBinding; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.XmppUri; -import me.drakeet.support.toast.ToastCompat; - -public abstract class OmemoActivity extends XmppActivity { - - private Account mSelectedAccount; - private String mSelectedFingerprint; - - protected XmppUri mPendingFingerprintVerificationUri = null; - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - Object account = v.getTag(R.id.TAG_ACCOUNT); - Object fingerprint = v.getTag(R.id.TAG_FINGERPRINT); - Object fingerprintStatus = v.getTag(R.id.TAG_FINGERPRINT_STATUS); - if (account != null - && fingerprint != null - && account instanceof Account - && fingerprintStatus != null - && fingerprint instanceof String - && fingerprintStatus instanceof FingerprintStatus) { - getMenuInflater().inflate(R.menu.omemo_key_context, menu); - MenuItem distrust = menu.findItem(R.id.distrust_key); - MenuItem verifyScan = menu.findItem(R.id.verify_scan); - if (this instanceof TrustKeysActivity) { - distrust.setVisible(false); - verifyScan.setVisible(false); - } else { - FingerprintStatus status = (FingerprintStatus) fingerprintStatus; - if (!status.isActive() || status.isVerified()) { - verifyScan.setVisible(false); - } - distrust.setVisible(status.isVerified() || (!status.isActive() && status.isTrusted())); - } - this.mSelectedAccount = (Account) account; - this.mSelectedFingerprint = (String) fingerprint; - } - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.distrust_key: - showPurgeKeyDialog(mSelectedAccount, mSelectedFingerprint); - break; - case R.id.copy_omemo_key: - copyOmemoFingerprint(mSelectedFingerprint); - break; - case R.id.verify_scan: - ScanActivity.scan(this); - break; - } - return true; - } - - @Override - public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) { - super.onActivityResult(requestCode, resultCode, intent); - if (requestCode == ScanActivity.REQUEST_SCAN_QR_CODE && resultCode == RESULT_OK) { - String result = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT); - XmppUri uri = new XmppUri(result == null ? "" : result); - if (xmppConnectionServiceBound) { - processFingerprintVerification(uri); - } else { - this.mPendingFingerprintVerificationUri = uri; - } - } - } - - protected abstract void processFingerprintVerification(XmppUri uri); - - protected void copyOmemoFingerprint(String fingerprint) { - if (copyTextToClipboard(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)), R.string.omemo_fingerprint)) { - ToastCompat.makeText( - this, - R.string.toast_message_omemo_fingerprint, - ToastCompat.LENGTH_SHORT).show(); - } - } - - protected void addFingerprintRow(LinearLayout keys, final XmppAxolotlSession session, boolean highlight) { - final Account account = session.getAccount(); - final String fingerprint = session.getFingerprint(); - addFingerprintRowWithListeners(keys, - session.getAccount(), - fingerprint, - highlight, - session.getTrust(), - true, - true, - (buttonView, isChecked) -> account.getAxolotlService().setFingerprintTrust(fingerprint, FingerprintStatus.createActive(isChecked))); - } - - protected void addFingerprintRowWithListeners(LinearLayout keys, final Account account, - final String fingerprint, - boolean highlight, - FingerprintStatus status, - boolean showTag, - boolean undecidedNeedEnablement, - CompoundButton.OnCheckedChangeListener - onCheckedChangeListener) { - ContactKeyBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.contact_key, keys, true); - binding.tglTrust.setVisibility(View.VISIBLE); - registerForContextMenu(binding.getRoot()); - binding.getRoot().setTag(R.id.TAG_ACCOUNT, account); - binding.getRoot().setTag(R.id.TAG_FINGERPRINT, fingerprint); - binding.getRoot().setTag(R.id.TAG_FINGERPRINT_STATUS, status); - boolean x509 = Config.X509_VERIFICATION && status.getTrust() == FingerprintStatus.Trust.VERIFIED_X509; - final View.OnClickListener toast; - binding.tglTrust.setChecked(status.isTrusted()); - - if (status.isActive()) { - binding.key.setTextAppearance(this, R.style.TextAppearance_Conversations_Fingerprint); - binding.keyType.setTextAppearance(this, R.style.TextAppearance_Conversations_Caption); - if (status.isVerified()) { - binding.verifiedFingerprint.setVisibility(View.VISIBLE); - binding.verifiedFingerprint.setAlpha(1.0f); - binding.tglTrust.setVisibility(View.GONE); - binding.verifiedFingerprint.setOnClickListener(v -> replaceToast(getString(R.string.this_device_has_been_verified), false)); - toast = null; - } else { - binding.verifiedFingerprint.setVisibility(View.GONE); - binding.tglTrust.setVisibility(View.VISIBLE); - binding.tglTrust.setOnCheckedChangeListener(onCheckedChangeListener); - if (status.getTrust() == FingerprintStatus.Trust.UNDECIDED && undecidedNeedEnablement) { - binding.buttonEnableDevice.setVisibility(View.VISIBLE); - binding.buttonEnableDevice.setOnClickListener(v -> { - account.getAxolotlService().setFingerprintTrust(fingerprint, FingerprintStatus.createActive(false)); - binding.buttonEnableDevice.setVisibility(View.GONE); - binding.tglTrust.setVisibility(View.VISIBLE); - }); - binding.tglTrust.setVisibility(View.GONE); - } else { - binding.tglTrust.setOnClickListener(null); - binding.tglTrust.setEnabled(true); - } - toast = v -> hideToast(); - } - } else { - binding.key.setTextAppearance(this, R.style.TextAppearance_Conversations_Fingerprint_Disabled); - binding.keyType.setTextAppearance(this, R.style.TextAppearance_Conversations_Caption_Disabled); - toast = v -> replaceToast(getString(R.string.this_device_is_no_longer_in_use), false); - if (status.isVerified()) { - binding.tglTrust.setVisibility(View.GONE); - binding.verifiedFingerprint.setVisibility(View.VISIBLE); - binding.verifiedFingerprint.setAlpha(0.4368f); - binding.verifiedFingerprint.setOnClickListener(toast); - } else { - binding.tglTrust.setVisibility(View.VISIBLE); - binding.verifiedFingerprint.setVisibility(View.GONE); - binding.tglTrust.setEnabled(false); - } - } - - binding.getRoot().setOnClickListener(toast); - binding.key.setOnClickListener(toast); - binding.keyType.setOnClickListener(toast); - if (showTag) { - binding.keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint)); - } else { - binding.keyType.setVisibility(View.GONE); - } - if (highlight) { - binding.keyType.setTextAppearance(this, R.style.TextAppearance_Conversations_Caption_Highlight); - binding.keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message)); - } else { - binding.keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint)); - } - - binding.key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2))); - } - - public void showPurgeKeyDialog(final Account account, final String fingerprint) { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.distrust_omemo_key); - builder.setMessage(R.string.distrust_omemo_key_text); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(R.string.confirm, - (dialog, which) -> { - account.getAxolotlService().distrustFingerprint(fingerprint); - refreshUi(); - }); - builder.create().show(); - } - - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - ScanActivity.onRequestPermissionResult(this, requestCode, grantResults); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java deleted file mode 100644 index 40e9877c9..000000000 --- a/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui; - -import android.content.Intent; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.Bundle; -import android.util.Log; -import android.view.View; - -import androidx.annotation.StringRes; -import androidx.appcompat.widget.Toolbar; -import androidx.databinding.DataBindingUtil; - -import com.theartofdev.edmodo.cropper.CropImage; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ActivityPublishProfilePictureBinding; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.ui.interfaces.OnAvatarPublication; -import eu.siacs.conversations.ui.util.PendingItem; -import me.drakeet.support.toast.ToastCompat; - -import static eu.siacs.conversations.ui.PublishProfilePictureActivity.REQUEST_CHOOSE_PICTURE; - -public class PublishGroupChatProfilePictureActivity extends XmppActivity implements OnAvatarPublication { - - private final PendingItem pendingConversationUuid = new PendingItem<>(); - private ActivityPublishProfilePictureBinding binding; - private Conversation conversation; - private Uri uri; - - @Override - protected void refreshUiReal() { - - } - - @Override - void onBackendConnected() { - String uuid = pendingConversationUuid.pop(); - if (uuid != null) { - this.conversation = xmppConnectionService.findConversationByUuid(uuid); - } - if (this.conversation == null) { - return; - } - reloadAvatar(); - } - - private void reloadAvatar() { - final int size = getPixel(Config.AVATAR_SIZE); - Bitmap bitmap; - if (uri == null) { - bitmap = xmppConnectionService.getAvatarService().get(conversation, size); - } else { - Log.d(Config.LOGTAG, "loading " + uri.toString() + " into preview"); - bitmap = xmppConnectionService.getFileBackend().cropCenterSquare(uri, size); - } - this.binding.accountImage.setImageBitmap(bitmap); - this.binding.publishButton.setEnabled(uri != null); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_publish_profile_picture); - setSupportActionBar((Toolbar) this.binding.toolbar); - configureActionBar(getSupportActionBar()); - this.binding.cancelButton.setOnClickListener((v) -> this.finish()); - this.binding.secondaryHint.setVisibility(View.GONE); - this.binding.accountImage.setOnClickListener((v) -> PublishProfilePictureActivity.chooseAvatar(this)); - Intent intent = getIntent(); - String uuid = intent == null ? null : intent.getStringExtra("uuid"); - if (uuid != null) { - pendingConversationUuid.push(uuid); - } - this.binding.publishButton.setEnabled(uri != null); - this.binding.publishButton.setOnClickListener(this::publish); - } - - - private void publish(View view) { - binding.publishButton.setText(R.string.publishing); - binding.publishButton.setEnabled(false); - xmppConnectionService.publishMucAvatar(conversation, uri, this); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) { - final CropImage.ActivityResult result = CropImage.getActivityResult(data); - if (resultCode == RESULT_OK) { - this.uri = result.getUri(); - if (xmppConnectionServiceBound) { - reloadAvatar(); - } - } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) { - Exception error = result.getError(); - if (error != null) { - ToastCompat.makeText(this, error.getMessage(), ToastCompat.LENGTH_SHORT).show(); - } - } - } else if (requestCode == REQUEST_CHOOSE_PICTURE) { - if (resultCode == RESULT_OK) { - PublishProfilePictureActivity.cropUri(this, data.getData()); - } - } - } - - @Override - public void onAvatarPublicationSucceeded() { - runOnUiThread(() -> { - ToastCompat.makeText(this, R.string.avatar_has_been_published, ToastCompat.LENGTH_SHORT).show(); - finish(); - }); - } - - @Override - public void onAvatarPublicationFailed(@StringRes int res) { - runOnUiThread(() -> { - ToastCompat.makeText(this, res, ToastCompat.LENGTH_SHORT).show(); - this.binding.publishButton.setText(R.string.publish); - this.binding.publishButton.setEnabled(true); - }); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java deleted file mode 100644 index ef59db7cb..000000000 --- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ /dev/null @@ -1,304 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.app.Activity; -import android.content.Intent; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnLongClickListener; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; - -import com.theartofdev.edmodo.cropper.CropImage; - -import java.util.concurrent.atomic.AtomicBoolean; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.interfaces.OnAvatarPublication; -import eu.siacs.conversations.utils.PhoneHelper; -import me.drakeet.support.toast.ToastCompat; - -public class PublishProfilePictureActivity extends XmppActivity implements XmppConnectionService.OnAccountUpdate, OnAvatarPublication { - - public static final int REQUEST_CHOOSE_PICTURE = 0x1337; - - private ImageView avatar; - private TextView hintOrWarning; - private TextView secondaryHint; - private Button cancelButton; - private Button publishButton; - private Uri avatarUri; - private Uri defaultUri; - private Account account; - private boolean support = false; - private boolean publishing = false; - private AtomicBoolean handledExternalUri = new AtomicBoolean(false); - private OnLongClickListener backToDefaultListener = new OnLongClickListener() { - - @Override - public boolean onLongClick(View v) { - avatarUri = defaultUri; - loadImageIntoPreview(defaultUri); - return true; - } - }; - private boolean mInitialAccountSetup; - - @Override - public void onAvatarPublicationSucceeded() { - runOnUiThread(() -> { - if (mInitialAccountSetup) { - Intent intent = new Intent(getApplicationContext(), StartConversationActivity.class); - StartConversationActivity.addInviteUri(intent, getIntent()); - intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); - intent.putExtra("init", true); - startActivity(intent); - } - ToastCompat.makeText(PublishProfilePictureActivity.this, - R.string.avatar_has_been_published, - ToastCompat.LENGTH_SHORT).show(); - finish(); - }); - } - - @Override - public void onAvatarPublicationFailed(int res) { - runOnUiThread(() -> { - hintOrWarning.setText(res); - hintOrWarning.setTextAppearance(this, R.style.TextAppearance_Conversations_Body1_Warning); - hintOrWarning.setVisibility(View.VISIBLE); - publishing = false; - togglePublishButton(true, R.string.publish); - }); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_publish_profile_picture); - setSupportActionBar(findViewById(R.id.toolbar)); - - this.avatar = findViewById(R.id.account_image); - this.cancelButton = findViewById(R.id.cancel_button); - this.publishButton = findViewById(R.id.publish_button); - this.hintOrWarning = findViewById(R.id.hint_or_warning); - this.secondaryHint = findViewById(R.id.secondary_hint); - this.publishButton.setOnClickListener(v -> { - if (avatarUri != null) { - publishing = true; - togglePublishButton(false, R.string.publishing); - xmppConnectionService.publishAvatar(account, avatarUri, this); - } - }); - this.cancelButton.setOnClickListener( - v -> { - if (mInitialAccountSetup) { - final Intent intent = - new Intent( - getApplicationContext(), StartConversationActivity.class); - if (xmppConnectionService != null - && xmppConnectionService.getAccounts().size() == 1) { - intent.putExtra("init", true); - } - StartConversationActivity.addInviteUri(intent, getIntent()); - if (account != null) { - intent.putExtra( - EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); - } - startActivity(intent); - } - finish(); - }); - this.avatar.setOnClickListener(v -> chooseAvatar(this)); - this.defaultUri = PhoneHelper.getProfilePictureUri(getApplicationContext()); - if (savedInstanceState != null) { - this.avatarUri = savedInstanceState.getParcelable("uri"); - this.handledExternalUri.set(savedInstanceState.getBoolean("handle_external_uri", false)); - } - } - public boolean onCreateOptionsMenu(@NonNull final Menu menu) { - getMenuInflater().inflate(R.menu.activity_publish_avatar, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - if (item.getItemId() == R.id.action_delete_avatar) { - if (xmppConnectionService != null && account != null) { - xmppConnectionService.deleteAvatar(account); - } - return true; - } else { - return super.onOptionsItemSelected(item); - } - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - if (this.avatarUri != null) { - outState.putParcelable("uri", this.avatarUri); - } - outState.putBoolean("handle_external_uri", handledExternalUri.get()); - super.onSaveInstanceState(outState); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) { - CropImage.ActivityResult result = CropImage.getActivityResult(data); - if (resultCode == RESULT_OK) { - this.avatarUri = result.getUri(); - if (xmppConnectionServiceBound) { - loadImageIntoPreview(this.avatarUri); - } - } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) { - Exception error = result.getError(); - if (error != null) { - ToastCompat.makeText(this, error.getMessage(), ToastCompat.LENGTH_SHORT).show(); - } - } - } else if (requestCode == REQUEST_CHOOSE_PICTURE) { - if (resultCode == RESULT_OK) { - cropUri(this, data.getData()); - } - } - } - - public static void chooseAvatar(final Activity activity) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("image/*"); - activity.startActivityForResult( - Intent.createChooser(intent, activity.getString(R.string.attach_choose_picture)), - REQUEST_CHOOSE_PICTURE - ); - } else { - CropImage.activity() - .setOutputCompressFormat(Bitmap.CompressFormat.PNG) - .setAspectRatio(1, 1) - .setMinCropResultSize(Config.AVATAR_SIZE, Config.AVATAR_SIZE) - .start(activity); - } - } - - @Override - protected void onBackendConnected() { - this.account = extractAccount(getIntent()); - if (this.account != null) { - reloadAvatar(); - } - } - - private void reloadAvatar() { - this.support = this.account.getXmppConnection() != null && this.account.getXmppConnection().getFeatures().pep(); - if (this.avatarUri == null) { - if (this.account.getAvatar() != null || this.defaultUri == null) { - loadImageIntoPreview(null); - } else { - this.avatarUri = this.defaultUri; - loadImageIntoPreview(this.defaultUri); - } - } else { - loadImageIntoPreview(avatarUri); - } - } - - @Override - protected void onStart() { - super.onStart(); - final Intent intent = getIntent(); - this.mInitialAccountSetup = intent != null && intent.getBooleanExtra("setup", false); - - final Uri uri = intent != null ? intent.getData() : null; - - if (uri != null && handledExternalUri.compareAndSet(false, true)) { - cropUri(this, uri); - return; - } - - if (this.mInitialAccountSetup) { - this.cancelButton.setText(R.string.skip); - } - configureActionBar(getSupportActionBar(), !this.mInitialAccountSetup && !handledExternalUri.get()); - } - - public static void cropUri(final Activity activity, final Uri uri) { - CropImage.activity(uri).setOutputCompressFormat(Bitmap.CompressFormat.PNG) - .setAspectRatio(1, 1) - .setMinCropResultSize(Config.AVATAR_SIZE, Config.AVATAR_SIZE) - .start(activity); - } - - protected void loadImageIntoPreview(Uri uri) { - - Bitmap bm = null; - if (uri == null) { - bm = avatarService().get(account, getPixel(Config.AVATAR_SIZE)); - } else { - try { - bm = xmppConnectionService.getFileBackend().cropCenterSquare(uri, getPixel(Config.AVATAR_SIZE)); - } catch (Exception e) { - Log.d(Config.LOGTAG, "unable to load avatar into image view", e); - } - } - if (bm == null) { - togglePublishButton(false, R.string.publish); - this.hintOrWarning.setVisibility(View.VISIBLE); - this.hintOrWarning.setTextAppearance(this, R.style.TextAppearance_Conversations_Body1_Warning); - this.hintOrWarning.setText(R.string.error_publish_avatar_converting); - return; - } - this.avatar.setImageBitmap(bm); - if (support) { - togglePublishButton(uri != null, R.string.publish); - this.hintOrWarning.setVisibility(View.INVISIBLE); - } else { - togglePublishButton(false, R.string.publish); - this.hintOrWarning.setVisibility(View.VISIBLE); - this.hintOrWarning.setTextAppearance(this, R.style.TextAppearance_Conversations_Body1_Warning); - if (account.getStatus() == Account.State.ONLINE) { - this.hintOrWarning.setText(R.string.error_publish_avatar_no_server_support); - } else { - this.hintOrWarning.setText(R.string.error_publish_avatar_offline); - } - } - if (this.defaultUri == null || this.defaultUri.equals(uri)) { - this.secondaryHint.setVisibility(View.INVISIBLE); - this.avatar.setOnLongClickListener(null); - } else if (this.defaultUri != null) { - this.secondaryHint.setVisibility(View.VISIBLE); - this.avatar.setOnLongClickListener(this.backToDefaultListener); - } - } - - protected void togglePublishButton(boolean enabled, @StringRes int res) { - final boolean status = enabled && !publishing; - this.publishButton.setText(publishing ? R.string.publishing : res); - this.publishButton.setEnabled(status); - } - - public void refreshUiReal() { - if (this.account != null) { - reloadAvatar(); - } - } - - @Override - public void onAccountUpdate() { - refreshUi(); - } - -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java deleted file mode 100644 index b188778a9..000000000 --- a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java +++ /dev/null @@ -1,280 +0,0 @@ -package eu.siacs.conversations.ui; - -import static eu.siacs.conversations.persistance.FileBackend.SENT_AUDIOS; -import static eu.siacs.conversations.utils.StorageHelper.getConversationsDirectory; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.media.MediaRecorder; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.FileObserver; -import android.os.Handler; -import android.os.SystemClock; -import android.util.Log; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; - -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.databinding.DataBindingUtil; - -import java.io.File; -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ActivityRecordingBinding; -import eu.siacs.conversations.utils.ThemeHelper; -import eu.siacs.conversations.utils.TimeFrameUtils; -import me.drakeet.support.toast.ToastCompat; - -public class RecordingActivity extends AppCompatActivity implements View.OnClickListener { - - private ActivityRecordingBinding binding; - - private MediaRecorder mRecorder; - private Integer oldOrientation; - private long mStartTime = 0; - private boolean alternativeCodec = false; - private boolean recording = false; - - private CountDownLatch outputFileWrittenLatch = new CountDownLatch(1); - - private Handler mHandler = new Handler(); - private Runnable mTickExecutor = new Runnable() { - @Override - public void run() { - tick(); - mHandler.postDelayed(mTickExecutor, 100); - } - }; - - private File mOutputFile; - - private FileObserver mFileObserver; - - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(ThemeHelper.findDialog(this)); - super.onCreate(savedInstanceState); - supportRequestWindowFeature(Window.FEATURE_NO_TITLE); - oldOrientation = getRequestedOrientation(); - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_recording); - this.setTitle(R.string.attach_record_voice); - this.binding.cancelButton.setOnClickListener(this); - this.binding.shareButton.setOnClickListener(this); - this.setFinishOnTouchOutside(false); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - - @Override - protected void onStart() { - super.onStart(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED); - } - Intent intent = getIntent(); - alternativeCodec = intent != null && intent.getBooleanExtra("ALTERNATIVE_CODEC", getResources().getBoolean(R.bool.alternative_voice_settings)); - if (!startRecording()) { - this.binding.shareButton.setEnabled(false); - this.binding.timer.setTextAppearance(this, R.style.TextAppearance_Conversations_Title); - this.binding.timer.setText(R.string.unable_to_start_recording); - } - } - - @Override - protected void onStop() { - super.onStop(); - if (mRecorder != null) { - mHandler.removeCallbacks(mTickExecutor); - stopRecording(false); - } - if (mFileObserver != null) { - mFileObserver.stopWatching(); - } - setRequestedOrientation(oldOrientation); - } - - private boolean startRecording() { - mRecorder = new MediaRecorder(); - mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); - if (alternativeCodec) { - mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); - mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); - } else { - mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); - mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); - mRecorder.setAudioEncodingBitRate(96000); - mRecorder.setAudioSamplingRate(22050); - } - setupOutputFile(); - mRecorder.setOutputFile(mOutputFile.getAbsolutePath()); - - try { - mRecorder.prepare(); - mRecorder.start(); - recording = true; - mStartTime = SystemClock.elapsedRealtime(); - mHandler.postDelayed(mTickExecutor, 100); - Log.d("Voice Recorder", "started recording to " + mOutputFile.getAbsolutePath()); - return true; - } catch (Exception e) { - Log.e("Voice Recorder", "prepare() failed " + e.getMessage()); - return false; - } - } - - protected void stopRecording() { - try { - mRecorder.stop(); - mRecorder.release(); - recording = false; - } catch (Exception e) { - e.printStackTrace(); - } - } - - protected void stopRecording(final boolean saveFile) { - try { - if (recording) { - stopRecording(); - } - } catch (Exception e) { - if (saveFile) { - ToastCompat.makeText(this, R.string.unable_to_save_recording, ToastCompat.LENGTH_SHORT).show(); - return; - } - } finally { - mRecorder = null; - mStartTime = 0; - } - if (!saveFile && mOutputFile != null) { - if (mOutputFile.delete()) { - Log.d(Config.LOGTAG, "deleted canceled recording"); - } - } - if (saveFile) { - new Thread(new Finisher(outputFileWrittenLatch, mOutputFile, this)).start(); - } - } - - private static class Finisher implements Runnable { - - private final CountDownLatch latch; - private final File outputFile; - private final WeakReference activityReference; - - private Finisher(CountDownLatch latch, File outputFile, Activity activity) { - this.latch = latch; - this.outputFile = outputFile; - this.activityReference = new WeakReference<>(activity); - } - - @Override - public void run() { - try { - if (!latch.await(8, TimeUnit.SECONDS)) { - Log.d(Config.LOGTAG, "time out waiting for output file to be written"); - } - } catch (InterruptedException e) { - Log.d(Config.LOGTAG, "interrupted while waiting for output file to be written", e); - } - final Activity activity = activityReference.get(); - if (activity == null) { - return; - } - activity.runOnUiThread( - () -> { - activity.setResult( - Activity.RESULT_OK, new Intent().setData(Uri.fromFile(outputFile))); - activity.finish(); - }); - } - } - - private static File generateOutputFilename(Context context) { - final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); - return new File(getConversationsDirectory(context, SENT_AUDIOS) - + dateFormat.format(new Date()) - + ".m4a"); - } - - private void setupOutputFile() { - mOutputFile = generateOutputFilename(this); - final File parentDirectory = mOutputFile.getParentFile(); - if (parentDirectory.mkdirs()) { - Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath()); - } - final File noMedia = new File(parentDirectory, ".nomedia"); - if (!noMedia.exists()) { - try { - if (noMedia.createNewFile()) { - Log.d(Config.LOGTAG, "created nomedia file in " + parentDirectory.getAbsolutePath()); - } - } catch (IOException e) { - Log.d(Config.LOGTAG, "unable to create nomedia file in " + parentDirectory.getAbsolutePath(), e); - } - } - setupFileObserver(parentDirectory); - } - - private void setupFileObserver(final File directory) { - mFileObserver = new FileObserver(directory.getAbsolutePath()) { - @Override - public void onEvent(int event, String s) { - if (s != null && s.equals(mOutputFile.getName()) && event == FileObserver.CLOSE_WRITE) { - outputFileWrittenLatch.countDown(); - } - } - }; - mFileObserver.startWatching(); - } - - private void tick() { - this.binding.timer.setText(TimeFrameUtils.formatTimePassed(mStartTime, true)); - } - - @Override - public void onClick(final View view) { - switch (view.getId()) { - case R.id.cancel_button: - showCancelDialog(); - break; - case R.id.share_button: - this.binding.shareButton.setEnabled(false); - this.binding.shareButton.setText(R.string.please_wait); - mHandler.removeCallbacks(mTickExecutor); - mHandler.postDelayed(() -> stopRecording(true), 500); - break; - } - } - - private void showCancelDialog() { - stopRecording(); - final AlertDialog.Builder builder = new AlertDialog.Builder(RecordingActivity.this); - builder.setTitle(getString(R.string.cancel)); - builder.setMessage(R.string.delete_recording_dialog_message); - builder.setPositiveButton(R.string.attach, (dialog, which) -> { - mHandler.removeCallbacks(mTickExecutor); - mHandler.postDelayed(() -> stopRecording(true), 500); - }); - builder.setNegativeButton(R.string.delete, (dialog, which) -> { - mHandler.removeCallbacks(mTickExecutor); - stopRecording(false); - setResult(RESULT_CANCELED); - finish(); - }); - builder.create().show(); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java deleted file mode 100644 index 324a61a34..000000000 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ /dev/null @@ -1,1494 +0,0 @@ -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.Activity; -import android.app.PictureInPictureParams; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.PowerManager; -import android.os.SystemClock; -import android.util.Log; -import android.util.Rational; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.WindowManager; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import androidx.annotation.StringRes; -import androidx.databinding.DataBindingUtil; - -import com.google.common.base.Optional; -import com.google.common.base.Preconditions; -import com.google.common.base.Throwables; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; - -import org.webrtc.RendererCommon; -import org.webrtc.SurfaceViewRenderer; -import org.webrtc.VideoTrack; - -import java.lang.ref.WeakReference; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Set; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ActivityRtpSessionBinding; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.services.AppRTCAudioManager; -import eu.siacs.conversations.services.XmppConnectionService; -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; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; -import eu.siacs.conversations.xmpp.jingle.ContentAddition; -import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; -import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; -import eu.siacs.conversations.xmpp.jingle.Media; -import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; - -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"; - public static final String EXTRA_LAST_REPORTED_STATE = "last_reported_state"; - public static final String EXTRA_LAST_ACTION = "last_action"; - public static final String ACTION_ACCEPT_CALL = "action_accept_call"; - public static final String ACTION_MAKE_VOICE_CALL = "action_make_voice_call"; - public static final String ACTION_MAKE_VIDEO_CALL = "action_make_video_call"; - - private static final int CALL_DURATION_UPDATE_INTERVAL = 333; - - private static final List END_CARD = - Arrays.asList( - RtpEndUserState.APPLICATION_ERROR, - RtpEndUserState.SECURITY_ERROR, - RtpEndUserState.DECLINED_OR_BUSY, - RtpEndUserState.CONNECTIVITY_ERROR, - RtpEndUserState.CONNECTIVITY_LOST_ERROR, - RtpEndUserState.RETRACTED); - private static final List STATES_SHOWING_HELP_BUTTON = - Arrays.asList( - RtpEndUserState.APPLICATION_ERROR, - RtpEndUserState.CONNECTIVITY_ERROR, - RtpEndUserState.SECURITY_ERROR); - private static final List STATES_SHOWING_SWITCH_TO_CHAT = - Arrays.asList( - RtpEndUserState.CONNECTING, - RtpEndUserState.CONNECTED, - RtpEndUserState.RECONNECTING, - RtpEndUserState.INCOMING_CONTENT_ADD); - private static final List STATES_CONSIDERED_CONNECTED = - Arrays.asList( - RtpEndUserState.CONNECTED, - RtpEndUserState.RECONNECTING); - private static final List STATES_SHOWING_PIP_PLACEHOLDER = - Arrays.asList( - RtpEndUserState.ACCEPTING_CALL, - RtpEndUserState.CONNECTING, - RtpEndUserState.RECONNECTING); - private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session"; - private static final int REQUEST_ACCEPT_CALL = 0x1111; - private static final int REQUEST_ACCEPT_CONTENT = 0x1112; - private static final int REQUEST_ADD_CONTENT = 0x1113; - private WeakReference rtpConnectionReference; - - private ActivityRtpSessionBinding binding; - private PowerManager.WakeLock mProximityWakeLock; - - private final Handler mHandler = new Handler(); - private final Runnable mTickExecutor = - new Runnable() { - @Override - public void run() { - updateCallDuration(); - mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL); - } - }; - - private static Set actionToMedia(final String action) { - if (ACTION_MAKE_VIDEO_CALL.equals(action)) { - return ImmutableSet.of(Media.AUDIO, Media.VIDEO); - } else { - return ImmutableSet.of(Media.AUDIO); - } - } - - private static void addSink( - final VideoTrack videoTrack, final SurfaceViewRenderer surfaceViewRenderer) { - try { - videoTrack.addSink(surfaceViewRenderer); - } catch (final IllegalStateException e) { - Log.e( - Config.LOGTAG, - "possible race condition on trying to display video track. ignoring", - e); - } - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - getWindow() - .addFlags( - WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON - | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD - | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED - | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session); - setSupportActionBar(binding.toolbar); - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.activity_rtp_session, menu); - final MenuItem help = menu.findItem(R.id.action_help); - final MenuItem gotoChat = menu.findItem(R.id.action_goto_chat); - final MenuItem switchToVideo = menu.findItem(R.id.action_switch_to_video); - help.setVisible(Config.HELP != null && isHelpButtonVisible()); - gotoChat.setVisible(isSwitchToConversationVisible()); - switchToVideo.setVisible(isSwitchToVideoVisible()); - return super.onCreateOptionsMenu(menu); - } - - @Override - public boolean onKeyDown(final int keyCode, final KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { - if (xmppConnectionService != null) { - if (xmppConnectionService.getNotificationService().stopSoundAndVibration()) { - return true; - } - } - } - return super.onKeyDown(keyCode, event); - } - - private boolean isHelpButtonVisible() { - try { - return STATES_SHOWING_HELP_BUTTON.contains(requireRtpConnection().getEndUserState()); - } catch (IllegalStateException e) { - final Intent intent = getIntent(); - final String state = - intent != null ? intent.getStringExtra(EXTRA_LAST_REPORTED_STATE) : null; - if (state != null) { - return STATES_SHOWING_HELP_BUTTON.contains(RtpEndUserState.valueOf(state)); - } else { - return false; - } - } - } - - private boolean isSwitchToConversationVisible() { - final JingleRtpConnection connection = - this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; - return connection != null - && STATES_SHOWING_SWITCH_TO_CHAT.contains(connection.getEndUserState()); - } - - private boolean isSwitchToVideoVisible() { - final JingleRtpConnection connection = - this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; - if (connection == null) { - return false; - } - return connection.isSwitchToVideoAvailable(); - } - - private void switchToConversation() { - final Contact contact = getWith(); - final Conversation conversation = - xmppConnectionService.findOrCreateConversation( - contact.getAccount(), contact.getJid(), false, true); - switchToConversation(conversation); - } - - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case R.id.action_help: - launchHelpInBrowser(); - return true; - case R.id.action_goto_chat: - switchToConversation(); - return true; - case R.id.action_switch_to_video: - requestPermissionAndSwitchToVideo(); - return true; - } - return super.onOptionsItemSelected(item); - } - - private void launchHelpInBrowser() { - final Intent intent = new Intent(Intent.ACTION_VIEW, Config.HELP); - try { - startActivity(intent); - } catch (final ActivityNotFoundException e) { - Toast.makeText(this, R.string.no_application_found_to_open_link, Toast.LENGTH_LONG) - .show(); - } - } - - private void endCall(View view) { - endCall(); - } - - private void endCall() { - if (this.rtpConnectionReference == null) { - retractSessionProposal(); - finish(); - } else { - requireRtpConnection().endCall(); - } - } - - private void retractSessionProposal() { - final Intent intent = getIntent(); - final String action = intent.getAction(); - final Account account = extractAccount(intent); - final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH)); - final String state = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE); - if (!Intent.ACTION_VIEW.equals(action) - || state == null - || !END_CARD.contains(RtpEndUserState.valueOf(state))) { - resetIntent( - account, with, RtpEndUserState.RETRACTED, actionToMedia(intent.getAction())); - } - xmppConnectionService - .getJingleConnectionManager() - .retractSessionProposal(account, with.asBareJid()); - } - - private void rejectCall(View view) { - requireRtpConnection().rejectCall(); - finish(); - } - - private void acceptCall(View view) { - requestPermissionsAndAcceptCall(); - } - - private void acceptContentAdd() { - try { - requireRtpConnection() - .acceptContentAdd(requireRtpConnection().getPendingContentAddition().summary); - } catch (final IllegalStateException e) { - Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); - } - } - - private void requestPermissionAndSwitchToVideo() { - final List permissions = permissions(ImmutableSet.of(Media.VIDEO, Media.AUDIO)); - if (PermissionUtils.hasPermission(this, permissions, REQUEST_ADD_CONTENT)) { - switchToVideo(); - } - } - - private void switchToVideo() { - try { - requireRtpConnection().addMedia(Media.VIDEO); - } catch (final IllegalStateException e) { - Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); - } - } - - private void acceptContentAdd(final ContentAddition contentAddition) { - if (contentAddition == null || contentAddition.direction != ContentAddition.Direction.INCOMING) { - Log.d(Config.LOGTAG,"ignore press on content-accept button"); - return; - } - requestPermissionAndAcceptContentAdd(contentAddition); - } - - private void requestPermissionAndAcceptContentAdd(final ContentAddition contentAddition) { - final List permissions = permissions(contentAddition.media()); - if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CONTENT)) { - requireRtpConnection().acceptContentAdd(contentAddition.summary); - } - } - - private void rejectContentAdd(final View view) { - requireRtpConnection().rejectContentAdd(); - } - - private void requestPermissionsAndAcceptCall() { - final List permissions = permissions(getMedia()); - if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) { - putScreenInCallMode(); - checkRecorderAndAcceptCall(); - } - } - - private List permissions(final Set media) { - final ImmutableList.Builder permissions = ImmutableList.builder(); - if (media.contains(Media.VIDEO)) { - permissions.add(Manifest.permission.CAMERA).add(Manifest.permission.RECORD_AUDIO); - } else { - permissions.add(Manifest.permission.RECORD_AUDIO); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - permissions.add(Manifest.permission.BLUETOOTH_CONNECT); - } - return permissions.build(); - } - - private void checkRecorderAndAcceptCall() { - checkMicrophoneAvailabilityAsync(); - try { - requireRtpConnection().acceptCall(); - } catch (final IllegalStateException e) { - Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); - } - } - - private void checkMicrophoneAvailabilityAsync() { - new Thread(new MicrophoneAvailabilityCheck(this)).start(); - } - - private static class MicrophoneAvailabilityCheck implements Runnable { - - private final WeakReference activityReference; - - private MicrophoneAvailabilityCheck(final Activity activity) { - this.activityReference = new WeakReference<>(activity); - } - - @Override - public void run() { - final long start = SystemClock.elapsedRealtime(); - final boolean isMicrophoneAvailable = AppRTCAudioManager.isMicrophoneAvailable(); - final long stop = SystemClock.elapsedRealtime(); - Log.d(Config.LOGTAG, "checking microphone availability took " + (stop - start) + "ms"); - if (isMicrophoneAvailable) { - return; - } - final Activity activity = activityReference.get(); - if (activity == null) { - return; - } - activity.runOnUiThread( - () -> - Toast.makeText( - activity, - R.string.microphone_unavailable, - Toast.LENGTH_LONG) - .show()); - } - } - - private void putScreenInCallMode() { - putScreenInCallMode(requireRtpConnection().getMedia()); - } - - private void putScreenInCallMode(final Set media) { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - if (!media.contains(Media.VIDEO)) { - final JingleRtpConnection rtpConnection = - rtpConnectionReference != null ? rtpConnectionReference.get() : null; - final AppRTCAudioManager audioManager = - rtpConnection == null ? null : rtpConnection.getAudioManager(); - if (audioManager == null - || audioManager.getSelectedAudioDevice() - == AppRTCAudioManager.AudioDevice.EARPIECE) { - acquireProximityWakeLock(); - } - } - } - - @SuppressLint("WakelockTimeout") - private void acquireProximityWakeLock() { - final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); - if (powerManager == null) { - Log.e(Config.LOGTAG, "power manager not available"); - return; - } - if (isFinishing()) { - Log.e(Config.LOGTAG, "do not acquire wakelock. activity is finishing"); - return; - } - if (this.mProximityWakeLock == null) { - this.mProximityWakeLock = - powerManager.newWakeLock( - PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, PROXIMITY_WAKE_LOCK_TAG); - } - if (!this.mProximityWakeLock.isHeld()) { - Log.d(Config.LOGTAG, "acquiring proximity wake lock"); - this.mProximityWakeLock.acquire(); - } - } - - private void releaseProximityWakeLock() { - if (this.mProximityWakeLock != null && mProximityWakeLock.isHeld()) { - Log.d(Config.LOGTAG, "releasing proximity wake lock"); - this.mProximityWakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY); - this.mProximityWakeLock = null; - } - } - - private void putProximityWakeLockInProperState( - final AppRTCAudioManager.AudioDevice audioDevice) { - if (audioDevice == AppRTCAudioManager.AudioDevice.EARPIECE) { - acquireProximityWakeLock(); - } else { - releaseProximityWakeLock(); - } - } - - @Override - protected void refreshUiReal() {} - - @Override - public void onNewIntent(final Intent intent) { - Log.d(Config.LOGTAG, this.getClass().getName() + ".onNewIntent()"); - super.onNewIntent(intent); - setIntent(intent); - if (xmppConnectionService == null) { - Log.d( - Config.LOGTAG, - "RtpSessionActivity: background service wasn't bound in onNewIntent()"); - return; - } - final Account account = extractAccount(intent); - final String action = intent.getAction(); - final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH)); - final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID); - if (sessionId != null) { - Log.d(Config.LOGTAG, "reinitializing from onNewIntent()"); - if (initializeActivityWithRunningRtpSession(account, with, sessionId)) { - return; - } - if (ACTION_ACCEPT_CALL.equals(intent.getAction())) { - Log.d(Config.LOGTAG, "accepting call from onNewIntent()"); - requestPermissionsAndAcceptCall(); - resetIntent(intent.getExtras()); - } - } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) { - proposeJingleRtpSession(account, with, actionToMedia(action)); - setWith(account.getRoster().getContact(with), null); - } else { - throw new IllegalStateException("received onNewIntent without sessionId"); - } - } - - @Override - void onBackendConnected() { - final Intent intent = getIntent(); - final String action = intent.getAction(); - final Account account = extractAccount(intent); - final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH)); - final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID); - if (sessionId != null) { - if (initializeActivityWithRunningRtpSession(account, with, sessionId)) { - return; - } - if (ACTION_ACCEPT_CALL.equals(intent.getAction())) { - Log.d(Config.LOGTAG, "intent action was accept"); - requestPermissionsAndAcceptCall(); - resetIntent(intent.getExtras()); - } - } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) { - proposeJingleRtpSession(account, with, actionToMedia(action)); - setWith(account.getRoster().getContact(with), null); - } else if (Intent.ACTION_VIEW.equals(action)) { - final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE); - final RtpEndUserState state = - extraLastState == null ? null : RtpEndUserState.valueOf(extraLastState); - if (state != null) { - Log.d(Config.LOGTAG, "restored last state from intent extra"); - updateButtonConfiguration(state); - updateVerifiedShield(false); - updateStateDisplay(state); - updateIncomingCallScreen(state); - invalidateOptionsMenu(); - } - setWith(account.getRoster().getContact(with), state); - if (xmppConnectionService - .getJingleConnectionManager() - .fireJingleRtpConnectionStateUpdates()) { - return; - } - if (END_CARD.contains(state) - || xmppConnectionService - .getJingleConnectionManager() - .hasMatchingProposal(account, with)) { - return; - } - Log.d(Config.LOGTAG, "restored state (" + state + ") was not an end card. finishing"); - finish(); - } - } - - private void setWidth(final RtpEndUserState state) { - setWith(getWith(), state); - } - - private void setWith(final Contact contact, final RtpEndUserState state) { - binding.with.setText(contact.getDisplayName()); - if (Arrays.asList(RtpEndUserState.INCOMING_CALL, RtpEndUserState.ACCEPTING_CALL) - .contains(state)) { - binding.withJid.setText(contact.getJid().asBareJid().toEscapedString()); - binding.withJid.setVisibility(View.VISIBLE); - } else { - binding.withJid.setVisibility(View.GONE); - } - } - - private void proposeJingleRtpSession( - final Account account, final Jid with, final Set media) { - checkMicrophoneAvailabilityAsync(); - if (with.isBareJid()) { - xmppConnectionService - .getJingleConnectionManager() - .proposeJingleRtpSession(account, with, media); - } else { - final String sessionId = - xmppConnectionService - .getJingleConnectionManager() - .initializeRtpSession(account, with, media); - initializeActivityWithRunningRtpSession(account, with, sessionId); - resetIntent(account, with, sessionId); - } - putScreenInCallMode(media); - } - - @Override - public void onRequestPermissionsResult( - int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - final PermissionUtils.PermissionResult permissionResult = - PermissionUtils.removeBluetoothConnect(permissions, grantResults); - if (PermissionUtils.allGranted(permissionResult.grantResults)) { - if (requestCode == REQUEST_ACCEPT_CALL) { - checkRecorderAndAcceptCall(); - } else if (requestCode == REQUEST_ACCEPT_CONTENT) { - acceptContentAdd(); - } else if (requestCode == REQUEST_ADD_CONTENT) { - switchToVideo(); - } - } else { - @StringRes int res; - final String firstDenied = - getFirstDenied(permissionResult.grantResults, permissionResult.permissions); - if (firstDenied == null) { - return; - } - if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) { - res = R.string.no_microphone_permission; - } else if (Manifest.permission.CAMERA.equals(firstDenied)) { - res = R.string.no_camera_permission; - } else { - throw new IllegalStateException("Invalid permission result request"); - } - Toast.makeText(this, getString(res, getString(R.string.app_name)), Toast.LENGTH_SHORT) - .show(); - } - } - - @Override - 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(); - if (jingleRtpConnection != null) { - releaseVideoTracks(jingleRtpConnection); - } - releaseProximityWakeLock(); - super.onStop(); - } - - private void releaseVideoTracks(final JingleRtpConnection jingleRtpConnection) { - final Optional remoteVideo = jingleRtpConnection.getRemoteVideoTrack(); - if (remoteVideo.isPresent()) { - remoteVideo.get().removeSink(binding.remoteVideo); - } - final Optional localVideo = jingleRtpConnection.getLocalVideoTrack(); - if (localVideo.isPresent()) { - localVideo.get().removeSink(binding.localVideo); - } - } - - @Override - public void onBackPressed() { - if (isConnected()) { - if (switchToPictureInPicture()) { - return; - } - } else { - endCall(); - } - super.onBackPressed(); - } - - @Override - public void onUserLeaveHint() { - super.onUserLeaveHint(); - if (switchToPictureInPicture()) { - return; - } - // TODO apparently this method is not getting called on Android 10 when using the task - // switcher - if (emptyReference(rtpConnectionReference) && xmppConnectionService != null) { - retractSessionProposal(); - } - } - - private boolean isConnected() { - final JingleRtpConnection connection = - this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; - final RtpEndUserState endUserState = connection == null ? null : connection.getEndUserState(); - return STATES_CONSIDERED_CONNECTED.contains(endUserState) || endUserState == RtpEndUserState.INCOMING_CONTENT_ADD; - } - - private boolean switchToPictureInPicture() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && deviceSupportsPictureInPicture()) { - if (shouldBePictureInPicture()) { - startPictureInPicture(); - return true; - } - } - return false; - } - - @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(clippedRational).build()); - } catch (final IllegalStateException e) { - // this sometimes happens on Samsung phones (possibly when Knox is enabled) - Log.w(Config.LOGTAG, "unable to enter picture in picture mode", e); - } - } - - @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); - } else { - return false; - } - } - - private boolean shouldBePictureInPicture() { - try { - final JingleRtpConnection rtpConnection = requireRtpConnection(); - return rtpConnection.getMedia().contains(Media.VIDEO) - && Arrays.asList( - RtpEndUserState.ACCEPTING_CALL, - RtpEndUserState.CONNECTING, - RtpEndUserState.CONNECTED) - .contains(rtpConnection.getEndUserState()); - } catch (final IllegalStateException e) { - return false; - } - } - - private boolean initializeActivityWithRunningRtpSession( - final Account account, Jid with, String sessionId) { - final WeakReference reference = - xmppConnectionService - .getJingleConnectionManager() - .findJingleRtpConnection(account, with, sessionId); - if (reference == null || reference.get() == null) { - final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession = - xmppConnectionService - .getJingleConnectionManager() - .getTerminalSessionState(with, sessionId); - if (terminatedRtpSession == null) { - throw new IllegalStateException( - "failed to initialize activity with running rtp session. session not found"); - } - initializeWithTerminatedSessionState(account, with, terminatedRtpSession); - return true; - } - this.rtpConnectionReference = reference; - final RtpEndUserState currentState = requireRtpConnection().getEndUserState(); - final boolean verified = requireRtpConnection().isVerified(); - if (currentState == RtpEndUserState.ENDED) { - finish(); - return true; - } - final Set media = getMedia(); - final ContentAddition contentAddition = getPendingContentAddition(); - if (currentState == RtpEndUserState.INCOMING_CALL) { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - if (JingleRtpConnection.STATES_SHOWING_ONGOING_CALL.contains( - requireRtpConnection().getState())) { - putScreenInCallMode(); - } - setWidth(currentState); - updateVideoViews(currentState); - updateStateDisplay(currentState, media, contentAddition); - updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(currentState)); - updateButtonConfiguration(currentState, media, contentAddition); - updateIncomingCallScreen(currentState); - invalidateOptionsMenu(); - return false; - } - - private void initializeWithTerminatedSessionState( - final Account account, - final Jid with, - final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession) { - Log.d(Config.LOGTAG, "initializeWithTerminatedSessionState()"); - if (terminatedRtpSession.state == RtpEndUserState.ENDED) { - finish(); - return; - } - final RtpEndUserState state = terminatedRtpSession.state; - resetIntent(account, with, terminatedRtpSession.state, terminatedRtpSession.media); - updateButtonConfiguration(state); - updateStateDisplay(state); - updateIncomingCallScreen(state); - updateCallDuration(); - updateVerifiedShield(false); - invalidateOptionsMenu(); - setWith(account.getRoster().getContact(with), state); - } - - private void reInitializeActivityWithRunningRtpSession( - final Account account, Jid with, String sessionId) { - runOnUiThread(() -> initializeActivityWithRunningRtpSession(account, with, sessionId)); - resetIntent(account, with, sessionId); - } - - private void resetIntent(final Account account, final Jid with, final String sessionId) { - final Intent intent = new Intent(Intent.ACTION_VIEW); - intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString()); - intent.putExtra(EXTRA_WITH, with.toEscapedString()); - intent.putExtra(EXTRA_SESSION_ID, sessionId); - setIntent(intent); - } - - private void ensureSurfaceViewRendererIsSetup(final SurfaceViewRenderer surfaceViewRenderer) { - surfaceViewRenderer.setVisibility(View.VISIBLE); - try { - surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null); - } catch (final IllegalStateException e) { - // Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized"); - } - surfaceViewRenderer.setEnableHardwareScaler(true); - } - - private void updateStateDisplay(final RtpEndUserState state) { - updateStateDisplay(state, Collections.emptySet(), null); - } - - private void updateStateDisplay(final RtpEndUserState state, final Set media, final ContentAddition contentAddition) { - switch (state) { - case INCOMING_CALL: - Preconditions.checkArgument(media.size() > 0, "Media must not be empty"); - if (media.contains(Media.VIDEO)) { - setTitle(R.string.rtp_state_incoming_video_call); - } else { - setTitle(R.string.rtp_state_incoming_call); - } - break; - case INCOMING_CONTENT_ADD: - if (contentAddition != null && contentAddition.media().contains(Media.VIDEO)) { - setTitle(R.string.rtp_state_content_add_video); - } else { - setTitle(R.string.rtp_state_content_add); - } - break; - case CONNECTING: - setTitle(R.string.rtp_state_connecting); - break; - case CONNECTED: - setTitle(R.string.rtp_state_connected); - break; - case RECONNECTING: - setTitle(R.string.rtp_state_reconnecting); - break; - case ACCEPTING_CALL: - setTitle(R.string.rtp_state_accepting_call); - break; - case ENDING_CALL: - setTitle(R.string.rtp_state_ending_call); - break; - case FINDING_DEVICE: - setTitle(R.string.rtp_state_finding_device); - break; - case RINGING: - setTitle(R.string.rtp_state_ringing); - break; - case DECLINED_OR_BUSY: - setTitle(R.string.rtp_state_declined_or_busy); - break; - case CONNECTIVITY_ERROR: - setTitle(R.string.rtp_state_connectivity_error); - break; - case CONNECTIVITY_LOST_ERROR: - setTitle(R.string.rtp_state_connectivity_lost_error); - break; - case RETRACTED: - setTitle(R.string.rtp_state_retracted); - break; - case APPLICATION_ERROR: - setTitle(R.string.rtp_state_application_failure); - break; - case SECURITY_ERROR: - setTitle(R.string.rtp_state_security_error); - break; - case ENDED: - throw new IllegalStateException( - "Activity should have called finishAndReleaseWakeLock();"); - default: - throw new IllegalStateException( - String.format("State %s has not been handled in UI", state)); - } - } - - private void updateVerifiedShield(final boolean verified) { - if (isPictureInPicture()) { - this.binding.verified.setVisibility(View.GONE); - return; - } - this.binding.verified.setVisibility(verified ? View.VISIBLE : View.GONE); - } - - private void updateIncomingCallScreen(final RtpEndUserState state) { - updateIncomingCallScreen(state, null); - } - - private void updateIncomingCallScreen(final RtpEndUserState state, final Contact contact) { - if (state == RtpEndUserState.INCOMING_CALL || state == RtpEndUserState.ACCEPTING_CALL) { - final boolean show = getResources().getBoolean(R.bool.show_avatar_incoming_call); - if (show) { - binding.contactPhoto.setVisibility(View.VISIBLE); - if (contact == null) { - AvatarWorkerTask.loadAvatar( - getWith(), binding.contactPhoto, R.dimen.publish_avatar_size); - } else { - AvatarWorkerTask.loadAvatar( - contact, binding.contactPhoto, R.dimen.publish_avatar_size); - } - } else { - binding.contactPhoto.setVisibility(View.GONE); - } - final Account account = contact == null ? getWith().getAccount() : contact.getAccount(); - binding.detailsAccount.setVisibility(View.VISIBLE); - binding.detailsAccount.setText( - getString( - R.string.using_account, - account.getJid().asBareJid().toEscapedString())); - } else { - binding.detailsAccount.setVisibility(View.GONE); - binding.contactPhoto.setVisibility(View.GONE); - } - } - - private Set getMedia() { - return requireRtpConnection().getMedia(); - } - - public ContentAddition getPendingContentAddition() { - return requireRtpConnection().getPendingContentAddition(); - } - - private void updateButtonConfiguration(final RtpEndUserState state) { - updateButtonConfiguration(state, Collections.emptySet(), null); - } - - @SuppressLint("RestrictedApi") - private void updateButtonConfiguration(final RtpEndUserState state, final Set media, final ContentAddition contentAddition) { - if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) { - this.binding.rejectCall.setVisibility(View.INVISIBLE); - this.binding.endCall.setVisibility(View.INVISIBLE); - this.binding.acceptCall.setVisibility(View.INVISIBLE); - } else if (state == RtpEndUserState.INCOMING_CALL) { - this.binding.rejectCall.setContentDescription(getString(R.string.dismiss_call)); - this.binding.rejectCall.setOnClickListener(this::rejectCall); - this.binding.rejectCall.setImageResource(R.drawable.ic_call_end_white_48dp); - this.binding.rejectCall.setVisibility(View.VISIBLE); - this.binding.endCall.setVisibility(View.INVISIBLE); - this.binding.acceptCall.setContentDescription(getString(R.string.answer_call)); - this.binding.acceptCall.setOnClickListener(this::acceptCall); - this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp); - this.binding.acceptCall.setVisibility(View.VISIBLE); - } else if (state == RtpEndUserState.INCOMING_CONTENT_ADD) { - this.binding.rejectCall.setContentDescription(getString(R.string.reject_switch_to_video)); - this.binding.rejectCall.setOnClickListener(this::rejectContentAdd); - this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp); - this.binding.rejectCall.setVisibility(View.VISIBLE); - this.binding.endCall.setVisibility(View.INVISIBLE); - this.binding.acceptCall.setContentDescription(getString(R.string.accept)); - this.binding.acceptCall.setOnClickListener((v -> acceptContentAdd(contentAddition))); - this.binding.acceptCall.setImageResource(R.drawable.ic_baseline_check_24); - this.binding.acceptCall.setVisibility(View.VISIBLE); - } else if (state == RtpEndUserState.DECLINED_OR_BUSY) { - this.binding.rejectCall.setContentDescription(getString(R.string.exit)); - this.binding.rejectCall.setOnClickListener(this::exit); - this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp); - this.binding.rejectCall.setVisibility(View.VISIBLE); - this.binding.endCall.setVisibility(View.INVISIBLE); - this.binding.acceptCall.setContentDescription(getString(R.string.record_voice_mail)); - this.binding.acceptCall.setOnClickListener(this::recordVoiceMail); - this.binding.acceptCall.setImageResource(R.drawable.ic_voicemail_white_24dp); - this.binding.acceptCall.setVisibility(View.VISIBLE); - } else if (asList( - RtpEndUserState.CONNECTIVITY_ERROR, - RtpEndUserState.CONNECTIVITY_LOST_ERROR, - RtpEndUserState.APPLICATION_ERROR, - RtpEndUserState.RETRACTED, - RtpEndUserState.SECURITY_ERROR) - .contains(state)) { - this.binding.rejectCall.setContentDescription(getString(R.string.exit)); - this.binding.rejectCall.setOnClickListener(this::exit); - this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp); - this.binding.rejectCall.setVisibility(View.VISIBLE); - this.binding.endCall.setVisibility(View.INVISIBLE); - this.binding.acceptCall.setContentDescription(getString(R.string.try_again)); - this.binding.acceptCall.setOnClickListener(this::retry); - this.binding.acceptCall.setImageResource(R.drawable.ic_replay_white_48dp); - this.binding.acceptCall.setVisibility(View.VISIBLE); - } else { - this.binding.rejectCall.setVisibility(View.INVISIBLE); - this.binding.endCall.setContentDescription(getString(R.string.hang_up)); - this.binding.endCall.setOnClickListener(this::endCall); - this.binding.endCall.setImageResource(R.drawable.ic_call_end_white_48dp); - this.binding.endCall.setVisibility(View.VISIBLE); - this.binding.acceptCall.setVisibility(View.INVISIBLE); - } - updateInCallButtonConfiguration(state, media); - } - - private boolean isPictureInPicture() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - return isInPictureInPictureMode(); - } else { - return false; - } - } - - private void updateInCallButtonConfiguration() { - updateInCallButtonConfiguration( - requireRtpConnection().getEndUserState(), requireRtpConnection().getMedia()); - } - - @SuppressLint("RestrictedApi") - private void updateInCallButtonConfiguration( - final RtpEndUserState state, final Set media) { - if (STATES_CONSIDERED_CONNECTED.contains(state) && !isPictureInPicture()) { - Preconditions.checkArgument(media.size() > 0, "Media must not be empty"); - if (media.contains(Media.VIDEO)) { - final JingleRtpConnection rtpConnection = requireRtpConnection(); - updateInCallButtonConfigurationVideo( - rtpConnection.isVideoEnabled(), rtpConnection.isCameraSwitchable()); - } else { - final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager(); - updateInCallButtonConfigurationSpeaker( - audioManager.getSelectedAudioDevice(), - audioManager.getAudioDevices().size()); - this.binding.inCallActionFarRight.setVisibility(View.GONE); - } - if (media.contains(Media.AUDIO)) { - updateInCallButtonConfigurationMicrophone( - requireRtpConnection().isMicrophoneEnabled()); - } else { - this.binding.inCallActionLeft.setVisibility(View.GONE); - } - } else { - this.binding.inCallActionLeft.setVisibility(View.GONE); - this.binding.inCallActionRight.setVisibility(View.GONE); - this.binding.inCallActionFarRight.setVisibility(View.GONE); - } - } - - @SuppressLint("RestrictedApi") - private void updateInCallButtonConfigurationSpeaker( - final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices) { - switch (selectedAudioDevice) { - case EARPIECE: - this.binding.inCallActionRight.setImageResource( - R.drawable.ic_volume_off_black_24dp); - if (numberOfChoices >= 2) { - this.binding.inCallActionRight.setOnClickListener(this::switchToSpeaker); - } else { - this.binding.inCallActionRight.setOnClickListener(null); - this.binding.inCallActionRight.setClickable(false); - } - break; - case WIRED_HEADSET: - this.binding.inCallActionRight.setImageResource(R.drawable.ic_headset_black_24dp); - this.binding.inCallActionRight.setOnClickListener(null); - this.binding.inCallActionRight.setClickable(false); - break; - case SPEAKER_PHONE: - this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_up_black_24dp); - if (numberOfChoices >= 2) { - this.binding.inCallActionRight.setOnClickListener(this::switchToEarpiece); - } else { - this.binding.inCallActionRight.setOnClickListener(null); - this.binding.inCallActionRight.setClickable(false); - } - break; - case BLUETOOTH: - this.binding.inCallActionRight.setImageResource( - R.drawable.ic_bluetooth_audio_black_24dp); - this.binding.inCallActionRight.setOnClickListener(null); - this.binding.inCallActionRight.setClickable(false); - break; - } - this.binding.inCallActionRight.setVisibility(View.VISIBLE); - } - - @SuppressLint("RestrictedApi") - private void updateInCallButtonConfigurationVideo( - final boolean videoEnabled, final boolean isCameraSwitchable) { - this.binding.inCallActionRight.setVisibility(View.VISIBLE); - if (isCameraSwitchable) { - this.binding.inCallActionFarRight.setImageResource( - R.drawable.ic_flip_camera_android_black_24dp); - this.binding.inCallActionFarRight.setVisibility(View.VISIBLE); - this.binding.inCallActionFarRight.setOnClickListener(this::switchCamera); - } else { - this.binding.inCallActionFarRight.setVisibility(View.GONE); - } - if (videoEnabled) { - this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_black_24dp); - this.binding.inCallActionRight.setOnClickListener(this::disableVideo); - } else { - this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_off_black_24dp); - this.binding.inCallActionRight.setOnClickListener(this::enableVideo); - } - } - - private void switchCamera(final View view) { - Futures.addCallback( - requireRtpConnection().switchCamera(), - new FutureCallback() { - @Override - public void onSuccess(@Nullable Boolean isFrontCamera) { - binding.localVideo.setMirror(isFrontCamera); - } - - @Override - public void onFailure(@NonNull final Throwable throwable) { - Log.d( - Config.LOGTAG, - "could not switch camera", - Throwables.getRootCause(throwable)); - Toast.makeText( - RtpSessionActivity.this, - R.string.could_not_switch_camera, - Toast.LENGTH_LONG) - .show(); - } - }, - MainThreadExecutor.getInstance()); - } - - private void enableVideo(View view) { - try { - requireRtpConnection().setVideoEnabled(true); - } catch (final IllegalStateException e) { - Toast.makeText(this, R.string.unable_to_enable_video, Toast.LENGTH_SHORT).show(); - return; - } - updateInCallButtonConfigurationVideo(true, requireRtpConnection().isCameraSwitchable()); - } - - private void disableVideo(View view) { - final JingleRtpConnection rtpConnection = requireRtpConnection(); - final ContentAddition pending = rtpConnection.getPendingContentAddition(); - if (pending != null && pending.direction == ContentAddition.Direction.OUTGOING) { - rtpConnection.retractContentAdd(); - return; - } - requireRtpConnection().setVideoEnabled(false); - updateInCallButtonConfigurationVideo(false, requireRtpConnection().isCameraSwitchable()); - } - - @SuppressLint("RestrictedApi") - private void updateInCallButtonConfigurationMicrophone(final boolean microphoneEnabled) { - if (microphoneEnabled) { - this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_black_24dp); - this.binding.inCallActionLeft.setOnClickListener(this::disableMicrophone); - } else { - this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_off_black_24dp); - this.binding.inCallActionLeft.setOnClickListener(this::enableMicrophone); - } - this.binding.inCallActionLeft.setVisibility(View.VISIBLE); - } - - private void updateCallDuration() { - final JingleRtpConnection connection = - this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; - if (connection == null || connection.getMedia().contains(Media.VIDEO)) { - this.binding.duration.setVisibility(View.GONE); - return; - } - 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); - } - } - - private void updateVideoViews(final RtpEndUserState state) { - if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) { - binding.localVideo.setVisibility(View.GONE); - binding.localVideo.release(); - binding.remoteVideoWrapper.setVisibility(View.GONE); - binding.remoteVideo.release(); - binding.pipLocalMicOffIndicator.setVisibility(View.GONE); - if (isPictureInPicture()) { - binding.appBarLayout.setVisibility(View.GONE); - binding.pipPlaceholder.setVisibility(View.VISIBLE); - if (Arrays.asList( - RtpEndUserState.APPLICATION_ERROR, - RtpEndUserState.CONNECTIVITY_ERROR, - RtpEndUserState.SECURITY_ERROR) - .contains(state)) { - binding.pipWarning.setVisibility(View.VISIBLE); - binding.pipWaiting.setVisibility(View.GONE); - } else { - binding.pipWarning.setVisibility(View.GONE); - binding.pipWaiting.setVisibility(View.GONE); - } - } else { - binding.appBarLayout.setVisibility(View.VISIBLE); - binding.pipPlaceholder.setVisibility(View.GONE); - } - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - return; - } - if (isPictureInPicture() && STATES_SHOWING_PIP_PLACEHOLDER.contains(state)) { - binding.localVideo.setVisibility(View.GONE); - binding.remoteVideoWrapper.setVisibility(View.GONE); - binding.appBarLayout.setVisibility(View.GONE); - binding.pipPlaceholder.setVisibility(View.VISIBLE); - binding.pipWarning.setVisibility(View.GONE); - binding.pipWaiting.setVisibility(View.VISIBLE); - binding.pipLocalMicOffIndicator.setVisibility(View.GONE); - return; - } - final Optional localVideoTrack = getLocalVideoTrack(); - if (localVideoTrack.isPresent() && !isPictureInPicture()) { - ensureSurfaceViewRendererIsSetup(binding.localVideo); - // paint local view over remote view - binding.localVideo.setZOrderMediaOverlay(true); - binding.localVideo.setMirror(requireRtpConnection().isFrontCamera()); - addSink(localVideoTrack.get(), binding.localVideo); - } else { - binding.localVideo.setVisibility(View.GONE); - } - final Optional remoteVideoTrack = getRemoteVideoTrack(); - if (remoteVideoTrack.isPresent()) { - ensureSurfaceViewRendererIsSetup(binding.remoteVideo); - addSink(remoteVideoTrack.get(), binding.remoteVideo); - binding.remoteVideo.setScalingType( - RendererCommon.ScalingType.SCALE_ASPECT_FILL, - RendererCommon.ScalingType.SCALE_ASPECT_FIT); - if (state == RtpEndUserState.CONNECTED) { - binding.appBarLayout.setVisibility(View.GONE); - 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); - } - if (isPictureInPicture() && !requireRtpConnection().isMicrophoneEnabled()) { - binding.pipLocalMicOffIndicator.setVisibility(View.VISIBLE); - } else { - binding.pipLocalMicOffIndicator.setVisibility(View.GONE); - } - } else { - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - binding.remoteVideoWrapper.setVisibility(View.GONE); - binding.pipLocalMicOffIndicator.setVisibility(View.GONE); - } - } - - private Optional getLocalVideoTrack() { - final JingleRtpConnection connection = - this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; - if (connection == null) { - return Optional.absent(); - } - return connection.getLocalVideoTrack(); - } - - private Optional getRemoteVideoTrack() { - final JingleRtpConnection connection = - this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; - if (connection == null) { - return Optional.absent(); - } - return connection.getRemoteVideoTrack(); - } - - private void disableMicrophone(View view) { - final JingleRtpConnection rtpConnection = requireRtpConnection(); - if (rtpConnection.setMicrophoneEnabled(false)) { - updateInCallButtonConfiguration(); - } - } - - private void enableMicrophone(View view) { - final JingleRtpConnection rtpConnection = requireRtpConnection(); - if (rtpConnection.setMicrophoneEnabled(true)) { - updateInCallButtonConfiguration(); - } - } - - private void switchToEarpiece(View view) { - requireRtpConnection() - .getAudioManager() - .setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE); - acquireProximityWakeLock(); - } - - private void switchToSpeaker(View view) { - requireRtpConnection() - .getAudioManager() - .setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE); - releaseProximityWakeLock(); - } - - private void retry(View view) { - final Intent intent = getIntent(); - final Account account = extractAccount(intent); - final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH)); - final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION); - final String action = intent.getAction(); - final Set media = actionToMedia(lastAction == null ? action : lastAction); - this.rtpConnectionReference = null; - Log.d(Config.LOGTAG, "attempting retry with " + with.toEscapedString()); - proposeJingleRtpSession(account, with, media); - } - - private void exit(final View view) { - finish(); - } - - private void recordVoiceMail(final View view) { - final Intent intent = getIntent(); - final Account account = extractAccount(intent); - final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH)); - final Conversation conversation = - xmppConnectionService.findOrCreateConversation(account, with, false, true); - final Intent launchIntent = new Intent(this, ConversationsActivity.class); - launchIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION); - launchIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid()); - launchIntent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP); - launchIntent.putExtra( - ConversationsActivity.EXTRA_POST_INIT_ACTION, - ConversationsActivity.POST_ACTION_RECORD_VOICE); - startActivity(launchIntent); - finish(); - } - - private Contact getWith() { - final AbstractJingleConnection.Id id = requireRtpConnection().getId(); - final Account account = id.account; - return account.getRoster().getContact(id.with); - } - - private JingleRtpConnection requireRtpConnection() { - final JingleRtpConnection connection = - this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null; - if (connection == null) { - throw new IllegalStateException("No RTP connection found"); - } - return connection; - } - - @Override - public void onJingleRtpConnectionUpdate( - Account account, Jid with, final String sessionId, RtpEndUserState state) { - Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")"); - if (END_CARD.contains(state)) { - Log.d(Config.LOGTAG, "end card reached"); - releaseProximityWakeLock(); - runOnUiThread( - () -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)); - } - if (with.isBareJid()) { - updateRtpSessionProposalState(account, with, state); - return; - } - if (emptyReference(this.rtpConnectionReference)) { - if (END_CARD.contains(state)) { - Log.d(Config.LOGTAG, "not reinitializing session"); - return; - } - // this happens when going from proposed session to actual session - reInitializeActivityWithRunningRtpSession(account, with, sessionId); - return; - } - final AbstractJingleConnection.Id id = requireRtpConnection().getId(); - final boolean verified = requireRtpConnection().isVerified(); - final Set media = getMedia(); - final ContentAddition contentAddition = getPendingContentAddition(); - final Contact contact = getWith(); - if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) { - if (state == RtpEndUserState.ENDED) { - finish(); - return; - } - runOnUiThread( - () -> { - updateStateDisplay(state, media, contentAddition); - updateVerifiedShield( - verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(state)); - updateButtonConfiguration(state, media, contentAddition); - updateVideoViews(state); - updateIncomingCallScreen(state, contact); - invalidateOptionsMenu(); - }); - if (END_CARD.contains(state)) { - final JingleRtpConnection rtpConnection = requireRtpConnection(); - resetIntent(account, with, state, rtpConnection.getMedia()); - releaseVideoTracks(rtpConnection); - this.rtpConnectionReference = null; - } - } else { - Log.d(Config.LOGTAG, "received update for other rtp session"); - } - } - - @Override - public void onAudioDeviceChanged( - final AppRTCAudioManager.AudioDevice selectedAudioDevice, - final Set availableAudioDevices) { - Log.d( - Config.LOGTAG, - "onAudioDeviceChanged in activity: selected:" - + selectedAudioDevice - + ", available:" - + availableAudioDevices); - try { - final RtpEndUserState endUserState = requireRtpConnection().getEndUserState(); - final Set media = getMedia(); - if (END_CARD.contains(endUserState)) { - Log.d( - Config.LOGTAG, - "onAudioDeviceChanged() nothing to do because end card has been reached"); - } else { - if (Media.audioOnly(media) && endUserState == RtpEndUserState.CONNECTED) { - final AppRTCAudioManager audioManager = - requireRtpConnection().getAudioManager(); - updateInCallButtonConfigurationSpeaker( - audioManager.getSelectedAudioDevice(), - audioManager.getAudioDevices().size()); - } - Log.d( - Config.LOGTAG, - "put proximity wake lock into proper state after device update"); - putProximityWakeLockInProperState(selectedAudioDevice); - } - } catch (final IllegalStateException e) { - Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed"); - } - } - - private void updateRtpSessionProposalState( - final Account account, final Jid with, final RtpEndUserState state) { - final Intent currentIntent = getIntent(); - final String withExtra = - currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH); - if (withExtra == null) { - return; - } - if (Jid.ofEscaped(withExtra).asBareJid().equals(with)) { - runOnUiThread( - () -> { - updateVerifiedShield(false); - updateStateDisplay(state); - updateButtonConfiguration(state); - updateIncomingCallScreen(state); - invalidateOptionsMenu(); - }); - resetIntent(account, with, state, actionToMedia(currentIntent.getAction())); - } - } - - private void resetIntent(final Bundle extras) { - final Intent intent = new Intent(Intent.ACTION_VIEW); - intent.putExtras(extras); - setIntent(intent); - } - - private void resetIntent( - final Account account, Jid with, final RtpEndUserState state, final Set media) { - final Intent intent = new Intent(Intent.ACTION_VIEW); - intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString()); - if (account.getRoster() - .getContact(with) - .getPresences() - .anySupport(Namespace.JINGLE_MESSAGE)) { - intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString()); - } else { - intent.putExtra(EXTRA_WITH, with.toEscapedString()); - } - intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString()); - intent.putExtra( - EXTRA_LAST_ACTION, - media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL); - setIntent(intent); - } - - private static boolean emptyReference(final WeakReference weakReference) { - return weakReference == null || weakReference.get() == null; - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/ScanActivity.java b/src/main/java/eu/siacs/conversations/ui/ScanActivity.java deleted file mode 100644 index 32b374d34..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ScanActivity.java +++ /dev/null @@ -1,322 +0,0 @@ -/* - * Copyright 2012-2015 the original author or authors. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package eu.siacs.conversations.ui; - -import android.Manifest; -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.SurfaceTexture; -import android.hardware.Camera; -import android.hardware.Camera.CameraInfo; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Process; -import android.os.Vibrator; -import android.util.Log; -import android.view.KeyEvent; -import android.view.Surface; -import android.view.TextureView; -import android.view.TextureView.SurfaceTextureListener; -import android.view.View; -import android.view.WindowManager; - -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; - -import com.google.zxing.BinaryBitmap; -import com.google.zxing.DecodeHintType; -import com.google.zxing.PlanarYUVLuminanceSource; -import com.google.zxing.ReaderException; -import com.google.zxing.Result; -import com.google.zxing.ResultPointCallback; -import com.google.zxing.common.HybridBinarizer; -import com.google.zxing.qrcode.QRCodeReader; - -import java.util.EnumMap; -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.widget.ScannerView; -import me.drakeet.support.toast.ToastCompat; - -/** - * @author Andreas Schildbach - */ -@SuppressWarnings("deprecation") -public final class ScanActivity extends Activity implements SurfaceTextureListener, ActivityCompat.OnRequestPermissionsResultCallback { - public static final String INTENT_EXTRA_RESULT = "result"; - - public static final int REQUEST_SCAN_QR_CODE = 0x0987; - private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN = 0x6789; - - private static final long VIBRATE_DURATION = 50L; - private static final long AUTO_FOCUS_INTERVAL_MS = 2500L; - private static boolean DISABLE_CONTINUOUS_AUTOFOCUS = - Build.MODEL.equals("GT-I9100") // Galaxy S2 - || Build.MODEL.equals("SGH-T989") // Galaxy S2 - || Build.MODEL.equals("SGH-T989D") // Galaxy S2 X - || Build.MODEL.equals("SAMSUNG-SGH-I727") // Galaxy S2 Skyrocket - || Build.MODEL.equals("GT-I9300") // Galaxy S3 - || Build.MODEL.equals("GT-N7000"); // Galaxy Note - private final CameraManager cameraManager = new CameraManager(); - private ScannerView scannerView; - private TextureView previewView; - private volatile boolean surfaceCreated = false; - private Vibrator vibrator; - private HandlerThread cameraThread; - private volatile Handler cameraHandler; - private final Runnable closeRunnable = new Runnable() { - @Override - public void run() { - cameraHandler.removeCallbacksAndMessages(null); - cameraManager.close(); - } - }; - private final Runnable fetchAndDecodeRunnable = new Runnable() { - private final QRCodeReader reader = new QRCodeReader(); - private final Map hints = new EnumMap(DecodeHintType.class); - - @Override - public void run() { - cameraManager.requestPreviewFrame((data, camera) -> decode(data)); - } - - private void decode(final byte[] data) { - final PlanarYUVLuminanceSource source = cameraManager.buildLuminanceSource(data); - final BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); - - try { - hints.put(DecodeHintType.NEED_RESULT_POINT_CALLBACK, (ResultPointCallback) dot -> runOnUiThread(() -> scannerView.addDot(dot))); - final Result scanResult = reader.decode(bitmap, hints); - - runOnUiThread(() -> handleResult(scanResult)); - } catch (final ReaderException x) { - // retry - cameraHandler.post(fetchAndDecodeRunnable); - } finally { - reader.reset(); - } - } - }; - private final Runnable openRunnable = new Runnable() { - @Override - public void run() { - try { - final Camera camera = cameraManager.open(previewView, displayRotation(), !DISABLE_CONTINUOUS_AUTOFOCUS); - - final Rect framingRect = cameraManager.getFrame(); - final RectF framingRectInPreview = new RectF(cameraManager.getFramePreview()); - framingRectInPreview.offsetTo(0, 0); - final boolean cameraFlip = cameraManager.getFacing() == CameraInfo.CAMERA_FACING_FRONT; - final int cameraRotation = cameraManager.getOrientation(); - - runOnUiThread(() -> scannerView.setFraming(framingRect, framingRectInPreview, displayRotation(), cameraRotation, cameraFlip)); - - final String focusMode = camera.getParameters().getFocusMode(); - final boolean nonContinuousAutoFocus = Camera.Parameters.FOCUS_MODE_AUTO.equals(focusMode) - || Camera.Parameters.FOCUS_MODE_MACRO.equals(focusMode); - - if (nonContinuousAutoFocus) - cameraHandler.post(new AutoFocusRunnable(camera)); - - cameraHandler.post(fetchAndDecodeRunnable); - } catch (final Exception x) { - Log.d(Config.LOGTAG, "problem opening camera", x); - } - } - - private int displayRotation() { - final int rotation = getWindowManager().getDefaultDisplay().getRotation(); - if (rotation == Surface.ROTATION_0) - return 0; - else if (rotation == Surface.ROTATION_90) - return 90; - else if (rotation == Surface.ROTATION_180) - return 180; - else if (rotation == Surface.ROTATION_270) - return 270; - else - throw new IllegalStateException("rotation: " + rotation); - } - }; - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); - - setContentView(R.layout.activity_scan); - scannerView = findViewById(R.id.scan_activity_mask); - previewView = findViewById(R.id.scan_activity_preview); - previewView.setSurfaceTextureListener(this); - - cameraThread = new HandlerThread("cameraThread", Process.THREAD_PRIORITY_BACKGROUND); - cameraThread.start(); - cameraHandler = new Handler(cameraThread.getLooper()); - } - - @Override - protected void onResume() { - super.onResume(); - maybeOpenCamera(); - } - - @Override - protected void onPause() { - cameraHandler.post(closeRunnable); - - super.onPause(); - } - - @Override - protected void onDestroy() { - // cancel background thread - cameraHandler.removeCallbacksAndMessages(null); - cameraThread.quit(); - - previewView.setSurfaceTextureListener(null); - - super.onDestroy(); - } - - private void maybeOpenCamera() { - if (surfaceCreated && ContextCompat.checkSelfPermission(this, - Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) - cameraHandler.post(openRunnable); - } - - @Override - public void onSurfaceTextureAvailable(final SurfaceTexture surface, final int width, final int height) { - surfaceCreated = true; - maybeOpenCamera(); - } - - @Override - public boolean onSurfaceTextureDestroyed(final SurfaceTexture surface) { - surfaceCreated = false; - return true; - } - - @Override - public void onSurfaceTextureSizeChanged(final SurfaceTexture surface, final int width, final int height) { - } - - @Override - public void onSurfaceTextureUpdated(final SurfaceTexture surface) { - } - - @Override - public void onAttachedToWindow() { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); - } - - @Override - public void onBackPressed() { - scannerView.setVisibility(View.GONE); - setResult(RESULT_CANCELED); - postFinish(); - } - - @Override - public boolean onKeyDown(final int keyCode, final KeyEvent event) { - switch (keyCode) { - case KeyEvent.KEYCODE_FOCUS: - case KeyEvent.KEYCODE_CAMERA: - // don't launch camera app - return true; - case KeyEvent.KEYCODE_VOLUME_DOWN: - case KeyEvent.KEYCODE_VOLUME_UP: - cameraHandler.post(() -> cameraManager.setTorch(keyCode == KeyEvent.KEYCODE_VOLUME_UP)); - return true; - } - - return super.onKeyDown(keyCode, event); - } - - public void handleResult(final Result scanResult) { - vibrator.vibrate(VIBRATE_DURATION); - - scannerView.setIsResult(true); - - final Intent result = new Intent(); - result.putExtra(INTENT_EXTRA_RESULT, scanResult.getText()); - setResult(RESULT_OK, result); - postFinish(); - } - - private void postFinish() { - new Handler().postDelayed(this::finish, 50); - } - - public static void scan(Activity activity) { - if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { - Intent intent = new Intent(activity, ScanActivity.class); - activity.startActivityForResult(intent, REQUEST_SCAN_QR_CODE); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } else { - ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSIONS_TO_SCAN); - } - - } - - public static void onRequestPermissionResult(Activity activity, int requestCode, int[] grantResults) { - if (requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN) { - return; - } - if (grantResults.length > 0) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - scan(activity); - } else { - ToastCompat.makeText(activity, R.string.qr_code_scanner_needs_access_to_camera, ToastCompat.LENGTH_SHORT).show(); - } - } - } - - private final class AutoFocusRunnable implements Runnable { - private final Camera camera; - private final Camera.AutoFocusCallback autoFocusCallback = new Camera.AutoFocusCallback() { - @Override - public void onAutoFocus(final boolean success, final Camera camera) { - // schedule again - cameraHandler.postDelayed(AutoFocusRunnable.this, AUTO_FOCUS_INTERVAL_MS); - } - }; - - public AutoFocusRunnable(final Camera camera) { - this.camera = camera; - } - - @Override - public void run() { - try { - camera.autoFocus(autoFocusCallback); - } catch (final Exception x) { - Log.d(Config.LOGTAG, "problem with auto-focus, will not schedule again", x); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/SearchActivity.java b/src/main/java/eu/siacs/conversations/ui/SearchActivity.java deleted file mode 100644 index b21809e06..000000000 --- a/src/main/java/eu/siacs/conversations/ui/SearchActivity.java +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui; - -import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.hideSoftKeyboard; -import static eu.siacs.conversations.ui.util.SoftKeyboardUtils.showKeyboard; - -import android.content.Intent; -import android.os.Bundle; -import android.text.Editable; -import android.text.InputType; -import android.text.TextWatcher; -import android.util.Log; -import android.view.ContextMenu; -import android.view.Menu; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import android.widget.AdapterView; -import android.widget.EditText; - -import androidx.appcompat.widget.Toolbar; -import androidx.databinding.DataBindingUtil; - -import com.google.common.base.Strings; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ActivitySearchBinding; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Conversational; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.services.MessageSearchTask; -import eu.siacs.conversations.ui.adapter.MessageAdapter; -import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable; -import eu.siacs.conversations.ui.util.ChangeWatcher; -import eu.siacs.conversations.ui.util.DateSeparator; -import eu.siacs.conversations.ui.util.ListViewUtils; -import eu.siacs.conversations.ui.util.PendingItem; -import eu.siacs.conversations.ui.util.ShareUtil; -import eu.siacs.conversations.ui.util.StyledAttributes; -import eu.siacs.conversations.utils.FtsUtils; -import eu.siacs.conversations.utils.MessageUtils; -import eu.siacs.conversations.utils.UIHelper; - -public class SearchActivity extends XmppActivity implements TextWatcher, OnSearchResultsAvailable, MessageAdapter.OnContactPictureClicked { - - private static final String EXTRA_SEARCH_TERM = "search-term"; - public static final String EXTRA_CONVERSATION_UUID = "uuid"; - private ActivitySearchBinding binding; - private MessageAdapter messageListAdapter; - private final List messages = new ArrayList<>(); - private WeakReference selectedMessageReference = new WeakReference<>(null); - private String uuid; - private final ChangeWatcher> currentSearch = new ChangeWatcher<>(); - private final PendingItem pendingSearchTerm = new PendingItem<>(); - private final PendingItem> pendingSearch = new PendingItem<>(); - - @Override - public void onCreate(final Bundle bundle) { - final Intent intent = getIntent(); - this.uuid = intent == null ? null : Strings.emptyToNull(intent.getStringExtra(EXTRA_CONVERSATION_UUID)); - final String searchTerm = bundle == null ? null : bundle.getString(EXTRA_SEARCH_TERM); - if (searchTerm != null) { - pendingSearchTerm.push(searchTerm); - } - super.onCreate(bundle); - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_search); - setSupportActionBar((Toolbar) this.binding.toolbar); - configureActionBar(getSupportActionBar()); - this.messageListAdapter = new MessageAdapter(this, this.messages, uuid == null); - this.messageListAdapter.setOnContactPictureClicked(this); - this.binding.searchResults.setAdapter(messageListAdapter); - registerForContextMenu(this.binding.searchResults); - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.activity_search, menu); - final MenuItem searchActionMenuItem = menu.findItem(R.id.action_search); - final EditText searchField = searchActionMenuItem.getActionView().findViewById(R.id.search_field); - final String term = pendingSearchTerm.pop(); - if (term != null) { - searchField.append(term); - final List searchTerm = FtsUtils.parse(term); - if (xmppConnectionService != null) { - if (currentSearch.watch(searchTerm)) { - xmppConnectionService.search(searchTerm, uuid, this); - } - } else { - pendingSearch.push(searchTerm); - } - } - searchField.addTextChangedListener(this); - searchField.setHint(R.string.search_messages); - searchField.setContentDescription(getString(R.string.search_messages)); - searchField.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE); - showKeyboard(searchField); - return super.onCreateOptionsMenu(menu); - } - - @Override - public void onCreateContextMenu(final ContextMenu menu, final View v, ContextMenu.ContextMenuInfo menuInfo) { - v.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)); - AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo; - final Message message = this.messages.get(acmi.position); - this.selectedMessageReference = new WeakReference<>(message); - getMenuInflater().inflate(R.menu.search_result_context, menu); - MenuItem copy = menu.findItem(R.id.copy_message); - MenuItem quote = menu.findItem(R.id.quote_message); - MenuItem copyUrl = menu.findItem(R.id.copy_url); - if (message.isGeoUri()) { - copy.setVisible(false); - quote.setVisible(false); - } else { - copyUrl.setVisible(false); - } - super.onCreateContextMenu(menu, v, menuInfo); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - hideSoftKeyboard(this); - } - return super.onOptionsItemSelected(item); - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - final Message message = selectedMessageReference.get(); - final boolean multi = message.getConversation().getMode() == Conversational.MODE_MULTI; - final String user = multi ? UIHelper.getDisplayedMucCounterpart(message.getCounterpart()) : null; - if (message != null) { - switch (item.getItemId()) { - case R.id.open_conversation: - switchToConversation(wrap(message.getConversation())); - break; - case R.id.share_with: - ShareUtil.share(this, message, user); - break; - case R.id.copy_message: - ShareUtil.copyToClipboard(this, message); - break; - case R.id.copy_url: - ShareUtil.copyUrlToClipboard(this, message); - break; - case R.id.quote_message: - quote(message, user); - break; - } - } - return super.onContextItemSelected(item); - } - - @Override - public void onSaveInstanceState(Bundle bundle) { - List term = currentSearch.get(); - if (term != null && term.size() > 0) { - bundle.putString(EXTRA_SEARCH_TERM, FtsUtils.toUserEnteredString(term)); - } - super.onSaveInstanceState(bundle); - } - - private void quote(Message message, String user) { - Log.d(Config.LOGTAG, "Quote User: " + user); - switchToConversationAndQuote(wrap(message.getConversation()), MessageUtils.prepareQuote(message), user); - } - - private Conversation wrap(Conversational conversational) { - if (conversational instanceof Conversation) { - return (Conversation) conversational; - } else { - return xmppConnectionService.findOrCreateConversation(conversational.getAccount(), - conversational.getJid(), - conversational.getMode() == Conversational.MODE_MULTI, - true, - true); - } - } - - @Override - protected void refreshUiReal() { - - } - - @Override - void onBackendConnected() { - final List searchTerm = pendingSearch.pop(); - if (searchTerm != null && currentSearch.watch(searchTerm)) { - xmppConnectionService.search(searchTerm, uuid,this); - } - } - - private void changeBackground(boolean hasSearch, boolean hasResults) { - if (hasSearch) { - if (hasResults) { - binding.searchResults.setBackgroundColor(StyledAttributes.getColor(this, R.attr.color_background_tertiary)); - } else { - binding.searchResults.setBackground(StyledAttributes.getDrawable(this, R.attr.activity_background_no_results)); - } - } else { - binding.searchResults.setBackground(StyledAttributes.getDrawable(this, R.attr.activity_background_search)); - } - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - - } - - @Override - public void afterTextChanged(Editable s) { - final List term = FtsUtils.parse(s.toString().trim()); - if (!currentSearch.watch(term)) { - return; - } - if (term.size() > 0) { - xmppConnectionService.search(term, uuid,this); - } else { - MessageSearchTask.cancelRunningTasks(); - this.messages.clear(); - messageListAdapter.setHighlightedTerm(null); - messageListAdapter.notifyDataSetChanged(); - changeBackground(false, false); - } - } - - @Override - public void onSearchResultsAvailable(List term, List messages) { - runOnUiThread(() -> { - this.messages.clear(); - messageListAdapter.setHighlightedTerm(term); - DateSeparator.addAll(messages); - this.messages.addAll(messages); - messageListAdapter.notifyDataSetChanged(); - changeBackground(true, messages.size() > 0); - ListViewUtils.scrollToBottom(this.binding.searchResults); - }); - } - - @Override - public void onContactPictureClicked(Message message) { - String fingerprint; - if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { - fingerprint = "pgp"; - } else { - fingerprint = message.getFingerprint(); - } - if (message.getStatus() == Message.STATUS_RECEIVED) { - final Contact contact = message.getContact(); - if (contact != null) { - if (contact.isSelf()) { - switchToAccount(message.getConversation().getAccount(), fingerprint); - } else { - switchToContactDetails(contact, fingerprint); - } - } - } else { - switchToAccount(message.getConversation().getAccount(), fingerprint); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/SetSettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SetSettingsActivity.java deleted file mode 100644 index 99b2cbc7b..000000000 --- a/src/main/java/eu/siacs/conversations/ui/SetSettingsActivity.java +++ /dev/null @@ -1,178 +0,0 @@ -package eu.siacs.conversations.ui; - -import static eu.siacs.conversations.ui.SettingsActivity.BROADCAST_LAST_ACTIVITY; -import static eu.siacs.conversations.ui.SettingsActivity.CHAT_STATES; -import static eu.siacs.conversations.ui.SettingsActivity.CONFIRM_MESSAGES; -import static eu.siacs.conversations.ui.SettingsActivity.EASY_DOWNLOADER; -import static eu.siacs.conversations.ui.SettingsActivity.FORBID_SCREENSHOTS; -import static eu.siacs.conversations.ui.SettingsActivity.SHOW_LINKS_INSIDE; -import static eu.siacs.conversations.ui.SettingsActivity.SHOW_MAPS_INSIDE; -import static eu.siacs.conversations.ui.SettingsActivity.USE_INNER_STORAGE; -import static eu.siacs.conversations.ui.SettingsActivity.USE_INVIDIOUS; - -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.util.Log; -import android.view.View; - -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.Toolbar; -import androidx.databinding.DataBindingUtil; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ActivitySetSettingsBinding; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.AccountUtils; -import eu.siacs.conversations.utils.FirstStartManager; -import eu.siacs.conversations.utils.ThemeHelper; - -public class SetSettingsActivity extends XmppActivity implements XmppConnectionService.OnAccountUpdate { - ActivitySetSettingsBinding binding; - Account account; - static final int FORDBIDSCREENSHOTS = 1; - static final int SHOWWEBLINKS = 2; - static final int SHOWMAPPREVIEW = 3; - static final int CHATSTATES = 4; - static final int CONFIRMMESSAGES = 5; - static final int LASTSEEN = 6; - static final int INVIDIOUS = 7; - static final int INNER_STORAGE = 8; - static final int EASY_DOWNLOADER_ATTACHMENTS = 9; - - @Override - protected void refreshUiReal() { - createInfoMenu(); - } - - @Override - void onBackendConnected() { - this.account = AccountUtils.getFirst(xmppConnectionService); - refreshUi(); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_set_settings); - setSupportActionBar((Toolbar) this.binding.toolbar); - this.binding.next.setOnClickListener(this::next); - createInfoMenu(); - getDefaults(); - setTheme(ThemeHelper.find(this)); - } - - private void createInfoMenu() { - this.binding.actionInfoForbidScreenshots.setOnClickListener(string -> showInfo(FORDBIDSCREENSHOTS)); - this.binding.actionInfoShowWeblinks.setOnClickListener(string -> showInfo(SHOWWEBLINKS)); - this.binding.actionInfoShowMapPreviews.setOnClickListener(string -> showInfo(SHOWMAPPREVIEW)); - this.binding.actionInfoChatStates.setOnClickListener(string -> showInfo(CHATSTATES)); - this.binding.actionInfoConfirmMessages.setOnClickListener(string -> showInfo(CONFIRMMESSAGES)); - this.binding.actionInfoLastSeen.setOnClickListener(string -> showInfo(LASTSEEN)); - this.binding.actionInfoInvidious.setOnClickListener(string -> showInfo(INVIDIOUS)); - this.binding.actionInfoUsingInnerStorage.setOnClickListener(string -> showInfo(INNER_STORAGE)); - this.binding.actionInfoEasyDownloader.setOnClickListener(string -> showInfo(EASY_DOWNLOADER_ATTACHMENTS)); - } - - private void getDefaults() { - this.binding.forbidScreenshots.setChecked(getResources().getBoolean(R.bool.screen_security)); - this.binding.showLinks.setChecked(getResources().getBoolean(R.bool.show_links_inside)); - this.binding.showMappreview.setChecked(getResources().getBoolean(R.bool.show_maps_inside)); - this.binding.chatStates.setChecked(getResources().getBoolean(R.bool.chat_states)); - this.binding.confirmMessages.setChecked(getResources().getBoolean(R.bool.confirm_messages)); - this.binding.lastSeen.setChecked(getResources().getBoolean(R.bool.last_activity)); - this.binding.invidious.setChecked(getResources().getBoolean(R.bool.use_invidious)); - this.binding.usingInnerStorage.setChecked(getResources().getBoolean(R.bool.use_inner_storage)); - this.binding.easyDownloader.setChecked(getResources().getBoolean(R.bool.easy_downloader)); - } - - private void next(View view) { - setSettings(); - FirstStartManager firstStartManager = new FirstStartManager(this); - firstStartManager.setFirstTimeLaunch(false); - if (account != null) { - Intent intent = new Intent(this, PublishProfilePictureActivity.class); - intent.putExtra(PublishProfilePictureActivity.EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); - intent.putExtra("setup", true); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - finish(); - } - - private void showInfo(int setting) { - String title; - String message; - switch (setting) { - case FORDBIDSCREENSHOTS: - title = getString(R.string.pref_screen_security); - message = getString(R.string.pref_screen_security_summary); - break; - case SHOWWEBLINKS: - title = getString(R.string.pref_show_links_inside); - message = getString(R.string.pref_show_links_inside_summary); - break; - case SHOWMAPPREVIEW: - title = getString(R.string.pref_show_mappreview_inside); - message = getString(R.string.pref_show_mappreview_inside_summary); - break; - case CHATSTATES: - title = getString(R.string.pref_chat_states); - message = getString(R.string.pref_chat_states_summary); - break; - case CONFIRMMESSAGES: - title = getString(R.string.pref_confirm_messages); - message = getString(R.string.pref_confirm_messages_summary); - break; - case LASTSEEN: - title = getString(R.string.pref_broadcast_last_activity); - message = getString(R.string.pref_broadcast_last_activity_summary); - break; - case INVIDIOUS: - title = getString(R.string.pref_use_invidious); - message = getString(R.string.pref_use_invidious_summary); - break; - case INNER_STORAGE: - title = getString(R.string.pref_use_inner_storage); - message = getString(R.string.pref_use_inner_storage_summary); - break; - case EASY_DOWNLOADER_ATTACHMENTS: - title = getString(R.string.pref_easy_downloader); - message = getString(R.string.pref_easy_downloader_summary); - break; - default: - title = getString(R.string.error); - message = getString(R.string.error); - } - Log.d(Config.LOGTAG, "STRING value " + title); - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(title); - builder.setMessage(message); - builder.setNeutralButton(getString(R.string.ok), null); - builder.create().show(); - } - - - private void setSettings() { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - preferences.edit().putBoolean(FORBID_SCREENSHOTS, this.binding.forbidScreenshots.isChecked()).apply(); - preferences.edit().putBoolean(SHOW_LINKS_INSIDE, this.binding.showLinks.isChecked()).apply(); - preferences.edit().putBoolean(SHOW_MAPS_INSIDE, this.binding.showMappreview.isChecked()).apply(); - preferences.edit().putBoolean(CHAT_STATES, this.binding.chatStates.isChecked()).apply(); - preferences.edit().putBoolean(CONFIRM_MESSAGES, this.binding.confirmMessages.isChecked()).apply(); - preferences.edit().putBoolean(BROADCAST_LAST_ACTIVITY, this.binding.lastSeen.isChecked()).apply(); - preferences.edit().putBoolean(USE_INVIDIOUS, this.binding.invidious.isChecked()).apply(); - preferences.edit().putBoolean(USE_INNER_STORAGE, this.binding.usingInnerStorage.isChecked()).apply(); - preferences.edit().putBoolean(EASY_DOWNLOADER, this.binding.easyDownloader.isChecked()).apply(); - FileBackend.switchStorage(xmppConnectionService.usingInnerStorage()); - } - - @Override - public void onAccountUpdate() { - refreshUi(); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java deleted file mode 100644 index 1c13c081c..000000000 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ /dev/null @@ -1,818 +0,0 @@ -package eu.siacs.conversations.ui; - -import static eu.siacs.conversations.persistance.FileBackend.APP_DIRECTORY; -import static eu.siacs.conversations.utils.StorageHelper.getBackupDirectory; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; - -import android.app.FragmentManager; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.preference.CheckBoxPreference; -import android.preference.ListPreference; -import android.preference.Preference; -import android.preference.PreferenceCategory; -import android.preference.PreferenceManager; -import android.preference.PreferenceScreen; -import android.util.Log; -import static eu.siacs.conversations.utils.CameraUtils.showCameraChooser; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.core.content.ContextCompat; -import eu.siacs.conversations.utils.CameraUtils; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.security.KeyStoreException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import android.provider.MediaStore; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.OmemoSetting; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.services.ExportBackupService; -import eu.siacs.conversations.services.MemorizingTrustManager; -import eu.siacs.conversations.ui.util.StyledAttributes; -import eu.siacs.conversations.utils.Compatibility; -import eu.siacs.conversations.utils.ThemeHelper; -import eu.siacs.conversations.utils.TimeFrameUtils; -import eu.siacs.conversations.xmpp.Jid; -import me.drakeet.support.toast.ToastCompat; -import eu.siacs.conversations.services.UnifiedPushDistributor; - -public class SettingsActivity extends XmppActivity implements OnSharedPreferenceChangeListener { - - public static final String AWAY_WHEN_SCREEN_IS_OFF = "away_when_screen_off"; - public static final String TREAT_VIBRATE_AS_SILENT = "treat_vibrate_as_silent"; - public static final String DND_ON_SILENT_MODE = "dnd_on_silent_mode"; - public static final String MANUALLY_CHANGE_PRESENCE = "manually_change_presence"; - public static final String BLIND_TRUST_BEFORE_VERIFICATION = "btbv"; - public static final String AUTOMATIC_MESSAGE_DELETION = "automatic_message_deletion"; - public static final String AUTOMATIC_ATTACHMENT_DELETION = "automatic_attachment_deletion"; - public static final String BROADCAST_LAST_ACTIVITY = "last_activity"; - public static final String WARN_UNENCRYPTED_CHAT = "warn_unencrypted_chat"; - public static final String HIDE_YOU_ARE_NOT_PARTICIPATING = "hide_you_are_not_participating"; - public static final String HIDE_MEMORY_WARNING = "hide_memory_warning"; - public static final String THEME = "theme"; - public static final String THEME_COLOR = "theme_color"; - public static final String SHOW_DYNAMIC_TAGS = "show_dynamic_tags"; - public static final String OMEMO_SETTING = "omemo"; - public static final String SHOW_FOREGROUND_SERVICE = "show_foreground_service"; - public static final String USE_BUNDLED_EMOJIS = "use_bundled_emoji"; - public static final String ENABLE_MULTI_ACCOUNTS = "enable_multi_accounts"; - public static final String SHOW_OWN_ACCOUNTS = "show_own_accounts"; - public static final String QUICK_SHARE_ATTACHMENT_CHOICE = "quick_share_attachment_choice"; - public static final String NUMBER_OF_ACCOUNTS = "number_of_accounts"; - public static final String PLAY_GIF_INSIDE = "play_gif_inside"; - public static final String USE_INTERNAL_UPDATER = "use_internal_updater"; - public static final String SHOW_LINKS_INSIDE = "show_links_inside"; - public static final String SHOW_MAPS_INSIDE = "show_maps_inside"; - public static final String PREFER_XMPP_AVATAR = "prefer_xmpp_avatar"; - public static final String CHAT_STATES = "chat_states"; - public static final String FORBID_SCREENSHOTS = "screen_security"; - public static final String CONFIRM_MESSAGES = "confirm_messages"; - public static final String INDICATE_RECEIVED = "indicate_received"; - public static final String USE_INVIDIOUS = "use_invidious"; - public static final String USE_INNER_STORAGE = "use_inner_storage"; - public static final String INVIDIOUS_HOST = "invidious_host"; - public static final String MAPPREVIEW_HOST = "mappreview_host"; - public static final String ALLOW_MESSAGE_CORRECTION = "allow_message_correction"; - public static final String ALLOW_MESSAGE_RETRACTION = "allow_message_retraction"; - public static final String ENABLE_OTR_ENCRYPTION = "enable_otr_encryption"; - public static final String USE_UNICOLORED_CHATBG = "unicolored_chatbg"; - public static final String EASY_DOWNLOADER = "easy_downloader"; - public static final String MIN_ANDROID_SDK21_SHOWN = "min_android_sdk21_shown"; - public static final String INDIVIDUAL_NOTIFICATION_PREFIX = "individual_notification_set_"; - public static final String CAMERA_CHOICE = "camera_choice"; - public static final String PAUSE_VOICE = "pause_voice_on_move_from_ear"; - public static final String PERSISTENT_ROOM = "enable_persistent_rooms"; - public static final String MAX_RESEND_TIME = "max_resend_time"; - public static final String RESEND_DELAY = "resend_delay"; - - public static final int REQUEST_CREATE_BACKUP = 0xbf8701; - public static final int REQUEST_IMPORT_SETTINGS = 0xbf8702; - Preference multiAccountPreference; - Preference autoMessageExpiryPreference; - Preference autoFileExpiryPreference; - Preference BundledEmojiPreference; - Preference QuickShareAttachmentChoicePreference; - boolean isMultiAccountChecked = false; - boolean isBundledEmojiChecked; - boolean isQuickShareAttachmentChoiceChecked = false; - private SettingsFragment mSettingsFragment; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setTheme(ThemeHelper.find(this)); - setContentView(R.layout.activity_settings); - FragmentManager fm = getFragmentManager(); - mSettingsFragment = (SettingsFragment) fm.findFragmentById(R.id.settings_content); - if (mSettingsFragment == null - || !mSettingsFragment.getClass().equals(SettingsFragment.class)) { - mSettingsFragment = new SettingsFragment(); - fm.beginTransaction().replace(R.id.settings_content, mSettingsFragment).commit(); - } - mSettingsFragment.setActivityIntent(getIntent()); - getWindow().getDecorView().setBackgroundColor(StyledAttributes.getColor(this, R.attr.color_background_secondary)); - setSupportActionBar(findViewById(R.id.toolbar)); - configureActionBar(getSupportActionBar()); - } - - @Override - void onBackendConnected() { - final Preference accountPreference = - mSettingsFragment.findPreference(UnifiedPushDistributor.PREFERENCE_ACCOUNT); - reconfigureUpAccountPreference(accountPreference); - } - - private void reconfigureUpAccountPreference(final Preference preference) { - final ListPreference listPreference; - if (preference instanceof ListPreference) { - listPreference = (ListPreference) preference; - } else { - return; - } - final List accounts = - ImmutableList.copyOf( - Lists.transform( - xmppConnectionService.getAccounts(), - a -> a.getJid().asBareJid().toEscapedString())); - final ImmutableList.Builder entries = new ImmutableList.Builder<>(); - final ImmutableList.Builder entryValues = new ImmutableList.Builder<>(); - entries.add(getString(R.string.no_account_deactivated)); - entryValues.add("none"); - entries.addAll(accounts); - entryValues.addAll(accounts); - listPreference.setEntries(entries.build().toArray(new CharSequence[0])); - listPreference.setEntryValues(entryValues.build().toArray(new CharSequence[0])); - if (!accounts.contains(listPreference.getValue())) { - listPreference.setValue("none"); - } - } - - @Override - public void onStart() { - super.onStart(); - PreferenceManager.getDefaultSharedPreferences(this) - .registerOnSharedPreferenceChangeListener(this); - multiAccountPreference = mSettingsFragment.findPreference("enable_multi_accounts"); - if (multiAccountPreference != null) { - isMultiAccountChecked = ((CheckBoxPreference) multiAccountPreference).isChecked(); - //handleMultiAccountChanges(); - } - - BundledEmojiPreference = mSettingsFragment.findPreference("use_bundled_emoji"); - if (BundledEmojiPreference != null) { - isBundledEmojiChecked = ((CheckBoxPreference) BundledEmojiPreference).isChecked(); - } - - QuickShareAttachmentChoicePreference = mSettingsFragment.findPreference("quick_share_attachment_choice"); - if (QuickShareAttachmentChoicePreference != null) { - QuickShareAttachmentChoicePreference.setOnPreferenceChangeListener((preference, newValue) -> { - refreshUiReal(); - return true; - }); - isQuickShareAttachmentChoiceChecked = ((CheckBoxPreference) QuickShareAttachmentChoicePreference).isChecked(); - } - - changeOmemoSettingSummary(); - - if (Config.FORCE_ORBOT) { - PreferenceCategory connectionOptions = (PreferenceCategory) mSettingsFragment.findPreference("connection_options"); - PreferenceScreen expert = (PreferenceScreen) mSettingsFragment.findPreference("expert"); - if (connectionOptions != null) { - expert.removePreference(connectionOptions); - } - } - - PreferenceScreen mainPreferenceScreen = (PreferenceScreen) mSettingsFragment.findPreference("main_screen"); - PreferenceScreen UIPreferenceScreen = (PreferenceScreen) mSettingsFragment.findPreference("userinterface"); - - // this feature is only available on Huawei Android 6. - PreferenceScreen huaweiPreferenceScreen = - (PreferenceScreen) mSettingsFragment.findPreference("huawei"); - if (huaweiPreferenceScreen != null) { - Intent intent = huaweiPreferenceScreen.getIntent(); - // remove when Api version is above M (Version 6.0) or if the intent is not callable - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M || !isCallable(intent)) { - PreferenceCategory generalCategory = - (PreferenceCategory) mSettingsFragment.findPreference("general"); - generalCategory.removePreference(huaweiPreferenceScreen); - if (generalCategory.getPreferenceCount() == 0) { - if (mainPreferenceScreen != null) { - mainPreferenceScreen.removePreference(generalCategory); - } - } - } - } - - - ListPreference automaticMessageDeletionList = - (ListPreference) mSettingsFragment.findPreference(AUTOMATIC_MESSAGE_DELETION); - if (automaticMessageDeletionList != null) { - final int[] choices = - getResources().getIntArray(R.array.automatic_message_deletion_values); - CharSequence[] entries = new CharSequence[choices.length]; - CharSequence[] entryValues = new CharSequence[choices.length]; - for (int i = 0; i < choices.length; ++i) { - entryValues[i] = String.valueOf(choices[i]); - if (choices[i] == 0) { - entries[i] = getString(R.string.never); - } else { - entries[i] = TimeFrameUtils.resolve(this, 1000L * choices[i]); - } - } - automaticMessageDeletionList.setEntries(entries); - automaticMessageDeletionList.setEntryValues(entryValues); - } - - ListPreference automaticAttachmentDeletionList = (ListPreference) mSettingsFragment.findPreference(AUTOMATIC_ATTACHMENT_DELETION); - if (automaticAttachmentDeletionList != null) { - final int[] choices = getResources().getIntArray(R.array.automatic_message_deletion_values); - CharSequence[] entries = new CharSequence[choices.length]; - CharSequence[] entryValues = new CharSequence[choices.length]; - for (int i = 0; i < choices.length; ++i) { - entryValues[i] = String.valueOf(choices[i]); - if (choices[i] == 0) { - entries[i] = getString(R.string.never); - } else { - entries[i] = TimeFrameUtils.resolve(this, 1000L * choices[i]); - } - } - automaticAttachmentDeletionList.setEntries(entries); - automaticAttachmentDeletionList.setEntryValues(entryValues); - } - - boolean removeLocation = new Intent("eu.siacs.conversations.location.request").resolveActivity(getPackageManager()) == null; - boolean removeVoice = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION).resolveActivity(getPackageManager()) == null; - - ListPreference quickAction = (ListPreference) mSettingsFragment.findPreference("quick_action"); - if (quickAction != null && (removeLocation || removeVoice)) { - ArrayList entries = - new ArrayList<>(Arrays.asList(quickAction.getEntries())); - ArrayList entryValues = - new ArrayList<>(Arrays.asList(quickAction.getEntryValues())); - int index = entryValues.indexOf("location"); - if (index > 0 && removeLocation) { - entries.remove(index); - entryValues.remove(index); - } - index = entryValues.indexOf("voice"); - if (index > 0 && removeVoice) { - entries.remove(index); - entryValues.remove(index); - } - quickAction.setEntries(entries.toArray(new CharSequence[entries.size()])); - quickAction.setEntryValues(entryValues.toArray(new CharSequence[entryValues.size()])); - } - - if (isQuickShareAttachmentChoiceChecked) { - if (UIPreferenceScreen != null && quickAction != null) { - UIPreferenceScreen.removePreference(quickAction); - } - } - - final Preference removeCertsPreference = mSettingsFragment.findPreference("remove_trusted_certificates"); - if (removeCertsPreference != null) { - removeCertsPreference.setOnPreferenceClickListener( - preference -> { - final MemorizingTrustManager mtm = - xmppConnectionService.getMemorizingTrustManager(); - final ArrayList aliases = Collections.list(mtm.getCertificates()); - if (aliases.size() == 0) { - displayToast(getString(R.string.toast_no_trusted_certs)); - return true; - } - final ArrayList selectedItems = new ArrayList<>(); - final AlertDialog.Builder dialogBuilder = - new AlertDialog.Builder(SettingsActivity.this); - dialogBuilder.setTitle( - getResources().getString(R.string.dialog_manage_certs_title)); - dialogBuilder.setMultiChoiceItems( - aliases.toArray(new CharSequence[aliases.size()]), - null, - (dialog, indexSelected, isChecked) -> { - if (isChecked) { - selectedItems.add(indexSelected); - } else if (selectedItems.contains(indexSelected)) { - selectedItems.remove(Integer.valueOf(indexSelected)); - } - if (selectedItems.size() > 0) - ((AlertDialog) dialog).getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true); - else { - ((AlertDialog) dialog).getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false); - } - }); - - dialogBuilder.setPositiveButton( - getResources() - .getString(R.string.dialog_manage_certs_positivebutton), - (dialog, which) -> { - int count = selectedItems.size(); - if (count > 0) { - for (int i = 0; i < count; i++) { - try { - Integer item = - Integer.valueOf( - selectedItems.get(i).toString()); - String alias = aliases.get(item); - mtm.deleteCertificate(alias); - } catch (KeyStoreException e) { - e.printStackTrace(); - displayToast("Error: " + e.getLocalizedMessage()); - } - } - if (xmppConnectionServiceBound) { - reconnectAccounts(); - } - displayToast( - getResources() - .getQuantityString( - R.plurals.toast_delete_certificates, - count, - count)); - } - }); - dialogBuilder.setNegativeButton( - getResources() - .getString(R.string.dialog_manage_certs_negativebutton), - null); - AlertDialog removeCertsDialog = dialogBuilder.create(); - removeCertsDialog.show(); - removeCertsDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - return true; - }); - updateTheme(); - } - - final Preference createBackupPreference = mSettingsFragment.findPreference("create_backup"); - if (createBackupPreference != null) { - createBackupPreference.setSummary(getString(R.string.pref_create_backup_summary, getBackupDirectory(null))); - createBackupPreference.setOnPreferenceClickListener(preference -> { - if (hasStoragePermission(REQUEST_CREATE_BACKUP)) { - createBackup(true); - } - return true; - }); - } - - final Preference importSettingsPreference = mSettingsFragment.findPreference("import_settings"); - if (importSettingsPreference != null) { - importSettingsPreference.setSummary(getString(R.string.pref_import_settings_summary)); - importSettingsPreference.setOnPreferenceClickListener(preference -> { - if (hasStoragePermission(REQUEST_IMPORT_SETTINGS)) { - importSettings(); - } - return true; - }); - } - - final Preference prefereXmppAvatarPreference = mSettingsFragment.findPreference(PREFER_XMPP_AVATAR); - if (prefereXmppAvatarPreference != null) { - prefereXmppAvatarPreference.setOnPreferenceClickListener(preference -> { - new Thread(() -> xmppConnectionService.getBitmapCache().evictAll()).start(); - return true; - }); - } - - final Preference showIntroAgainPreference = mSettingsFragment.findPreference("show_intro"); - if (showIntroAgainPreference != null) { - showIntroAgainPreference.setSummary(getString(R.string.pref_show_intro_summary)); - showIntroAgainPreference.setOnPreferenceClickListener(preference -> { - showIntroAgain(); - return true; - }); - } - - - final Preference cameraChooserPreference = mSettingsFragment.findPreference(CAMERA_CHOICE); - if (cameraChooserPreference != null) { - cameraChooserPreference.setOnPreferenceClickListener(preference -> { - final List cameraApps = CameraUtils.getCameraApps(this); - showCameraChooser(this, cameraApps); - return true; - }); - } - - if (Config.ONLY_INTERNAL_STORAGE) { - final Preference cleanCachePreference = mSettingsFragment.findPreference("clean_cache"); - if (cleanCachePreference != null) { - cleanCachePreference.setOnPreferenceClickListener(preference -> cleanCache()); - } - - final Preference cleanPrivateStoragePreference = - mSettingsFragment.findPreference("clean_private_storage"); - if (cleanPrivateStoragePreference != null) { - cleanPrivateStoragePreference.setOnPreferenceClickListener( - preference -> cleanPrivateStorage()); - } - } - - final Preference deleteOmemoPreference = - mSettingsFragment.findPreference("delete_omemo_identities"); - if (deleteOmemoPreference != null) { - deleteOmemoPreference.setOnPreferenceClickListener( - preference -> deleteOmemoIdentities()); - } - if (Config.omemoOnly()) { - final PreferenceScreen securityScreen = - (PreferenceScreen) mSettingsFragment.findPreference("security"); - final Preference omemoPreference = mSettingsFragment.findPreference(OMEMO_SETTING); - if (omemoPreference != null) { - securityScreen.removePreference(omemoPreference); - } - } - - PreferenceScreen ExpertPreferenceScreen = (PreferenceScreen) mSettingsFragment.findPreference("expert"); - final Preference useBundledEmojis = mSettingsFragment.findPreference("use_bundled_emoji"); - if (useBundledEmojis != null) { - Log.d(Config.LOGTAG, "Bundled Emoji checkbox checked: " + isBundledEmojiChecked); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (isBundledEmojiChecked) { - ((CheckBoxPreference) BundledEmojiPreference).setChecked(false); - useBundledEmojis.setEnabled(false); - } - PreferenceCategory UICatergory = (PreferenceCategory) mSettingsFragment.findPreference("UI"); - UICatergory.removePreference(useBundledEmojis); - if (UICatergory.getPreferenceCount() == 0) { - if (ExpertPreferenceScreen != null) { - ExpertPreferenceScreen.removePreference(UICatergory); - } - } - } - } - - final Preference enableMultiAccountsPreference = mSettingsFragment.findPreference("enable_multi_accounts"); - if (enableMultiAccountsPreference != null) { - Log.d(Config.LOGTAG, "Multi account checkbox checked: " + isMultiAccountChecked); - if (isMultiAccountChecked) { - enableMultiAccountsPreference.setEnabled(false); - int accounts = getNumberOfAccounts(); - Log.d(Config.LOGTAG, "Disable multi account: Number of accounts " + accounts); - if (accounts > 1) { - Log.d(Config.LOGTAG, "Disabling multi account not possible because you have more than one account"); - enableMultiAccountsPreference.setEnabled(false); - } else { - Log.d(Config.LOGTAG, "Disabling multi account possible because you have only one account"); - enableMultiAccountsPreference.setEnabled(true); - enableMultiAccountsPreference.setOnPreferenceClickListener(preference -> { - refreshUiReal(); - return true; - }); - } - } else { - enableMultiAccountsPreference.setEnabled(true); - enableMultiAccountsPreference.setOnPreferenceClickListener(preference -> { - enableMultiAccounts(); - return true; - }); - } - } - - final Preference removeAllIndividualNotifications = mSettingsFragment.findPreference("remove_all_individual_notifications"); - if (removeAllIndividualNotifications != null) { - removeAllIndividualNotifications.setOnPreferenceClickListener(preference -> { - final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(SettingsActivity.this); - dialogBuilder.setTitle(getResources().getString(R.string.remove_individual_notifications)); - dialogBuilder.setMessage(R.string.remove_all_individual_notifications_message); - dialogBuilder.setPositiveButton( - getResources().getString(R.string.yes), (dialog, which) -> { - if (Compatibility.runsTwentySix()) { - try { - xmppConnectionService.getNotificationService().cleanAllNotificationChannels(SettingsActivity.this); - } catch (Exception e) { - e.printStackTrace(); - } - xmppConnectionService.updateNotificationChannels(); - } - }); - dialogBuilder.setNegativeButton(getResources().getString(R.string.no), null); - AlertDialog alertDialog = dialogBuilder.create(); - alertDialog.show(); - return true; - }); - updateTheme(); - } - } - - private void updateTheme() { - final int theme = findTheme(); - if (this.mTheme != theme) { - refreshUiReal(); - } - } - - private void changeOmemoSettingSummary() { - final ListPreference omemoPreference = - (ListPreference) mSettingsFragment.findPreference(OMEMO_SETTING); - if (omemoPreference == null) { - return; - } - final String value = omemoPreference.getValue(); - switch (value) { - case "always": - omemoPreference.setSummary(R.string.pref_omemo_setting_summary_always); - break; - case "default_on": - omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_on); - break; - case "default_off": - omemoPreference.setSummary(R.string.pref_omemo_setting_summary_default_off); - break; - case "always_off": - omemoPreference.setSummary(R.string.pref_omemo_setting_summary_always_off); - break; - } - } - - private boolean isCallable(final Intent i) { - return i != null - && getPackageManager() - .queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY) - .size() - > 0; - } - - private boolean cleanCache() { - Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.setData(Uri.parse("package:" + getPackageName())); - startActivity(intent); - return true; - } - - private boolean cleanPrivateStorage() { - for (String type : Arrays.asList("Images", "Videos", "Files", "Audios")) { - cleanPrivateFiles(type); - } - return true; - } - - private void cleanPrivateFiles(final String type) { - try { - File dir = new File(getFilesDir().getAbsolutePath(), File.separator + type + File.separator); - File[] array = dir.listFiles(); - if (array != null) { - for (int b = 0; b < array.length; b++) { - String name = array[b].getName().toLowerCase(); - if (name.equals(".nomedia")) { - continue; - } - if (array[b].isFile()) { - array[b].delete(); - } - } - } - } catch (Throwable e) { - Log.e("CleanCache", e.toString()); - } - } - - private boolean deleteOmemoIdentities() { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.pref_delete_omemo_identities); - final List accounts = new ArrayList<>(); - for (Account account : xmppConnectionService.getAccounts()) { - if (account.isEnabled()) { - accounts.add(account.getJid().asBareJid().toString()); - } - } - final boolean[] checkedItems = new boolean[accounts.size()]; - builder.setMultiChoiceItems( - accounts.toArray(new CharSequence[accounts.size()]), - checkedItems, - (dialog, which, isChecked) -> { - checkedItems[which] = isChecked; - final AlertDialog alertDialog = (AlertDialog) dialog; - for (boolean item : checkedItems) { - if (item) { - alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(true); - return; - } - } - alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(false); - }); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton( - R.string.delete_selected_keys, - (dialog, which) -> { - for (int i = 0; i < checkedItems.length; ++i) { - if (checkedItems[i]) { - try { - Jid jid = Jid.of(accounts.get(i).toString()); - Account account = xmppConnectionService.findAccountByJid(jid); - if (account != null) { - account.getAxolotlService().regenerateKeys(true); - } - } catch (IllegalArgumentException e) { - // - } - } - } - }); - final AlertDialog dialog = builder.create(); - dialog.show(); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); - return true; - } - - private void enableMultiAccounts() { - if (!isMultiAccountChecked) { - multiAccountPreference.setEnabled(true); - } - } - - - @Override - public void onStop() { - super.onStop(); - PreferenceManager.getDefaultSharedPreferences(this) - .unregisterOnSharedPreferenceChangeListener(this); - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences preferences, String name) { - final List resendPresence = Arrays.asList( - CONFIRM_MESSAGES, - DND_ON_SILENT_MODE, - AWAY_WHEN_SCREEN_IS_OFF, - ALLOW_MESSAGE_CORRECTION, - TREAT_VIBRATE_AS_SILENT, - MANUALLY_CHANGE_PRESENCE, - BROADCAST_LAST_ACTIVITY); - FileBackend.switchStorage(preferences.getBoolean(USE_INNER_STORAGE, true)); - if (name.equals(OMEMO_SETTING)) { - OmemoSetting.load(this, preferences); - changeOmemoSettingSummary(); - } else if (name.equals(SHOW_FOREGROUND_SERVICE)) { - xmppConnectionService.toggleForegroundService(); - } else if (resendPresence.contains(name)) { - if (xmppConnectionServiceBound) { - if (name.equals(AWAY_WHEN_SCREEN_IS_OFF) - || name.equals(MANUALLY_CHANGE_PRESENCE)) { - xmppConnectionService.toggleScreenEventReceiver(); - } - xmppConnectionService.refreshAllPresences(); - } - } else if (name.equals("dont_trust_system_cas")) { - xmppConnectionService.updateMemorizingTrustmanager(); - reconnectAccounts(); - } else if (name.equals("use_tor")) { - if (preferences.getBoolean(name, false)) { - displayToast(getString(R.string.audio_video_disabled_tor)); - } - reconnectAccounts(); - xmppConnectionService.reinitializeMuclumbusService(); - } else if (name.equals(AUTOMATIC_MESSAGE_DELETION)) { - xmppConnectionService.expireOldMessages(true); - } else if (name.equals(THEME) || name.equals(THEME_COLOR)) { - updateTheme(); - } else if (name.equals(USE_UNICOLORED_CHATBG)) { - xmppConnectionService.updateConversationUi(); - } - else if (UnifiedPushDistributor.PREFERENCES.contains(name)) { - if (xmppConnectionService.reconfigurePushDistributor()) { - xmppConnectionService.renewUnifiedPushEndpoints(); - } - } - } - - @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_CREATE_BACKUP) { - createBackup(true); - } - if (requestCode == REQUEST_IMPORT_SETTINGS) { - importSettings(); - } - } else { - ToastCompat.makeText( - this, - - R.string.no_storage_permission, - ToastCompat.LENGTH_SHORT).show(); - } - } - } - - private void createBackup(boolean notify) { - final Intent intent = new Intent(this, ExportBackupService.class); - intent.putExtra("NOTIFY_ON_BACKUP_COMPLETE", notify); - ContextCompat.startForegroundService(this, intent); - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(R.string.backup_started_message); - builder.setPositiveButton(R.string.ok, null); - builder.create().show(); - } - - @SuppressWarnings({ "unchecked" }) - private boolean importSettings() { - boolean success; - ObjectInputStream input = null; - try { - final File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + File.separator + APP_DIRECTORY + File.separator + "Database" + File.separator, "settings.dat"); - input = new ObjectInputStream(new FileInputStream(file)); - SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).edit(); - prefEdit.clear(); - Map entries = (Map) input.readObject(); - for (Map.Entry entry : entries.entrySet()) { - Object value = entry.getValue(); - String key = entry.getKey(); - - if (value instanceof Boolean) - prefEdit.putBoolean(key, ((Boolean) value).booleanValue()); - else if (value instanceof Float) - prefEdit.putFloat(key, ((Float) value).floatValue()); - else if (value instanceof Integer) - prefEdit.putInt(key, ((Integer) value).intValue()); - else if (value instanceof Long) - prefEdit.putLong(key, ((Long) value).longValue()); - else if (value instanceof String) - prefEdit.putString(key, ((String) value)); - } - prefEdit.commit(); - success = true; - } catch (Exception e) { - success = false; - e.printStackTrace(); - } finally { - try { - if (input != null) { - input.close(); - } - } catch (IOException ex) { - ex.printStackTrace(); - } - } - if (success) { - ToastCompat.makeText(this, R.string.success_import_settings, ToastCompat.LENGTH_SHORT).show(); - } else { - ToastCompat.makeText(this, R.string.error_import_settings, ToastCompat.LENGTH_SHORT).show(); - } - return success; - } - - private void displayToast(final String msg) { - runOnUiThread(() -> ToastCompat.makeText(SettingsActivity.this, msg, ToastCompat.LENGTH_LONG).show()); - } - - private void reconnectAccounts() { - for (Account account : xmppConnectionService.getAccounts()) { - if (account.isEnabled()) { - xmppConnectionService.reconnectAccountInBackground(account); - } - } - } - - public void refreshUiReal() { - recreate(); - } - - private int getNumberOfAccounts() { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - int NumberOfAccounts = preferences.getInt(NUMBER_OF_ACCOUNTS, 0); - Log.d(Config.LOGTAG, "Get number of accounts from file: " + NumberOfAccounts); - return NumberOfAccounts; - } - - private void showIntroAgain() { - SharedPreferences getPrefs = PreferenceManager.getDefaultSharedPreferences(this.getBaseContext()); - Map allEntries = getPrefs.getAll(); - int success = -1; - for (Map.Entry entry : allEntries.entrySet()) { - if (entry.getKey().contains("intro_shown_on_activity")) { - SharedPreferences.Editor e = getPrefs.edit(); - e.putBoolean(entry.getKey(), true); - if (e.commit()) { - if (success != 0) { - success = 1; - } - } else { - success = 0; - } - } - } - if (success == 1) { - ToastCompat.makeText(this, R.string.show_intro_again, ToastCompat.LENGTH_SHORT).show(); - } else { - ToastCompat.makeText(this, R.string.show_intro_again_failed, ToastCompat.LENGTH_SHORT).show(); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java b/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java deleted file mode 100644 index 7958fecc6..000000000 --- a/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java +++ /dev/null @@ -1,63 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.Intent; -import android.os.Bundle; -import android.preference.Preference; -import android.preference.PreferenceCategory; -import android.preference.PreferenceFragment; -import android.preference.PreferenceScreen; -import android.text.TextUtils; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.utils.Compatibility; - -public class SettingsFragment extends PreferenceFragment { - - private String page = null; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.preferences); - // Remove from standard preferences if the flag ONLY_INTERNAL_STORAGE is false - if (!Config.ONLY_INTERNAL_STORAGE) { - PreferenceCategory mCategory = (PreferenceCategory) findPreference("security_options"); - if (mCategory != null) { - Preference cleanCache = findPreference("clean_cache"); - Preference cleanPrivateStorage = findPreference("clean_private_storage"); - mCategory.removePreference(cleanCache); - mCategory.removePreference(cleanPrivateStorage); - } - } - Compatibility.removeUnusedPreferences(this); - - if (!TextUtils.isEmpty(page)) { - openPreferenceScreen(page); - } - } - - public void setActivityIntent(final Intent intent) { - boolean wasEmpty = TextUtils.isEmpty(page); - if (intent != null) { - if (Intent.ACTION_VIEW.equals(intent.getAction())) { - if (intent.getExtras() != null) { - this.page = intent.getExtras().getString("page"); - if (wasEmpty) { - openPreferenceScreen(page); - } - } - } - } - } - - private void openPreferenceScreen(final String screenName) { - final Preference pref = findPreference(screenName); - if (pref instanceof PreferenceScreen) { - final PreferenceScreen preferenceScreen = (PreferenceScreen) pref; - getActivity().setTitle(preferenceScreen.getTitle()); - preferenceScreen.setDependency(""); - setPreferenceScreen((PreferenceScreen) pref); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java deleted file mode 100644 index 7e9b73c00..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java +++ /dev/null @@ -1,336 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.Manifest; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.location.Address; -import android.location.Geocoder; -import android.location.Location; -import android.location.LocationListener; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.text.Html; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.appcompat.widget.Toolbar; -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.lang.ref.WeakReference; -import java.math.RoundingMode; -import java.util.List; -import java.util.Locale; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ActivityShareLocactionBinding; -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.utils.LocationProvider; -import eu.siacs.conversations.utils.ThemeHelper; - -public class ShareLocationActivity extends LocationActivity implements LocationListener { - - private Snackbar snackBar; - private ActivityShareLocactionBinding 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 refreshUiReal() { - - } - - @Override - void onBackendConnected() { - - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_share_locaction); - setSupportActionBar((Toolbar) 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_sharing_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 - public 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))); - new getAddressAsync(this, this.myLoc).execute(); - } else { - this.binding.map.getOverlays().add(new Marker(marker_icon)); - new getAddressAsync(this, getMarkerPosition()).execute(); - } - } else { - this.binding.map.getOverlays().add(new Marker(marker_icon)); - hideAddress(); - } - } - - private Location getMarkerPosition() { - final IGeoPoint markerPoint = this.binding.map.getMapCenter(); - final Location location = new Location(""); - location.setLatitude(markerPoint.getLatitude()); - location.setLongitude(markerPoint.getLongitude()); - return location; - } - - private void showAddress(final Location myLoc) { - this.binding.address.setText(Html.fromHtml(getAddress(this, myLoc))); - if (Html.fromHtml(getAddress(this, myLoc)).length() > 0) { - this.binding.address.setVisibility(View.VISIBLE); - } else { - hideAddress(); - } - } - - private void hideAddress() { - this.binding.address.setVisibility(View.GONE); - } - - @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 static String getAddress(final Context context, final Location location) { - final double longitude = location.getLongitude(); - final double latitude = location.getLatitude(); - String address = ""; - if (latitude != 0 && longitude != 0) { - try { - final Geocoder geoCoder = new Geocoder(context, Locale.getDefault()); - final List

addresses = geoCoder.getFromLocation(latitude, longitude, 1); - if (addresses != null && addresses.size() > 0) { - final Address Address = addresses.get(0); - StringBuilder strAddress = new StringBuilder(""); - - if (Address.getAddressLine(0).length() > 0) { - strAddress.append(Address.getAddressLine(0)); - } - address = strAddress.toString().replace(", ", "
"); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - return address; - } - - private class getAddressAsync extends AsyncTask { - String address = null; - Location location; - - private WeakReference activityReference; - - getAddressAsync(final ShareLocationActivity context, final Location location) { - activityReference = new WeakReference<>(context); - this.location = location; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - } - - @Override - protected Void doInBackground(Void... params) { - address = getAddress(ShareLocationActivity.this, this.location); - return null; - } - - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - showAddress(this.location); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java deleted file mode 100644 index b34a8b090..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ShareViaAccountActivity.java +++ /dev/null @@ -1,90 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.os.Bundle; -import android.widget.ListView; - -import java.util.ArrayList; -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.ui.adapter.AccountAdapter; -import eu.siacs.conversations.xmpp.Jid; - -public class ShareViaAccountActivity extends XmppActivity { - public static final String EXTRA_CONTACT = "contact"; - public static final String EXTRA_BODY = "body"; - - protected final List accountList = new ArrayList<>(); - protected ListView accountListView; - protected AccountAdapter mAccountAdapter; - - @Override - protected void refreshUiReal() { - synchronized (this.accountList) { - accountList.clear(); - accountList.addAll(xmppConnectionService.getAccounts()); - } - mAccountAdapter.notifyDataSetChanged(); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_manage_accounts); - setSupportActionBar(findViewById(R.id.toolbar)); - configureActionBar(getSupportActionBar()); - accountListView = findViewById(R.id.account_list); - this.mAccountAdapter = new AccountAdapter(this, accountList, false); - accountListView.setAdapter(this.mAccountAdapter); - accountListView.setOnItemClickListener((arg0, view, position, arg3) -> { - final Account account = accountList.get(position); - final String body = getIntent().getStringExtra(EXTRA_BODY); - - try { - final Jid contact = Jid.of(getIntent().getStringExtra(EXTRA_CONTACT)); - final Conversation conversation = xmppConnectionService.findOrCreateConversation( - account, contact, false, false); - switchToConversation(conversation, body); - } catch (IllegalArgumentException e) { - // ignore error - } - - finish(); - }); - } - - @Override - protected void onStart() { - super.onStart(); - final int theme = findTheme(); - if (this.mTheme != theme) { - recreate(); - } - } - - @Override - void onBackendConnected() { - final int numAccounts = xmppConnectionService.getAccounts().size(); - - if (numAccounts == 1) { - final String body = getIntent().getStringExtra(EXTRA_BODY); - final Account account = xmppConnectionService.getAccounts().get(0); - - try { - final Jid contact = Jid.of(getIntent().getStringExtra(EXTRA_CONTACT)); - final Conversation conversation = xmppConnectionService.findOrCreateConversation( - account, contact, false, false); - switchToConversation(conversation, body); - } catch (IllegalArgumentException e) { - // ignore error - } - - finish(); - } else { - refreshUiReal(); - } - } -} \ 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 deleted file mode 100644 index d7ed5ee39..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java +++ /dev/null @@ -1,215 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Bundle; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; - -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.ArrayList; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.adapter.ConversationAdapter; -import eu.siacs.conversations.xmpp.Jid; -import me.drakeet.support.toast.ToastCompat; - -public class ShareWithActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate { - - private static final int REQUEST_STORAGE_PERMISSION = 0x733f32; - private Conversation mPendingConversation = null; - - @Override - public void onConversationUpdate() { - refreshUi(); - } - - private static class Share { - public String type; - ArrayList uris = new ArrayList<>(); - public String account; - public String contact; - public String text; - public boolean asQuote = false; - } - - private Share share; - - private static final int REQUEST_START_NEW_CONVERSATION = 0x0501; - private ConversationAdapter mAdapter; - private List mConversations = new ArrayList<>(); - - - protected void onActivityResult(int requestCode, int resultCode, final Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_START_NEW_CONVERSATION - && resultCode == RESULT_OK) { - share.contact = data.getStringExtra("contact"); - share.account = data.getStringExtra(EXTRA_ACCOUNT); - } - if (xmppConnectionServiceBound - && share != null - && share.contact != null - && share.account != null) { - share(); - } - } - - @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) { - if (this.mPendingConversation != null) { - share(this.mPendingConversation); - } else { - Log.d(Config.LOGTAG, "unable to find stored conversation"); - } - } - } else { - ToastCompat.makeText(this, R.string.no_storage_permission, ToastCompat.LENGTH_SHORT).show(); - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_share_with); - setSupportActionBar(findViewById(R.id.toolbar)); - if (getSupportActionBar() != null) { - getSupportActionBar().setDisplayHomeAsUpEnabled(false); - getSupportActionBar().setHomeButtonEnabled(false); - } - setTitle(getString(R.string.title_activity_sharewith)); - RecyclerView mListView = findViewById(R.id.choose_conversation_list); - mAdapter = new ConversationAdapter(this, this.mConversations); - mListView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)); - mListView.setAdapter(mAdapter); - mAdapter.setConversationClickListener((view, conversation) -> share(conversation)); - this.share = new Share(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.share_with, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case R.id.action_add: - final Intent intent = new Intent(getApplicationContext(), ChooseContactActivity.class); - intent.putExtra("direct_search", true); - startActivityForResult(intent, REQUEST_START_NEW_CONVERSATION); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public void onStart() { - super.onStart(); - Intent intent = getIntent(); - if (intent == null) { - return; - } - final String type = intent.getType(); - final String action = intent.getAction(); - final Uri data = intent.getData(); - if (Intent.ACTION_SEND.equals(action)) { - final String text = intent.getStringExtra(Intent.EXTRA_TEXT); - final Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); - final boolean asQuote = intent.getBooleanExtra(ConversationsActivity.EXTRA_AS_QUOTE, false); - if (data != null && "geo".equals(data.getScheme())) { - this.share.uris.clear(); - this.share.uris.add(data); - } 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; - } - } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) { - final ArrayList uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); - this.share.uris = uris == null ? new ArrayList<>() : uris; - } - if (xmppConnectionServiceBound) { - xmppConnectionService.populateWithOrderedConversations(mConversations, this.share.uris.size() == 0, false); - } - } - - @Override - void onBackendConnected() { - if (xmppConnectionServiceBound && share != null && ((share.contact != null && share.account != null))) { - share(); - return; - } - refreshUiReal(); - } - - private void share() { - final Conversation conversation; - Account account; - try { - account = xmppConnectionService.findAccountByJid(Jid.ofEscaped(share.account)); - } catch (final IllegalArgumentException e) { - account = null; - } - if (account == null) { - return; - } - try { - conversation = xmppConnectionService.findOrCreateConversation(account, Jid.of(share.contact), false, true); - } catch (final IllegalArgumentException e) { - return; - } - share(conversation); - } - - private void share(final Conversation conversation) { - if (share.uris.size() != 0 && !hasStoragePermission(REQUEST_STORAGE_PERMISSION)) { - mPendingConversation = conversation; - return; - } - Intent intent = new Intent(this, ConversationsActivity.class); - intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid()); - if (share.uris.size() > 0) { - intent.setAction(Intent.ACTION_SEND_MULTIPLE); - intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, share.uris); - intent.setFlags(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); - intent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, share.asQuote); - } - try { - startActivity(intent); - } catch (SecurityException e) { - ToastCompat.makeText(this, R.string.sharing_application_not_grant_permission, ToastCompat.LENGTH_SHORT).show(); - return; - } - finish(); - } - - public void refreshUiReal() { - //TODO inject desired order to not resort on refresh - xmppConnectionService.populateWithOrderedConversations(mConversations, this.share != null && this.share.uris.size() == 0, false); - mAdapter.notifyDataSetChanged(); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/ShortcutActivity.java b/src/main/java/eu/siacs/conversations/ui/ShortcutActivity.java deleted file mode 100644 index 5cc8765ac..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ShortcutActivity.java +++ /dev/null @@ -1,74 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.view.inputmethod.InputMethodManager; - -import androidx.appcompat.app.ActionBar; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.ListItem; - -public class ShortcutActivity extends AbstractSearchableListItemActivity { - - private static final List BLACKLISTED_ACTIVITIES = Arrays.asList("com.teslacoilsw.launcher.ChooseActionIntentActivity"); - - @Override - protected void refreshUiReal() { - - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - getListView().setOnItemClickListener((parent, view, position, id) -> { - final ComponentName callingActivity = getCallingActivity(); - final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(getSearchEditText().getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY); - - ListItem listItem = getListItems().get(position); - final boolean legacy = BLACKLISTED_ACTIVITIES.contains(callingActivity == null ? null : callingActivity.getClassName()); - Intent shortcut = xmppConnectionService.getShortcutService().createShortcut(((Contact) listItem), legacy); - setResult(RESULT_OK, shortcut); - finish(); - }); - } - - @Override - protected void onStart() { - super.onStart(); - ActionBar bar = getSupportActionBar(); - if (bar != null) { - bar.setTitle(R.string.create_shortcut); - } - } - - @Override - protected void filterContacts(String needle) { - getListItems().clear(); - if (xmppConnectionService == null) { - getListItemAdapter().notifyDataSetChanged(); - return; - } - for (final Account account : xmppConnectionService.getAccounts()) { - if (account.getStatus() != Account.State.DISABLED) { - for (final Contact contact : account.getRoster().getContacts()) { - if (contact.showInContactList() - && contact.match(this, needle)) { - getListItems().add(contact); - } - } - } - } - Collections.sort(getListItems()); - getListItemAdapter().notifyDataSetChanged(); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java b/src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java deleted file mode 100644 index 6c45327d4..000000000 --- a/src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java +++ /dev/null @@ -1,296 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.location.Address; -import android.location.Geocoder; -import android.location.Location; -import android.location.LocationListener; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Bundle; -import android.text.Html; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.appcompat.widget.Toolbar; -import androidx.databinding.DataBindingUtil; - -import org.jetbrains.annotations.NotNull; -import org.osmdroid.util.GeoPoint; - -import java.lang.ref.WeakReference; -import java.util.List; -import java.util.Locale; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ActivityShowLocationBinding; -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.utils.LocationProvider; -import me.drakeet.support.toast.ToastCompat; - -public class ShowLocationActivity extends LocationActivity implements LocationListener { - - private GeoPoint loc = LocationProvider.FALLBACK; - private ActivityShowLocationBinding binding; - private String name; - - private Uri createGeoUri() { - return Uri.parse("geo:" + this.loc.getLatitude() + "," + this.loc.getLongitude()); - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_show_location); - setSupportActionBar((Toolbar) binding.toolbar); - - configureActionBar(getSupportActionBar()); - setupMapView(this.binding.map, this.loc); - - this.binding.fab.setOnClickListener(view -> startNavigation()); - - final Intent intent = getIntent(); - if (intent != null) { - this.name = intent.hasExtra("name") ? intent.getStringExtra("name") : null; - if (intent.hasExtra("longitude") && intent.hasExtra("latitude")) { - final double longitude = intent.getDoubleExtra("longitude", 0); - final double latitude = intent.getDoubleExtra("latitude", 0); - this.loc = new GeoPoint(latitude, longitude); - } - - } - updateLocationMarkers(); - } - - @Override - protected void gotoLoc(final boolean setZoomLevel) { - if (this.loc != null && mapController != null) { - if (setZoomLevel) { - mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL); - } - mapController.animateTo(new GeoPoint(this.loc)); - } - } - - @Override - public void onRequestPermissionsResult( - final int requestCode, - @NonNull final String[] permissions, - @NonNull final int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - updateUi(); - } - - @Override - protected void setMyLoc(final Location location) { - this.myLoc = location; - } - - @Override - public boolean onCreateOptionsMenu(@NotNull final Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_show_location, menu); - updateUi(); - return true; - } - - @Override - protected void updateLocationMarkers() { - super.updateLocationMarkers(); - if (this.myLoc != null) { - this.binding.map.getOverlays().add(new MyLocation(this, null, this.myLoc)); - } - this.binding.map.getOverlays().add(new Marker(this.marker_icon, this.loc)); - new getAddressAsync(this, this.loc, this.name).execute(); - } - - private void showAddress(final GeoPoint loc, final String name) { - this.binding.address.setText(Html.fromHtml(getAddress(this, loc, name))); - if (Html.fromHtml(getAddress(this, loc, name)).length() > 0) { - this.binding.address.setVisibility(View.VISIBLE); - } else { - hideAddress(); - } - } - - private void hideAddress() { - this.binding.address.setVisibility(View.GONE); - } - - @Override - public void onPause() { - super.onPause(); - } - - @Override - protected void refreshUiReal() { - - } - - @Override - void onBackendConnected() { - - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case R.id.action_copy_location: - final ClipboardManager clipboard = - (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); - if (clipboard != null) { - final ClipData clip = - ClipData.newPlainText("location", createGeoUri().toString()); - clipboard.setPrimaryClip(clip); - ToastCompat.makeText(this, R.string.url_copied_to_clipboard, ToastCompat.LENGTH_SHORT) - .show(); - } - return true; - case R.id.action_share_location: - final Intent shareIntent = new Intent(); - shareIntent.setAction(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_TEXT, createGeoUri().toString()); - shareIntent.setType("text/plain"); - try { - startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with))); - } catch (final ActivityNotFoundException e) { - //This should happen only on faulty androids because normally chooser is always available - ToastCompat.makeText( - this, - R.string.no_application_found_to_open_file, - ToastCompat.LENGTH_SHORT) - .show(); - } - return true; - } - return super.onOptionsItemSelected(item); - } - - private void startNavigation() { - final Intent intent = getStartNavigationIntent(); - startActivity(intent); - } - - private Intent getStartNavigationIntent() { - return new Intent( - Intent.ACTION_VIEW, - Uri.parse( - "google.navigation:q=" - + this.loc.getLatitude() - + "," - + this.loc.getLongitude())); - } - - @Override - protected void updateUi() { - final Intent intent = getStartNavigationIntent(); - final ActivityInfo activityInfo = intent.resolveActivityInfo(getPackageManager(), 0); - this.binding.fab.setVisibility(activityInfo == null ? View.GONE : View.VISIBLE); - } - - @Override - public void onLocationChanged(@NotNull final Location location) { - if (LocationHelper.isBetterLocation(location, this.myLoc)) { - this.myLoc = location; - 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 static String getAddress(final Activity context, final GeoPoint location, final String name) { - final double longitude = location.getLongitude(); - final double latitude = location.getLatitude(); - String address = ""; - if (latitude != 0 && longitude != 0) { - try { - final Geocoder geoCoder = new Geocoder(context, Locale.getDefault()); - final List
addresses = geoCoder.getFromLocation(latitude, longitude, 1); - if (addresses != null && addresses.size() > 0) { - final Address Address = addresses.get(0); - StringBuilder strAddress = new StringBuilder(""); - if (name != null && name.length() > 0) { - strAddress.append(""); - strAddress.append(name); - strAddress.append(":
"); - } - if (Address.getAddressLine(0).length() > 0) { - strAddress.append(Address.getAddressLine(0)); - } - address = strAddress.toString().replace(", ", "
"); - } else { - StringBuilder strAddress = new StringBuilder(""); - if (name != null && name.length() > 0) { - strAddress.append(""); - strAddress.append(name); - strAddress.append(""); - } - address = strAddress.toString(); - } - } catch (Exception e) { - e.printStackTrace(); - StringBuilder strAddress = new StringBuilder(""); - if (name != null && name.length() > 0) { - strAddress.append(""); - strAddress.append(name); - strAddress.append(""); - } - address = strAddress.toString(); - } - } - return address; - } - - private class getAddressAsync extends AsyncTask { - String address = null; - String name = null; - GeoPoint location; - - private WeakReference activityReference; - - getAddressAsync(final ShowLocationActivity context, final GeoPoint location, final String name) { - activityReference = new WeakReference<>(context); - this.location = location; - this.name = name; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - } - - @Override - protected Void doInBackground(Void... params) { - address = getAddress(ShowLocationActivity.this, this.location, this.name); - return null; - } - - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - showAddress(this.location, this.name); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java deleted file mode 100644 index 4ac48a0c5..000000000 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ /dev/null @@ -1,1389 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.app.Dialog; -import android.app.PendingIntent; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.res.ColorStateList; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.util.Log; -import android.util.Pair; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; -import android.widget.AdapterView; -import android.widget.AdapterView.AdapterContextMenuInfo; -import android.widget.ArrayAdapter; -import android.widget.AutoCompleteTextView; -import android.widget.CheckBox; -import android.widget.EditText; -import android.widget.ListView; -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.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.Toolbar; -import androidx.core.content.ContextCompat; -import androidx.databinding.DataBindingUtil; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentTransaction; -import androidx.viewpager.widget.PagerAdapter; -import androidx.viewpager.widget.ViewPager; -import android.text.method.LinkMovementMethod; -import androidx.annotation.StringRes; -import androidx.appcompat.widget.PopupMenu; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -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; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ActivityStartConversationBinding; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Bookmark; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.ListItem; -import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.entities.Presence; -import eu.siacs.conversations.services.QuickConversationsService; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; -import eu.siacs.conversations.ui.adapter.ListItemAdapter; -import eu.siacs.conversations.ui.interfaces.OnBackendConnected; -import eu.siacs.conversations.ui.util.IntroHelper; -import eu.siacs.conversations.ui.util.JidDialog; -import eu.siacs.conversations.ui.util.PendingItem; -import eu.siacs.conversations.ui.util.SoftKeyboardUtils; -import eu.siacs.conversations.utils.AccountUtils; -import eu.siacs.conversations.utils.MenuDoubleTabUtil; -import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnUpdateBlocklist; -import eu.siacs.conversations.xmpp.XmppConnection; -import me.drakeet.support.toast.ToastCompat; -import eu.siacs.conversations.ui.widget.SwipeRefreshListFragment; - -public class StartConversationActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, CreatePrivateGroupChatDialog.CreateConferenceDialogListener, JoinConferenceDialog.JoinConferenceDialogListener, SwipeRefreshLayout.OnRefreshListener, CreatePublicChannelDialog.CreatePublicChannelDialogListener { - - public static final String EXTRA_INVITE_URI = "eu.siacs.conversations.invite_uri"; - - private final int REQUEST_SYNC_CONTACTS = 0x28cf; - private final int REQUEST_CREATE_CONFERENCE = 0x39da; - private final PendingItem pendingViewIntent = new PendingItem<>(); - private final PendingItem mInitialSearchValue = new PendingItem<>(); - private final AtomicBoolean oneShotKeyboardSuppress = new AtomicBoolean(); - public int conference_context_id; - public int contact_context_id; - private ListPagerAdapter mListPagerAdapter; - private final List contacts = new ArrayList<>(); - private ListItemAdapter mContactsAdapter; - private final List conferences = new ArrayList<>(); - private ListItemAdapter mConferenceAdapter; - private final List mActivatedAccounts = new ArrayList<>(); - private EditText mSearchEditText; - private final AtomicBoolean mRequestedContactsPermission = new AtomicBoolean(false); - private final AtomicBoolean mOpenedFab = new AtomicBoolean(false); - private boolean mHideOfflineContacts = false; - private boolean createdByViewIntent = false; - private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() { - - @Override - public boolean onMenuItemActionExpand(MenuItem item) { - mSearchEditText.post(() -> { - updateSearchViewHint(); - mSearchEditText.requestFocus(); - if (oneShotKeyboardSuppress.compareAndSet(true, false)) { - return; - } - InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null) { - imm.showSoftInput(mSearchEditText, InputMethodManager.SHOW_IMPLICIT); - } - }); - if (binding.speedDial.isOpen()) { - binding.speedDial.close(); - } - return true; - } - - @Override - public boolean onMenuItemActionCollapse(MenuItem item) { - SoftKeyboardUtils.hideSoftKeyboard(StartConversationActivity.this); - mSearchEditText.setText(""); - filter(null); - return true; - } - }; - private final TextWatcher mSearchTextWatcher = new TextWatcher() { - - @Override - public void afterTextChanged(Editable editable) { - filter(editable.toString()); - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - }; - private MenuItem mMenuSearchView; - private final ListItemAdapter.OnTagClickedListener mOnTagClickedListener = new ListItemAdapter.OnTagClickedListener() { - @Override - public void onTagClicked(String tag) { - if (mMenuSearchView != null) { - mMenuSearchView.expandActionView(); - mSearchEditText.setText(""); - mSearchEditText.append(tag); - filter(tag); - } - } - }; - private Pair mPostponedActivityResult; - private Toast mToast; - private final UiCallback mAdhocConferenceCallback = new UiCallback() { - @Override - public void success(final Conversation conversation) { - runOnUiThread(() -> { - hideToast(); - switchToConversation(conversation); - }); - } - - @Override - public void error(final int errorCode, Conversation object) { - runOnUiThread(() -> replaceToast(getString(errorCode))); - } - - @Override - public void userInputRequired(PendingIntent pi, Conversation object) { - - } - - @Override - public void progress(int progress) { - - } - }; - - private ActivityStartConversationBinding binding; - private final TextView.OnEditorActionListener mSearchDone = new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - int pos = binding.startConversationViewPager.getCurrentItem(); - if (pos == 0) { - if (contacts.size() == 1) { - openConversationForContact((Contact) contacts.get(0)); - return true; - } else if (contacts.size() == 0 && conferences.size() == 1) { - openConversationsForBookmark((Bookmark) conferences.get(0)); - return true; - } - } else { - if (conferences.size() == 1) { - openConversationsForBookmark((Bookmark) conferences.get(0)); - return true; - } else if (conferences.size() == 0 && contacts.size() == 1) { - openConversationForContact((Contact) contacts.get(0)); - return true; - } - } - SoftKeyboardUtils.hideSoftKeyboard(StartConversationActivity.this); - mListPagerAdapter.requestFocus(pos); - return true; - } - }; - - public static void populateAccountSpinner(Context context, List accounts, Spinner spinner) { - if (accounts.size() > 0) { - ArrayAdapter adapter = new ArrayAdapter<>(context, R.layout.simple_list_item, accounts); - adapter.setDropDownViewResource(R.layout.simple_list_item); - spinner.setAdapter(adapter); - spinner.setEnabled(true); - } else { - ArrayAdapter adapter = new ArrayAdapter<>(context, - R.layout.simple_list_item, - Collections.singletonList(context.getString(R.string.no_accounts))); - adapter.setDropDownViewResource(R.layout.simple_list_item); - spinner.setAdapter(adapter); - spinner.setEnabled(false); - } - } - - public static void launch(Context context) { - final Intent intent = new Intent(context, StartConversationActivity.class); - context.startActivity(intent); - } - - private static Intent createLauncherIntent(Context context) { - final Intent intent = new Intent(context, StartConversationActivity.class); - intent.setAction(Intent.ACTION_MAIN); - intent.addCategory(Intent.CATEGORY_LAUNCHER); - return intent; - } - - private static boolean isViewIntent(final Intent i) { - return i != null && (Intent.ACTION_VIEW.equals(i.getAction()) || Intent.ACTION_SENDTO.equals(i.getAction()) || i.hasExtra(EXTRA_INVITE_URI)); - } - - protected void hideToast() { - if (mToast != null) { - mToast.cancel(); - } - } - - protected void replaceToast(String msg) { - hideToast(); - mToast = ToastCompat.makeText(this, msg, ToastCompat.LENGTH_LONG); - mToast.show(); - } - - @Override - public void onRosterUpdate() { - this.refreshUi(); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_start_conversation); - Toolbar toolbar = (Toolbar) binding.toolbar; - setSupportActionBar(toolbar); - configureActionBar(getSupportActionBar()); - - inflateFab(binding.speedDial, R.menu.start_conversation_fab_submenu); - binding.tabLayout.setupWithViewPager(binding.startConversationViewPager); - binding.startConversationViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { - @Override - public void onPageSelected(int position) { - updateSearchViewHint(); - } - }); - mListPagerAdapter = new ListPagerAdapter(getSupportFragmentManager()); - binding.startConversationViewPager.setAdapter(mListPagerAdapter); - - mConferenceAdapter = new ListItemAdapter(this, conferences); - mContactsAdapter = new ListItemAdapter(this, contacts); - mContactsAdapter.setOnTagClickedListener(this.mOnTagClickedListener); - IntroHelper.showIntro(this, false); - final SharedPreferences preferences = getPreferences(); - - this.mHideOfflineContacts = QuickConversationsService.isConversations() && preferences.getBoolean("hide_offline", false); - - final boolean startSearching = preferences.getBoolean("start_searching", getResources().getBoolean(R.bool.start_searching)); - - final Intent intent; - if (savedInstanceState == null) { - intent = getIntent(); - } else { - createdByViewIntent = savedInstanceState.getBoolean("created_by_view_intent", false); - final String search = savedInstanceState.getString("search"); - if (search != null) { - mInitialSearchValue.push(search); - } - intent = savedInstanceState.getParcelable("intent"); - } - - if (isViewIntent(intent)) { - pendingViewIntent.push(intent); - createdByViewIntent = true; - setIntent(createLauncherIntent(this)); - } else if (startSearching && mInitialSearchValue.peek() == null) { - mInitialSearchValue.push(""); - } - mRequestedContactsPermission.set(savedInstanceState != null && savedInstanceState.getBoolean("requested_contacts_permission", false)); - mOpenedFab.set(savedInstanceState != null && savedInstanceState.getBoolean("opened_fab", false)); - binding.speedDial.setOnActionSelectedListener(actionItem -> { - final String searchString = mSearchEditText != null ? mSearchEditText.getText().toString() : null; - final String prefilled; - if (isValidJid(searchString)) { - prefilled = Jid.ofEscaped(searchString).toEscapedString(); - } else { - prefilled = null; - } - switch (actionItem.getId()) { - case R.id.discover_public_channels: - startActivity(new Intent(this, ChannelDiscoveryActivity.class)); - break; - case R.id.join_public_channel: - showJoinConferenceDialog(prefilled); - break; - case R.id.create_private_group_chat: - showCreatePrivateGroupChatDialog(); - break; - case R.id.create_public_channel: - showPublicChannelDialog(); - break; - case R.id.create_contact: - showCreateContactDialog(prefilled, null); - break; - } - return false; - }); - binding.speedDial.getMainFab().setSupportImageTintList(ColorStateList.valueOf(getResources().getColor(R.color.realwhite))); - } - - 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); - return !jid.isDomainJid(); - } catch (IllegalArgumentException e) { - return false; - } - } - - @Override - public void onSaveInstanceState(Bundle savedInstanceState) { - Intent pendingIntent = pendingViewIntent.peek(); - savedInstanceState.putParcelable("intent", pendingIntent != null ? pendingIntent : getIntent()); - savedInstanceState.putBoolean("requested_contacts_permission", mRequestedContactsPermission.get()); - savedInstanceState.putBoolean("opened_fab", mOpenedFab.get()); - savedInstanceState.putBoolean("created_by_view_intent", createdByViewIntent); - if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) { - savedInstanceState.putString("search", mSearchEditText != null ? mSearchEditText.getText().toString() : null); - } - super.onSaveInstanceState(savedInstanceState); - } - - @Override - public void onStart() { - super.onStart(); - final int theme = findTheme(); - if (this.mTheme != theme) { - recreate(); - } else { - if (pendingViewIntent.peek() == null) { - askForContactsPermissions(); - } - } - mConferenceAdapter.refreshSettings(); - mContactsAdapter.refreshSettings(); - } - - @Override - public void onNewIntent(final Intent intent) { - super.onNewIntent(intent); - if (xmppConnectionServiceBound) { - processViewIntent(intent); - } else { - pendingViewIntent.push(intent); - } - setIntent(createLauncherIntent(this)); - } - - protected void openConversationForContact(int position) { - Contact contact = (Contact) contacts.get(position); - openConversationForContact(contact); - } - - protected void openConversationForContact(Contact contact) { - Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true); - SoftKeyboardUtils.hideSoftKeyboard(this); - switchToConversation(conversation); - } - - protected void openConversationForBookmark(int position) { - Bookmark bookmark = (Bookmark) conferences.get(position); - openConversationsForBookmark(bookmark); - } - - protected void shareBookmarkUri() { - shareBookmarkUri(conference_context_id); - } - - protected void shareBookmarkUri(int position) { - Bookmark bookmark = (Bookmark) conferences.get(position); - shareAsChannel(this, bookmark.getJid().asBareJid().toEscapedString()); - } - - public static void shareAsChannel(final Context context, final String address) { - Intent shareIntent = new Intent(); - shareIntent.setAction(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_TEXT, "xmpp:" + address + "?join"); - shareIntent.setType("text/plain"); - try { - context.startActivity(Intent.createChooser(shareIntent, context.getText(R.string.share_uri_with))); - } catch (ActivityNotFoundException e) { - ToastCompat.makeText(context, R.string.no_application_to_share_uri, ToastCompat.LENGTH_SHORT).show(); - } - } - - protected void openConversationsForBookmark(Bookmark bookmark) { - final Jid jid = bookmark.getFullJid(); - if (jid == null) { - ToastCompat.makeText(this, R.string.invalid_jid, ToastCompat.LENGTH_SHORT).show(); - return; - } - Conversation conversation = xmppConnectionService.findOrCreateConversation(bookmark.getAccount(), jid, true, true, true); - bookmark.setConversation(conversation); - if (!bookmark.autojoin() && getPreferences().getBoolean("autojoin", getResources().getBoolean(R.bool.autojoin))) { - bookmark.setAutojoin(true); - xmppConnectionService.createBookmark(bookmark.getAccount(), bookmark); - } - SoftKeyboardUtils.hideSoftKeyboard(this); - switchToConversation(conversation); - } - - protected void openDetailsForContact() { - int position = contact_context_id; - Contact contact = (Contact) contacts.get(position); - switchToContactDetails(contact); - } - - protected void showQrForContact() { - int position = contact_context_id; - Contact contact = (Contact) contacts.get(position); - showQrCode("xmpp:" + contact.getJid().asBareJid().toEscapedString()); - } - - protected void toggleContactBlock() { - final int position = contact_context_id; - BlockContactDialog.show(this, (Contact) contacts.get(position)); - } - - protected void deleteContact() { - final int position = contact_context_id; - final Contact contact = (Contact) contacts.get(position); - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setNegativeButton(R.string.cancel, null); - builder.setTitle(R.string.action_delete_contact); - builder.setMessage(JidDialog.style(this, R.string.remove_contact_text, contact.getJid().toEscapedString())); - builder.setPositiveButton(R.string.delete, (dialog, which) -> { - xmppConnectionService.deleteContactOnServer(contact); - filter(mSearchEditText.getText().toString()); - }); - builder.create().show(); - } - - protected void deleteConference() { - int position = conference_context_id; - final Bookmark bookmark = (Bookmark) conferences.get(position); - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setNegativeButton(R.string.cancel, null); - builder.setTitle(R.string.delete_bookmark); - builder.setMessage(JidDialog.style(this, R.string.remove_bookmark_text, bookmark.getJid().toEscapedString())); - builder.setPositiveButton(R.string.delete, (dialog, which) -> { - bookmark.setConversation(null); - final Account account = bookmark.getAccount(); - xmppConnectionService.deleteBookmark(account, bookmark); - filter(mSearchEditText.getText().toString()); - }); - builder.create().show(); - - } - - @SuppressLint("InflateParams") - protected void showCreateContactDialog(final String prefilledJid, final Invite invite) { - FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); - Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG); - if (prev != null) { - ft.remove(prev); - } - boolean multiAccount = false; - try { - multiAccount = xmppConnectionService.multipleAccounts(); - } catch (Exception e) { - e.printStackTrace(); - } - ft.addToBackStack(null); - EnterJidDialog dialog = EnterJidDialog.newInstance( - mActivatedAccounts, - getString(R.string.add_contact), - getString(R.string.add), - prefilledJid, - invite == null ? null : invite.account, - invite == null || !invite.hasFingerprints(), - multiAccount, - true - ); - - dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid) -> { - if (!xmppConnectionServiceBound) { - return false; - } - - final Account account = xmppConnectionService.findAccountByJid(accountJid); - if (account == null) { - return true; - } - - final Contact contact = account.getRoster().getContact(contactJid); - if (invite != null && invite.getName() != null) { - contact.setServerName(invite.getName()); - } - if (contact.isSelf()) { - switchToConversation(contact); - return true; - } else if (contact.showInRoster()) { - throw new EnterJidDialog.JidError(getString(R.string.contact_already_exists)); - } else { - final String preAuth = invite == null ? null : invite.getParameter(XmppUri.PARAMETER_PRE_AUTH); - xmppConnectionService.createContact(contact, true, preAuth); - if (invite != null && invite.hasFingerprints()) { - xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints()); - } - switchToConversationDoNotAppend(contact, invite == null ? null : invite.getBody()); - return true; - } - }); - dialog.show(ft, FRAGMENT_TAG_DIALOG); - } - - @SuppressLint("InflateParams") - protected void showJoinConferenceDialog(final String prefilledJid) { - FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); - Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG); - if (prev != null) { - ft.remove(prev); - } - ft.addToBackStack(null); - JoinConferenceDialog joinConferenceFragment = JoinConferenceDialog.newInstance(prefilledJid, mActivatedAccounts, xmppConnectionService.multipleAccounts()); - joinConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG); - } - - private void showCreatePrivateGroupChatDialog() { - FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); - Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG); - if (prev != null) { - ft.remove(prev); - } - ft.addToBackStack(null); - CreatePrivateGroupChatDialog createConferenceFragment = CreatePrivateGroupChatDialog.newInstance(mActivatedAccounts, xmppConnectionService.multipleAccounts()); - createConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG); - } - - private void showPublicChannelDialog() { - FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); - Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG); - if (prev != null) { - ft.remove(prev); - } - ft.addToBackStack(null); - CreatePublicChannelDialog dialog = CreatePublicChannelDialog.newInstance(mActivatedAccounts, xmppConnectionService.multipleAccounts()); - dialog.show(ft, FRAGMENT_TAG_DIALOG); - } - - public static Account getSelectedAccount(Context context, Spinner spinner) { - if (spinner == null || !spinner.isEnabled()) { - return null; - } - if (context instanceof XmppActivity) { - Jid jid; - try { - if (Config.DOMAIN_LOCK != null) { - jid = Jid.ofEscaped((String) spinner.getSelectedItem(), Config.DOMAIN_LOCK, null); - } else { - jid = Jid.ofEscaped((String) spinner.getSelectedItem()); - } - } catch (final IllegalArgumentException e) { - return null; - } - final XmppConnectionService service = ((XmppActivity) context).xmppConnectionService; - if (service == null) { - return null; - } - return service.findAccountByJid(jid); - } else { - return null; - } - } - - protected void switchToConversation(Contact contact) { - Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true); - switchToConversation(conversation); - } - - protected void switchToConversationDoNotAppend(Contact contact, String body) { - Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true); - switchToConversationDoNotAppend(conversation, body); - } - - @Override - public void invalidateOptionsMenu() { - boolean isExpanded = mMenuSearchView != null && mMenuSearchView.isActionViewExpanded(); - String text = mSearchEditText != null ? mSearchEditText.getText().toString() : ""; - if (isExpanded) { - mInitialSearchValue.push(text); - oneShotKeyboardSuppress.set(true); - } - super.invalidateOptionsMenu(); - } - - private void updateSearchViewHint() { - if (binding == null || mSearchEditText == null) { - return; - } - if (binding.startConversationViewPager.getCurrentItem() == 0) { - mSearchEditText.setHint(R.string.search_contacts); - mSearchEditText.setContentDescription(getString(R.string.search_contacts)); - } else { - mSearchEditText.setHint(R.string.search_bookmarks); - mSearchEditText.setContentDescription(getString(R.string.search_bookmarks)); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.start_conversation, menu); - MenuItem menuHideOffline = menu.findItem(R.id.action_hide_offline); - MenuItem menuActionAccounts = menu.findItem(R.id.action_accounts); - if (xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 1 && !xmppConnectionService.multipleAccounts()) { - menuActionAccounts.setTitle(R.string.action_account); - } else { - menuActionAccounts.setTitle(R.string.action_accounts); - } - MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code); - qrCodeScanMenuItem.setVisible(isCameraFeatureAvailable()); - menuHideOffline.setVisible(true); - menuHideOffline.setChecked(this.mHideOfflineContacts); - mMenuSearchView = menu.findItem(R.id.action_search); - mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener); - View mSearchView = mMenuSearchView.getActionView(); - mSearchEditText = mSearchView.findViewById(R.id.search_field); - mSearchEditText.addTextChangedListener(mSearchTextWatcher); - mSearchEditText.setOnEditorActionListener(mSearchDone); - String initialSearchValue = mInitialSearchValue.pop(); - if (initialSearchValue != null) { - mMenuSearchView.expandActionView(); - mSearchEditText.append(initialSearchValue); - filter(initialSearchValue); - } - updateSearchViewHint(); - return super.onCreateOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (MenuDoubleTabUtil.shouldIgnoreTap()) { - return false; - } - switch (item.getItemId()) { - case android.R.id.home: - navigateBack(); - return true; - case R.id.action_scan_qr_code: - UriHandlerActivity.scan(this); - return true; - case R.id.action_hide_offline: - mHideOfflineContacts = !item.isChecked(); - getPreferences().edit().putBoolean("hide_offline", mHideOfflineContacts).commit(); - if (mSearchEditText != null) { - filter(mSearchEditText.getText().toString()); - } - invalidateOptionsMenu(); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_SEARCH && !event.isLongPress()) { - openSearch(); - return true; - } - int c = event.getUnicodeChar(); - if (c > 32) { - if (mSearchEditText != null && !mSearchEditText.isFocused()) { - openSearch(); - mSearchEditText.append(Character.toString((char) c)); - return true; - } - } - return super.onKeyUp(keyCode, event); - } - - private void openSearch() { - if (mMenuSearchView != null) { - mMenuSearchView.expandActionView(); - } - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent intent) { - if (resultCode == RESULT_OK) { - if (xmppConnectionServiceBound) { - this.mPostponedActivityResult = null; - if (requestCode == REQUEST_CREATE_CONFERENCE) { - Account account = extractAccount(intent); - final String name = intent.getStringExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME); - final List jids = ChooseContactActivity.extractJabberIds(intent); - if (account != null && jids.size() > 0) { - if (xmppConnectionService.createAdhocConference(account, name, jids, mAdhocConferenceCallback)) { - mToast = ToastCompat.makeText(this, R.string.creating_conference, ToastCompat.LENGTH_LONG); - mToast.show(); - } - } - } - } else { - this.mPostponedActivityResult = new Pair<>(requestCode, intent); - } - } - super.onActivityResult(requestCode, requestCode, intent); - } - - private void askForContactsPermissions() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { - if (mRequestedContactsPermission.compareAndSet(false, true)) { - - if (QuickConversationsService.isQuicksy() || shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - final AtomicBoolean requestPermission = new AtomicBoolean(false); - builder.setTitle(R.string.sync_with_contacts); - builder.setMessage(getString(R.string.sync_with_contacts_long)); - @StringRes int confirmButtonText; - if (QuickConversationsService.isConversations()) { - confirmButtonText = R.string.next; - } else { - confirmButtonText = R.string.confirm; - } - builder.setPositiveButton(confirmButtonText, (dialog, which) -> { - if (requestPermission.compareAndSet(false, true)) { - requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS); - } - }); - builder.setOnDismissListener(dialog -> { - if (QuickConversationsService.isConversations() && requestPermission.compareAndSet(false, true)) { - requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS); - - } - }); - SharedPreferences pref = this.getSharedPreferences("PACKAGE.NAME",MODE_PRIVATE); - Boolean firstTime = pref.getBoolean("firstTime",true); - if(firstTime){ - builder.setCancelable(QuickConversationsService.isQuicksy()); - final AlertDialog dialog = builder.create(); - dialog.setCanceledOnTouchOutside(QuickConversationsService.isQuicksy()); - dialog.setOnShowListener(dialogInterface -> { - final TextView tv = dialog.findViewById(android.R.id.message); - if (tv != null) { - tv.setMovementMethod(LinkMovementMethod.getInstance()); - } - }); - dialog.show(); - pref.edit().putBoolean("firstTime",false).apply(); - } - } else { - requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS); - } - - } - } - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (grantResults.length > 0) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - ScanActivity.onRequestPermissionResult(this, requestCode, grantResults); - if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) { - if (QuickConversationsService.isQuicksy()) { - setRefreshing(true); - } - xmppConnectionService.loadPhoneContacts(); - xmppConnectionService.startContactObserver(); - } - } - } - } - - private void configureHomeButton() { - final ActionBar actionBar = getSupportActionBar(); - if (actionBar == null) { - return; - } - boolean openConversations = !createdByViewIntent && !xmppConnectionService.isConversationsListEmpty(null); - actionBar.setDisplayHomeAsUpEnabled(openConversations); - actionBar.setDisplayHomeAsUpEnabled(openConversations); - - } - - @Override - protected void onBackendConnected() { - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) { - xmppConnectionService.getQuickConversationsService().considerSyncBackground(false); - } - if (mPostponedActivityResult != null) { - onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second); - this.mPostponedActivityResult = null; - } - this.mActivatedAccounts.clear(); - this.mActivatedAccounts.addAll(AccountUtils.getEnabledAccounts(xmppConnectionService)); - configureHomeButton(); - Intent intent = pendingViewIntent.pop(); - if (intent != null && processViewIntent(intent)) { - filter(null); - } else { - if (mSearchEditText != null) { - filter(mSearchEditText.getText().toString()); - } else { - filter(null); - } - } - Fragment fragment = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG); - if (fragment instanceof OnBackendConnected) { - Log.d(Config.LOGTAG, "calling on backend connected on dialog"); - ((OnBackendConnected) fragment).onBackendConnected(); - } - if (QuickConversationsService.isQuicksy()) { - setRefreshing(xmppConnectionService.getQuickConversationsService().isSynchronizing()); - } - if (QuickConversationsService.isConversations() && AccountUtils.hasEnabledAccounts(xmppConnectionService) && this.contacts.size() == 0 && this.conferences.size() == 0 && mOpenedFab.compareAndSet(false, true)) { - binding.speedDial.open(); - } - } - - protected boolean processViewIntent(@NonNull Intent intent) { - final String inviteUri = intent.getStringExtra(EXTRA_INVITE_URI); - if (inviteUri != null) { - final Invite invite = new Invite(inviteUri); - invite.account = intent.getStringExtra(EXTRA_ACCOUNT); - if (invite.isValidJid()) { - return invite.invite(); - } - } - final String action = intent.getAction(); - if (action == null) { - return false; - } - switch (action) { - case Intent.ACTION_SENDTO: - case Intent.ACTION_VIEW: - Uri uri = intent.getData(); - if (uri != null) { - Invite invite = new Invite(intent.getData(), intent.getBooleanExtra("scanned", false)); - invite.account = intent.getStringExtra(EXTRA_ACCOUNT); - invite.forceDialog = intent.getBooleanExtra("force_dialog", false); - return invite.invite(); - } else { - return false; - } - } - return false; - } - - private boolean handleJid(Invite invite) { - List contacts = xmppConnectionService.findContacts(invite.getJid(), invite.account); - if (invite.isAction(XmppUri.ACTION_JOIN)) { - Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid()); - if (muc != null && !invite.forceDialog) { - switchToConversationDoNotAppend(muc, invite.getBody()); - return true; - } else { - showJoinConferenceDialog(invite.getJid().asBareJid().toEscapedString()); - return false; - } - } else if (contacts.size() == 0) { - showCreateContactDialog(invite.getJid().toEscapedString(), invite); - return false; - } else if (contacts.size() == 1) { - Contact contact = contacts.get(0); - if (!invite.isSafeSource() && invite.hasFingerprints()) { - displayVerificationWarningDialog(contact, invite); - } else { - if (invite.hasFingerprints()) { - if (xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints())) { - ToastCompat.makeText(this, R.string.verified_fingerprints, ToastCompat.LENGTH_SHORT).show(); - } - } - if (invite.account != null) { - xmppConnectionService.getShortcutService().report(contact); - } - switchToConversationDoNotAppend(contact, invite.getBody()); - } - return true; - } else { - if (mMenuSearchView != null) { - mMenuSearchView.expandActionView(); - mSearchEditText.setText(""); - mSearchEditText.append(invite.getJid().toEscapedString()); - filter(invite.getJid().toEscapedString()); - } else { - mInitialSearchValue.push(invite.getJid().toEscapedString()); - } - return true; - } - } - - private void displayVerificationWarningDialog(final Contact contact, final Invite invite) { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.verify_omemo_keys); - final View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null); - final CheckBox isTrustedSource = view.findViewById(R.id.trusted_source); - final TextView warning = view.findViewById(R.id.warning); - warning.setText(JidDialog.style(this, R.string.verifying_omemo_keys_trusted_source, contact.getJid().asBareJid().toEscapedString(), contact.getDisplayName())); - builder.setView(view); - builder.setPositiveButton(R.string.confirm, (dialog, which) -> { - if (isTrustedSource.isChecked() && invite.hasFingerprints()) { - xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints()); - } - switchToConversationDoNotAppend(contact, invite.getBody()); - }); - builder.setNegativeButton(R.string.cancel, (dialog, which) -> StartConversationActivity.this.finish()); - final AlertDialog dialog = builder.create(); - dialog.setCanceledOnTouchOutside(false); - dialog.setOnCancelListener(dialog1 -> StartConversationActivity.this.finish()); - dialog.show(); - } - - protected void filter(String needle) { - if (xmppConnectionServiceBound) { - this.filterContacts(needle); - this.filterConferences(needle); - } - } - - protected void filterContacts(String needle) { - this.contacts.clear(); - final List accounts = xmppConnectionService.getAccounts(); - for (Account account : accounts) { - if (account.getStatus() != Account.State.DISABLED) { - for (Contact contact : account.getRoster().getContacts()) { - Presence.Status s = contact.getShownStatus(); - if (contact.showInContactList() - && contact.match(this, needle) - && (!this.mHideOfflineContacts - || (needle != null && !needle.trim().isEmpty()) - || s.compareTo(Presence.Status.OFFLINE) < 0)) { - this.contacts.add(contact); - } - } - } - } - Collections.sort(this.contacts); - mContactsAdapter.notifyDataSetChanged(); - } - - protected void filterConferences(String needle) { - this.conferences.clear(); - for (final Account account : xmppConnectionService.getAccounts()) { - if (account.getStatus() != Account.State.DISABLED) { - for (final Bookmark bookmark : account.getBookmarks()) { - if (bookmark.match(this, needle)) { - this.conferences.add(bookmark); - } - } - } - } - Collections.sort(this.conferences); - mConferenceAdapter.notifyDataSetChanged(); - } - - @Override - public void OnUpdateBlocklist(final Status status) { - refreshUi(); - } - - @Override - protected void refreshUiReal() { - if (mSearchEditText != null) { - filter(mSearchEditText.getText().toString()); - } - configureHomeButton(); - if (QuickConversationsService.isQuicksy()) { - setRefreshing(xmppConnectionService.getQuickConversationsService().isSynchronizing()); - } - } - - @Override - public void onBackPressed() { - if (binding.speedDial.isOpen()) { - binding.speedDial.close(); - return; - } - navigateBack(); - } - - private void navigateBack() { - if (!createdByViewIntent && xmppConnectionService != null && !xmppConnectionService.isConversationsListEmpty(null)) { - Intent intent = new Intent(this, ConversationsActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - startActivity(intent); - } - finish(); - } - - @Override - public void onCreateDialogPositiveClick(Spinner spinner, String name) { - if (!xmppConnectionServiceBound) { - return; - } - final Account account = getSelectedAccount(this, spinner); - if (account == null) { - return; - } - Intent intent = new Intent(getApplicationContext(), ChooseContactActivity.class); - intent.putExtra(ChooseContactActivity.EXTRA_SHOW_ENTER_JID, false); - intent.putExtra(ChooseContactActivity.EXTRA_SELECT_MULTIPLE, true); - intent.putExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME, name.trim()); - intent.putExtra(ChooseContactActivity.EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); - intent.putExtra(ChooseContactActivity.EXTRA_TITLE_RES_ID, R.string.choose_participants); - startActivityForResult(intent, REQUEST_CREATE_CONFERENCE); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - - @Override - public void onJoinDialogPositiveClick(Dialog dialog, Spinner spinner, TextInputLayout layout, AutoCompleteTextView jid, boolean isBookmarkChecked) { - if (!xmppConnectionServiceBound) { - return; - } - final Account account = getSelectedAccount(this, spinner); - if (account == null) { - return; - } - final String input = jid.getText().toString().trim(); - Jid conferenceJid; - try { - conferenceJid = Jid.ofEscaped(input); - } catch (final IllegalArgumentException e) { - final XmppUri xmppUri = new XmppUri(input); - if (xmppUri.isValidJid() && xmppUri.isAction(XmppUri.ACTION_JOIN)) { - final Editable editable = jid.getEditableText(); - editable.clear(); - editable.append(xmppUri.getJid().toEscapedString()); - conferenceJid = xmppUri.getJid(); - } else { - layout.setError(getString(R.string.invalid_jid)); - return; - } - } - - if (isBookmarkChecked) { - Bookmark bookmark = account.getBookmark(conferenceJid); - if (bookmark != null) { - dialog.dismiss(); - openConversationsForBookmark(bookmark); - } else { - bookmark = new Bookmark(account, conferenceJid.asBareJid()); - bookmark.setAutojoin(getBooleanPreference("autojoin", R.bool.autojoin)); - final String nick = conferenceJid.getResource(); - if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) { - bookmark.setNick(nick); - } - xmppConnectionService.createBookmark(account, bookmark); - final Conversation conversation = xmppConnectionService - .findOrCreateConversation(account, conferenceJid, true, true, true); - bookmark.setConversation(conversation); - dialog.dismiss(); - switchToConversation(conversation); - } - } else { - final Conversation conversation = xmppConnectionService - .findOrCreateConversation(account, conferenceJid, true, true, true); - dialog.dismiss(); - switchToConversation(conversation); - } - } - - @Override - public void onConversationUpdate() { - refreshUi(); - } - @Override - public void onRefresh() { - Log.d(Config.LOGTAG, "user requested to refresh"); - if (QuickConversationsService.isQuicksy() && xmppConnectionService != null) { - xmppConnectionService.getQuickConversationsService().considerSyncBackground(true); - } - } - - - private void setRefreshing(boolean refreshing) { - MyListFragment fragment = (MyListFragment) mListPagerAdapter.getItem(0); - if (fragment != null) { - fragment.setRefreshing(refreshing); - } - } - - @Override - public void onCreatePublicChannel(Account account, String name, Jid address) { - mToast = ToastCompat.makeText(this, R.string.creating_channel, ToastCompat.LENGTH_LONG); - mToast.show(); - xmppConnectionService.createPublicChannel(account, name, address, new UiCallback() { - @Override - public void success(Conversation conversation) { - runOnUiThread(() -> { - hideToast(); - switchToConversation(conversation); - }); - - } - - @Override - public void error(int errorCode, Conversation conversation) { - runOnUiThread(() -> { - replaceToast(getString(errorCode)); - switchToConversation(conversation); - }); - } - - @Override - public void userInputRequired(PendingIntent pi, Conversation object) { - - } - - @Override - public void progress(int progress) { - - } - }); - } - - public static class MyListFragment extends SwipeRefreshListFragment { - private AdapterView.OnItemClickListener mOnItemClickListener; - private int mResContextMenu; - - public void setContextMenu(final int res) { - this.mResContextMenu = res; - } - - @Override - public void onListItemClick(final ListView l, final View v, final int position, final long id) { - if (mOnItemClickListener != null) { - mOnItemClickListener.onItemClick(l, v, position, id); - } - } - - public void setOnListItemClickListener(AdapterView.OnItemClickListener l) { - this.mOnItemClickListener = l; - } - - @Override - public void onViewCreated(@NonNull final View view, final Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - registerForContextMenu(getListView()); - getListView().setFastScrollEnabled(true); - getListView().setDivider(null); - getListView().setDividerHeight(0); - } - - @Override - public void onCreateContextMenu(final ContextMenu menu, final View v, final ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - final StartConversationActivity activity = (StartConversationActivity) getActivity(); - if (activity == null) { - return; - } - activity.getMenuInflater().inflate(mResContextMenu, menu); - final AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; - if (mResContextMenu == R.menu.conference_context) { - activity.conference_context_id = acmi.position; - final Bookmark bookmark = (Bookmark) activity.conferences.get(acmi.position); - final Conversation conversation = bookmark.getConversation(); - final MenuItem share = menu.findItem(R.id.context_share_uri); - share.setVisible(conversation == null || !conversation.isPrivateAndNonAnonymous()); - } else if (mResContextMenu == R.menu.contact_context) { - activity.contact_context_id = acmi.position; - final Contact contact = (Contact) activity.contacts.get(acmi.position); - final MenuItem blockUnblockItem = menu.findItem(R.id.context_contact_block_unblock); - final MenuItem showContactDetailsItem = menu.findItem(R.id.context_contact_details); - final MenuItem deleteContactMenuItem = menu.findItem(R.id.context_delete_contact); - if (contact.isSelf()) { - showContactDetailsItem.setVisible(false); - } - deleteContactMenuItem.setVisible(contact.showInRoster() && !contact.getOption(Contact.Options.SYNCED_VIA_OTHER)); - final XmppConnection xmpp = contact.getAccount().getXmppConnection(); - if (xmpp != null && xmpp.getFeatures().blocking() && !contact.isSelf()) { - if (contact.isBlocked()) { - blockUnblockItem.setTitle(R.string.unblock_contact); - } else { - blockUnblockItem.setTitle(R.string.block_contact); - } - } else { - blockUnblockItem.setVisible(false); - } - } - } - - @Override - public boolean onContextItemSelected(final MenuItem item) { - StartConversationActivity activity = (StartConversationActivity) getActivity(); - if (activity == null) { - return true; - } - switch (item.getItemId()) { - case R.id.context_contact_details: - activity.openDetailsForContact(); - break; - case R.id.context_show_qr: - activity.showQrForContact(); - break; - case R.id.context_contact_block_unblock: - activity.toggleContactBlock(); - break; - case R.id.context_delete_contact: - activity.deleteContact(); - break; - case R.id.context_share_uri: - activity.shareBookmarkUri(); - break; - case R.id.context_delete_conference: - activity.deleteConference(); - } - return true; - } - } - - public class ListPagerAdapter extends PagerAdapter { - private final FragmentManager fragmentManager; - private final MyListFragment[] fragments; - - ListPagerAdapter(FragmentManager fm) { - fragmentManager = fm; - fragments = new MyListFragment[2]; - } - - public void requestFocus(int pos) { - if (fragments.length > pos) { - fragments[pos].getListView().requestFocus(); - } - } - - @Override - public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { - FragmentTransaction trans = fragmentManager.beginTransaction(); - trans.remove(fragments[position]); - trans.commit(); - fragments[position] = null; - } - - @NonNull - @Override - public Fragment instantiateItem(@NonNull ViewGroup container, int position) { - final Fragment fragment = getItem(position); - final FragmentTransaction trans = fragmentManager.beginTransaction(); - trans.add(container.getId(), fragment, "fragment:" + position); - try { - trans.commit(); - } catch (IllegalStateException e) { - //ignore - } - return fragment; - } - - @Override - public int getCount() { - return fragments.length; - } - - @Override - public boolean isViewFromObject(@NonNull View view, @NonNull Object fragment) { - return ((Fragment) fragment).getView() == view; - } - - @Nullable - @Override - public CharSequence getPageTitle(int position) { - switch (position) { - case 0: - return getResources().getString(R.string.contacts); - case 1: - return getResources().getString(R.string.bookmarks); - default: - return super.getPageTitle(position); - } - } - - Fragment getItem(int position) { - if (fragments[position] == null) { - final MyListFragment listFragment = new MyListFragment(); - if (position == 1) { - listFragment.setListAdapter(mConferenceAdapter); - listFragment.setContextMenu(R.menu.conference_context); - listFragment.setOnListItemClickListener((arg0, arg1, p, arg3) -> openConversationForBookmark(p)); - } else { - listFragment.setListAdapter(mContactsAdapter); - listFragment.setContextMenu(R.menu.contact_context); - listFragment.setOnListItemClickListener((arg0, arg1, p, arg3) -> openConversationForContact(p)); - if (QuickConversationsService.isQuicksy()) { - listFragment.setOnRefreshListener(StartConversationActivity.this); - } - } - fragments[position] = listFragment; - } - return fragments[position]; - } - } - - public static void addInviteUri(Intent to, Intent from) { - if (from != null && from.hasExtra(EXTRA_INVITE_URI)) { - final String invite = from.getStringExtra(EXTRA_INVITE_URI); - to.putExtra(EXTRA_INVITE_URI, invite); - } - } - - private class Invite extends XmppUri { - - public String account; - - boolean forceDialog = false; - - Invite(final String uri) { - super(uri); - } - - Invite(Uri uri, boolean safeSource) { - super(uri, safeSource); - } - - boolean invite() { - if (!isValidJid()) { - ToastCompat.makeText(StartConversationActivity.this, R.string.invalid_jid, ToastCompat.LENGTH_SHORT).show(); - return false; - } - if (getJid() != null) { - return handleJid(this); - } - return false; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/TimePreference.java b/src/main/java/eu/siacs/conversations/ui/TimePreference.java deleted file mode 100644 index 671b5ae87..000000000 --- a/src/main/java/eu/siacs/conversations/ui/TimePreference.java +++ /dev/null @@ -1,96 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.Context; -import android.content.res.TypedArray; -import android.preference.DialogPreference; -import android.preference.Preference; -import android.util.AttributeSet; -import android.view.View; -import android.widget.TimePicker; - -import java.text.DateFormat; -import java.util.Calendar; -import java.util.Date; - -public class TimePreference extends DialogPreference implements Preference.OnPreferenceChangeListener { - private TimePicker picker = null; - public final static long DEFAULT_VALUE = 0; - - public TimePreference(final Context context, final AttributeSet attrs) { - super(context, attrs, 0); - this.setOnPreferenceChangeListener(this); - } - - protected void setTime(final long time) { - persistLong(time); - notifyDependencyChange(shouldDisableDependents()); - notifyChanged(); - updateSummary(time); - } - - private void updateSummary(final long time) { - final DateFormat dateFormat = android.text.format.DateFormat.getTimeFormat(getContext()); - final Date date = minutesToCalender(time).getTime(); - setSummary(dateFormat.format(date.getTime())); - } - - @Override - protected View onCreateDialogView() { - picker = new TimePicker(getContext()); - picker.setIs24HourView(android.text.format.DateFormat.is24HourFormat(getContext())); - return picker; - } - - @SuppressWarnings("NullableProblems") - @Override - protected void onBindDialogView(final View v) { - super.onBindDialogView(v); - long time = getPersistedLong(DEFAULT_VALUE); - picker.setCurrentHour((int) ((time % (24 * 60)) / 60)); - picker.setCurrentMinute((int) ((time % (24 * 60)) % 60)); - } - - @Override - protected void onDialogClosed(final boolean positiveResult) { - super.onDialogClosed(positiveResult); - - if (positiveResult) { - setTime(picker.getCurrentHour() * 60 + picker.getCurrentMinute()); - } - } - - private static Calendar minutesToCalender(long time) { - final Calendar c = Calendar.getInstance(); - c.set(Calendar.HOUR_OF_DAY, (int) ((time % (24 * 60)) / 60)); - c.set(Calendar.MINUTE, (int) ((time % (24 * 60)) % 60)); - return c; - } - - public static long minutesToTimestamp(long time) { - return minutesToCalender(time).getTimeInMillis(); - } - - @Override - protected Object onGetDefaultValue(final TypedArray a, final int index) { - return a.getInteger(index, 0); - } - - @Override - protected void onSetInitialValue(final boolean restorePersistedValue, final Object defaultValue) { - long time; - if (defaultValue instanceof Long) { - time = restorePersistedValue ? getPersistedLong((Long) defaultValue) : (Long) defaultValue; - } else { - time = restorePersistedValue ? getPersistedLong(DEFAULT_VALUE) : DEFAULT_VALUE; - } - - setTime(time); - updateSummary(time); - } - - @Override - public boolean onPreferenceChange(final Preference preference, final Object newValue) { - ((TimePreference) preference).updateSummary((Long) newValue); - return true; - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java deleted file mode 100644 index 0dbd18bba..000000000 --- a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java +++ /dev/null @@ -1,457 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.view.Gravity; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.Toast; - -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.Toolbar; -import androidx.databinding.DataBindingUtil; - -import org.whispersystems.libsignal.IdentityKey; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.OmemoSetting; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; -import eu.siacs.conversations.databinding.ActivityTrustKeysBinding; -import eu.siacs.conversations.databinding.KeysCardBinding; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.IrregularUnicodeDetector; -import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; -import me.drakeet.support.toast.ToastCompat; - -public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdated { - private final Map ownKeysToTrust = new HashMap<>(); - private final Map> foreignKeysToTrust = new HashMap<>(); - private final OnClickListener mCancelButtonListener = v -> { - setResult(RESULT_CANCELED); - finish(); - }; - private List contactJids; - private Account mAccount; - private Conversation mConversation; - private final OnClickListener mSaveButtonListener = v -> { - commitTrusts(); - finishOk(false); - }; - private AtomicBoolean mUseCameraHintShown = new AtomicBoolean(false); - private AxolotlService.FetchStatus lastFetchReport = AxolotlService.FetchStatus.SUCCESS; - private Toast mUseCameraHintToast = null; - private ActivityTrustKeysBinding binding; - - @Override - protected void refreshUiReal() { - invalidateOptionsMenu(); - populateView(); - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_trust_keys); - this.contactJids = new ArrayList<>(); - for (String jid : getIntent().getStringArrayExtra("contacts")) { - try { - this.contactJids.add(Jid.of(jid)); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } - } - - binding.cancelButton.setOnClickListener(mCancelButtonListener); - binding.saveButton.setOnClickListener(mSaveButtonListener); - - setSupportActionBar((Toolbar) binding.toolbar); - configureActionBar(getSupportActionBar()); - - if (savedInstanceState != null) { - mUseCameraHintShown.set(savedInstanceState.getBoolean("camera_hint_shown", false)); - } - } - - @Override - public void onSaveInstanceState(Bundle savedInstanceState) { - savedInstanceState.putBoolean("camera_hint_shown", mUseCameraHintShown.get()); - super.onSaveInstanceState(savedInstanceState); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.trust_keys, menu); - MenuItem scanQrCode = menu.findItem(R.id.action_scan_qr_code); - scanQrCode.setVisible((ownKeysToTrust.size() > 0 || foreignActuallyHasKeys()) && isCameraFeatureAvailable()); - return super.onCreateOptionsMenu(menu); - } - - private void showCameraToast() { - mUseCameraHintToast = ToastCompat.makeText(this, R.string.use_camera_icon_to_scan_barcode, ToastCompat.LENGTH_LONG); - ActionBar actionBar = getSupportActionBar(); - mUseCameraHintToast.setGravity(Gravity.TOP | Gravity.END, 0, actionBar == null ? 0 : actionBar.getHeight()); - mUseCameraHintToast.show(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_scan_qr_code: - if (hasPendingKeyFetches()) { - ToastCompat.makeText(this, R.string.please_wait_for_keys_to_be_fetched, ToastCompat.LENGTH_SHORT).show(); - } else { - ScanActivity.scan(this); - return true; - } - } - return super.onOptionsItemSelected(item); - } - - @Override - protected void onStop() { - super.onStop(); - if (mUseCameraHintToast != null) { - mUseCameraHintToast.cancel(); - } - } - - @Override - protected void processFingerprintVerification(XmppUri uri) { - if (mConversation != null - && mAccount != null - && uri.hasFingerprints() - && mAccount.getAxolotlService().getCryptoTargets(mConversation).contains(uri.getJid())) { - boolean performedVerification = xmppConnectionService.verifyFingerprints(mAccount.getRoster().getContact(uri.getJid()), uri.getFingerprints()); - boolean keys = reloadFingerprints(); - if (performedVerification && !keys && !hasNoOtherTrustedKeys() && !hasPendingKeyFetches()) { - ToastCompat.makeText(this, R.string.all_omemo_keys_have_been_verified, ToastCompat.LENGTH_SHORT).show(); - finishOk(false); - return; - } else if (performedVerification) { - ToastCompat.makeText(this, R.string.verified_fingerprints, ToastCompat.LENGTH_SHORT).show(); - } - } else { - reloadFingerprints(); - Log.d(Config.LOGTAG, "xmpp uri was: " + uri.getJid() + " has Fingerprints: " + Boolean.toString(uri.hasFingerprints())); - ToastCompat.makeText(this, R.string.barcode_does_not_contain_fingerprints_for_this_conversation, ToastCompat.LENGTH_SHORT).show(); - } - populateView(); - } - - private void populateView() { - setTitle(getString(R.string.trust_omemo_fingerprints)); - binding.ownKeysDetails.removeAllViews(); - binding.foreignKeys.removeAllViews(); - boolean hasOwnKeys = false; - boolean hasForeignKeys = false; - for (final String fingerprint : ownKeysToTrust.keySet()) { - hasOwnKeys = true; - addFingerprintRowWithListeners(binding.ownKeysDetails, mAccount, fingerprint, false, - FingerprintStatus.createActive(ownKeysToTrust.get(fingerprint)), false, false, - (buttonView, isChecked) -> { - ownKeysToTrust.put(fingerprint, isChecked); - // own fingerprints have no impact on locked status. - } - ); - } - - synchronized (this.foreignKeysToTrust) { - for (Map.Entry> entry : foreignKeysToTrust.entrySet()) { - hasForeignKeys = true; - KeysCardBinding keysCardBinding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.keys_card, binding.foreignKeys, false); - final Jid jid = entry.getKey(); - keysCardBinding.foreignKeysTitle.setText(IrregularUnicodeDetector.style(this, jid)); - keysCardBinding.foreignKeysTitle.setOnClickListener(v -> switchToContactDetails(mAccount.getRoster().getContact(jid))); - final Map fingerprints = entry.getValue(); - for (final String fingerprint : fingerprints.keySet()) { - addFingerprintRowWithListeners(keysCardBinding.foreignKeysDetails, mAccount, fingerprint, false, - FingerprintStatus.createActive(fingerprints.get(fingerprint)), false, false, - (buttonView, isChecked) -> { - fingerprints.put(fingerprint, isChecked); - lockOrUnlockAsNeeded(); - } - ); - } - if (fingerprints.size() == 0) { - keysCardBinding.noKeysToAccept.setVisibility(View.VISIBLE); - if (hasNoOtherTrustedKeys(jid)) { - if (!mAccount.getRoster().getContact(jid).mutualPresenceSubscription()) { - keysCardBinding.noKeysToAccept.setText(R.string.error_no_keys_to_trust_presence); - } else { - keysCardBinding.noKeysToAccept.setText(R.string.error_no_keys_to_trust_server_error); - } - } else { - keysCardBinding.noKeysToAccept.setText(getString(R.string.no_keys_just_confirm, mAccount.getRoster().getContact(jid).getDisplayName())); - } - } else { - keysCardBinding.noKeysToAccept.setVisibility(View.GONE); - } - binding.foreignKeys.addView(keysCardBinding.foreignKeysCard); - } - } - - if ((hasOwnKeys || foreignActuallyHasKeys()) && isCameraFeatureAvailable() && mUseCameraHintShown.compareAndSet(false, true)) { - showCameraToast(); - } - - binding.ownKeysTitle.setText(mAccount.getJid().asBareJid().toEscapedString()); - binding.ownKeysCard.setVisibility(hasOwnKeys ? View.VISIBLE : View.GONE); - binding.foreignKeys.setVisibility(hasForeignKeys ? View.VISIBLE : View.GONE); - if (hasPendingKeyFetches()) { - setFetching(); - lock(); - } else { - if (!hasForeignKeys && hasNoOtherTrustedKeys()) { - binding.keyErrorMessageCard.setVisibility(View.VISIBLE); - boolean lastReportWasError = lastFetchReport == AxolotlService.FetchStatus.ERROR; - boolean errorFetchingBundle = mAccount.getAxolotlService().fetchMapHasErrors(contactJids); - boolean errorFetchingDeviceList = mAccount.getAxolotlService().hasErrorFetchingDeviceList(contactJids); - boolean anyWithoutMutualPresenceSubscription = anyWithoutMutualPresenceSubscription(contactJids); - if (errorFetchingDeviceList) { - binding.keyErrorMessage.setVisibility(View.VISIBLE); - binding.keyErrorMessage.setText(R.string.error_trustkey_device_list); - } else if (errorFetchingBundle || lastReportWasError) { - binding.keyErrorMessage.setVisibility(View.VISIBLE); - binding.keyErrorMessage.setText(R.string.error_trustkey_bundle); - } else { - binding.keyErrorMessage.setVisibility(View.GONE); - } - this.binding.keyErrorHintMutual.setVisibility(anyWithoutMutualPresenceSubscription ? View.VISIBLE : View.GONE); - Contact contact = mAccount.getRoster().getContact(contactJids.get(0)); - binding.keyErrorGeneral.setText(getString(R.string.error_trustkey_general, contact.getDisplayName())); - binding.ownKeysDetails.removeAllViews(); - if (OmemoSetting.isAlways()) { - binding.disableButton.setVisibility(View.GONE); - } else { - binding.disableButton.setVisibility(View.VISIBLE); - binding.disableButton.setOnClickListener(this::disableEncryptionDialog); - } - binding.ownKeysCard.setVisibility(View.GONE); - binding.foreignKeys.removeAllViews(); - binding.foreignKeys.setVisibility(View.GONE); - } - lockOrUnlockAsNeeded(); - setDone(); - } - } - - private void disableEncryptionDialog(View view) { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.disable_encryption); - builder.setMessage(R.string.disable_encryption_message); - builder.setPositiveButton(R.string.disable_now, (dialog, which) -> { - mConversation.setNextEncryption(Message.ENCRYPTION_NONE); - xmppConnectionService.updateConversation(mConversation); - finishOk(true); - }); - builder.setNegativeButton(R.string.cancel, null); - builder.create().show(); - } - - private boolean anyWithoutMutualPresenceSubscription(List contactJids) { - for (Jid jid : contactJids) { - if (!mAccount.getRoster().getContact(jid).mutualPresenceSubscription()) { - return true; - } - } - return false; - } - - private boolean foreignActuallyHasKeys() { - synchronized (this.foreignKeysToTrust) { - for (Map.Entry> entry : foreignKeysToTrust.entrySet()) { - if (entry.getValue().size() > 0) { - return true; - } - } - } - return false; - } - - private boolean reloadFingerprints() { - List acceptedTargets = mConversation == null ? new ArrayList<>() : mConversation.getAcceptedCryptoTargets(); - ownKeysToTrust.clear(); - if (this.mAccount == null) { - return false; - } - AxolotlService service = this.mAccount.getAxolotlService(); - Set ownKeysSet = service.getKeysWithTrust(FingerprintStatus.createActiveUndecided()); - for (final IdentityKey identityKey : ownKeysSet) { - final String fingerprint = CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()); - if (!ownKeysToTrust.containsKey(fingerprint)) { - ownKeysToTrust.put(fingerprint, false); - } - } - synchronized (this.foreignKeysToTrust) { - foreignKeysToTrust.clear(); - for (Jid jid : contactJids) { - Set foreignKeysSet = service.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), jid); - if (hasNoOtherTrustedKeys(jid) && ownKeysSet.size() == 0) { - foreignKeysSet.addAll(service.getKeysWithTrust(FingerprintStatus.createActive(false), jid)); - } - Map foreignFingerprints = new HashMap<>(); - for (final IdentityKey identityKey : foreignKeysSet) { - final String fingerprint = CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()); - if (!foreignFingerprints.containsKey(fingerprint)) { - foreignFingerprints.put(fingerprint, false); - } - } - if (foreignFingerprints.size() > 0 || !acceptedTargets.contains(jid)) { - foreignKeysToTrust.put(jid, foreignFingerprints); - } - } - } - return ownKeysSet.size() + foreignKeysToTrust.size() > 0; - } - - public void onBackendConnected() { - Intent intent = getIntent(); - this.mAccount = extractAccount(intent); - if (this.mAccount != null && intent != null) { - String uuid = intent.getStringExtra("conversation"); - this.mConversation = xmppConnectionService.findConversationByUuid(uuid); - if (this.mPendingFingerprintVerificationUri != null) { - processFingerprintVerification(this.mPendingFingerprintVerificationUri); - this.mPendingFingerprintVerificationUri = null; - } else { - final boolean keysToTrust = reloadFingerprints(); - if (keysToTrust || hasPendingKeyFetches() || hasNoOtherTrustedKeys()) { - populateView(); - invalidateOptionsMenu(); - } else { - finishOk(false); - } - } - } - } - - private boolean hasNoOtherTrustedKeys() { - return mAccount == null || mAccount.getAxolotlService().anyTargetHasNoTrustedKeys(contactJids); - } - - private boolean hasNoOtherTrustedKeys(Jid contact) { - return mAccount == null || mAccount.getAxolotlService().getNumTrustedKeys(contact) == 0; - } - - private boolean hasPendingKeyFetches() { - return mAccount != null && mAccount.getAxolotlService().hasPendingKeyFetches(contactJids); - } - - - @Override - public void onKeyStatusUpdated(final AxolotlService.FetchStatus report) { - final boolean keysToTrust = reloadFingerprints(); - if (report != null) { - lastFetchReport = report; - runOnUiThread(() -> { - if (mUseCameraHintToast != null && !keysToTrust) { - mUseCameraHintToast.cancel(); - } - switch (report) { - case ERROR: - ToastCompat.makeText(TrustKeysActivity.this, R.string.error_fetching_omemo_key, ToastCompat.LENGTH_SHORT).show(); - break; - case SUCCESS_TRUSTED: - ToastCompat.makeText(TrustKeysActivity.this, R.string.blindly_trusted_omemo_keys, ToastCompat.LENGTH_LONG).show(); - break; - case SUCCESS_VERIFIED: - ToastCompat.makeText(TrustKeysActivity.this, - Config.X509_VERIFICATION ? R.string.verified_omemo_key_with_certificate : R.string.all_omemo_keys_have_been_verified, - ToastCompat.LENGTH_LONG).show(); - break; - } - }); - - } - if (keysToTrust || hasPendingKeyFetches() || hasNoOtherTrustedKeys()) { - refreshUi(); - } else { - runOnUiThread(() -> finishOk(false)); - - } - } - - private void finishOk(boolean disabled) { - Intent data = new Intent(); - data.putExtra("choice", getIntent().getIntExtra("choice", ConversationFragment.ATTACHMENT_CHOICE_INVALID)); - data.putExtra("disabled", disabled); - setResult(RESULT_OK, data); - finish(); - } - - private void commitTrusts() { - for (final String fingerprint : ownKeysToTrust.keySet()) { - mAccount.getAxolotlService().setFingerprintTrust( - fingerprint, - FingerprintStatus.createActive(ownKeysToTrust.get(fingerprint))); - } - List acceptedTargets = mConversation == null ? new ArrayList<>() : mConversation.getAcceptedCryptoTargets(); - synchronized (this.foreignKeysToTrust) { - for (Map.Entry> entry : foreignKeysToTrust.entrySet()) { - Jid jid = entry.getKey(); - Map value = entry.getValue(); - if (!acceptedTargets.contains(jid)) { - acceptedTargets.add(jid); - } - for (final String fingerprint : value.keySet()) { - mAccount.getAxolotlService().setFingerprintTrust( - fingerprint, - FingerprintStatus.createActive(value.get(fingerprint))); - } - } - } - if (mConversation != null && mConversation.getMode() == Conversation.MODE_MULTI) { - mConversation.setAcceptedCryptoTargets(acceptedTargets); - xmppConnectionService.updateConversation(mConversation); - } - } - - private void unlock() { - binding.saveButton.setEnabled(true); - } - - private void lock() { - binding.saveButton.setEnabled(false); - } - - private void lockOrUnlockAsNeeded() { - synchronized (this.foreignKeysToTrust) { - for (Jid jid : contactJids) { - Map fingerprints = foreignKeysToTrust.get(jid); - if (hasNoOtherTrustedKeys(jid) && (fingerprints == null || !fingerprints.values().contains(true))) { - lock(); - return; - } - } - } - unlock(); - - } - - private void setDone() { - binding.saveButton.setText(getString(R.string.done)); - } - - private void setFetching() { - binding.saveButton.setText(getString(R.string.fetching_keys)); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/UiCallback.java b/src/main/java/eu/siacs/conversations/ui/UiCallback.java deleted file mode 100644 index 74b6a956a..000000000 --- a/src/main/java/eu/siacs/conversations/ui/UiCallback.java +++ /dev/null @@ -1,13 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.app.PendingIntent; - -public interface UiCallback { - void success(T object); - - void error(int errorCode, T object); - - void userInputRequired(PendingIntent pi, T object); - - void progress(int progress); -} diff --git a/src/main/java/eu/siacs/conversations/ui/UiInformableCallback.java b/src/main/java/eu/siacs/conversations/ui/UiInformableCallback.java deleted file mode 100644 index cef22ebe6..000000000 --- a/src/main/java/eu/siacs/conversations/ui/UiInformableCallback.java +++ /dev/null @@ -1,5 +0,0 @@ -package eu.siacs.conversations.ui; - -public interface UiInformableCallback extends UiCallback { - void inform(String text); -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/UpdaterActivity.java b/src/main/java/eu/siacs/conversations/ui/UpdaterActivity.java deleted file mode 100644 index 9c8cfd4c6..000000000 --- a/src/main/java/eu/siacs/conversations/ui/UpdaterActivity.java +++ /dev/null @@ -1,437 +0,0 @@ -package eu.siacs.conversations.ui; - -import static eu.siacs.conversations.Config.monocles; -import static eu.siacs.conversations.http.HttpConnectionManager.getProxy; -import static eu.siacs.conversations.services.XmppConnectionService.FDroid; -import static eu.siacs.conversations.services.XmppConnectionService.PlayStore; -import static eu.siacs.conversations.utils.StorageHelper.getAppUpdateDirectory; - -import android.Manifest; -import android.app.ProgressDialog; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.os.PowerManager; -import android.util.Log; -import android.widget.TextView; - -import androidx.appcompat.app.AlertDialog; -import androidx.core.app.ActivityCompat; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URL; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.util.List; - -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; - -import eu.siacs.conversations.BuildConfig; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.http.NoSSLv3SocketFactory; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.util.CustomTab; -import eu.siacs.conversations.utils.WakeLockHelper; -import me.drakeet.support.toast.ToastCompat; - -public class UpdaterActivity extends XmppActivity { - static final private String FileName = "update.apk"; - String appURI = ""; - String changelog = ""; - Integer filesize = 0; - String store; - ProgressDialog mProgressDialog; - DownloadTask downloadTask; - TextView textView; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - //set activity - setContentView(R.layout.activity_updater); - this.mTheme = findTheme(); - setTheme(this.mTheme); - - textView = findViewById(R.id.updater); - - mProgressDialog = new ProgressDialog(UpdaterActivity.this) { - //show warning on back pressed - @Override - public void onBackPressed() { - showCancelDialog(); - } - }; - mProgressDialog.setMessage(getString(R.string.download_started)); - mProgressDialog.setProgressNumberFormat(null); - mProgressDialog.setIndeterminate(true); - mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - mProgressDialog.setCancelable(false); - mProgressDialog.setCanceledOnTouchOutside(false); - } - - @Override - protected void refreshUiReal() { - //ignored - } - - @Override - protected void onStart() { - super.onStart(); - this.mTheme = findTheme(); - setTheme(this.mTheme); - setTitle(getString(R.string.update_service)); - textView.setText(R.string.update_info); - setSupportActionBar(findViewById(R.id.toolbar)); - configureActionBar(getSupportActionBar()); - if (getIntent() != null && getIntent().getStringExtra("update").equals("MonoclesMessenger_UpdateService")) { - try { - appURI = getIntent().getStringExtra("url"); - } catch (Exception e) { - ToastCompat.makeText(getApplicationContext(), getText(R.string.failed), ToastCompat.LENGTH_LONG).show(); - UpdaterActivity.this.finish(); - } - try { - changelog = getIntent().getStringExtra("changelog"); - } catch (Exception e) { - ToastCompat.makeText(getApplicationContext(), getText(R.string.failed), ToastCompat.LENGTH_LONG).show(); - UpdaterActivity.this.finish(); - } - try { - store = getIntent().getStringExtra("store"); - } catch (Exception e) { - store = null; - } - //delete old downloaded localVersion files - File dir = new File(getAppUpdateDirectory()); - if (dir.isDirectory()) { - String[] children = dir.list(); - for (String aChildren : children) { - Log.d(Config.LOGTAG, "AppUpdater: delete old update files " + aChildren + " in " + dir); - new File(dir, aChildren).delete(); - } - } - - //oh yeah we do need an upgrade, let the user know send an alert message - final AlertDialog.Builder builder = new AlertDialog.Builder(UpdaterActivity.this); - builder.setCancelable(false); - //open link to changelog - //if the user agrees to upgrade - builder.setMessage(getString(R.string.install_update)) - .setPositiveButton(R.string.update, (dialog, id) -> { - Log.d(Config.LOGTAG, "AppUpdater: downloading " + FileName + " from " + appURI); - //ask for permissions on devices >= SDK 23 - if (isStoragePermissionGranted() && isNetworkAvailable(getApplicationContext())) { - //start downloading the file using the download manager - if (store != null && store.equalsIgnoreCase(PlayStore) || BuildConfig.APPLICATION_ID.equals("im.blabber.messenger")) { - ToastCompat.makeText(getApplicationContext(), getText(R.string.download_started), ToastCompat.LENGTH_LONG).show(); - downloadTask = new DownloadTask(UpdaterActivity.this); - downloadTask.execute(appURI); - } else if (store != null && store.equalsIgnoreCase(FDroid)) { - Uri uri = Uri.parse("https://f-droid.org/de/packages/" + getString(R.string.applicationId) + "/"); - Intent marketIntent = new Intent(Intent.ACTION_VIEW, uri); - PackageManager manager = getApplicationContext().getPackageManager(); - List infos = manager.queryIntentActivities(marketIntent, 0); - if (infos.size() > 0) { - startActivity(marketIntent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } else { - uri = Uri.parse("https://" + monocles()); - try { - CustomTab.openTab(this, uri, isDarkTheme()); - } catch (Exception e) { - ToastCompat.makeText(this, R.string.no_application_found_to_open_link, ToastCompat.LENGTH_SHORT).show(); - } - } - } else { - ToastCompat.makeText(getApplicationContext(), getText(R.string.download_started), ToastCompat.LENGTH_LONG).show(); - downloadTask = new DownloadTask(UpdaterActivity.this); - downloadTask.execute(appURI); - } - } else { - Log.d(Config.LOGTAG, "AppUpdater: failed - has storage permissions " + isStoragePermissionGranted() + " and internet " + isNetworkAvailable(getApplicationContext())); - } - }) - .setNeutralButton(R.string.changelog, (dialog, id) -> { - Uri uri = Uri.parse(Config.CHANGELOG_URL); // missing 'http://' will cause crash - try { - CustomTab.openTab(this, uri, isDarkTheme()); - } catch (Exception e) { - ToastCompat.makeText(this, R.string.no_application_found_to_open_link, ToastCompat.LENGTH_SHORT).show(); - } finally { - //restart updater to show dialog again after coming back after opening changelog - recreate(); - } - }) - .setNegativeButton(R.string.remind_later, (dialog, id) -> { - // User cancelled the dialog - UpdaterActivity.this.finish(); - }); - //show the alert message - builder.create().show(); - } else { - ToastCompat.makeText(getApplicationContext(), getText(R.string.failed), ToastCompat.LENGTH_LONG).show(); - UpdaterActivity.this.finish(); - } - } - - @Override - void onBackendConnected() { - //ignored - } - - @Override - public void onDestroy() { - super.onDestroy(); - } - - @Override - public void onSaveInstanceState(Bundle savedInstanceState) { - super.onSaveInstanceState(savedInstanceState); - } - - @Override - public void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - } - - //check for internet connection - private boolean isNetworkAvailable(Context context) { - ConnectivityManager connectivity = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - if (connectivity != null) { - NetworkInfo[] info = connectivity.getAllNetworkInfo(); - if (info != null) { - for (NetworkInfo anInfo : info) { - if (anInfo.getState() == NetworkInfo.State.CONNECTED) { - return true; - } - } - } - } - return false; - } - - public boolean isStoragePermissionGranted() { - if (Build.VERSION.SDK_INT >= 23) { - if (checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED && checkSelfPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { - return true; - } else { - ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, 1); - return false; - } - } else { //permission is automatically granted on sdk<23 upon installation - return true; - } - } - - //show warning on back pressed - @Override - public void onBackPressed() { - showCancelDialog(); - } - - private void showCancelDialog() { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(R.string.cancel_update) - .setCancelable(false) - .setPositiveButton(R.string.yes, (dialog, id) -> { - if (downloadTask != null && !downloadTask.getStatus().equals(AsyncTask.Status.FINISHED)) { - downloadTask.cancel(true); - } - if (mProgressDialog.isShowing()) { - mProgressDialog.dismiss(); - } - UpdaterActivity.this.finish(); - }) - .setNegativeButton(R.string.no, (dialog, id) -> dialog.cancel()); - final AlertDialog alert = builder.create(); - alert.show(); - } - - @Override - public void onPause() { - super.onPause(); - if (downloadTask != null && !downloadTask.getStatus().equals(AsyncTask.Status.FINISHED)) { - downloadTask.cancel(true); - } - UpdaterActivity.this.finish(); - } - - @Override - protected void onStop() { - super.onStop(); - if (downloadTask != null && !downloadTask.getStatus().equals(AsyncTask.Status.FINISHED)) { - downloadTask.cancel(true); - } - UpdaterActivity.this.finish(); - } - - private class DownloadTask extends AsyncTask { - XmppActivity activity; - XmppConnectionService xmppConnectionService; - private Context context; - private PowerManager.WakeLock mWakeLock; - private long startTime = 0; - private boolean mUseTor; - private boolean mUseI2P; - - DownloadTask(Context context) { - this.context = context; - } - - File dir = new File(getAppUpdateDirectory()); - File file = new File(dir, FileName); - - @Override - protected void onPreExecute() { - super.onPreExecute(); - startTime = System.currentTimeMillis(); - // take CPU lock to prevent CPU from going off if the user - // presses the power button during download - PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - if (pm != null) { - mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, getClass().getName()); - mWakeLock.acquire(); - mUseTor = xmppConnectionService != null && xmppConnectionService.useTorToConnect(); - mUseI2P = xmppConnectionService != null && xmppConnectionService.useI2PToConnect(); - } - mProgressDialog.show(); - } - - @Override - protected void onProgressUpdate(Integer... progress) { - super.onProgressUpdate(progress); - // if we get here, length is known, now set indeterminate to false - mProgressDialog.setIndeterminate(false); - mProgressDialog.setMax(100); - mProgressDialog.setProgress(progress[0]); - } - - @Override - protected String doInBackground(String... sUrl) { - InputStream is = null; - OutputStream os = null; - SSLContext sslcontext = null; - SSLSocketFactory NoSSLv3Factory = null; - try { - sslcontext = SSLContext.getInstance("TLSv1"); - if (sslcontext != null) { - sslcontext.init(null, null, null); - NoSSLv3Factory = new NoSSLv3SocketFactory(sslcontext.getSocketFactory()); - } - } catch (NoSuchAlgorithmException | KeyManagementException e) { - e.printStackTrace(); - } - HttpsURLConnection.setDefaultSSLSocketFactory(NoSSLv3Factory); - HttpsURLConnection connection = null; - try { - Log.d(Config.LOGTAG, "AppUpdater: save file to " + file.toString()); - Log.d(Config.LOGTAG, "AppUpdater: download update from url: " + sUrl[0] + " to file name: " + file.toString()); - - URL url = new URL(sUrl[0]); - if (mUseTor && !mUseI2P) { - connection = (HttpsURLConnection) url.openConnection(getProxy(false)); - } else if (mUseI2P) { - connection = (HttpsURLConnection) url.openConnection(getProxy(true)); - } else { - connection = (HttpsURLConnection) url.openConnection(); - } - connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); - connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); - connection.setRequestProperty("User-agent", System.getProperty("http.agent")); - connection.connect(); - - // expect HTTP 200 OK, so we don't mistakenly save error report - // instead of the file - if (connection.getResponseCode() != HttpsURLConnection.HTTP_OK) { - ToastCompat.makeText(getApplicationContext(), getText(R.string.failed), ToastCompat.LENGTH_LONG).show(); - return connection.getResponseCode() + ": " + connection.getResponseMessage(); - } - - // this will be useful to display download percentage - // might be -1: server did not report the length - int fileLength = connection.getContentLength(); - - // create folders - File parentDirectory = file.getParentFile(); - if (parentDirectory.mkdirs()) { - Log.d(Config.LOGTAG, "created " + parentDirectory.getAbsolutePath()); - } - - // download the file - is = connection.getInputStream(); - os = new FileOutputStream(file); - - byte[] data = new byte[4096]; - long total = 0; - int count; - while ((count = is.read(data)) != -1) { - // allow canceling with back button - if (isCancelled()) { - is.close(); - return "canceled"; - } - total += count; - // publishing the progress.... - if (fileLength > 0) // only if total length is known - publishProgress((int) (total * 100 / fileLength)); - os.write(data, 0, count); - } - } catch (Exception e) { - e.printStackTrace(); - return e.toString(); - } finally { - try { - if (os != null) - os.close(); - if (is != null) - is.close(); - } catch (IOException ignored) { - } - - if (connection != null) - connection.disconnect(); - } - return null; - } - - @Override - protected void onPostExecute(String result) { - WakeLockHelper.release(mWakeLock); - mProgressDialog.dismiss(); - if (result != null) { - ToastCompat.makeText(getApplicationContext(), getString(R.string.failed), ToastCompat.LENGTH_LONG).show(); - Log.d(Config.LOGTAG, "AppUpdater: failed with " + result); - UpdaterActivity.this.finish(); - } else { - Log.d(Config.LOGTAG, "AppUpdater: download ready in " + ((System.currentTimeMillis() - startTime) / 1000) + " sec"); - - //start the installation of the latest localVersion - Intent installIntent = new Intent(Intent.ACTION_INSTALL_PACKAGE); - installIntent.setDataAndType(FileBackend.getUriForFile(UpdaterActivity.this, file), "application/vnd.android.package-archive"); - installIntent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true); - installIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true); - installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - installIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - startActivity(installIntent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - UpdaterActivity.this.finish(); - } - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java deleted file mode 100644 index c61f6fad7..000000000 --- a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java +++ /dev/null @@ -1,328 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.Manifest; -import android.app.Activity; -import android.content.Intent; -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 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 me.drakeet.support.toast.ToastCompat; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.HttpUrl; -import okhttp3.Request; -import okhttp3.Response; - -public class UriHandlerActivity extends AppCompatActivity { - - public static final String ACTION_SCAN_QR_CODE = "scan_qr_code"; - private static final String EXTRA_ALLOW_PROVISIONING = "extra_allow_provisioning"; - private static final int REQUEST_SCAN_QR_CODE = 0x1234; - 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 static final Pattern LINK_HEADER_PATTERN = Pattern.compile("<(.*?)>"); - private ActivityUriHandlerBinding binding; - private Call call; - - public static void scan(final Activity activity) { - scan(activity, false); - } - - public static void scan(final Activity activity, final boolean provisioning) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { - final Intent intent = new Intent(activity, UriHandlerActivity.class); - intent.setAction(UriHandlerActivity.ACTION_SCAN_QR_CODE); - if (provisioning) { - intent.putExtra(EXTRA_ALLOW_PROVISIONING, true); - } - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - activity.startActivity(intent); - } else { - activity.requestPermissions( - new String[]{Manifest.permission.CAMERA}, - provisioning ? REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION : REQUEST_CAMERA_PERMISSIONS_TO_SCAN - ); - } - } - - public static void onRequestPermissionResult(Activity activity, int requestCode, int[] grantResults) { - if (requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN && requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION) { - return; - } - if (grantResults.length > 0) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - if (requestCode == REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION) { - scan(activity, true); - } else { - scan(activity); - } - } else { - ToastCompat.makeText(activity, R.string.qr_code_scanner_needs_access_to_camera, ToastCompat.LENGTH_SHORT).show(); - } - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_uri_handler); - } - - @Override - public void onStart() { - super.onStart(); - handleIntent(getIntent()); - } - - @Override - public void onNewIntent(final Intent intent) { - super.onNewIntent(intent); - handleIntent(intent); - } - - private boolean handleUri(final Uri uri) { - return handleUri(uri, false); - } - - 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); - if (SignupUtils.isSupportTokenRegistry() && xmppUri.isValidJid()) { - final String preAuth = xmppUri.getParameter(XmppUri.PARAMETER_PRE_AUTH); - final Jid jid = xmppUri.getJid(); - if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) { - if (jid.getEscapedLocal() != null && accounts.contains(jid.asBareJid())) { - showError(R.string.account_already_exists); - return false; - } - intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth, true); - startActivity(intent); - 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 true; - } - } else if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) { - showError(R.string.account_registrations_are_not_supported); - return false; - } - if (accounts.size() == 0) { - if (xmppUri.isValidJid()) { - intent = SignupUtils.getSignUpIntent(this); - intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString()); - startActivity(intent); - return true; - } else { - showError(R.string.invalid_jid); - return false; - } - } - if (xmppUri.isAction(XmppUri.ACTION_MESSAGE)) { - final Jid jid = xmppUri.getJid(); - final String body = xmppUri.getBody(); - if (jid != null) { - final Class clazz = findShareViaAccountClass(); - if (clazz != null) { - intent = new Intent(this, clazz); - intent.putExtra("contact", jid.toEscapedString()); - intent.putExtra("body", body); - } else { - intent = new Intent(this, StartConversationActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(uri); - intent.putExtra("account", accounts.get(0).toEscapedString()); - } - } else { - intent = new Intent(this, ShareWithActivity.class); - intent.setAction(Intent.ACTION_SEND); - intent.setType("text/plain"); - intent.putExtra(Intent.EXTRA_TEXT, body); - } - } else if (accounts.contains(xmppUri.getJid())) { - intent = new Intent(getApplicationContext(), EditAccountActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra("jid", xmppUri.getJid().asBareJid().toString()); - intent.setData(uri); - intent.putExtra("scanned", scanned); - } else if (xmppUri.isValidJid()) { - intent = new Intent(getApplicationContext(), StartConversationActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - intent.putExtra("scanned", scanned); - intent.setData(uri); - } else { - showError(R.string.invalid_jid); - return false; - } - startActivity(intent); - return true; - } - - 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); - } - - private static Class findShareViaAccountClass() { - try { - return Class.forName("eu.siacs.conversations.ui.ShareViaAccountActivity"); - } catch (final ClassNotFoundException e) { - return null; - } - } - - 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: - if (handleUri(data.getData())) { - finish(); - } - break; - case ACTION_SCAN_QR_CODE: - Log.d(Config.LOGTAG, "scan. allow=" + allowProvisioning()); - setIntent(createMainIntent()); - startActivityForResult(new Intent(this, ScanActivity.class), REQUEST_SCAN_QR_CODE); - break; - } - } - private Intent createMainIntent() { - final Intent intent = new Intent(Intent.ACTION_MAIN); - intent.putExtra(EXTRA_ALLOW_PROVISIONING, allowProvisioning()); - return intent; - } - - private boolean allowProvisioning() { - final Intent launchIntent = getIntent(); - return launchIntent != null && launchIntent.getBooleanExtra(EXTRA_ALLOW_PROVISIONING, false); - } - - @Override - 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(); - return; - } - if (result.startsWith("BEGIN:VCARD\n")) { - final Matcher matcher = V_CARD_XMPP_PATTERN.matcher(result); - if (matcher.find()) { - if (handleUri(Uri.parse(matcher.group(2)), true)) { - finish(); - } - } else { - showError(R.string.no_xmpp_adddress_found); - } - return; - } else if (QuickConversationsService.isConversations() && looksLikeJsonObject(result) && allowProvisioning) { - ProvisioningUtils.provision(this, result); - finish(); - return; - } - final Uri uri = Uri.parse(result.trim()); - if (allowProvisioning && "https".equalsIgnoreCase(uri.getScheme()) && !Config.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(); - } - } - - private static boolean looksLikeJsonObject(final String input) { - 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/VerifyOTRActivity.java b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java deleted file mode 100644 index 6528fb38a..000000000 --- a/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java +++ /dev/null @@ -1,450 +0,0 @@ -package eu.siacs.conversations.ui; - -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.view.Menu; -import android.view.View; -import android.widget.Button; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; - -import net.java.otr4j.OtrException; -import net.java.otr4j.session.Session; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.xmpp.Jid; -import me.drakeet.support.toast.ToastCompat; - -public class VerifyOTRActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate { - - public static final String ACTION_VERIFY_CONTACT = "verify_contact"; - public static final int MODE_SCAN_FINGERPRINT = -0x0502; - public static final int MODE_ASK_QUESTION = 0x0503; - public static final int MODE_ANSWER_QUESTION = 0x0504; - public static final int MODE_MANUAL_VERIFICATION = 0x0505; - - private LinearLayout mManualVerificationArea; - private LinearLayout mSmpVerificationArea; - private TextView mRemoteFingerprint; - private TextView mYourFingerprint; - private TextView mVerificationExplain; - private TextView mStatusMessage; - private TextView mSharedSecretHint; - private EditText mSharedSecretHintEditable; - private EditText mSharedSecretSecret; - private Button mLeftButton; - private Button mRightButton; - private Account mAccount; - private Conversation mConversation; - private int mode = MODE_MANUAL_VERIFICATION; - private XmppUri mPendingUri = null; - - private DialogInterface.OnClickListener mVerifyFingerprintListener = new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialogInterface, int click) { - mConversation.verifyOtrFingerprint(); - xmppConnectionService.syncRosterToDisk(mConversation.getAccount()); - ToastCompat.makeText(VerifyOTRActivity.this, R.string.verified, Toast.LENGTH_SHORT).show(); - finish(); - } - }; - - private View.OnClickListener mCreateSharedSecretListener = new View.OnClickListener() { - @Override - public void onClick(final View view) { - if (isAccountOnline()) { - final String question = mSharedSecretHintEditable.getText().toString(); - final String secret = mSharedSecretSecret.getText().toString(); - if (question.trim().isEmpty()) { - mSharedSecretHintEditable.requestFocus(); - mSharedSecretHintEditable.setError(getString(R.string.shared_secret_hint_should_not_be_empty)); - } else if (secret.trim().isEmpty()) { - mSharedSecretSecret.requestFocus(); - mSharedSecretSecret.setError(getString(R.string.shared_secret_can_not_be_empty)); - } else { - mSharedSecretSecret.setError(null); - mSharedSecretHintEditable.setError(null); - initSmp(question, secret); - updateView(); - } - } - } - }; - private View.OnClickListener mCancelSharedSecretListener = new View.OnClickListener() { - @Override - public void onClick(View view) { - if (isAccountOnline()) { - abortSmp(); - updateView(); - } - } - }; - private View.OnClickListener mRespondSharedSecretListener = new View.OnClickListener() { - - @Override - public void onClick(View view) { - if (isAccountOnline()) { - final String question = mSharedSecretHintEditable.getText().toString(); - final String secret = mSharedSecretSecret.getText().toString(); - respondSmp(question, secret); - updateView(); - } - } - }; - private View.OnClickListener mRetrySharedSecretListener = new View.OnClickListener() { - @Override - public void onClick(View view) { - mConversation.smp().status = Conversation.Smp.STATUS_NONE; - mConversation.smp().hint = null; - mConversation.smp().secret = null; - updateView(); - } - }; - private View.OnClickListener mFinishListener = new View.OnClickListener() { - @Override - public void onClick(View view) { - mConversation.smp().status = Conversation.Smp.STATUS_NONE; - finish(); - } - }; - - protected boolean initSmp(final String question, final String secret) { - final Session session = mConversation.getOtrSession(); - if (session != null) { - try { - session.initSmp(question, secret); - mConversation.smp().status = Conversation.Smp.STATUS_WE_REQUESTED; - mConversation.smp().secret = secret; - mConversation.smp().hint = question; - return true; - } catch (OtrException e) { - return false; - } - } else { - return false; - } - } - - protected boolean abortSmp() { - final Session session = mConversation.getOtrSession(); - if (session != null) { - try { - session.abortSmp(); - mConversation.smp().status = Conversation.Smp.STATUS_NONE; - mConversation.smp().hint = null; - mConversation.smp().secret = null; - return true; - } catch (OtrException e) { - return false; - } - } else { - return false; - } - } - - protected boolean respondSmp(final String question, final String secret) { - final Session session = mConversation.getOtrSession(); - if (session != null) { - try { - session.respondSmp(question, secret); - return true; - } catch (OtrException e) { - return false; - } - } else { - return false; - } - } - - protected boolean verifyWithUri(XmppUri uri) { - Contact contact = mConversation.getContact(); - if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.hasFingerprints()) { - xmppConnectionService.verifyFingerprints(contact, uri.getFingerprints()); - ToastCompat.makeText(this, R.string.verified, Toast.LENGTH_SHORT).show(); - updateView(); - return true; - } else { - ToastCompat.makeText(this, R.string.could_not_verify_fingerprint, Toast.LENGTH_SHORT).show(); - return false; - } - } - - protected boolean isAccountOnline() { - if (this.mAccount.getStatus() != Account.State.ONLINE) { - ToastCompat.makeText(this, R.string.not_connected_try_again, Toast.LENGTH_SHORT).show(); - return false; - } else { - return true; - } - } - - protected boolean handleIntent(Intent intent) { - if (intent != null && intent.getAction().equals(ACTION_VERIFY_CONTACT)) { - this.mAccount = extractAccount(intent); - if (this.mAccount == null) { - return false; - } - try { - this.mConversation = this.xmppConnectionService.find(this.mAccount, Jid.of(intent.getExtras().getString("contact"))); - if (this.mConversation == null) { - return false; - } - } catch (final IllegalArgumentException ignored) { - ignored.printStackTrace(); - return false; - } catch (Exception e) { - e.printStackTrace(); - return false; - } - this.mode = intent.getIntExtra("mode", MODE_MANUAL_VERIFICATION); - // todo scan OTR fingerprint - if (this.mode == MODE_SCAN_FINGERPRINT) { - Log.d(Config.LOGTAG, "Scan OTR fingerprint is not implemented in this version"); - //new IntentIntegrator(this).initiateScan(); - return false; - } - return true; - } else { - return false; - } - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent intent) { - // todo onActivityResult for OTR scan - Log.d(Config.LOGTAG, "Scan OTR fingerprint result is not implemented in this version"); - /*if ((requestCode & 0xFFFF) == IntentIntegrator.REQUEST_CODE) { - IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); - if (scanResult != null && scanResult.getFormatName() != null) { - String data = scanResult.getContents(); - XmppUri uri = new XmppUri(data); - if (xmppConnectionServiceBound) { - verifyWithUri(uri); - finish(); - } else { - this.mPendingUri = uri; - } - } else { - finish(); - } - }*/ - super.onActivityResult(requestCode, requestCode, intent); - } - - @Override - protected void onBackendConnected() { - if (handleIntent(getIntent())) { - updateView(); - } else if (mPendingUri != null) { - verifyWithUri(mPendingUri); - finish(); - mPendingUri = null; - } - setIntent(null); - } - - protected void updateView() { - if (this.mConversation != null && this.mConversation.hasValidOtrSession()) { - final ActionBar actionBar = getSupportActionBar(); - this.mVerificationExplain.setText(R.string.no_otr_session_found); - invalidateOptionsMenu(); - switch (this.mode) { - case MODE_ASK_QUESTION: - if (actionBar != null) { - actionBar.setTitle(R.string.ask_question); - } - this.updateViewAskQuestion(); - break; - case MODE_ANSWER_QUESTION: - if (actionBar != null) { - actionBar.setTitle(R.string.smp_requested); - } - this.updateViewAnswerQuestion(); - break; - case MODE_MANUAL_VERIFICATION: - default: - if (actionBar != null) { - actionBar.setTitle(R.string.manually_verify); - } - this.updateViewManualVerification(); - break; - } - } else { - this.mManualVerificationArea.setVisibility(View.GONE); - this.mSmpVerificationArea.setVisibility(View.GONE); - } - } - - protected void updateViewManualVerification() { - this.mVerificationExplain.setText(R.string.manual_verification_explanation); - this.mManualVerificationArea.setVisibility(View.VISIBLE); - this.mSmpVerificationArea.setVisibility(View.GONE); - this.mYourFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mAccount.getOtrFingerprint())); - this.mRemoteFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mConversation.getOtrFingerprint())); - if (this.mConversation.isOtrFingerprintVerified()) { - deactivateButton(this.mRightButton, R.string.verified); - activateButton(this.mLeftButton, R.string.cancel, this.mFinishListener); - } else { - activateButton(this.mLeftButton, R.string.cancel, this.mFinishListener); - activateButton(this.mRightButton, R.string.verify, new View.OnClickListener() { - @Override - public void onClick(View view) { - showManuallyVerifyDialog(); - } - }); - } - } - - protected void updateViewAskQuestion() { - this.mManualVerificationArea.setVisibility(View.GONE); - this.mSmpVerificationArea.setVisibility(View.VISIBLE); - this.mVerificationExplain.setText(R.string.smp_explain_question); - final int smpStatus = this.mConversation.smp().status; - switch (smpStatus) { - case Conversation.Smp.STATUS_WE_REQUESTED: - this.mStatusMessage.setVisibility(View.GONE); - this.mSharedSecretHintEditable.setVisibility(View.VISIBLE); - this.mSharedSecretSecret.setVisibility(View.VISIBLE); - this.mSharedSecretHintEditable.setText(this.mConversation.smp().hint); - this.mSharedSecretSecret.setText(this.mConversation.smp().secret); - this.activateButton(this.mLeftButton, R.string.cancel, this.mCancelSharedSecretListener); - this.deactivateButton(this.mRightButton, R.string.in_progress); - break; - case Conversation.Smp.STATUS_FAILED: - this.mStatusMessage.setVisibility(View.GONE); - this.mSharedSecretHintEditable.setVisibility(View.VISIBLE); - this.mSharedSecretSecret.setVisibility(View.VISIBLE); - this.mSharedSecretSecret.requestFocus(); - this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match)); - this.deactivateButton(this.mLeftButton, R.string.cancel); - this.activateButton(this.mRightButton, R.string.try_again, this.mRetrySharedSecretListener); - break; - case Conversation.Smp.STATUS_VERIFIED: - this.mSharedSecretHintEditable.setText(""); - this.mSharedSecretHintEditable.setVisibility(View.GONE); - this.mSharedSecretSecret.setText(""); - this.mSharedSecretSecret.setVisibility(View.GONE); - this.mStatusMessage.setVisibility(View.VISIBLE); - this.deactivateButton(this.mLeftButton, R.string.cancel); - this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener); - break; - default: - this.mStatusMessage.setVisibility(View.GONE); - this.mSharedSecretHintEditable.setVisibility(View.VISIBLE); - this.mSharedSecretSecret.setVisibility(View.VISIBLE); - this.activateButton(this.mLeftButton, R.string.cancel, this.mFinishListener); - this.activateButton(this.mRightButton, R.string.ask_question, this.mCreateSharedSecretListener); - break; - } - } - - protected void updateViewAnswerQuestion() { - this.mManualVerificationArea.setVisibility(View.GONE); - this.mSmpVerificationArea.setVisibility(View.VISIBLE); - this.mVerificationExplain.setText(R.string.smp_explain_answer); - this.mSharedSecretHintEditable.setVisibility(View.GONE); - this.mSharedSecretHint.setVisibility(View.VISIBLE); - this.deactivateButton(this.mLeftButton, R.string.cancel); - final int smpStatus = this.mConversation.smp().status; - switch (smpStatus) { - case Conversation.Smp.STATUS_CONTACT_REQUESTED: - this.mStatusMessage.setVisibility(View.GONE); - this.mSharedSecretHint.setText(this.mConversation.smp().hint); - this.activateButton(this.mRightButton, R.string.respond, this.mRespondSharedSecretListener); - break; - case Conversation.Smp.STATUS_VERIFIED: - this.mSharedSecretHintEditable.setText(""); - this.mSharedSecretHintEditable.setVisibility(View.GONE); - this.mSharedSecretHint.setVisibility(View.GONE); - this.mSharedSecretSecret.setText(""); - this.mSharedSecretSecret.setVisibility(View.GONE); - this.mStatusMessage.setVisibility(View.VISIBLE); - this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener); - break; - case Conversation.Smp.STATUS_FAILED: - default: - this.mSharedSecretSecret.requestFocus(); - this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match)); - this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener); - break; - } - } - - protected void activateButton(Button button, int text, View.OnClickListener listener) { - button.setEnabled(true); - button.setText(text); - button.setOnClickListener(listener); - } - - protected void deactivateButton(Button button, int text) { - button.setEnabled(false); - button.setText(text); - button.setOnClickListener(null); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_verify_otr); - this.mRemoteFingerprint = findViewById(R.id.remote_fingerprint); - this.mYourFingerprint = findViewById(R.id.your_fingerprint); - this.mLeftButton = findViewById(R.id.left_button); - this.mRightButton = findViewById(R.id.right_button); - this.mVerificationExplain = findViewById(R.id.verification_explanation); - this.mStatusMessage = findViewById(R.id.status_message); - this.mSharedSecretSecret = findViewById(R.id.shared_secret_secret); - this.mSharedSecretHintEditable = findViewById(R.id.shared_secret_hint_editable); - this.mSharedSecretHint = findViewById(R.id.shared_secret_hint); - this.mManualVerificationArea = findViewById(R.id.manual_verification_area); - this.mSmpVerificationArea = findViewById(R.id.smp_verification_area); - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - super.onCreateOptionsMenu(menu); - getMenuInflater().inflate(R.menu.verify_otr, menu); - return true; - } - - private void showManuallyVerifyDialog() { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.manually_verify); - builder.setMessage(R.string.are_you_sure_verify_fingerprint); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.verify, mVerifyFingerprintListener); - builder.create().show(); - } - - @Override - protected String getShareableUri() { - if (mAccount != null) { - return mAccount.getShareableUri(); - } else { - return ""; - } - } - - public void onConversationUpdate() { - refreshUi(); - } - - @Override - protected void refreshUiReal() { - updateView(); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/WelcomeActivity.java b/src/main/java/eu/siacs/conversations/ui/WelcomeActivity.java deleted file mode 100644 index 36f656b54..000000000 --- a/src/main/java/eu/siacs/conversations/ui/WelcomeActivity.java +++ /dev/null @@ -1,262 +0,0 @@ -package eu.siacs.conversations.ui; - -import static eu.siacs.conversations.Config.DISALLOW_REGISTRATION_IN_UI; -import static eu.siacs.conversations.utils.PermissionUtils.allGranted; -import static eu.siacs.conversations.utils.PermissionUtils.readGranted; - -import android.Manifest; -import android.content.ActivityNotFoundException; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.net.Uri; -import android.os.Bundle; -import android.security.KeyChain; -import android.security.KeyChainAliasCallback; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.databinding.DataBindingUtil; - -import java.util.Arrays; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.WelcomeBinding; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.util.IntroHelper; -import eu.siacs.conversations.ui.util.UpdateHelper; -import eu.siacs.conversations.utils.Compatibility; -import eu.siacs.conversations.utils.InstallReferrerUtils; -import eu.siacs.conversations.utils.SignupUtils; -import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.xmpp.Jid; -import me.drakeet.support.toast.ToastCompat; - -import static eu.siacs.conversations.utils.PermissionUtils.allGranted; -import static eu.siacs.conversations.utils.PermissionUtils.writeGranted; - - -public class WelcomeActivity extends XmppActivity implements XmppConnectionService.OnAccountCreated, KeyChainAliasCallback { - - private static final int REQUEST_IMPORT_BACKUP = 0x63fb; - private static final int REQUEST_READ_EXTERNAL_STORAGE = 0XD737; - - private XmppUri inviteUri; - - - public void onInstallReferrerDiscovered(final Uri referrer) { - Log.d(Config.LOGTAG, "welcome activity: on install referrer discovered " + referrer); - if ("xmpp".equalsIgnoreCase(referrer.getScheme())) { - final XmppUri xmppUri = new XmppUri(referrer); - runOnUiThread(() -> processXmppUri(xmppUri)); - } else { - Log.i(Config.LOGTAG, "install referrer was not an XMPP uri"); - } - } - - private void processXmppUri(final XmppUri xmppUri) { - if (!xmppUri.isValidJid()) { - return; - } - final String preAuth = xmppUri.getParameter(XmppUri.PARAMETER_PRE_AUTH); - final Jid jid = xmppUri.getJid(); - final Intent intent; - if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) { - intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth, true); - } else if (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()); - } else { - intent = null; - } - if (intent != null) { - startActivity(intent); - finish(); - return; - } - this.inviteUri = xmppUri; - } - - @Override - protected void refreshUiReal() { - } - - @Override - void onBackendConnected() { - } - - @Override - public void onStart() { - super.onStart(); - final int theme = findTheme(); - if (this.mTheme != theme) { - recreate(); - } - new InstallReferrerUtils(this); - } - - @Override - public void onStop() { - super.onStop(); - } - - @Override - public void onNewIntent(final Intent intent) { - super.onNewIntent(intent); - if (intent != null) { - setIntent(intent); - } - } - - @Override - protected void onCreate(final Bundle savedInstanceState) { - if (getResources().getBoolean(R.bool.portrait_only)) { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - } - super.onCreate(savedInstanceState); - WelcomeBinding binding = DataBindingUtil.setContentView(this, R.layout.welcome); - setSupportActionBar(findViewById(R.id.toolbar)); - final ActionBar ab = getSupportActionBar(); - if (ab != null) { - ab.setDisplayShowHomeEnabled(false); - ab.setDisplayHomeAsUpEnabled(false); - } - IntroHelper.showIntro(this, false); - UpdateHelper.showPopup(this); - if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) { - binding.importDatabase.setVisibility(View.VISIBLE); - binding.importText.setVisibility(View.VISIBLE); - } - binding.importDatabase.setOnClickListener(v -> startActivity(new Intent(this, ImportBackupActivity.class))); - binding.createAccount.setOnClickListener(v -> { - final Intent intent = new Intent(WelcomeActivity.this, MagicCreateActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - addInviteUri(intent); - startActivity(intent); - }); - if (DISALLOW_REGISTRATION_IN_UI) { - binding.createAccount.setVisibility(View.GONE); - } - binding.useExistingAccount.setOnClickListener(v -> { - final List accounts = xmppConnectionService.getAccounts(); - Intent intent = new Intent(WelcomeActivity.this, EditAccountActivity.class); - if (accounts.size() == 1) { - intent.putExtra("jid", accounts.get(0).getJid().asBareJid().toString()); - intent.putExtra("init", true); - } else if (accounts.size() >= 1) { - intent = new Intent(WelcomeActivity.this, ManageAccountActivity.class); - } - intent.putExtra("existing", true); - addInviteUri(intent); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - finish(); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - }); - } - - public void addInviteUri(Intent to) { - final Intent from = getIntent(); - if (from != null && from.hasExtra(StartConversationActivity.EXTRA_INVITE_URI)) { - final String invite = from.getStringExtra(StartConversationActivity.EXTRA_INVITE_URI); - to.putExtra(StartConversationActivity.EXTRA_INVITE_URI, invite); - } else if (this.inviteUri != null) { - Log.d(Config.LOGTAG, "injecting referrer uri into on-boarding flow"); - to.putExtra(StartConversationActivity.EXTRA_INVITE_URI, this.inviteUri.toString()); - } - } - - public static void launch(AppCompatActivity activity) { - Intent intent = new Intent(activity, WelcomeActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - activity.startActivity(intent); - activity.overridePendingTransition(0, 0); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.welcome_menu, menu); - final MenuItem scan = menu.findItem(R.id.action_scan_qr_code); - scan.setVisible(Compatibility.hasFeatureCamera(this)); - return super.onCreateOptionsMenu(menu); - } - - - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_import_backup: - if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) { - startActivity(new Intent(this, ImportBackupActivity.class)); - } - break; - case R.id.action_scan_qr_code: - UriHandlerActivity.scan(this, true); - break; - case R.id.action_add_account_with_cert: - addAccountFromKey(); - break; - } - return super.onOptionsItemSelected(item); - } - - - private void addAccountFromKey() { - try { - KeyChain.choosePrivateKeyAlias(this, this, null, null, null, -1, null); - } catch (ActivityNotFoundException e) { - ToastCompat.makeText(this, R.string.device_does_not_support_certificates, ToastCompat.LENGTH_LONG).show(); - } - } - - @Override - public void alias(final String alias) { - if (alias != null) { - xmppConnectionService.createAccountFromKey(alias, this); - } - } - - @Override - public void onAccountCreated(final Account account) { - final Intent intent = new Intent(this, EditAccountActivity.class); - intent.putExtra("jid", account.getJid().asBareJid().toEscapedString()); - intent.putExtra("init", true); - addInviteUri(intent); - startActivity(intent); - } - - @Override - public void informUser(final int r) { - runOnUiThread(() -> ToastCompat.makeText(this, r, ToastCompat.LENGTH_LONG).show()); - } - - @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)) { - switch (requestCode) { - case REQUEST_IMPORT_BACKUP: - startActivity(new Intent(this, ImportBackupActivity.class)); - break; - } - } else if (Arrays.asList(permissions).contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - ToastCompat.makeText(this, R.string.no_storage_permission, ToastCompat.LENGTH_SHORT).show(); - } - } - if (readGranted(grantResults, permissions)) { - if (xmppConnectionService != null) { - xmppConnectionService.restartFileObserver(); - } - } - } -} \ 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 deleted file mode 100644 index f69f7cc7f..000000000 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ /dev/null @@ -1,1518 +0,0 @@ -package eu.siacs.conversations.ui; - -import static eu.siacs.conversations.ui.SettingsActivity.USE_INTERNAL_UPDATER; -import eu.siacs.conversations.utils.Compatibility; -import androidx.annotation.RequiresApi; -import android.Manifest; -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.app.Activity; -import android.app.PendingIntent; -import android.content.ActivityNotFoundException; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.ContextWrapper; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentSender.SendIntentException; -import android.content.ServiceConnection; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Bitmap; -import android.graphics.Color; -import android.graphics.Point; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; -import android.net.ConnectivityManager; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.PowerManager; -import android.os.SystemClock; -import android.preference.PreferenceManager; -import android.provider.Settings; -import android.text.Html; -import android.text.InputType; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.Window; -import android.widget.ImageView; -import android.widget.Spinner; -import android.widget.Toast; - -import androidx.annotation.BoolRes; -import androidx.annotation.IntegerRes; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatDelegate; -import androidx.core.content.ContextCompat; -import androidx.databinding.DataBindingUtil; - -import com.google.common.base.Strings; -import com.google.common.collect.Collections2; - -import java.io.File; -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.atomic.AtomicReference; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.PgpEngine; -import eu.siacs.conversations.databinding.DialogQuickeditBinding; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.entities.Presences; -import eu.siacs.conversations.services.AvatarService; -import eu.siacs.conversations.services.BarcodeProvider; -import eu.siacs.conversations.services.EmojiInitializationService; -import eu.siacs.conversations.services.QuickConversationsService; -import eu.siacs.conversations.services.UpdateService; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; -import eu.siacs.conversations.ui.util.AvatarWorkerTask; -import eu.siacs.conversations.ui.util.CustomTab; -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.EasyOnboardingInvite; -import eu.siacs.conversations.utils.ExceptionHelper; -import eu.siacs.conversations.utils.MenuDoubleTabUtil; -import eu.siacs.conversations.utils.ThemeHelper; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; -import eu.siacs.conversations.xmpp.OnUpdateBlocklist; -import eu.siacs.conversations.xmpp.XmppConnection; -import me.drakeet.support.toast.ToastCompat; -import pl.droidsonroids.gif.GifDrawable; -import android.util.Pair; -import net.java.otr4j.session.SessionID; -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; -import eu.siacs.conversations.utils.CryptoHelper; -import static eu.siacs.conversations.ui.SettingsActivity.ENABLE_OTR_ENCRYPTION; - -public abstract class XmppActivity extends ActionBarActivity { - - protected static final int REQUEST_ANNOUNCE_PGP = 0x0101; - protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102; - protected static final int REQUEST_CHOOSE_PGP_ID = 0x0103; - protected static final int REQUEST_BATTERY_OP = 0x49ff; - protected static final int REQUEST_UNKNOWN_SOURCE_OP = 0x98ff; - - public static final String EXTRA_ACCOUNT = "account"; - - public XmppConnectionService xmppConnectionService; - public MediaBrowserActivity mediaBrowserActivity; - public boolean xmppConnectionServiceBound = false; - - public AlertDialog AvatarPopup; - - protected int mColorWarningButton; - protected int mColorWarningText; - protected int mColorDefaultButtonText; - protected int mColorWhite; - - protected static final String FRAGMENT_TAG_DIALOG = "dialog"; - - private boolean isCameraFeatureAvailable = false; - - protected int mTheme; - protected boolean mUsingEnterKey = false; - public boolean mUseTor = false; - public boolean mUseI2P = false; - - protected Toast mToast; - protected Runnable onOpenPGPKeyPublished = () -> ToastCompat.makeText(XmppActivity.this, R.string.openpgp_has_been_published, ToastCompat.LENGTH_SHORT).show(); - protected ConferenceInvite mPendingConferenceInvite = null; - protected ServiceConnection mConnection = new ServiceConnection() { - - @Override - public void onServiceConnected(ComponentName className, IBinder service) { - XmppConnectionBinder binder = (XmppConnectionBinder) service; - xmppConnectionService = binder.getService(); - xmppConnectionServiceBound = true; - registerListeners(); - invalidateOptionsMenu(); - onBackendConnected(); - } - - @Override - public void onServiceDisconnected(ComponentName arg0) { - xmppConnectionServiceBound = false; - } - }; - private DisplayMetrics metrics; - private long mLastUiRefresh = 0; - private Handler mRefreshUiHandler = new Handler(); - private Runnable mRefreshUiRunnable = () -> { - mLastUiRefresh = SystemClock.elapsedRealtime(); - refreshUiReal(); - }; - private UiCallback adhocCallback = new UiCallback() { - @Override - public void success(final Conversation conversation) { - runOnUiThread(() -> { - switchToConversation(conversation); - hideToast(); - }); - } - - @Override - public void error(final int errorCode, Conversation object) { - runOnUiThread(() -> replaceToast(getString(errorCode))); - } - - @Override - public void userInputRequired(PendingIntent pi, Conversation object) { - - } - - @Override - public void progress(int progress) { - - } - }; - - public boolean mSkipBackgroundBinding = false; - - public static boolean cancelPotentialWork(Message message, ImageView imageView) { - final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); - - if (bitmapWorkerTask != null) { - final Message oldMessage = bitmapWorkerTask.message; - if (oldMessage == null || message != oldMessage) { - bitmapWorkerTask.cancel(true); - } else { - return false; - } - } - return true; - } - - private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { - if (imageView != null) { - final Drawable drawable = imageView.getDrawable(); - if (drawable instanceof AsyncDrawable) { - final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; - return asyncDrawable.getBitmapWorkerTask(); - } - } - return null; - } - - protected void hideToast() { - if (mToast != null) { - mToast.cancel(); - } - } - - protected void replaceToast(String msg) { - replaceToast(msg, true); - } - - protected void replaceToast(String msg, boolean showlong) { - hideToast(); - mToast = ToastCompat.makeText(this, msg, showlong ? ToastCompat.LENGTH_LONG : ToastCompat.LENGTH_SHORT); - mToast.show(); - } - - public final void refreshUi() { - final long diff = SystemClock.elapsedRealtime() - mLastUiRefresh; - if (diff > Config.REFRESH_UI_INTERVAL) { - mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable); - runOnUiThread(mRefreshUiRunnable); - } else { - final long next = Config.REFRESH_UI_INTERVAL - diff; - mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable); - mRefreshUiHandler.postDelayed(mRefreshUiRunnable, next); - } - } - - abstract protected void refreshUiReal(); - - @Override - protected void onStart() { - super.onStart(); - if (!xmppConnectionServiceBound) { - if (this.mSkipBackgroundBinding) { - Log.d(Config.LOGTAG, "skipping background binding"); - } else { - connectToBackend(); - } - } else { - this.registerListeners(); - this.onBackendConnected(); - } - this.mUsingEnterKey = usingEnterKey(); - this.mUseTor = useTor(); - this.mUseI2P = useI2P(); - } - - public void connectToBackend() { - Intent intent = new Intent(this, XmppConnectionService.class); - intent.setAction("ui"); - try { - startService(intent); - } catch (IllegalStateException e) { - Log.w(Config.LOGTAG, "unable to start service from " + getClass().getSimpleName()); - } - bindService(intent, mConnection, Context.BIND_AUTO_CREATE); - } - - @Override - protected void onStop() { - super.onStop(); - if (xmppConnectionServiceBound) { - this.unregisterListeners(); - unbindService(mConnection); - xmppConnectionServiceBound = false; - } - } - - public boolean hasPgp() { - return xmppConnectionService.getPgpEngine() != null; - } - - public void showInstallPgpDialog() { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.openkeychain_required)); - builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setMessage(Html.fromHtml(getString(R.string.openkeychain_required_long, getString(R.string.app_name)))); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setNeutralButton(getString(R.string.restart), - (dialog, which) -> { - if (xmppConnectionServiceBound) { - unbindService(mConnection); - xmppConnectionServiceBound = false; - } - stopService(new Intent(XmppActivity.this, - XmppConnectionService.class)); - finish(); - }); - builder.setPositiveButton(getString(R.string.install), - (dialog, which) -> { - Uri uri = Uri - .parse("market://details?id=org.sufficientlysecure.keychain"); - Intent marketIntent = new Intent(Intent.ACTION_VIEW, - uri); - PackageManager manager = getApplicationContext() - .getPackageManager(); - List infos = manager - .queryIntentActivities(marketIntent, 0); - if (infos.size() > 0) { - startActivity(marketIntent); - } else { - uri = Uri.parse("http://www.openkeychain.org/"); - try { - CustomTab.openTab(this, uri, isDarkTheme()); - } catch (Exception e) { - ToastCompat.makeText(this, R.string.no_application_found_to_open_link, ToastCompat.LENGTH_SHORT).show(); - } - } - finish(); - }); - builder.create().show(); - } - - abstract void onBackendConnected(); - - protected void registerListeners() { - if (this instanceof XmppConnectionService.OnConversationUpdate) { - this.xmppConnectionService.setOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this); - } - if (this instanceof XmppConnectionService.OnAccountUpdate) { - this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this); - } - if (this instanceof XmppConnectionService.OnCaptchaRequested) { - this.xmppConnectionService.setOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this); - } - if (this instanceof XmppConnectionService.OnRosterUpdate) { - this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this); - } - if (this instanceof XmppConnectionService.OnMucRosterUpdate) { - this.xmppConnectionService.setOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this); - } - if (this instanceof OnUpdateBlocklist) { - this.xmppConnectionService.setOnUpdateBlocklistListener((OnUpdateBlocklist) this); - } - if (this instanceof XmppConnectionService.OnShowErrorToast) { - this.xmppConnectionService.setOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this); - } - if (this instanceof OnKeyStatusUpdated) { - this.xmppConnectionService.setOnKeyStatusUpdatedListener((OnKeyStatusUpdated) this); - } - if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) { - this.xmppConnectionService.setOnRtpConnectionUpdateListener((XmppConnectionService.OnJingleRtpConnectionUpdate) this); - } - } - - protected void unregisterListeners() { - if (this instanceof XmppConnectionService.OnConversationUpdate) { - this.xmppConnectionService.removeOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this); - } - if (this instanceof XmppConnectionService.OnAccountUpdate) { - this.xmppConnectionService.removeOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this); - } - if (this instanceof XmppConnectionService.OnCaptchaRequested) { - this.xmppConnectionService.removeOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this); - } - if (this instanceof XmppConnectionService.OnRosterUpdate) { - this.xmppConnectionService.removeOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this); - } - if (this instanceof XmppConnectionService.OnMucRosterUpdate) { - this.xmppConnectionService.removeOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this); - } - if (this instanceof OnUpdateBlocklist) { - this.xmppConnectionService.removeOnUpdateBlocklistListener((OnUpdateBlocklist) this); - } - if (this instanceof XmppConnectionService.OnShowErrorToast) { - this.xmppConnectionService.removeOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this); - } - if (this instanceof OnKeyStatusUpdated) { - this.xmppConnectionService.removeOnNewKeysAvailableListener((OnKeyStatusUpdated) this); - } - if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) { - this.xmppConnectionService.removeRtpConnectionUpdateListener((XmppConnectionService.OnJingleRtpConnectionUpdate) this); - } - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case R.id.action_create_issue: - createIssue(); - break; - case R.id.action_settings: - startActivity(new Intent(this, SettingsActivity.class)); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - break; - case R.id.action_accounts: - if (xmppConnectionServiceBound && this.xmppConnectionService.getAccounts().size() == 1 && !this.xmppConnectionService.multipleAccounts()) { - final Intent intent = new Intent(getApplicationContext(), EditAccountActivity.class); - Account mAccount = xmppConnectionService.getAccounts().get(0); - intent.putExtra("jid", mAccount.getJid().asBareJid().toString()); - intent.putExtra("init", false); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } else { - AccountUtils.launchManageAccounts(this); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - break; - case android.R.id.home: - finish(); - break; - case R.id.action_show_qr_code: - showQrCode(); - break; - } - return super.onOptionsItemSelected(item); - } - - public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) { - final Contact contact = conversation.getContact(); - if (conversation.hasValidOtrSession()) { - SessionID id = conversation.getOtrSession().getSessionID(); - Jid jid; - try { - jid = Jid.of(id.getAccountID() + "/" + id.getUserID()); - } catch (IllegalArgumentException e) { - jid = null; - } - conversation.setNextCounterpart(jid); - listener.onPresenceSelected(); - } else if (contact.showInRoster() || contact.isSelf()) { - final Presences presences = contact.getPresences(); - if (presences.size() == 0) { - if (contact.isSelf()) { - conversation.setNextCounterpart(null); - listener.onPresenceSelected(); - } else if (!contact.getOption(Contact.Options.TO) - && !contact.getOption(Contact.Options.ASKING) - && contact.getAccount().getStatus() == Account.State.ONLINE) { - showAskForPresenceDialog(contact); - } else if (!contact.getOption(Contact.Options.TO) - || !contact.getOption(Contact.Options.FROM)) { - PresenceSelector.warnMutualPresenceSubscription(this, conversation, listener); - } else { - conversation.setNextCounterpart(null); - listener.onPresenceSelected(); - } - } else if (presences.size() == 1) { - final String presence = presences.toResourceArray()[0]; - conversation.setNextCounterpart(PresenceSelector.getNextCounterpart(contact, presence)); - listener.onPresenceSelected(); - } else { - PresenceSelector.showPresenceSelectionDialog(this, conversation, listener); - } - } else { - showAddToRosterDialog(conversation.getContact()); - } - } - - @SuppressLint("UnsupportedChromeOsCameraSystemFeature") - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - this.mTheme = findTheme(); - setTheme(this.mTheme); - metrics = getResources().getDisplayMetrics(); - ExceptionHelper.init(getApplicationContext()); - EmojiInitializationService.execute(this); - this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); - if (isDarkTheme()) { - mColorWarningButton = ContextCompat.getColor(this, R.color.warning_button_dark); - mColorWarningText = ContextCompat.getColor(this, R.color.warning_button); - } else { - mColorWarningButton = ContextCompat.getColor(this, R.color.warning_button); - mColorWarningText = ContextCompat.getColor(this, R.color.warning_button_dark); - } - mColorDefaultButtonText = ContextCompat.getColor(this, R.color.realwhite); - mColorWhite = ContextCompat.getColor(this, R.color.white70); - this.mUsingEnterKey = usingEnterKey(); - } - - protected boolean isCameraFeatureAvailable() { - return this.isCameraFeatureAvailable; - } - - public boolean isDarkTheme() { - return ThemeHelper.isDark(mTheme); - } - - public String getThemeColor() { - return getStringPreference("theme_color", R.string.theme_color); - } - - public boolean unicoloredBG() { - return getBooleanPreference("unicolored_chatbg", R.bool.use_unicolored_chatbg) || getPreferences().getString(SettingsActivity.THEME, getString(R.string.theme)).equals("black"); - } - public boolean enableOTR() { - return getBooleanPreference(ENABLE_OTR_ENCRYPTION, R.bool.enable_otr); - } - public boolean showDateInQuotes() { - return getBooleanPreference("show_date_in_quotes", R.bool.show_date_in_quotes); - } - - public void setBubbleColor(final View v, final int backgroundColor, final int borderColor) { - GradientDrawable shape = (GradientDrawable) v.getBackground(); - shape.setColor(backgroundColor); - if (borderColor != -1) { - shape.setStroke(2, borderColor); - } - v.setBackground(shape); - } - - public int getThemeResource(int r_attr_name, int r_drawable_def) { - int[] attrs = {r_attr_name}; - TypedArray ta = this.getTheme().obtainStyledAttributes(attrs); - - int res = ta.getResourceId(0, r_drawable_def); - ta.recycle(); - - return res; - } - - protected boolean isOptimizingBattery() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - final PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); - return pm != null && !pm.isIgnoringBatteryOptimizations(getPackageName()); - } else { - return false; - } - } - - protected boolean isAffectedByDataSaver() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - return cm != null - && cm.isActiveNetworkMetered() - && Compatibility.getRestrictBackgroundStatus(cm) == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED; - } else { - return false; - } - } - - @RequiresApi(api = Build.VERSION_CODES.N) - private static int getRestrictBackgroundStatus(@NonNull final ConnectivityManager connectivityManager) { - try { - return connectivityManager.getRestrictBackgroundStatus(); - } catch (final Exception e) { - Log.d(Config.LOGTAG, "platform bug detected. Unable to get restrict background status", e); - return -1; - } - } - - protected boolean usingEnterKey() { - return getBooleanPreference("display_enter_key", R.bool.display_enter_key); - } - - public boolean useInternalUpdater() { - return getBooleanPreference(USE_INTERNAL_UPDATER, R.bool.use_internal_updater); - } - - private boolean useTor() { - return QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor); - } - - private boolean useI2P() { - return QuickConversationsService.isConversations() && getBooleanPreference("use_i2p", R.bool.use_i2p); - } - - public SharedPreferences getPreferences() { - return PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - } - - protected boolean getBooleanPreference(String name, @BoolRes int res) { - return getPreferences().getBoolean(name, getResources().getBoolean(res)); - } - - protected String getStringPreference(String name, int res) { - return getPreferences().getString(name, getResources().getString(res)); - } - - public long getLongPreference(String name, @IntegerRes int res) { - long defaultValue = getResources().getInteger(res); - try { - return Long.parseLong(getPreferences().getString(name, String.valueOf(defaultValue))); - } catch (NumberFormatException e) { - return defaultValue; - } - } - - public void switchToConversation(Conversation conversation) { - switchToConversation(conversation, null); - } - - public void switchToConversationAndQuote(Conversation conversation, String text, String user) { - switchToConversation(conversation, text, true, user, false, false); - } - - public void switchToConversation(Conversation conversation, String text) { - switchToConversation(conversation, text, false, null, false, false); - } - - public void switchToConversationDoNotAppend(Conversation conversation, String text) { - switchToConversation(conversation, text, false, null, false, true); - } - - public void highlightInMuc(Conversation conversation, String nick) { - switchToConversation(conversation, null, false, nick, false, false); - } - - public void privateMsgInMuc(Conversation conversation, String nick) { - switchToConversation(conversation, null, false, nick, true, false); - } - - private void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend) { - Intent intent = new Intent(this, ConversationsActivity.class); - intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION); - intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid()); - if (text != null) { - intent.putExtra(Intent.EXTRA_TEXT, text); - if (asQuote) { - intent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, true); - intent.putExtra(ConversationsActivity.EXTRA_USER, nick); - } - } - if (nick != null && !asQuote) { - intent.putExtra(ConversationsActivity.EXTRA_NICK, nick); - intent.putExtra(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, pm); - } - if (doNotAppend) { - intent.putExtra(ConversationsActivity.EXTRA_DO_NOT_APPEND, true); - } - intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - finish(); - } - public void switchToMUCDetails(Conversation conversation) { - Intent intent = new Intent(this, ConferenceDetailsActivity.class); - intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); - intent.putExtra("uuid", conversation.getUuid()); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - - public void switchToContactDetails(Contact contact) { - switchToContactDetails(contact, null); - } - - public void switchToContactDetails(Contact contact, String messageFingerprint) { - Intent intent = new Intent(this, ContactDetailsActivity.class); - intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT); - intent.putExtra(EXTRA_ACCOUNT, contact.getAccount().getJid().asBareJid().toEscapedString()); - intent.putExtra("contact", contact.getJid().toEscapedString()); - intent.putExtra("fingerprint", messageFingerprint); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - - public void switchToMucContactDetails(MucOptions.User user) { - Intent intent = new Intent(this, ConferenceContactDetailsActivity.class); - intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT); - intent.putExtra(EXTRA_ACCOUNT, user.getAccount().getJid().asBareJid().toEscapedString()); - intent.putExtra("user", user.getFullJid().toEscapedString()); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - - public void switchToAccount(Account account, String fingerprint) { - switchToAccount(account, false, fingerprint); - } - - public void switchToAccount(Account account) { - switchToAccount(account, false, null); - } - - public void switchToAccount(Account account, boolean init, String fingerprint) { - Intent intent = new Intent(this, EditAccountActivity.class); - intent.putExtra("jid", account.getJid().asBareJid().toEscapedString()); - intent.putExtra("init", init); - if (init) { - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION); - } - if (fingerprint != null) { - intent.putExtra("fingerprint", fingerprint); - } - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - if (init) { - overridePendingTransition(0, 0); - } - } - - protected void delegateUriPermissionsToService(Uri uri) { - Intent intent = new Intent(this, XmppConnectionService.class); - intent.setAction(Intent.ACTION_SEND); - intent.setData(uri); - intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - try { - startService(intent); - } catch (Exception e) { - Log.e(Config.LOGTAG, "unable to delegate uri permission", e); - } - } - - protected void inviteToConversation(Conversation conversation) { - startActivityForResult(ChooseContactActivity.create(this, conversation), REQUEST_INVITE_TO_CONVERSATION); - } - - protected void announcePgp(final Account account, final Conversation conversation, Intent intent, final Runnable onSuccess) { - if (account.getPgpId() == 0) { - choosePgpSignId(account); - } else { - final String status = Strings.nullToEmpty(account.getPresenceStatusMessage()); - xmppConnectionService.getPgpEngine().generateSignature(intent, account, status, new UiCallback() { - - @Override - public void userInputRequired(PendingIntent pi, String signature) { - try { - startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); - } catch (final SendIntentException ignored) { - } - } - - @Override - public void progress(int progress) { - - } - - @Override - public void success(String signature) { - account.setPgpSignature(signature); - xmppConnectionService.databaseBackend.updateAccount(account); - xmppConnectionService.sendPresence(account); - if (conversation != null) { - conversation.setNextEncryption(Message.ENCRYPTION_PGP); - xmppConnectionService.updateConversation(conversation); - refreshUi(); - } - if (onSuccess != null) { - runOnUiThread(onSuccess); - } - } - - @Override - public void error(int error, String signature) { - if (error == 0) { - account.setPgpSignId(0); - account.unsetPgpSignature(); - xmppConnectionService.databaseBackend.updateAccount(account); - choosePgpSignId(account); - } else { - displayErrorDialog(error); - } - } - }); - } - } - - @SuppressWarnings("deprecation") - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - protected void setListItemBackgroundOnView(View view) { - int sdk = android.os.Build.VERSION.SDK_INT; - if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) { - view.setBackgroundDrawable(getResources().getDrawable(R.drawable.greybackground)); - } else { - view.setBackground(getResources().getDrawable(R.drawable.greybackground)); - } - } - - protected void choosePgpSignId(Account account) { - xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback() { - @Override - public void success(Account account1) { - } - - @Override - public void error(int errorCode, Account object) { - - } - - @Override - public void userInputRequired(PendingIntent pi, Account object) { - try { - startIntentSenderForResult(pi.getIntentSender(), - REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0); - } catch (final SendIntentException ignored) { - } - } - - @Override - public void progress(int progress) { - - } - }); - } - - protected void displayErrorDialog(final int errorCode) { - runOnUiThread(() -> { - final AlertDialog.Builder builder = new AlertDialog.Builder(XmppActivity.this); - builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setTitle(getString(R.string.error)); - builder.setMessage(errorCode); - builder.setNeutralButton(R.string.accept, null); - builder.create().show(); - }); - - } - - public void showAddToRosterDialog(final Conversation conversation) { - showAddToRosterDialog(conversation.getContact()); - } - - public void showAddToRosterDialog(final Contact contact) { - if (contact == null) { - return; - } - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(contact.getJid().toString()); - builder.setMessage(getString(R.string.not_in_roster)); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.add_contact), (dialog, which) -> { - xmppConnectionService.createContact(contact, true); - recreate(); - }); - builder.create().show(); - } - - private void showAskForPresenceDialog(final Contact contact) { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(contact.getJid().toString()); - builder.setMessage(R.string.request_presence_updates); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.request_now, - (dialog, which) -> { - if (xmppConnectionServiceBound) { - xmppConnectionService.sendPresencePacket(contact - .getAccount(), xmppConnectionService - .getPresenceGenerator() - .requestPresenceUpdatesFrom(contact)); - } - }); - builder.create().show(); - } - - private void warnMutalPresenceSubscription(final Conversation conversation, - final OnPresenceSelected listener) { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(conversation.getContact().getJid().toString()); - builder.setMessage(R.string.without_mutual_presence_updates); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.ignore, (dialog, which) -> { - conversation.setNextCounterpart(null); - if (listener != null) { - listener.onPresenceSelected(); - } - }); - builder.create().show(); - } - - protected void quickEdit(String previousValue, @StringRes int hint, OnValueEdited callback) { - quickEdit(previousValue, callback, hint, false, false); - } - - protected void quickEdit(String previousValue, @StringRes int hint, OnValueEdited callback, boolean permitEmpty) { - quickEdit(previousValue, callback, hint, false, permitEmpty); - } - - protected void quickPasswordEdit(String previousValue, OnValueEdited callback) { - quickEdit(previousValue, callback, R.string.password, true, false); - } - - @SuppressLint("InflateParams") - private void quickEdit(final String previousValue, - final OnValueEdited callback, - final @StringRes int hint, - boolean password, - boolean permitEmpty) { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - DialogQuickeditBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_quickedit, null, false); - if (password) { - binding.inputEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - } - builder.setPositiveButton(R.string.accept, null); - if (hint != 0) { - binding.inputLayout.setHint(getString(hint)); - } - binding.inputEditText.requestFocus(); - if (previousValue != null) { - binding.inputEditText.getText().append(previousValue); - } - builder.setView(binding.getRoot()); - builder.setNegativeButton(R.string.cancel, null); - final AlertDialog dialog = builder.create(); - dialog.setOnShowListener(d -> SoftKeyboardUtils.showKeyboard(binding.inputEditText)); - dialog.show(); - View.OnClickListener clickListener = v -> { - String value = binding.inputEditText.getText().toString(); - if (!value.equals(previousValue) && (!value.trim().isEmpty() || permitEmpty)) { - String error = callback.onValueEdited(value); - if (error != null) { - binding.inputLayout.setError(error); - return; - } - } - SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText); - dialog.dismiss(); - }; - dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(clickListener); - dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((v -> { - SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText); - dialog.dismiss(); - })); - dialog.setCanceledOnTouchOutside(false); - dialog.setOnDismissListener(dialog1 -> { - SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText); - }); - } - - protected boolean hasStoragePermission(int requestCode) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED && checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, requestCode); - return false; - } else { - return true; - } - } else { - return true; - } - } - - public boolean hasMicPermission(int requestCode) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { - requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, requestCode); - return false; - } else { - return true; - } - } else { - return true; - } - } - - public boolean hasLocationPermission(int requestCode) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED || checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { - requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, requestCode); - requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, requestCode); - return false; - } else { - return true; - } - } else { - return true; - } - } - - protected void onActivityResult(int requestCode, int resultCode, final Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) { - mPendingConferenceInvite = ConferenceInvite.parse(data); - if (xmppConnectionServiceBound && mPendingConferenceInvite != null) { - if (mPendingConferenceInvite.execute(this)) { - mToast = ToastCompat.makeText(this, R.string.creating_conference, ToastCompat.LENGTH_LONG); - mToast.show(); - } - mPendingConferenceInvite = null; - } - } - } - - public int getWarningButtonColor() { - return this.mColorWarningButton; - } - - public int getWarningTextColor() { - return this.mColorWarningText; - } - - public int getDefaultButtonTextColor() { - return this.mColorDefaultButtonText; - } - - public int getPixel(int dp) { - DisplayMetrics metrics = getResources().getDisplayMetrics(); - return ((int) (dp * metrics.density)); - } - - public boolean copyTextToClipboard(String text, int labelResId) { - ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); - String label = getResources().getString(labelResId); - if (mClipBoardManager != null) { - ClipData mClipData = ClipData.newPlainText(label, text); - mClipBoardManager.setPrimaryClip(mClipData); - return true; - } - return false; - } - - protected boolean manuallyChangePresence() { - return getBooleanPreference(SettingsActivity.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence); - } - - protected String getShareableUri() { - return getShareableUri(false); - } - - protected String getShareableUri(boolean http) { - return null; - } - - public void inviteUser() { - if (!xmppConnectionServiceBound) { - ToastCompat.makeText(this, R.string.not_connected_try_again, ToastCompat.LENGTH_SHORT).show(); - return; - } - if (xmppConnectionService.getAccounts() == null) { - ToastCompat.makeText(this, R.string.no_accounts, ToastCompat.LENGTH_SHORT).show(); - return; - } - if (!selectAccountToStartEasyInvite()) { - if (!xmppConnectionService.multipleAccounts()) { - final Account mAccount = xmppConnectionService.getAccounts().get(0); - final String user = Jid.ofEscaped(mAccount.getJid()).getLocal(); - final String domain = Jid.ofEscaped(mAccount.getJid()).getDomain().toEscapedString(); - String inviteURL; - try { - inviteURL = new getAdHocInviteUri(mAccount.getXmppConnection(), mAccount).execute().get(); - } catch (ExecutionException e) { - e.printStackTrace(); - inviteURL = Config.inviteUserURL + user + "/" + domain; - } catch (InterruptedException e) { - e.printStackTrace(); - inviteURL = Config.inviteUserURL + user + "/" + domain; - } - if (inviteURL == null) { - inviteURL = Config.inviteUserURL + user + "/" + domain; - } - Log.d(Config.LOGTAG, "Invite uri = " + inviteURL); - final String inviteText = getString(R.string.InviteText, user); - final Intent intent = new Intent(android.content.Intent.ACTION_SEND); - intent.setType("text/plain"); - intent.putExtra(Intent.EXTRA_SUBJECT, user + " " + getString(R.string.inviteUser_Subject) + " " + getString(R.string.app_name)); - intent.putExtra(Intent.EXTRA_TEXT, inviteText + "\n\n" + inviteURL); - startActivity(Intent.createChooser(intent, getString(R.string.invite_contact))); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } else { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.chooce_account); - final View dialogView = this.getLayoutInflater().inflate(R.layout.choose_account_dialog, null); - final Spinner spinner = dialogView.findViewById(R.id.account); - builder.setView(dialogView); - List mActivatedAccounts = new ArrayList<>(); - for (Account account : xmppConnectionService.getAccounts()) { - if (account.getStatus() != Account.State.DISABLED) { - if (Config.DOMAIN_LOCK != null) { - mActivatedAccounts.add(account.getJid().getLocal()); - } else { - mActivatedAccounts.add(account.getJid().asBareJid().toString()); - } - } - } - StartConversationActivity.populateAccountSpinner(this, mActivatedAccounts, spinner); - builder.setPositiveButton(R.string.ok, - (dialog, id) -> { - final String selection = spinner.getSelectedItem().toString(); - final Account mAccount = xmppConnectionService.findAccountByJid(Jid.of(selection).asBareJid()); - final String user = Jid.of(mAccount.getJid()).getLocal(); - final String domain = Jid.of(mAccount.getJid()).getDomain().toEscapedString(); - String inviteURL; - try { - inviteURL = new getAdHocInviteUri(mAccount.getXmppConnection(), mAccount).execute().get(); - } catch (ExecutionException e) { - e.printStackTrace(); - inviteURL = Config.inviteUserURL + user + "/" + domain; - } catch (InterruptedException e) { - e.printStackTrace(); - inviteURL = Config.inviteUserURL + user + "/" + domain; - } - if (inviteURL == null) { - inviteURL = Config.inviteUserURL + user + "/" + domain; - } - Log.d(Config.LOGTAG, "Invite uri = " + inviteURL); - final String inviteText = getString(R.string.InviteText, user); - final Intent intent = new Intent(Intent.ACTION_SEND); - intent.setType("text/plain"); - intent.putExtra(Intent.EXTRA_SUBJECT, user + " " + getString(R.string.inviteUser_Subject) + " " + getString(R.string.app_name)); - intent.putExtra(Intent.EXTRA_TEXT, inviteText + "\n\n" + inviteURL); - startActivity(Intent.createChooser(intent, getString(R.string.invite_contact))); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - }); - builder.setNegativeButton(R.string.cancel, null); - builder.create().show(); - } - } - } - - private boolean selectAccountToStartEasyInvite() { - final List accounts = EasyOnboardingInvite.getSupportingAccounts(this.xmppConnectionService); - if (accounts.size() == 0) { - //This can technically happen if opening the menu item races with accounts reconnecting or something - ToastCompat.makeText(this, R.string.no_active_accounts_support_this, ToastCompat.LENGTH_LONG).show(); - return false; - } else if (accounts.size() == 1) { - openEasyInviteScreen(accounts.get(0)); - } else { - final AtomicReference selectedAccount = new AtomicReference<>(accounts.get(0)); - final androidx.appcompat.app.AlertDialog.Builder alertDialogBuilder = new androidx.appcompat.app.AlertDialog.Builder(this); - alertDialogBuilder.setTitle(R.string.choose_account); - final String[] asStrings = Collections2.transform(accounts, a -> a.getJid().asBareJid().toEscapedString()).toArray(new String[0]); - alertDialogBuilder.setSingleChoiceItems(asStrings, 0, (dialog, which) -> selectedAccount.set(accounts.get(which))); - alertDialogBuilder.setNegativeButton(R.string.cancel, null); - alertDialogBuilder.setPositiveButton(R.string.ok, (dialog, which) -> openEasyInviteScreen(selectedAccount.get())); - alertDialogBuilder.create().show(); - } - return true; - } - - private void openEasyInviteScreen(final Account account) { - EasyOnboardingInviteActivity.launch(account, this); - } - - private class getAdHocInviteUri extends AsyncTask { - - private XmppConnection connection; - private Account account; - - public getAdHocInviteUri(XmppConnection c, Account a) { - this.connection = c; - this.account = a; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - } - - @Override - protected String doInBackground(XmppConnection... params) { - String uri = null; - if (this.connection != null) { - XmppConnection.Features features = this.connection.getFeatures(); - if (features != null && features.adhocinvite) { - int i = 0; - uri = this.connection.getAdHocInviteUrl(Jid.ofDomain(this.account.getJid().getDomain())); - try { - while (uri == null && i++ < 10) { - uri = this.connection.getAdHocInviteUrl(Jid.ofDomain(this.account.getJid().getDomain())); - Thread.sleep(1000); - } - } catch (Exception e) { - e.printStackTrace(); - } finally { - features.adhocinviteURI = null; - } - } - } - return uri; - } - - @Override - protected void onPostExecute(String result) { - super.onPostExecute(result); - } - } - - private void createIssue() { - String IssueURL = Config.ISSUE_URL; - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(IssueURL)); - startActivity(intent); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } - - protected void shareLink(boolean http) { - String uri = getShareableUri(http); - if (uri == null || uri.isEmpty()) { - return; - } - Intent intent = new Intent(Intent.ACTION_SEND); - intent.setType("text/plain"); - intent.putExtra(Intent.EXTRA_TEXT, getShareableUri(http)); - try { - startActivity(Intent.createChooser(intent, getText(R.string.share_uri_with))); - overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } catch (ActivityNotFoundException e) { - ToastCompat.makeText(this, R.string.no_application_to_share_uri, ToastCompat.LENGTH_SHORT).show(); - } - } - - protected void launchOpenKeyChain(long keyId) { - PgpEngine pgp = XmppActivity.this.xmppConnectionService.getPgpEngine(); - try { - startIntentSenderForResult( - pgp.getIntentForKey(keyId).getIntentSender(), 0, null, 0, - 0, 0); - } catch (Throwable e) { - ToastCompat.makeText(XmppActivity.this, R.string.openpgp_error, ToastCompat.LENGTH_SHORT).show(); - } - } - - @Override - public void onResume() { - super.onResume(); - initializeScreenshotSecurity(); - } - - protected int findTheme() { - return ThemeHelper.find(this); - } - - @Override - public void onPause() { - super.onPause(); - hideAvatarPopup(); - } - - @Override - public boolean onMenuOpened(int id, Menu menu) { - if (id == AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR && menu != null) { - MenuDoubleTabUtil.recordMenuOpen(); - } - return super.onMenuOpened(id, menu); - } - - protected void showQrCode() { - showQrCode(getShareableUri()); - } - - protected void showQrCode(final String uri) { - if (uri == null || uri.isEmpty()) { - return; - } - Point size = new Point(); - getWindowManager().getDefaultDisplay().getSize(size); - final int width = (size.x < size.y ? size.x : size.y); - Bitmap bitmap = BarcodeProvider.create2dBarcodeBitmap(uri, width); - ImageView view = new ImageView(this); - view.setBackgroundColor(Color.WHITE); - view.setImageBitmap(bitmap); - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setView(view); - builder.create().show(); - } - - protected Account extractAccount(Intent intent) { - final String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null; - try { - return jid != null ? xmppConnectionService.findAccountByJid(Jid.ofEscaped(jid)) : null; - } catch (IllegalArgumentException e) { - return null; - } - } - - public AvatarService avatarService() { - return xmppConnectionService.getAvatarService(); - } - - public void loadGif(File file, ImageView imageView) { - GifDrawable gifDrawable = null; - try { - gifDrawable = new GifDrawable(file); - } catch (IOException e) { - e.printStackTrace(); - } - imageView.setImageDrawable(gifDrawable); - } - - public void loadBitmap(Message message, ImageView imageView) { - Bitmap bm; - try { - bm = xmppConnectionService.getFileBackend().getThumbnail(message, (int) (metrics.density * 288), true); - } catch (IOException e) { - bm = null; - } - if (bm != null) { - cancelPotentialWork(message, imageView); - imageView.setImageBitmap(bm); - imageView.setBackgroundColor(0x00000000); - } else { - if (cancelPotentialWork(message, imageView)) { - imageView.setBackgroundColor(0xff333333); - imageView.setImageDrawable(null); - final BitmapWorkerTask task = new BitmapWorkerTask(imageView); - final AsyncDrawable asyncDrawable = new AsyncDrawable(getResources(), null, task); - imageView.setImageDrawable(asyncDrawable); - try { - task.execute(message); - } catch (final RejectedExecutionException ignored) { - ignored.printStackTrace(); - } - } - } - } - - protected interface OnValueEdited { - String onValueEdited(String value); - } - - public interface OnPresenceSelected { - void onPresenceSelected(); - } - - public static class ConferenceInvite { - private String uuid; - private List jids = new ArrayList<>(); - - public static ConferenceInvite parse(Intent data) { - ConferenceInvite invite = new ConferenceInvite(); - invite.uuid = data.getStringExtra(ChooseContactActivity.EXTRA_CONVERSATION); - if (invite.uuid == null) { - return null; - } - invite.jids.addAll(ChooseContactActivity.extractJabberIds(data)); - return invite; - } - - public boolean execute(XmppActivity activity) { - XmppConnectionService service = activity.xmppConnectionService; - Conversation conversation = service.findConversationByUuid(this.uuid); - if (conversation == null) { - return false; - } - if (conversation.getMode() == Conversation.MODE_MULTI) { - for (Jid jid : jids) { - service.invite(conversation, jid); - } - return false; - } else { - jids.add(conversation.getJid().asBareJid()); - return service.createAdhocConference(conversation.getAccount(), null, jids, activity.adhocCallback); - } - } - } - - static class BitmapWorkerTask extends AsyncTask { - private final WeakReference imageViewReference; - private Message message = null; - - private BitmapWorkerTask(ImageView imageView) { - this.imageViewReference = new WeakReference<>(imageView); - } - - @Override - protected Bitmap doInBackground(Message... params) { - if (isCancelled()) { - return null; - } - message = params[0]; - try { - final XmppActivity activity = find(imageViewReference); - if (activity != null && activity.xmppConnectionService != null) { - return activity.xmppConnectionService.getFileBackend().getThumbnail(message, (int) (activity.metrics.density * 288), false); - } else { - return null; - } - } catch (IOException e) { - return null; - } - } - - @Override - protected void onPostExecute(final Bitmap bitmap) { - if (!isCancelled()) { - final ImageView imageView = imageViewReference.get(); - if (imageView != null) { - imageView.setImageBitmap(bitmap); - imageView.setBackgroundColor(bitmap == null ? 0xff333333 : 0x00000000); - } - } - } - } - - private static class AsyncDrawable extends BitmapDrawable { - private final WeakReference bitmapWorkerTaskReference; - - AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { - super(res, bitmap); - bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask); - } - - BitmapWorkerTask getBitmapWorkerTask() { - return bitmapWorkerTaskReference.get(); - } - } - - public static XmppActivity find(@NonNull WeakReference viewWeakReference) { - final View view = viewWeakReference.get(); - return view == null ? null : find(view); - } - - public static XmppActivity find(@NonNull final View view) { - Context context = view.getContext(); - while (context instanceof ContextWrapper) { - if (context instanceof XmppActivity) { - return (XmppActivity) context; - } - context = ((ContextWrapper) context).getBaseContext(); - } - return null; - } - - protected boolean installFromUnknownSourceAllowed() { - boolean installFromUnknownSource = false; - final PackageManager packageManager = this.getPackageManager(); - int isUnknownAllowed = 0; - if (Build.VERSION.SDK_INT >= 26) { - /* - * On Android 8 with applications targeting lower versions, - * it's impossible to check unknown sources enabled: using old APIs will always return true - * and using the new one will always return false, - * so in order to avoid a stuck dialog that can't be bypassed we will assume true. - */ - installFromUnknownSource = this.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.O - || packageManager.canRequestPackageInstalls(); - } else if (Build.VERSION.SDK_INT >= 17 && Build.VERSION.SDK_INT < 26) { - try { - isUnknownAllowed = Settings.Global.getInt(this.getApplicationContext().getContentResolver(), Settings.Global.INSTALL_NON_MARKET_APPS); - } catch (Settings.SettingNotFoundException e) { - isUnknownAllowed = 0; - e.printStackTrace(); - } - installFromUnknownSource = isUnknownAllowed == 1; - } else { - try { - isUnknownAllowed = Settings.Secure.getInt(this.getApplicationContext().getContentResolver(), Settings.Secure.INSTALL_NON_MARKET_APPS); - } catch (Settings.SettingNotFoundException e) { - isUnknownAllowed = 0; - e.printStackTrace(); - } - installFromUnknownSource = isUnknownAllowed == 1; - } - Log.d(Config.LOGTAG, "Install from unknown sources for Android SDK " + Build.VERSION.SDK_INT + " allowed: " + installFromUnknownSource); - return installFromUnknownSource; - } - - protected void openInstallFromUnknownSourcesDialogIfNeeded(boolean interactive) { - // Interactive = true --> show toast or dialog - String beInteractive; - if (interactive) { - beInteractive = "true"; - } else { - beInteractive = "false"; - } - if (!installFromUnknownSourceAllowed() && xmppConnectionService.installedFrom() == null) { - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.install_from_unknown_sources_disabled); - builder.setMessage(R.string.install_from_unknown_sources_disabled_dialog); - builder.setPositiveButton(R.string.next, (dialog, which) -> { - Intent intent; - if (android.os.Build.VERSION.SDK_INT >= 26) { - intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES); - Uri uri = Uri.parse("package:" + getPackageName()); - intent.setData(uri); - } else { - intent = new Intent(Settings.ACTION_SECURITY_SETTINGS); - } - Log.d(Config.LOGTAG, "Allow install from unknown sources for Android SDK " + Build.VERSION.SDK_INT + " intent " + intent.toString()); - try { - startActivityForResult(intent, REQUEST_UNKNOWN_SOURCE_OP); - } catch (ActivityNotFoundException e) { - ToastCompat.makeText(XmppActivity.this, R.string.device_does_not_support_unknown_source_op, ToastCompat.LENGTH_SHORT).show(); - } finally { - UpdateService task = new UpdateService(this, xmppConnectionService.installedFrom(), xmppConnectionService); - task.executeOnExecutor(UpdateService.THREAD_POOL_EXECUTOR, beInteractive); - Log.d(Config.LOGTAG, "AppUpdater started"); - } - }); - builder.create().show(); - } else { - UpdateService task = new UpdateService(this, xmppConnectionService.installedFrom(), xmppConnectionService); - task.executeOnExecutor(UpdateService.THREAD_POOL_EXECUTOR, beInteractive); - Log.d(Config.LOGTAG, "AppUpdater started"); - } - } - - public void ShowAvatarPopup(final Activity activity, final AvatarService.Avatarable user) { - final AlertDialog.Builder builder = new AlertDialog.Builder(activity); - AvatarPopup = builder.create(); - final LayoutInflater inflater = getLayoutInflater(); - final View dialogLayout = inflater.inflate(R.layout.avatar_dialog, null); - AvatarPopup.setView(dialogLayout); - AvatarPopup.requestWindowFeature(Window.FEATURE_NO_TITLE); - final ImageView image = (ImageView) dialogLayout.findViewById(R.id.avatar); - AvatarWorkerTask.loadAvatar(user, image, R.dimen.avatar_big); - AvatarPopup.setOnShowListener((DialogInterface.OnShowListener) d -> { - int imageWidthInPX = 0; - if (image != null) { - imageWidthInPX = Math.round(image.getWidth()); - AvatarPopup.getWindow().setLayout(imageWidthInPX, imageWidthInPX); - } - }); - AvatarPopup.show(); - } - - private void hideAvatarPopup() { - if (AvatarPopup != null && AvatarPopup.isShowing()) { - AvatarPopup.cancel(); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/XmppFragment.java b/src/main/java/eu/siacs/conversations/ui/XmppFragment.java deleted file mode 100644 index 403b7310e..000000000 --- a/src/main/java/eu/siacs/conversations/ui/XmppFragment.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui; - -import android.app.Activity; -import android.app.Fragment; - -import eu.siacs.conversations.ui.interfaces.OnBackendConnected; - -public abstract class XmppFragment extends Fragment implements OnBackendConnected { - - abstract void refresh(); - - protected void runOnUiThread(Runnable runnable) { - final Activity activity = getActivity(); - if (activity != null) { - activity.runOnUiThread(runnable); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java deleted file mode 100644 index 2ac733ebe..000000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java +++ /dev/null @@ -1,97 +0,0 @@ -package eu.siacs.conversations.ui.adapter; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; - -import androidx.annotation.NonNull; -import androidx.databinding.DataBindingUtil; - -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.AccountRowBinding; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.ui.util.AvatarWorkerTask; -import eu.siacs.conversations.ui.util.StyledAttributes; - -public class AccountAdapter extends ArrayAdapter { - - private XmppActivity activity; - private boolean showStateButton; - - public AccountAdapter(XmppActivity activity, List objects, boolean showStateButton) { - super(activity, 0, objects); - this.activity = activity; - this.showStateButton = showStateButton; - } - - public AccountAdapter(XmppActivity activity, List objects) { - super(activity, 0, objects); - this.activity = activity; - this.showStateButton = true; - } - - @Override - public View getView(int position, View view, @NonNull ViewGroup parent) { - final Account account = getItem(position); - final ViewHolder viewHolder; - if (view == null) { - AccountRowBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.account_row, parent, false); - view = binding.getRoot(); - viewHolder = new ViewHolder(binding); - view.setTag(viewHolder); - } else { - viewHolder = (ViewHolder) view.getTag(); - } - if (Config.DOMAIN_LOCK != null) { - viewHolder.binding.accountJid.setText(account.getJid().getLocal()); - } else { - viewHolder.binding.accountJid.setText(account.getJid().asBareJid().toEscapedString()); - } - AvatarWorkerTask.loadAvatar(account, viewHolder.binding.accountImage, R.dimen.avatar); - viewHolder.binding.accountStatus.setText(getContext().getString(account.getStatus().getReadableId())); - switch (account.getStatus()) { - case ONLINE: - viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorOnline)); - break; - case DISABLED: - case CONNECTING: - viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, android.R.attr.textColorSecondary)); - break; - default: - viewHolder.binding.accountStatus.setTextColor(StyledAttributes.getColor(activity, R.attr.TextColorError)); - break; - } - final boolean isDisabled = (account.getStatus() == Account.State.DISABLED); - viewHolder.binding.tglAccountStatus.setOnCheckedChangeListener(null); - viewHolder.binding.tglAccountStatus.setChecked(!isDisabled); - if (this.showStateButton) { - viewHolder.binding.tglAccountStatus.setVisibility(View.VISIBLE); - } else { - viewHolder.binding.tglAccountStatus.setVisibility(View.GONE); - } - viewHolder.binding.tglAccountStatus.setOnCheckedChangeListener((compoundButton, b) -> { - if (b == isDisabled && activity instanceof OnTglAccountState) { - ((OnTglAccountState) activity).onClickTglAccountState(account, b); - } - }); - return view; - } - - private static class ViewHolder { - private final AccountRowBinding binding; - - private ViewHolder(AccountRowBinding binding) { - this.binding = binding; - } - } - - - public interface OnTglAccountState { - void onClickTglAccountState(Account account, boolean state); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java deleted file mode 100644 index 56bf42ce9..000000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java +++ /dev/null @@ -1,168 +0,0 @@ -package eu.siacs.conversations.ui.adapter; - -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.os.AsyncTask; -import android.text.format.DateUtils; -import android.util.DisplayMetrics; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; - -import androidx.annotation.NonNull; -import androidx.databinding.DataBindingUtil; -import androidx.recyclerview.widget.RecyclerView; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.RejectedExecutionException; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.AccountRowBinding; -import eu.siacs.conversations.services.AvatarService; -import eu.siacs.conversations.services.ImportBackupService; -import eu.siacs.conversations.utils.BackupFileHeader; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xmpp.Jid; - -public class BackupFileAdapter extends RecyclerView.Adapter { - - private OnItemClickedListener listener; - - private final List files = new ArrayList<>(); - - - @NonNull - @Override - public BackupFileViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { - return new BackupFileViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.account_row, viewGroup, false)); - } - - @Override - public void onBindViewHolder(@NonNull BackupFileViewHolder backupFileViewHolder, int position) { - final ImportBackupService.BackupFile backupFile = files.get(position); - final BackupFileHeader header = backupFile.getHeader(); - backupFileViewHolder.binding.accountJid.setText(header.getJid().asBareJid().toString()); - backupFileViewHolder.binding.accountStatus.setText(String.format("%s · %s", header.getApp(), DateUtils.formatDateTime(backupFileViewHolder.binding.getRoot().getContext(), header.getTimestamp(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_TIME))); - backupFileViewHolder.binding.tglAccountStatus.setVisibility(View.GONE); - backupFileViewHolder.binding.getRoot().setOnClickListener(v -> { - if (listener != null) { - listener.onClick(backupFile); - } - }); - loadAvatar(header.getJid(), backupFileViewHolder.binding.accountImage); - } - - @Override - public int getItemCount() { - return files.size(); - } - - public void setFiles(List files) { - this.files.clear(); - this.files.addAll(files); - notifyDataSetChanged(); - } - - public void setOnItemClickedListener(OnItemClickedListener listener) { - this.listener = listener; - } - - static class BackupFileViewHolder extends RecyclerView.ViewHolder { - private final AccountRowBinding binding; - - BackupFileViewHolder(AccountRowBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } - - public interface OnItemClickedListener { - void onClick(ImportBackupService.BackupFile backupFile); - } - - static class BitmapWorkerTask extends AsyncTask { - private final WeakReference imageViewReference; - private Jid jid = null; - private final int size; - - BitmapWorkerTask(ImageView imageView) { - imageViewReference = new WeakReference<>(imageView); - DisplayMetrics metrics = imageView.getContext().getResources().getDisplayMetrics(); - this.size = ((int) (48 * metrics.density)); - } - - @Override - protected Bitmap doInBackground(Jid... params) { - this.jid = params[0]; - return AvatarService.get(this.jid, size); - } - - @Override - protected void onPostExecute(Bitmap bitmap) { - if (bitmap != null && !isCancelled()) { - final ImageView imageView = imageViewReference.get(); - if (imageView != null) { - imageView.setImageBitmap(bitmap); - imageView.setBackgroundColor(0x00000000); - } - } - } - } - - private void loadAvatar(Jid jid, ImageView imageView) { - if (cancelPotentialWork(jid, imageView)) { - imageView.setBackgroundColor(UIHelper.getColorForName(jid.asBareJid().toString())); - imageView.setImageDrawable(null); - final BitmapWorkerTask task = new BitmapWorkerTask(imageView); - final AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getContext().getResources(), null, task); - imageView.setImageDrawable(asyncDrawable); - try { - task.execute(jid); - } catch (final RejectedExecutionException ignored) { - } - } - } - - private static boolean cancelPotentialWork(Jid jid, ImageView imageView) { - final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); - - if (bitmapWorkerTask != null) { - final Jid oldJid = bitmapWorkerTask.jid; - if (oldJid == null || jid != oldJid) { - bitmapWorkerTask.cancel(true); - } else { - return false; - } - } - return true; - } - - private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { - if (imageView != null) { - final Drawable drawable = imageView.getDrawable(); - if (drawable instanceof AsyncDrawable) { - final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; - return asyncDrawable.getBitmapWorkerTask(); - } - } - return null; - } - - static class AsyncDrawable extends BitmapDrawable { - private final WeakReference bitmapWorkerTaskReference; - - AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { - super(res, bitmap); - bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask); - } - - BitmapWorkerTask getBitmapWorkerTask() { - return bitmapWorkerTaskReference.get(); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ChannelSearchResultAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ChannelSearchResultAdapter.java deleted file mode 100644 index b8a30e9a0..000000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ChannelSearchResultAdapter.java +++ /dev/null @@ -1,124 +0,0 @@ -package eu.siacs.conversations.ui.adapter; - -import static eu.siacs.conversations.services.ChannelDiscoveryService.Method.JABBER_NETWORK; - -import android.app.Activity; -import android.text.TextUtils; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.databinding.DataBindingUtil; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.ListAdapter; -import androidx.recyclerview.widget.RecyclerView; - -import java.text.MessageFormat; -import java.util.Locale; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.SearchResultItemBinding; -import eu.siacs.conversations.entities.Room; -import eu.siacs.conversations.ui.ChannelDiscoveryActivity; -import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.ui.util.AvatarWorkerTask; -import eu.siacs.conversations.xmpp.Jid; - - -public class ChannelSearchResultAdapter extends ListAdapter implements View.OnCreateContextMenuListener { - - private XmppActivity activity; - - private static final DiffUtil.ItemCallback DIFF = new DiffUtil.ItemCallback() { - @Override - public boolean areItemsTheSame(@NonNull Room a, @NonNull Room b) { - return a.address != null && a.address.equals(b.address); - } - - @Override - public boolean areContentsTheSame(@NonNull Room a, @NonNull Room b) { - return a.equals(b); - } - }; - private OnChannelSearchResultSelected listener; - private Room current; - - public ChannelSearchResultAdapter(XmppActivity activity) { - super(DIFF); - this.activity = activity; - } - - @NonNull - @Override - public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { - return new ViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.search_result_item, viewGroup, false)); - } - - @Override - public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) { - final Room searchResult = getItem(position); - final String user = '[' + String.valueOf(searchResult.nusers) + ']'; - viewHolder.binding.name.setText(MessageFormat.format("{0} {1}", searchResult.getName(), user)); - final String description = searchResult.getDescription(); - final String language = searchResult.getLanguage(); - if (TextUtils.isEmpty(description)) { - viewHolder.binding.description.setVisibility(View.GONE); - } else { - viewHolder.binding.description.setText(description); - viewHolder.binding.description.setVisibility(View.VISIBLE); - } - if (language == null || language.length() != 2) { - viewHolder.binding.language.setVisibility(View.GONE); - } else { - viewHolder.binding.language.setText(language.toUpperCase(Locale.ENGLISH)); - viewHolder.binding.language.setVisibility(View.VISIBLE); - } - final Jid room = searchResult.getRoom(); - viewHolder.binding.room.setText(room != null ? room.asBareJid().toString() : ""); - String roomJID; - if (room != null) { - roomJID = ChannelDiscoveryActivity.getMethod(activity.xmppConnectionService) == JABBER_NETWORK ? room.toString() : null; - } else { - roomJID = null; - } - AvatarWorkerTask.loadAvatar(roomJID, searchResult, viewHolder.binding.avatar, R.dimen.avatar); - final View root = viewHolder.binding.getRoot(); - root.setTag(searchResult); - root.setOnClickListener(v -> listener.onChannelSearchResult(searchResult)); - root.setOnCreateContextMenuListener(this); - } - - public void setOnChannelSearchResultSelectedListener(OnChannelSearchResultSelected listener) { - this.listener = listener; - } - - public Room getCurrent() { - return this.current; - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - final Activity activity = XmppActivity.find(v); - final Object tag = v.getTag(); - if (activity != null && tag instanceof Room) { - activity.getMenuInflater().inflate(R.menu.channel_item_context, menu); - this.current = (Room) tag; - } - } - - public interface OnChannelSearchResultSelected { - void onChannelSearchResult(Room result); - } - - public static class ViewHolder extends RecyclerView.ViewHolder { - - private final SearchResultItemBinding binding; - - private ViewHolder(SearchResultItemBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java deleted file mode 100644 index 15d8751cd..000000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ /dev/null @@ -1,479 +0,0 @@ -package eu.siacs.conversations.ui.adapter; - -import static eu.siacs.conversations.ui.util.MyLinkify.replaceYoutube; - -import android.content.SharedPreferences; -import android.graphics.Typeface; -import android.preference.PreferenceManager; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.SpannableStringBuilder; -import android.text.style.StyleSpan; -import android.util.Pair; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; -import androidx.databinding.DataBindingUtil; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.common.base.Optional; -import com.google.common.base.Strings; - -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ConversationListRowBinding; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Conversational; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.services.AttachFileToConversationRunnable; -import eu.siacs.conversations.ui.ConversationFragment; -import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.ui.util.AvatarWorkerTask; -import eu.siacs.conversations.ui.util.StyledAttributes; -import eu.siacs.conversations.utils.IrregularUnicodeDetector; -import eu.siacs.conversations.utils.MimeUtils; -import eu.siacs.conversations.utils.StylingHelper; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.chatstate.ChatState; -import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; - -public class ConversationAdapter - extends RecyclerView.Adapter { - - private static final float INACTIVE_ALPHA = 0.4684f; - private static final float ACTIVE_ALPHA = 1.0f; - private XmppActivity activity; - private List conversations; - private OnConversationClickListener listener; - private boolean hasInternetConnection = false; - - public ConversationAdapter(XmppActivity activity, List conversations) { - this.activity = activity; - this.conversations = conversations; - } - - @NonNull - @Override - public ConversationViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return new ConversationViewHolder( - DataBindingUtil.inflate( - LayoutInflater.from(parent.getContext()), - R.layout.conversation_list_row, - parent, - false)); - } - - @Override - public void onBindViewHolder(@NonNull ConversationViewHolder viewHolder, int position) { - Conversation conversation = conversations.get(position); - if (conversation == null) { - return; - } - String UUID = conversation.getUuid(); - CharSequence name = conversation.getName(); - hasInternetConnection = activity.xmppConnectionService.hasInternetConnection(); - if (name instanceof Jid) { - viewHolder.binding.conversationName.setText( - IrregularUnicodeDetector.style(activity, (Jid) name)); - } else { - viewHolder.binding.conversationName.setText(name); - } - - if (activity.xmppConnectionService.multipleAccounts() && activity.xmppConnectionService.showOwnAccounts()) { - viewHolder.binding.account.setVisibility(View.VISIBLE); - viewHolder.binding.account.setText(conversation.getAccount().getJid().asBareJid()); - } else { - viewHolder.binding.account.setVisibility(View.GONE); - } - - if (conversation == ConversationFragment.getConversation(activity)) { - viewHolder.binding.frame.setBackgroundColor( - StyledAttributes.getColor(activity, R.attr.color_background_tertiary)); - } else { - viewHolder.binding.frame.setBackgroundColor( - StyledAttributes.getColor(activity, R.attr.color_background_secondary)); - } - - final Message message = conversation.getLatestMessage(); - final int failedCount = conversation.failedCount(); - final int unreadCount = conversation.unreadCount(); - final boolean isRead = conversation.isRead(); - final Conversation.Draft draft = isRead ? conversation.getDraft() : null; - - viewHolder.binding.indicatorReceived.setVisibility(View.GONE); - viewHolder.binding.unreadCount.setVisibility(View.GONE); - viewHolder.binding.failedCount.setVisibility(View.GONE); - - if (isRead) { - viewHolder.binding.conversationName.setTypeface(null, Typeface.NORMAL); - } else { - viewHolder.binding.conversationName.setTypeface(null, Typeface.BOLD); - } - - if (unreadCount > 0) { - viewHolder.binding.unreadCount.setVisibility(View.VISIBLE); - viewHolder.binding.unreadCount.setUnreadCount(unreadCount); - } else { - viewHolder.binding.unreadCount.setVisibility(View.GONE); - } - if (failedCount > 0) { - viewHolder.binding.failedCount.setVisibility(View.VISIBLE); - viewHolder.binding.failedCount.setFailedCount(failedCount); - } else { - viewHolder.binding.failedCount.setVisibility(View.GONE); - } - - if (draft != null) { - viewHolder.binding.conversationLastmsgImg.setVisibility(View.GONE); - viewHolder.binding.conversationLastmsg.setText(draft.getMessage()); - viewHolder.binding.senderName.setText(R.string.draft); - viewHolder.binding.senderName.setVisibility(View.VISIBLE); - viewHolder.binding.conversationLastmsg.setTypeface(null, Typeface.NORMAL); - viewHolder.binding.senderName.setTypeface(null, Typeface.ITALIC); - } else if (conversation.getMode() == Conversation.MODE_SINGLE && conversation.getIncomingChatState().equals(ChatState.COMPOSING)) { - viewHolder.binding.conversationLastmsg.setText(R.string.is_typing); - viewHolder.binding.conversationLastmsg.setTypeface(null, Typeface.BOLD_ITALIC); - viewHolder.binding.conversationLastmsgImg.setVisibility(View.GONE); - viewHolder.binding.senderName.setVisibility(View.GONE); - } else if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getMucOptions().getUsersWithChatState(ChatState.COMPOSING, 5).size() != 0) { - ChatState state = ChatState.COMPOSING; - List userWithChatStates = conversation.getMucOptions().getUsersWithChatState(state, 5); - if (userWithChatStates.size() == 0) { - state = ChatState.PAUSED; - userWithChatStates = conversation.getMucOptions().getUsersWithChatState(state, 5); - } - if (state == ChatState.COMPOSING) { - viewHolder.binding.senderName.setVisibility(View.GONE); - viewHolder.binding.conversationLastmsgImg.setVisibility(View.GONE); - if (userWithChatStates.size() > 0) { - if (userWithChatStates.size() == 1) { - MucOptions.User user = userWithChatStates.get(0); - viewHolder.binding.conversationLastmsg.setText(activity.getString(R.string.contact_is_typing, UIHelper.getDisplayName(user))); - viewHolder.binding.conversationLastmsg.setTypeface(null, Typeface.BOLD_ITALIC); - } else { - StringBuilder builder = new StringBuilder(); - for (MucOptions.User user : userWithChatStates) { - if (builder.length() != 0) { - builder.append(", "); - } - builder.append(UIHelper.getDisplayName(user)); - } - viewHolder.binding.conversationLastmsg.setText(activity.getString(R.string.contacts_are_typing, builder.toString())); - viewHolder.binding.conversationLastmsg.setTypeface(null, Typeface.BOLD_ITALIC); - } - } - } - } else if (UUID.equalsIgnoreCase(AttachFileToConversationRunnable.isCompressingVideo[0])) { - viewHolder.binding.conversationLastmsgImg.setVisibility(View.GONE); - viewHolder.binding.conversationLastmsg.setText(activity.getString(R.string.transcoding_video_x, AttachFileToConversationRunnable.isCompressingVideo[1])); - viewHolder.binding.conversationLastmsg.setTypeface(null, Typeface.ITALIC); - viewHolder.binding.senderName.setVisibility(View.GONE); - } else { - final boolean fileAvailable = !message.isFileDeleted(); - final boolean showPreviewText; - if (fileAvailable - && (message.isFileOrImage() - || message.treatAsDownloadable() - || message.isGeoUri())) { - final int imageResource; - if (message.isGeoUri()) { - imageResource = - activity.getThemeResource( - R.attr.ic_attach_location, R.drawable.ic_attach_location); - showPreviewText = false; - } else { - // TODO move this into static MediaPreview method and use same icons as in - // MediaAdapter - final String mime = message.getMimeType(); - if (MimeUtils.AMBIGUOUS_CONTAINER_FORMATS.contains(mime)) { - final Message.FileParams fileParams = message.getFileParams(); - if (fileParams.width > 0 && fileParams.height > 0) { - imageResource = - activity.getThemeResource( - R.attr.ic_attach_videocam, - R.drawable.ic_attach_videocam); - showPreviewText = false; - } else if (fileParams.runtime > 0) { - imageResource = - activity.getThemeResource( - R.attr.ic_attach_record, R.drawable.ic_attach_record); - showPreviewText = false; - } else { - imageResource = - activity.getThemeResource( - R.attr.ic_attach_document, - R.drawable.ic_attach_document); - showPreviewText = true; - } - } else { - switch (Strings.nullToEmpty(mime).split("/")[0]) { - case "image": - imageResource = - activity.getThemeResource( - R.attr.ic_attach_photo, R.drawable.ic_attach_photo); - showPreviewText = false; - break; - case "video": - imageResource = - activity.getThemeResource( - R.attr.ic_attach_video, - R.drawable.ic_attach_video); - showPreviewText = false; - break; - case "audio": - imageResource = - activity.getThemeResource( - R.attr.ic_attach_record, - R.drawable.ic_attach_record); - showPreviewText = false; - break; - default: - imageResource = - activity.getThemeResource( - R.attr.ic_attach_document, - R.drawable.ic_attach_document); - showPreviewText = true; - break; - } - } - } - viewHolder.binding.conversationLastmsgImg.setImageResource(imageResource); - viewHolder.binding.conversationLastmsgImg.setVisibility(View.VISIBLE); - } else { - viewHolder.binding.conversationLastmsgImg.setVisibility(View.GONE); - showPreviewText = true; - } - final Pair preview = - UIHelper.getMessagePreview( - activity, - message, - viewHolder.binding.conversationLastmsg.getCurrentTextColor()); - if (showPreviewText) { - if (message.hasDeletedBody()) { - viewHolder.binding.conversationLastmsg.setText(UIHelper.shorten(activity.getString(R.string.message_deleted))); - } else { - SpannableStringBuilder body = new SpannableStringBuilder(replaceYoutube(activity.getApplicationContext(), preview.first.toString())); - StylingHelper.format(body, viewHolder.binding.conversationLastmsg.getCurrentTextColor(), true); - viewHolder.binding.conversationLastmsg.setText(UIHelper.shorten(body)); - } - } else { - viewHolder.binding.conversationLastmsgImg.setContentDescription(preview.first); - } - viewHolder.binding.conversationLastmsg.setVisibility( - showPreviewText ? View.VISIBLE : View.GONE); - if (preview.second) { - if (isRead) { - viewHolder.binding.conversationLastmsg.setTypeface(null, Typeface.ITALIC); - viewHolder.binding.senderName.setTypeface(null, Typeface.NORMAL); - } else { - viewHolder.binding.conversationLastmsg.setTypeface(null, Typeface.BOLD_ITALIC); - viewHolder.binding.senderName.setTypeface(null, Typeface.BOLD); - } - } else { - if (isRead) { - viewHolder.binding.conversationLastmsg.setTypeface(null, Typeface.NORMAL); - viewHolder.binding.senderName.setTypeface(null, Typeface.NORMAL); - } else { - viewHolder.binding.conversationLastmsg.setTypeface(null, Typeface.BOLD); - viewHolder.binding.senderName.setTypeface(null, Typeface.BOLD); - } - } - if (message.getStatus() == Message.STATUS_RECEIVED) { - if (conversation.getMode() == Conversation.MODE_MULTI) { - viewHolder.binding.senderName.setVisibility(View.VISIBLE); - viewHolder.binding.senderName.setText( - UIHelper.getColoredUsername(activity.xmppConnectionService, message)); - viewHolder.binding.senderName.append(":"); - } else { - viewHolder.binding.senderName.setVisibility(View.GONE); - } - } else if (message.getType() != Message.TYPE_STATUS) { - viewHolder.binding.senderName.setVisibility(View.VISIBLE); - final SpannableString me; - me = SpannableString.valueOf(activity.getString(R.string.me)); - me.setSpan(new StyleSpan(Typeface.BOLD), 0, me.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - viewHolder.binding.senderName.setText(me); - viewHolder.binding.senderName.append(":"); - } else { - viewHolder.binding.senderName.setVisibility(View.GONE); - } - } - - final Optional ongoingCall; - if (conversation.getMode() == Conversational.MODE_MULTI) { - ongoingCall = Optional.absent(); - } else { - ongoingCall = - activity.xmppConnectionService - .getJingleConnectionManager() - .getOngoingRtpConnection(conversation.getContact()); - } - - if (ongoingCall.isPresent()) { - viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); - final int ic_ongoing_call = - activity.getThemeResource( - R.attr.ic_ongoing_call_hint, R.drawable.ic_phone_in_talk_black_18dp); - viewHolder.binding.notificationStatus.setImageResource(ic_ongoing_call); - } else { - final long muted_till = - conversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0); - if (muted_till == Long.MAX_VALUE) { - viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); - int ic_notifications_off = - activity.getThemeResource( - R.attr.icon_notifications_off, - R.drawable.ic_notifications_off_black_24dp); - viewHolder.binding.notificationStatus.setImageResource(ic_notifications_off); - } else if (muted_till >= System.currentTimeMillis()) { - viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); - int ic_notifications_paused = - activity.getThemeResource( - R.attr.icon_notifications_paused, - R.drawable.ic_notifications_paused_black_24dp); - viewHolder.binding.notificationStatus.setImageResource(ic_notifications_paused); - } else if (conversation.alwaysNotify()) { - viewHolder.binding.notificationStatus.setVisibility(View.GONE); - } else { - viewHolder.binding.notificationStatus.setVisibility(View.VISIBLE); - int ic_notifications_none = - activity.getThemeResource( - R.attr.icon_notifications_none, - R.drawable.ic_notifications_none_black_24dp); - viewHolder.binding.notificationStatus.setImageResource(ic_notifications_none); - } - } - - long timestamp; - if (draft != null) { - timestamp = draft.getTimestamp(); - } else { - timestamp = message.getTimeSent(); - } - final boolean isAccountDisabled = !conversation.getAccount().isEnabled(); - final boolean isPinned = conversation.getBooleanAttribute(Conversation.ATTRIBUTE_PINNED_ON_TOP,false); - if (isPinned) { - viewHolder.binding.chat.setBackgroundColor(StyledAttributes.getColor(this.activity, R.attr.colorAccentLight)); - viewHolder.binding.chat.setAlpha(ACTIVE_ALPHA); - if (isAccountDisabled) { - viewHolder.binding.chat.setBackgroundColor(StyledAttributes.getColor(this.activity, R.attr.colorAccentLightDisabled)); - viewHolder.binding.chat.setAlpha(INACTIVE_ALPHA); - } - } else { - viewHolder.binding.chat.setBackgroundColor(0); - viewHolder.binding.chat.setAlpha(ACTIVE_ALPHA); - if (isAccountDisabled) { - viewHolder.binding.chat.setBackgroundColor(StyledAttributes.getColor(this.activity, R.attr.colorAccentLightDisabled)); - viewHolder.binding.chat.setAlpha(INACTIVE_ALPHA); - } - } - viewHolder.binding.pinnedOnTop.setVisibility(isPinned ? View.VISIBLE - : View.GONE); - viewHolder.binding.conversationLastupdate.setText( - UIHelper.readableTimeDifference(activity, timestamp)); - AvatarWorkerTask.loadAvatar( - conversation, - viewHolder.binding.conversationImage, - R.dimen.avatar_on_conversation_overview); - if (conversation.getMode() == Conversational.MODE_SINGLE && conversation.getContact().isActive()) { - viewHolder.binding.userActiveIndicator.setVisibility(View.VISIBLE); - } else { - viewHolder.binding.userActiveIndicator.setVisibility(View.GONE); - } - viewHolder.itemView.setOnClickListener(v -> listener.onConversationClick(v, conversation)); - - if (conversation.getMode() == Conversation.MODE_SINGLE && ShowPresenceColoredNames()) { - if (hasInternetConnection) { - switch (conversation.getContact().getPresences().getShownStatus()) { - case CHAT: - case ONLINE: - viewHolder.binding.conversationName.setTextColor(ContextCompat.getColor(activity, R.color.online)); - break; - case AWAY: - viewHolder.binding.conversationName.setTextColor(ContextCompat.getColor(activity, R.color.away)); - break; - case XA: - case DND: - viewHolder.binding.conversationName.setTextColor(ContextCompat.getColor(activity, R.color.notavailable)); - break; - case OFFLINE: - default: - viewHolder.binding.conversationName.setTextColor(StyledAttributes.getColor(activity, R.attr.text_Color_Main)); - break; - } - } else { - viewHolder.binding.conversationName.setTextColor(StyledAttributes.getColor(activity, R.attr.text_Color_Main)); - } - } else { - viewHolder.binding.conversationName.setTextColor(StyledAttributes.getColor(activity, R.attr.text_Color_Main)); - } - if (activity.xmppConnectionService.indicateReceived()) { - switch (message.getMergedStatus()) { - case Message.STATUS_SEND_RECEIVED: - if (viewHolder.binding.indicatorReceived != null) { - viewHolder.binding.indicatorReceived.setVisibility(View.VISIBLE); - viewHolder.binding.indicatorReceived.setImageResource(activity.isDarkTheme() ? R.drawable.ic_check_white_18dp : R.drawable.ic_check_black_18dp); - viewHolder.binding.indicatorReceived.setAlpha(activity.isDarkTheme() ? 0.7f : 0.57f); - } - break; - case Message.STATUS_SEND_DISPLAYED: - if (viewHolder.binding.indicatorReceived != null) { - viewHolder.binding.indicatorReceived.setVisibility(View.VISIBLE); - viewHolder.binding.indicatorReceived.setImageResource(activity.isDarkTheme() ? R.drawable.ic_check_all_white_18dp : R.drawable.ic_check_all_black_18dp); - viewHolder.binding.indicatorReceived.setAlpha(activity.isDarkTheme() ? 0.7f : 0.57f); - } - break; - default: - viewHolder.binding.indicatorReceived.setVisibility(View.GONE); - } - } - } - - - @Override - public int getItemCount() { - return conversations.size(); - } - - public void setConversationClickListener(OnConversationClickListener listener) { - this.listener = listener; - } - - public void insert(Conversation c, int position) { - conversations.add(position, c); - notifyDataSetChanged(); - } - - public void remove(Conversation conversation, int position) { - conversations.remove(conversation); - notifyItemRemoved(position); - } - - public interface OnConversationClickListener { - void onConversationClick(View view, Conversation conversation); - } - - static class ConversationViewHolder extends RecyclerView.ViewHolder { - private final ConversationListRowBinding binding; - - private ConversationViewHolder(ConversationListRowBinding binding) { - super(binding.getRoot()); - this.binding = binding; - binding.getRoot().setLongClickable(true); - } - } - - private boolean ShowPresenceColoredNames() { - return getPreferences().getBoolean("presence_colored_names", activity.getResources().getBoolean(R.bool.presence_colored_names)); - } - - protected SharedPreferences getPreferences() { - return PreferenceManager.getDefaultSharedPreferences(activity.getApplicationContext()); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java deleted file mode 100644 index 4b44c5296..000000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java +++ /dev/null @@ -1,88 +0,0 @@ -package eu.siacs.conversations.ui.adapter; - -import android.content.Context; -import android.widget.ArrayAdapter; -import android.widget.Filter; - -import androidx.annotation.NonNull; - -import java.util.Collections; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Locale; -import java.util.regex.Pattern; - -import eu.siacs.conversations.Config; - -public class KnownHostsAdapter extends ArrayAdapter { - private static final Pattern E164_PATTERN = Pattern.compile("^\\+[1-9]\\d{1,14}$"); - private ArrayList domains; - private final Filter domainFilter = new Filter() { - - @Override - protected FilterResults performFiltering(CharSequence constraint) { - final ArrayList suggestions = new ArrayList<>(); - final String[] split = constraint == null ? new String[0] : constraint.toString().split("@"); - if (split.length == 1) { - final String local = split[0].toLowerCase(Locale.ENGLISH); - if (Config.QUICKSY_DOMAIN != null && E164_PATTERN.matcher(local).matches()) { - suggestions.add(local + '@' + Config.QUICKSY_DOMAIN.toEscapedString()); - } else { - for (String domain : domains) { - suggestions.add(local + '@' + domain); - } - } - } else if (split.length == 2) { - final String localPart = split[0].toLowerCase(Locale.ENGLISH); - final String domainPart = split[1].toLowerCase(Locale.ENGLISH); - if (domains.contains(domainPart)) { - return new FilterResults(); - } - for (String domain : domains) { - if (domain.contains(domainPart)) { - suggestions.add(localPart + "@" + domain); - } - } - } else { - return new FilterResults(); - } - FilterResults filterResults = new FilterResults(); - filterResults.values = suggestions; - filterResults.count = suggestions.size(); - return filterResults; - } - - @Override - protected void publishResults(CharSequence constraint, FilterResults results) { - ArrayList filteredList = (ArrayList) results.values; - if (results.count > 0) { - clear(); - addAll(filteredList); - notifyDataSetChanged(); - } - } - }; - - public KnownHostsAdapter(Context context, int viewResourceId, Collection mKnownHosts) { - super(context, viewResourceId, new ArrayList<>()); - domains = new ArrayList<>(mKnownHosts); - Collections.sort(domains); - } - - public KnownHostsAdapter(Context context, int viewResourceId) { - super(context, viewResourceId, new ArrayList<>()); - domains = new ArrayList<>(); - } - - public void refresh(Collection knownHosts) { - domains = new ArrayList<>(knownHosts); - Collections.sort(domains); - notifyDataSetChanged(); - } - - @Override - @NonNull - public Filter getFilter() { - return domainFilter; - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java deleted file mode 100644 index 8084ac6e9..000000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java +++ /dev/null @@ -1,164 +0,0 @@ -package eu.siacs.conversations.ui.adapter; - -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.databinding.DataBindingUtil; - -import com.wefika.flowlayout.FlowLayout; - -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.ContactBinding; -import eu.siacs.conversations.entities.ListItem; -import eu.siacs.conversations.ui.SettingsActivity; -import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.ui.util.AvatarWorkerTask; -import eu.siacs.conversations.ui.util.StyledAttributes; -import eu.siacs.conversations.utils.IrregularUnicodeDetector; -import eu.siacs.conversations.xmpp.Jid; - -public class ListItemAdapter extends ArrayAdapter { - - private static final float INACTIVE_ALPHA = 0.4684f; - private static final float ACTIVE_ALPHA = 1.0f; - protected XmppActivity activity; - private boolean showDynamicTags = false; - private boolean showPresenceColoredNames = false; - private OnTagClickedListener mOnTagClickedListener = null; - protected int color = 0; - protected boolean offline = false; - private View.OnClickListener onTagTvClick = new View.OnClickListener() { - @Override - public void onClick(View view) { - if (view instanceof TextView && mOnTagClickedListener != null) { - TextView tv = (TextView) view; - final String tag = tv.getText().toString(); - mOnTagClickedListener.onTagClicked(tag); - } - } - }; - - public ListItemAdapter(XmppActivity activity, List objects) { - super(activity, 0, objects); - this.activity = activity; - } - - public void refreshSettings() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); - this.showDynamicTags = preferences.getBoolean(SettingsActivity.SHOW_DYNAMIC_TAGS, activity.getResources().getBoolean(R.bool.show_dynamic_tags)); - this.showPresenceColoredNames = preferences.getBoolean("presence_colored_names", activity.getResources().getBoolean(R.bool.presence_colored_names)); - } - - @Override - public View getView(int position, View view, ViewGroup parent) { - LayoutInflater inflater = activity.getLayoutInflater(); - ListItem item = getItem(position); - ViewHolder viewHolder; - if (view == null) { - ContactBinding binding = DataBindingUtil.inflate(inflater, R.layout.contact, parent, false); - viewHolder = ViewHolder.get(binding); - view = binding.getRoot(); - } else { - viewHolder = (ViewHolder) view.getTag(); - } - view.setBackground(StyledAttributes.getDrawable(view.getContext(), R.attr.list_item_background)); - List tags = item.getTags(activity); - if (tags.size() == 0 || !this.showDynamicTags) { - viewHolder.tags.setVisibility(View.GONE); - } else { - viewHolder.tags.setVisibility(View.VISIBLE); - viewHolder.tags.removeAllViewsInLayout(); - for (ListItem.Tag tag : tags) { - TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag, viewHolder.tags, false); - tv.setText(tag.getName()); - tv.setBackgroundColor(tag.getColor()); - tv.setOnClickListener(this.onTagTvClick); - viewHolder.tags.addView(tv); - } - } - final Jid jid = item.getJid(); - if (jid != null) { - viewHolder.jid.setVisibility(View.VISIBLE); - viewHolder.jid.setText(IrregularUnicodeDetector.style(activity, jid)); - } else { - viewHolder.jid.setVisibility(View.GONE); - } - if (activity.xmppConnectionService.multipleAccounts() && activity.xmppConnectionService.showOwnAccounts()) { - viewHolder.account.setVisibility(View.VISIBLE); - viewHolder.account.setText(item.getAccount().getJid().asBareJid()); - } else { - viewHolder.account.setVisibility(View.GONE); - } - viewHolder.name.setText(item.getDisplayName()); - if (tags.size() != 0) { - for (ListItem.Tag tag : tags) { - offline = tag.getOffline() == 1; - color = tag.getColor(); - } - } - if (offline || !activity.xmppConnectionService.hasInternetConnection()) { - viewHolder.name.setTextColor(StyledAttributes.getColor(activity, R.attr.text_Color_Main)); - viewHolder.name.setAlpha(INACTIVE_ALPHA); - viewHolder.jid.setAlpha(INACTIVE_ALPHA); - viewHolder.avatar.setAlpha(INACTIVE_ALPHA); - viewHolder.tags.setAlpha(INACTIVE_ALPHA); - } else { - if (showPresenceColoredNames) { - viewHolder.name.setTextColor(color != 0 ? color : StyledAttributes.getColor(activity, R.attr.text_Color_Main)); - } else { - viewHolder.name.setTextColor(StyledAttributes.getColor(activity, R.attr.text_Color_Main)); - } - viewHolder.name.setAlpha(ACTIVE_ALPHA); - viewHolder.jid.setAlpha(ACTIVE_ALPHA); - viewHolder.avatar.setAlpha(ACTIVE_ALPHA); - viewHolder.tags.setAlpha(ACTIVE_ALPHA); - } - AvatarWorkerTask.loadAvatar(item, viewHolder.avatar, R.dimen.avatar); - if (item.getActive()) { - viewHolder.activeIndicator.setVisibility(View.VISIBLE); - } else { - viewHolder.activeIndicator.setVisibility(View.GONE); - } - return view; - } - - public void setOnTagClickedListener(OnTagClickedListener listener) { - this.mOnTagClickedListener = listener; - } - - public interface OnTagClickedListener { - void onTagClicked(String tag); - } - - private static class ViewHolder { - private TextView name; - private TextView jid; - private TextView account; - private ImageView avatar; - private FlowLayout tags; - private ImageView activeIndicator; - - private ViewHolder() { - } - - public static ViewHolder get(ContactBinding binding) { - ViewHolder viewHolder = new ViewHolder(); - viewHolder.name = binding.contactDisplayName; - viewHolder.jid = binding.contactJid; - viewHolder.account = binding.account; - viewHolder.avatar = binding.contactPhoto; - viewHolder.tags = binding.tags; - viewHolder.activeIndicator = binding.userActiveIndicator; - binding.getRoot().setTag(viewHolder); - return viewHolder; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java deleted file mode 100644 index 768494465..000000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java +++ /dev/null @@ -1,354 +0,0 @@ -package eu.siacs.conversations.ui.adapter; - -import static eu.siacs.conversations.utils.StorageHelper.getConversationsDirectory; - -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.AsyncTask; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; - -import androidx.annotation.AttrRes; -import androidx.annotation.DimenRes; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.PopupMenu; -import androidx.databinding.DataBindingUtil; -import androidx.recyclerview.widget.RecyclerView; - -import java.io.File; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.RejectedExecutionException; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.MediaBinding; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.services.ExportBackupService; -import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.ui.util.Attachment; -import eu.siacs.conversations.ui.util.StyledAttributes; -import eu.siacs.conversations.ui.util.ViewUtil; -import eu.siacs.conversations.utils.MimeUtils; -import me.drakeet.support.toast.ToastCompat; - -public class MediaAdapter extends RecyclerView.Adapter { - - private static final List DOCUMENT_MIMES = Arrays.asList( - "application/pdf", - "application/vnd.oasis.opendocument.text", - "application/msword", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "text/x-tex", - "text/plain" - ); - - private final ArrayList attachments = new ArrayList<>(); - - private final XmppActivity activity; - - private int mediaSize = 0; - - public MediaAdapter(XmppActivity activity, @DimenRes int mediaSize) { - this.activity = activity; - this.mediaSize = Math.round(activity.getResources().getDimension(mediaSize)); - } - - @SuppressWarnings("rawtypes") - public static void setMediaSize(RecyclerView recyclerView, int mediaSize) { - final RecyclerView.Adapter adapter = recyclerView.getAdapter(); - if (adapter instanceof MediaAdapter) { - ((MediaAdapter) adapter).setMediaSize(mediaSize); - } - } - - private static @AttrRes - int getImageAttr(Attachment attachment) { - final @AttrRes int attr; - if (attachment.getType() == Attachment.Type.LOCATION) { - attr = R.attr.media_preview_location; - } else if (attachment.getType() == Attachment.Type.RECORDING) { - attr = R.attr.media_preview_recording; - } else { - final String mime = attachment.getMime(); - Log.d(Config.LOGTAG, "mime=" + mime); - if (mime == null) { - attr = R.attr.media_preview_unknown; - } else if (mime.startsWith("audio/")) { - attr = R.attr.media_preview_audio; - } else if (mime.equals("text/calendar") || (mime.equals("text/x-vcalendar"))) { - attr = R.attr.media_preview_calendar; - } else if (mime.equals("text/x-vcard")) { - attr = R.attr.media_preview_contact; - } else if (mime.equals("application/vnd.android.package-archive")) { - attr = R.attr.media_preview_app; - } else if (mime.equals("application/zip") || mime.equals("application/rar")) { - attr = R.attr.media_preview_archive; - } else if (mime.equals("application/epub+zip") || mime.equals("application/vnd.amazon.mobi8-ebook")) { - attr = R.attr.media_preview_ebook; - } else if (mime.equals(ExportBackupService.MIME_TYPE)) { - attr = R.attr.media_preview_backup; - } else if (DOCUMENT_MIMES.contains(mime)) { - attr = R.attr.media_preview_document; - } else if (mime.equals("application/gpx+xml")) { - attr = R.attr.media_preview_tour; - } else { - attr = R.attr.media_preview_unknown; - } - } - return attr; - } - - static void renderPreview(Context context, Attachment attachment, ImageView imageView) { - imageView.setBackgroundColor(StyledAttributes.getColor(context, R.attr.color_background_tertiary)); - imageView.setImageAlpha(Math.round(StyledAttributes.getFloat(context, R.attr.icon_alpha) * 255)); - imageView.setImageDrawable(StyledAttributes.getDrawable(context, getImageAttr(attachment))); - } - - private static boolean cancelPotentialWork(Attachment attachment, ImageView imageView) { - final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); - - if (bitmapWorkerTask != null) { - final Attachment oldAttachment = bitmapWorkerTask.attachment; - if (oldAttachment == null || !oldAttachment.equals(attachment)) { - bitmapWorkerTask.cancel(true); - } else { - return false; - } - } - return true; - } - - private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { - if (imageView != null) { - final Drawable drawable = imageView.getDrawable(); - if (drawable instanceof AsyncDrawable) { - final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; - return asyncDrawable.getBitmapWorkerTask(); - } - } - return null; - } - - @NonNull - @Override - public MediaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); - MediaBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.media, parent, false); - return new MediaViewHolder(binding); - } - - @Override - public void onBindViewHolder(@NonNull MediaViewHolder holder, int position) { - final Attachment attachment = attachments.get(position); - if (attachment.renderThumbnail()) { - holder.binding.media.setImageAlpha(255); - loadPreview(attachment, holder.binding.media); - } else { - cancelPotentialWork(attachment, holder.binding.media); - renderPreview(this.activity, attachment, holder.binding.media); - } - holder.binding.getRoot().setOnClickListener(v -> ViewUtil.view(this.activity, attachment)); - holder.binding.getRoot().setOnLongClickListener(v -> { - setSelection(v); - final PopupMenu popupMenu = new PopupMenu(this.activity, v); - popupMenu.inflate(R.menu.media_viewer); - popupMenu.getMenu().findItem(R.id.action_delete).setVisible(isDeletableFile(new File(attachment.getUri().getPath()))); - popupMenu.setOnMenuItemClickListener(item -> { - switch (item.getItemId()) { - case R.id.action_share: - share(attachment); - return true; - case R.id.action_open: - open(attachment); - return true; - case R.id.action_delete: - deleteFile(attachment); - return true; - } - return false; - }); - popupMenu.setOnDismissListener(menu -> resetSelection(v)); - popupMenu.show(); - return true; - }); - } - - private void setSelection(final View v) { - v.setBackgroundColor(StyledAttributes.getColor(this.activity, R.attr.colorAccent)); - } - - private void resetSelection(final View v) { - v.setBackgroundColor(0); - } - - private void share(final Attachment attachment) { - final Intent share = new Intent(Intent.ACTION_SEND); - final File file = new File(attachment.getUri().getPath()); - share.setType(attachment.getMime()); - share.putExtra(Intent.EXTRA_STREAM, FileBackend.getUriForFile(this.activity, file)); - try { - this.activity.startActivity(Intent.createChooser(share, this.activity.getText(R.string.share_with))); - } catch (ActivityNotFoundException e) { - //This should happen only on faulty androids because normally chooser is always available - ToastCompat.makeText(this.activity, R.string.no_application_found_to_open_file, ToastCompat.LENGTH_SHORT).show(); - } - } - - private void deleteFile(final Attachment attachment) { - final File file = new File(attachment.getUri().getPath()); - final int hash = attachment.hashCode(); - final AlertDialog.Builder builder = new AlertDialog.Builder(this.activity); - builder.setNegativeButton(R.string.cancel, null); - builder.setTitle(R.string.delete_file_dialog); - builder.setMessage(R.string.delete_file_dialog_msg); - builder.setPositiveButton(R.string.confirm, (dialog, which) -> { - if (activity.xmppConnectionService.getFileBackend().deleteFile(file)) { - for (int i = 0; i < attachments.size(); i++) { - if (hash == attachments.get(i).hashCode()) { - attachments.remove(i); - notifyDataSetChanged(); - this.activity.refreshUi(); - return; - } - } - } - }); - builder.create().show(); - } - - private void open(final Attachment attachment) { - final File file = new File(attachment.getUri().getPath()); - final Uri uri; - try { - uri = FileBackend.getUriForFile(this.activity, file); - } catch (SecurityException e) { - Log.d(Config.LOGTAG, "No permission to access " + file.getAbsolutePath(), e); - ToastCompat.makeText(this.activity, this.activity.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), ToastCompat.LENGTH_SHORT).show(); - return; - } - String mime = MimeUtils.guessMimeTypeFromUri(this.activity, uri); - Intent openIntent = new Intent(Intent.ACTION_VIEW); - openIntent.setDataAndType(uri, mime); - openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - PackageManager manager = this.activity.getPackageManager(); - List info = manager.queryIntentActivities(openIntent, 0); - if (info.size() == 0) { - openIntent.setDataAndType(uri, "*/*"); - } - try { - this.activity.startActivity(openIntent); - } catch (ActivityNotFoundException e) { - ToastCompat.makeText(this.activity, R.string.no_application_found_to_open_file, ToastCompat.LENGTH_SHORT).show(); - } - } - - private boolean isDeletableFile(File file) { - return (file == null || !file.toString().startsWith("/") || file.toString().contains(getConversationsDirectory(this.activity, "null").getAbsolutePath())); - } - - public void setAttachments(List attachments) { - this.attachments.clear(); - this.attachments.addAll(attachments); - notifyDataSetChanged(); - } - - private void setMediaSize(int mediaSize) { - this.mediaSize = mediaSize; - } - - private void loadPreview(Attachment attachment, ImageView imageView) { - if (cancelPotentialWork(attachment, imageView)) { - final Bitmap bm = activity.xmppConnectionService.getFileBackend().getPreviewForUri(attachment, mediaSize, true); - if (bm != null) { - cancelPotentialWork(attachment, imageView); - imageView.setImageBitmap(bm); - imageView.setBackgroundColor(0x00000000); - } else { - imageView.setBackgroundColor(0xff333333); - imageView.setImageDrawable(null); - final BitmapWorkerTask task = new BitmapWorkerTask(mediaSize, imageView); - final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task); - imageView.setImageDrawable(asyncDrawable); - try { - task.execute(attachment); - } catch (final RejectedExecutionException ignored) { - } - } - } - } - - @Override - public int getItemCount() { - return attachments.size(); - } - - static class AsyncDrawable extends BitmapDrawable { - private final WeakReference bitmapWorkerTaskReference; - - AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { - super(res, bitmap); - bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask); - } - - BitmapWorkerTask getBitmapWorkerTask() { - return bitmapWorkerTaskReference.get(); - } - } - - class MediaViewHolder extends RecyclerView.ViewHolder { - - private final MediaBinding binding; - - MediaViewHolder(MediaBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } - - private static class BitmapWorkerTask extends AsyncTask { - private final WeakReference imageViewReference; - private Attachment attachment = null; - private final int mediaSize; - - BitmapWorkerTask(int mediaSize, ImageView imageView) { - this.mediaSize = mediaSize; - imageViewReference = new WeakReference<>(imageView); - } - - @Override - protected Bitmap doInBackground(Attachment... params) { - this.attachment = params[0]; - final XmppActivity activity = XmppActivity.find(imageViewReference); - if (activity == null) { - return null; - } - return activity.xmppConnectionService.getFileBackend().getPreviewForUri(this.attachment, mediaSize, false); - } - - @Override - protected void onPostExecute(Bitmap bitmap) { - if (bitmap != null && !isCancelled()) { - final ImageView imageView = imageViewReference.get(); - if (imageView != null) { - imageView.setImageBitmap(bitmap); - imageView.setBackgroundColor(0x00000000); - } - } - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java deleted file mode 100644 index 8a66d6f0a..000000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java +++ /dev/null @@ -1,210 +0,0 @@ -package eu.siacs.conversations.ui.adapter; - -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.AsyncTask; -import android.view.LayoutInflater; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.databinding.DataBindingUtil; -import androidx.recyclerview.widget.RecyclerView; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.RejectedExecutionException; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.MediaPreviewBinding; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.ui.ConversationFragment; -import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.ui.util.Attachment; - -public class MediaPreviewAdapter extends RecyclerView.Adapter { - - private final ArrayList mediaPreviews = new ArrayList<>(); - - private final ConversationFragment conversationFragment; - - public MediaPreviewAdapter(ConversationFragment fragment) { - this.conversationFragment = fragment; - } - - private static boolean cancelPotentialWork(Attachment attachment, ImageView imageView) { - final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); - - if (bitmapWorkerTask != null) { - final Attachment oldAttachment = bitmapWorkerTask.attachment; - if (oldAttachment == null || !oldAttachment.equals(attachment)) { - bitmapWorkerTask.cancel(true); - } else { - return false; - } - } - return true; - } - - private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { - if (imageView != null) { - final Drawable drawable = imageView.getDrawable(); - if (drawable instanceof AsyncDrawable) { - final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; - return asyncDrawable.getBitmapWorkerTask(); - } - } - return null; - } - - @NonNull - @Override - public MediaPreviewViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); - MediaPreviewBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.media_preview, parent, false); - return new MediaPreviewViewHolder(binding); - } - - @Override - public void onBindViewHolder(@NonNull MediaPreviewViewHolder holder, int position) { - final Context context = conversationFragment.getActivity(); - final Attachment attachment = mediaPreviews.get(position); - if (attachment.renderThumbnail()) { - holder.binding.mediaPreview.setImageAlpha(255); - loadPreview(attachment, holder.binding.mediaPreview); - } else { - cancelPotentialWork(attachment, holder.binding.mediaPreview); - MediaAdapter.renderPreview(context, attachment, holder.binding.mediaPreview); - } - holder.binding.deleteButton.setOnClickListener(v -> { - int pos = mediaPreviews.indexOf(attachment); - try { - mediaPreviews.remove(pos); - } catch (Exception e) { - e.printStackTrace(); - } - notifyItemRemoved(pos); - conversationFragment.toggleInputMethod(); - }); - holder.binding.mediaPreview.setOnClickListener(v -> view(context, attachment)); - } - - private static void view(final Context context, Attachment attachment) { - final Intent view = new Intent(Intent.ACTION_VIEW); - final Uri uri = FileBackend.getUriForUri(context, attachment.getUri()); - view.setDataAndType(uri, attachment.getMime()); - view.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - try { - context.startActivity(view); - } catch (ActivityNotFoundException e) { - Toast.makeText(context, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show(); - } catch (final SecurityException e) { - Toast.makeText(context, R.string.sharing_application_not_grant_permission, Toast.LENGTH_SHORT).show(); - } - } - - public void addMediaPreviews(List attachments) { - this.mediaPreviews.addAll(attachments); - notifyDataSetChanged(); - } - - private void loadPreview(Attachment attachment, ImageView imageView) { - if (cancelPotentialWork(attachment, imageView)) { - XmppActivity activity = (XmppActivity) conversationFragment.getActivity(); - final Bitmap bm = activity.xmppConnectionService.getFileBackend().getPreviewForUri(attachment, Math.round(activity.getResources().getDimension(R.dimen.media_preview_size)), true); - if (bm != null) { - cancelPotentialWork(attachment, imageView); - imageView.setImageBitmap(bm); - imageView.setBackgroundColor(0x00000000); - } else { - imageView.setBackgroundColor(0xff333333); - imageView.setImageDrawable(null); - final BitmapWorkerTask task = new BitmapWorkerTask(imageView); - final AsyncDrawable asyncDrawable = new AsyncDrawable(conversationFragment.getActivity().getResources(), null, task); - imageView.setImageDrawable(asyncDrawable); - try { - task.execute(attachment); - } catch (final RejectedExecutionException ignored) { - } - } - } - } - - @Override - public int getItemCount() { - return mediaPreviews.size(); - } - - public boolean hasAttachments() { - return mediaPreviews.size() > 0; - } - - public ArrayList getAttachments() { - return mediaPreviews; - } - - public void clearPreviews() { - this.mediaPreviews.clear(); - } - - static class AsyncDrawable extends BitmapDrawable { - private final WeakReference bitmapWorkerTaskReference; - - AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { - super(res, bitmap); - bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask); - } - - BitmapWorkerTask getBitmapWorkerTask() { - return bitmapWorkerTaskReference.get(); - } - } - - class MediaPreviewViewHolder extends RecyclerView.ViewHolder { - - private final MediaPreviewBinding binding; - - MediaPreviewViewHolder(MediaPreviewBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } - - private static class BitmapWorkerTask extends AsyncTask { - private final WeakReference imageViewReference; - private Attachment attachment = null; - - BitmapWorkerTask(ImageView imageView) { - imageViewReference = new WeakReference<>(imageView); - } - - @Override - protected Bitmap doInBackground(Attachment... params) { - this.attachment = params[0]; - final XmppActivity activity = XmppActivity.find(imageViewReference); - if (activity == null) { - return null; - } - return activity.xmppConnectionService.getFileBackend().getPreviewForUri(this.attachment, Math.round(activity.getResources().getDimension(R.dimen.media_preview_size)), false); - } - - @Override - protected void onPostExecute(Bitmap bitmap) { - if (bitmap != null && !isCancelled()) { - final ImageView imageView = imageViewReference.get(); - if (imageView != null) { - imageView.setImageBitmap(bitmap); - imageView.setBackgroundColor(0x00000000); - } - } - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java deleted file mode 100644 index 5fcac3f13..000000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ /dev/null @@ -1,1627 +0,0 @@ -package eu.siacs.conversations.ui.adapter; - -import eu.siacs.conversations.ui.widget.ClickableMovementMethod; -import me.saket.bettermovementmethod.BetterLinkMovementMethod; -import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; -import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY; -import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY_OLD; -import static eu.siacs.conversations.persistance.FileBackend.formatTime; -import static eu.siacs.conversations.persistance.FileBackend.safeLongToInt; -import static eu.siacs.conversations.ui.SettingsActivity.PLAY_GIF_INSIDE; -import static eu.siacs.conversations.ui.SettingsActivity.SHOW_LINKS_INSIDE; -import static eu.siacs.conversations.ui.SettingsActivity.SHOW_MAPS_INSIDE; -import static eu.siacs.conversations.ui.util.MyLinkify.removeTrackingParameter; -import static eu.siacs.conversations.ui.util.MyLinkify.removeTrailingBracket; -import static eu.siacs.conversations.ui.util.MyLinkify.replaceYoutube; -import eu.siacs.conversations.ui.util.ShareUtil; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.graphics.Rect; -import android.graphics.Typeface; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.preference.PreferenceManager; -import android.text.Editable; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.SpannableStringBuilder; -import android.text.format.DateUtils; -import android.text.style.ForegroundColorSpan; -import android.text.style.RelativeSizeSpan; -import android.text.style.StyleSpan; -import android.util.Base64; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ProgressBar; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.ColorUtils; -import androidx.core.graphics.drawable.DrawableCompat; - -import com.google.common.base.Strings; -import com.squareup.picasso.Picasso; - -import java.io.UnsupportedEncodingException; -import java.lang.ref.WeakReference; -import java.net.URI; -import java.util.List; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Conversational; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.Message.FileParams; -import eu.siacs.conversations.entities.RtpSessionStatus; -import eu.siacs.conversations.entities.Transferable; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.services.AudioPlayer; -import eu.siacs.conversations.services.MessageArchiveService; -import eu.siacs.conversations.services.NotificationService; -import eu.siacs.conversations.ui.ConversationFragment; -import eu.siacs.conversations.ui.ConversationsActivity; -import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.ui.text.DividerSpan; -import eu.siacs.conversations.ui.text.QuoteSpan; -import eu.siacs.conversations.ui.util.AvatarWorkerTask; -import eu.siacs.conversations.ui.util.MyLinkify; -import eu.siacs.conversations.ui.util.QuoteHelper; -import eu.siacs.conversations.ui.util.StyledAttributes; -import eu.siacs.conversations.ui.util.ViewUtil; -import eu.siacs.conversations.ui.widget.RichLinkView; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.Emoticons; -import eu.siacs.conversations.utils.GeoHelper; -import eu.siacs.conversations.utils.MessageUtils; -import eu.siacs.conversations.utils.RichPreview; -import eu.siacs.conversations.utils.StylingHelper; -import eu.siacs.conversations.utils.ThemeHelper; -import eu.siacs.conversations.utils.TimeFrameUtils; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.mam.MamReference; -import me.drakeet.support.toast.ToastCompat; -import pl.droidsonroids.gif.GifImageView; - -public class MessageAdapter extends ArrayAdapter { - - public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR"; - private static final int SENT = 0; - private static final int RECEIVED = 1; - private static final int STATUS = 2; - private static final int DATE_SEPARATOR = 3; - private static final int RTP_SESSION = 4; - boolean isResendable = false; - - private final XmppActivity activity; - private final AudioPlayer audioPlayer; - private List highlightedTerm = null; - private final DisplayMetrics metrics; - private OnContactPictureClicked mOnContactPictureClickedListener; - private OnContactPictureLongClicked mOnContactPictureLongClickedListener; - private boolean mIndicateReceived = false; - private boolean mPlayGifInside = false; - private boolean mShowLinksInside = false; - private boolean mShowMapsInside = false; - private final boolean mForceNames; - - public MessageAdapter(final XmppActivity activity, final List messages, final boolean forceNames) { - super(activity, 0, messages); - this.activity = activity; - this.audioPlayer = new AudioPlayer(this); - metrics = getContext().getResources().getDisplayMetrics(); - updatePreferences(); - this.mForceNames = forceNames; - } - - public MessageAdapter(final XmppActivity activity, final List messages) { - this(activity, messages, false); - } - - private static void resetClickListener(View... views) { - for (View view : views) { - view.setOnClickListener(null); - } - } - - public void flagDisableInputs() { - activity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); - } - - public void flagEnableInputs() { - activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); - } - - public void flagScreenOn() { - activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - - public void flagScreenOff() { - activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - - public boolean autoPauseVoice() { - return activity.xmppConnectionService.pauseVoiceOnMoveFromEar(); - } - - public void setVolumeControl(final int stream) { - activity.setVolumeControlStream(stream); - } - - public void setOnContactPictureClicked(OnContactPictureClicked listener) { - this.mOnContactPictureClickedListener = listener; - } - - public void setOnContactPictureLongClicked( - OnContactPictureLongClicked listener) { - this.mOnContactPictureLongClickedListener = listener; - } - - public Activity getActivity() { - return activity; - } - - @Override - public int getViewTypeCount() { - return 5; - } - - private int getItemViewType(Message message) { - if (message.getType() == Message.TYPE_STATUS) { - if (DATE_SEPARATOR_BODY.equals(message.getBody())) { - return DATE_SEPARATOR; - } else { - return STATUS; - } - } else if (message.getType() == Message.TYPE_RTP_SESSION) { - return RTP_SESSION; - } else if (message.getStatus() <= Message.STATUS_RECEIVED) { - return RECEIVED; - } else { - return SENT; - } - } - - @Override - public int getItemViewType(int position) { - return this.getItemViewType(getItem(position)); - } - - private void displayStatus(ViewHolder viewHolder, final Message message, int type, boolean darkBackground) { - String filesize = null; - String info = null; - boolean error = false; - viewHolder.user.setText(UIHelper.getDisplayedMucCounterpart(message.getCounterpart())); - if (viewHolder.indicatorReceived != null) { - viewHolder.indicatorReceived.setVisibility(View.GONE); - } - if (viewHolder.edit_indicator != null) { - if (message.edited() && message.getRetractId() == null) { - viewHolder.edit_indicator.setVisibility(View.VISIBLE); - viewHolder.edit_indicator.setImageResource(darkBackground ? R.drawable.ic_mode_edit_white_18dp : R.drawable.ic_mode_edit_black_18dp); - viewHolder.edit_indicator.setAlpha(darkBackground ? 0.7f : 0.57f); - } else { - viewHolder.edit_indicator.setVisibility(View.GONE); - } - } - if (viewHolder.retract_indicator != null) { - if (message.getRetractId() != null) { - viewHolder.retract_indicator.setVisibility(View.VISIBLE); - viewHolder.retract_indicator.setImageResource(darkBackground ? R.drawable.ic_delete_white_18dp : R.drawable.ic_delete_black_18dp); - viewHolder.retract_indicator.setAlpha(darkBackground ? 0.7f : 0.57f); - } else { - viewHolder.retract_indicator.setVisibility(View.GONE); - } - } - final Transferable transferable = message.getTransferable(); - boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI - && message.getMergedStatus() <= Message.STATUS_RECEIVED; - boolean singleReceived = message.getConversation().getMode() == Conversation.MODE_SINGLE - && message.getMergedStatus() <= Message.STATUS_RECEIVED; - if (message.isFileOrImage() || transferable != null || MessageUtils.unInitiatedButKnownSize(message)) { - FileParams params = message.getFileParams(); - filesize = params.size != null ? UIHelper.filesizeToString(params.size) : null; - if (transferable != null && (transferable.getStatus() == Transferable.STATUS_FAILED || transferable.getStatus() == Transferable.STATUS_CANCELLED)) { - error = true; - } - } - switch (message.getMergedStatus()) { - case Message.STATUS_WAITING: - info = getContext().getString(R.string.waiting); - break; - case Message.STATUS_UNSEND: - if (transferable != null) { - info = getContext().getString(R.string.sending); - showProgress(viewHolder, transferable, message); - } else { - info = getContext().getString(R.string.sending); - } - break; - case Message.STATUS_OFFERED: - info = getContext().getString(R.string.offering); - break; - case Message.STATUS_SEND_RECEIVED: - if (mIndicateReceived) { - if (viewHolder.indicatorReceived != null) { - viewHolder.indicatorReceived.setVisibility(View.VISIBLE); - viewHolder.indicatorReceived.setImageResource(darkBackground ? R.drawable.ic_check_white_18dp : R.drawable.ic_check_black_18dp); - viewHolder.indicatorReceived.setAlpha(darkBackground ? 0.7f : 0.57f); - } - } else { - viewHolder.indicatorReceived.setVisibility(View.GONE); - } - break; - case Message.STATUS_SEND_DISPLAYED: - if (mIndicateReceived) { - if (viewHolder.indicatorReceived != null) { - viewHolder.indicatorReceived.setVisibility(View.VISIBLE); - viewHolder.indicatorReceived.setImageResource(darkBackground ? R.drawable.ic_check_all_white_18dp : R.drawable.ic_check_all_black_18dp); - viewHolder.indicatorReceived.setAlpha(darkBackground ? 0.7f : 0.57f); - } - } else { - viewHolder.indicatorReceived.setVisibility(View.GONE); - } - break; - case Message.STATUS_SEND_FAILED: - DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); - if (isResendable && file.exists() || message.getResendCount() < activity.xmppConnectionService.maxResendTime()) { - info = getContext().getString(R.string.send_failed_resend); - } else { - final String errorMessage = message.getErrorMessage(); - if (Message.ERROR_MESSAGE_CANCELLED.equals(errorMessage)) { - info = getContext().getString(R.string.cancelled); - } else { - if (errorMessage != null) { - final String[] errorParts = errorMessage.split("\\u001f", 2); - if (errorParts.length == 2) { - switch (errorParts[0]) { - case "file-too-large": - info = getContext().getString(R.string.file_too_large); - break; - default: - info = getContext().getString(R.string.send_failed); - break; - } - } else { - info = getContext().getString(R.string.send_failed); - } - } else { - info = getContext().getString(R.string.send_failed); - } - } - } - error = true; - break; - default: - if (mForceNames || multiReceived) { - final int shadowSize = 10; - showUsername(viewHolder, message, darkBackground); - } else if (singleReceived) { - viewHolder.username.setVisibility(View.GONE); - } - break; - } - if (error && type == SENT) { - if (darkBackground) { - viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Warning_OnDark); - } else { - viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Warning); - } - DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); - if (file.exists()) { - if (activity.xmppConnectionService.mHttpConnectionManager.getAutoAcceptFileSize() >= message.getFileParams().size && (transferable != null && transferable.getStatus() == Transferable.STATUS_FAILED)) { - isResendable = true; - viewHolder.resend_button.setVisibility(View.GONE); - } else { - isResendable = false; - viewHolder.resend_button.setVisibility(View.GONE); - /* - viewHolder.resend_button.setVisibility(View.VISIBLE); - viewHolder.resend_button.setText(R.string.send_again); - viewHolder.resend_button.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_resend_grey600_48dp, 0, 0, 0); - viewHolder.resend_button.setOnClickListener(v -> mConversationFragment.resendMessage(message)); - */ - } - } else { - isResendable = false; - viewHolder.resend_button.setVisibility(View.GONE); - } - } else { - if (darkBackground) { - viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_OnDark); - } else { - viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption); - } - viewHolder.time.setTextColor(ThemeHelper.getMessageTextColor(getContext(), darkBackground, false)); - } - if (!error && type == SENT) { - viewHolder.resend_button.setVisibility(View.GONE); - } - if (message.getEncryption() == Message.ENCRYPTION_NONE) { - viewHolder.indicator.setVisibility(View.GONE); - } else { - boolean verified = false; - if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { - final FingerprintStatus status = message.getConversation().getAccount().getAxolotlService().getFingerprintTrust(message.getFingerprint()); - if (status != null && status.isVerified()) { - verified = true; - } - } - if (verified) { - viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_verified_user_white_18dp : R.drawable.ic_verified_user_black_18dp); - } else { - viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_lock_white_18dp : R.drawable.ic_lock_black_18dp); - } - if (darkBackground) { - viewHolder.indicator.setAlpha(0.7f); - } else { - viewHolder.indicator.setAlpha(0.57f); - } - viewHolder.indicator.setVisibility(View.VISIBLE); - } - - final String formattedTime = UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent()); - final String bodyLanguage = message.getBodyLanguage(); - final String bodyLanguageInfo = bodyLanguage == null ? "" : String.format(" \u00B7 %s", bodyLanguage.toUpperCase(Locale.US)); - if (message.getStatus() <= Message.STATUS_RECEIVED) { - if ((filesize != null) && (info != null)) { - viewHolder.time.setText(formattedTime + " \u00B7 " + filesize + " \u00B7 " + info + bodyLanguageInfo); - } else if ((filesize == null) && (info != null)) { - viewHolder.time.setText(formattedTime + " \u00B7 " + info + bodyLanguageInfo); - } else if ((filesize != null) && (info == null)) { - viewHolder.time.setText(formattedTime + " \u00B7 " + filesize + bodyLanguageInfo); - } else { - viewHolder.time.setText(formattedTime + bodyLanguageInfo); - } - } else { - if ((filesize != null) && (info != null)) { - viewHolder.time.setText(filesize + " \u00B7 " + info + bodyLanguageInfo); - } else if ((filesize == null) && (info != null)) { - if (error) { - viewHolder.time.setText(info + " \u00B7 " + formattedTime + bodyLanguageInfo); - } else { - viewHolder.time.setText(info); - } - } else if ((filesize != null) && (info == null)) { - viewHolder.time.setText(filesize + " \u00B7 " + formattedTime + bodyLanguageInfo); - } else { - viewHolder.time.setText(formattedTime + bodyLanguageInfo); - } - } - } - private void showUsername(ViewHolder viewHolder, Message message, boolean darkBackground) { - if (message == null || viewHolder == null) { - return; - } - viewHolder.username.setText(UIHelper.getColoredUsername(activity.xmppConnectionService, message)); - if (message.showUsername() || mForceNames) { - viewHolder.username.setVisibility(View.VISIBLE); - } else { - viewHolder.username.setVisibility(View.GONE); - } - if (activity.xmppConnectionService.colored_muc_names() && ThemeHelper.showColoredUsernameBackGround(activity, darkBackground)) { - viewHolder.username.setPadding(4, 2, 4, 2); - viewHolder.username.setBackground(ContextCompat.getDrawable(activity, R.drawable.duration_background)); - } else { - viewHolder.username.setPadding(4, 2, 4, 2); - viewHolder.username.setBackground(null); - } - } - - private void displayInfoMessage(ViewHolder viewHolder, CharSequence text, boolean darkBackground, Message message) { - viewHolder.download_button.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - showImages(false, viewHolder); - viewHolder.richlinkview.setVisibility(View.GONE); - viewHolder.transfer.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.VISIBLE); - viewHolder.messageBody.setText(text); - showProgress(viewHolder, message.getTransferable(), message); - if (darkBackground) { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Secondary_OnDark); - } else { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Secondary); - } - viewHolder.messageBody.setTextIsSelectable(false); - } - - private void showProgress(final ViewHolder viewHolder, final Transferable transferable, final Message message) { - if (transferable != null) { - if (message.fileIsTransferring()) { - viewHolder.transfer.setVisibility(View.VISIBLE); - viewHolder.progressBar.setProgress(transferable.getProgress()); - Drawable icon = activity.getResources().getDrawable(R.drawable.ic_cancel_black_24dp); - Drawable drawable = DrawableCompat.wrap(icon); - DrawableCompat.setTint(drawable, StyledAttributes.getColor(getContext(), R.attr.colorAccent)); - viewHolder.cancel_transfer.setImageDrawable(drawable); - viewHolder.cancel_transfer.setEnabled(true); - viewHolder.cancel_transfer.setOnClickListener(v -> { - try { - if (activity instanceof ConversationsActivity) { - ConversationFragment conversationFragment = ConversationFragment.get(activity); - if (conversationFragment != null) { - activity.invalidateOptionsMenu(); - conversationFragment.cancelTransmission(message); - } - } - } catch (Exception e) { - viewHolder.cancel_transfer.setEnabled(false); - e.printStackTrace(); - } - }); - } else { - viewHolder.transfer.setVisibility(View.GONE); - } - } else { - viewHolder.transfer.setVisibility(View.GONE); - } - } - - private void displayEmojiMessage(final ViewHolder viewHolder, final String body, final boolean darkBackground) { - viewHolder.download_button.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - showImages(false, viewHolder); - viewHolder.richlinkview.setVisibility(View.GONE); - viewHolder.transfer.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.VISIBLE); - if (darkBackground) { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Emoji_OnDark); - } else { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Emoji); - } - final Spannable span = new SpannableString(body); - final float size = Emoticons.isEmoji(body) ? 3.0f : 2.0f; - span.setSpan(new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - viewHolder.messageBody.setText(span); - } - - private void displayXmppMessage(final ViewHolder viewHolder, final String body) { - String contact = body.toLowerCase(); - contact = contact.split(":")[1]; - boolean group; - try { - group = ((contact.split("\\?")[1]) != null && (contact.split("\\?")[1]).length() > 0 && (contact.split("\\?")[1]).equalsIgnoreCase("join")); - } catch (Exception e) { - group = false; - } - contact = contact.split("\\?")[0]; - final String add_contact = activity.getString(R.string.add_to_contact_list) + " (" + contact + ")"; - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setText(add_contact); - if (group) { - final Drawable icon = activity.getResources().getDrawable(R.drawable.ic_account_multiple_plus_grey600_48dp); - final Drawable drawable = DrawableCompat.wrap(icon); - DrawableCompat.setTint(drawable, StyledAttributes.getColor(getContext(), R.attr.colorAccent)); - viewHolder.download_button.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); - } else { - final Drawable icon = activity.getResources().getDrawable(R.drawable.ic_account_plus_grey600_48dp); - final Drawable drawable = DrawableCompat.wrap(icon); - DrawableCompat.setTint(drawable, StyledAttributes.getColor(getContext(), R.attr.colorAccent)); - viewHolder.download_button.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); - } - viewHolder.download_button.setOnClickListener(v -> { - try { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(body)); - activity.startActivity(intent); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } catch (Exception e) { - ToastCompat.makeText(activity, R.string.no_application_found_to_view_contact, ToastCompat.LENGTH_LONG).show(); - } - - }); - showImages(false, viewHolder); - viewHolder.richlinkview.setVisibility(View.GONE); - viewHolder.transfer.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.GONE); - } - - private void applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground) { - if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) { - body.insert(start++, "\n"); - body.setSpan(new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - end++; - } - if (end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) { - body.insert(end, "\n"); - body.setSpan(new DividerSpan(false), end, end + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - int color = ThemeHelper.messageTextColor(activity); - final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); - body.setSpan(new QuoteSpan(color, metrics), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - /** - * Applies QuoteSpan to group of lines which starts with > or » characters. - * Appends likebreaks and applies DividerSpan to them to show a padding between quote and text. - */ - private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) { - boolean startsWithQuote = false; - 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++, " "); - } - lineStart = -1; - lineTextStart = -1; - } - } - previous = current; - } - if (quoteStart >= 0) { - // Apply spans to finishing open quote - applyQuoteSpan(body, quoteStart, body.length(), darkBackground); - } - quoteDepth++; - } - return startsWithQuote; - } - - private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground, int type) { - viewHolder.download_button.setVisibility(View.GONE); - showImages(false, viewHolder); - viewHolder.richlinkview.setVisibility(View.GONE); - viewHolder.transfer.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.VISIBLE); - if (darkBackground) { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_OnDark); - } else { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1); - } - viewHolder.messageBody.setHighlightColor(darkBackground ? type == SENT ? StyledAttributes.getColor(activity, R.attr.colorAccent) : StyledAttributes.getColor(activity, R.attr.colorAccent) : StyledAttributes.getColor(activity, R.attr.colorAccent)); - viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); - if (message.getBody() != null) { - final SpannableString nick = UIHelper.getColoredUsername(activity.xmppConnectionService, message); - SpannableStringBuilder body = new SpannableStringBuilder(replaceYoutube(activity.getApplicationContext(), message.getMergedBody())); - if (message.getBody().equals(DELETED_MESSAGE_BODY)) { - body = body.replace(0, DELETED_MESSAGE_BODY.length(), activity.getString(R.string.message_deleted)); - } else if (message.getBody().equals(DELETED_MESSAGE_BODY_OLD)) { - body = body.replace(0, DELETED_MESSAGE_BODY_OLD.length(), activity.getString(R.string.message_deleted)); - } else { - boolean hasMeCommand = message.hasMeCommand(); - if (hasMeCommand) { - body = body.replace(0, Message.ME_COMMAND.length(), nick); - } - if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) { - body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS); - body.append("\u2026"); - } - final Message.MergeSeparator[] mergeSeparators = body.getSpans(0, body.length(), Message.MergeSeparator.class); - for (Message.MergeSeparator mergeSeparator : mergeSeparators) { - int start = body.getSpanStart(mergeSeparator); - int end = body.getSpanEnd(mergeSeparator); - body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - final boolean startsWithQuote = handleTextQuotes(body, darkBackground); - if (!message.isPrivateMessage()) { - if (hasMeCommand) { - body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } else { - String privateMarker; - if (message.getStatus() <= Message.STATUS_RECEIVED) { - privateMarker = activity.getString(R.string.private_message); - } else { - Jid cp = message.getCounterpart(); - privateMarker = activity.getString(R.string.private_message_to, Strings.nullToEmpty(cp == null ? null : cp.getResource())); - } - body.insert(0, privateMarker); - final int privateMarkerIndex = privateMarker.length(); - if (startsWithQuote) { - body.insert(privateMarkerIndex, "\n\n"); - body.setSpan(new DividerSpan(false), privateMarkerIndex, privateMarkerIndex + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } else { - body.insert(privateMarkerIndex, " "); - } - body.setSpan(new ForegroundColorSpan(ThemeHelper.getMessageTextColorPrivate(activity)), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - body.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - if (hasMeCommand) { - body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), privateMarkerIndex + 1, privateMarkerIndex + 1 + nick.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - if (message.getConversation().getMode() == Conversation.MODE_MULTI && message.getStatus() == Message.STATUS_RECEIVED) { - if (message.getConversation() instanceof Conversation) { - final Conversation conversation = (Conversation) message.getConversation(); - final Pattern pattern = NotificationService.generateNickHighlightPattern(conversation.getMucOptions().getActualNick()); - final Matcher matcher = pattern.matcher(body); - while (matcher.find()) { - body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - } - final Matcher matcher = Emoticons.getEmojiPattern(body).matcher(body); - while (matcher.find()) { - if (matcher.start() < matcher.end()) { - body.setSpan(new RelativeSizeSpan(1.5f), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor(), true); - if (highlightedTerm != null) { - StylingHelper.highlight(activity, body, highlightedTerm, StylingHelper.isDarkText(viewHolder.messageBody)); - } - } - if (message.isWebUri() || message.getWebUri() != null) { - displayRichLinkMessage(viewHolder, message, darkBackground); - } - MyLinkify.addLinks(body, message.getConversation().getAccount()); - viewHolder.messageBody.setText(body); - viewHolder.messageBody.setAutoLinkMask(0); - BetterLinkMovementMethod method = BetterLinkMovementMethod.newInstance(); - method.setOnLinkLongClickListener((tv, url) -> { - tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)); - ShareUtil.copyLinkToClipboard(activity, url); - return true; - }); - viewHolder.messageBody.setMovementMethod(method); - } else { - viewHolder.messageBody.setText(""); - viewHolder.messageBody.setTextIsSelectable(false); - } - } - - private void displayDownloadableMessage(ViewHolder viewHolder, final Message message, String text, final boolean darkBackground) { - toggleWhisperInfo(viewHolder, message, false, darkBackground); - viewHolder.audioPlayer.setVisibility(View.GONE); - showImages(false, viewHolder); - viewHolder.richlinkview.setVisibility(View.GONE); - viewHolder.transfer.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setText(text); - final Drawable icon = activity.getResources().getDrawable(R.drawable.ic_download_grey600_48dp); - final Drawable drawable = DrawableCompat.wrap(icon); - DrawableCompat.setTint(drawable, StyledAttributes.getColor(getContext(), R.attr.colorAccent)); - viewHolder.download_button.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); - viewHolder.download_button.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message)); - } - - private void displayOpenableMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground) { - toggleWhisperInfo(viewHolder, message, false, darkBackground); - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.audioPlayer.setVisibility(View.GONE); - showImages(false, viewHolder); - viewHolder.richlinkview.setVisibility(View.GONE); - viewHolder.transfer.setVisibility(View.GONE); - final String mimeType = message.getMimeType(); - if (mimeType != null && message.getMimeType().contains("vcard")) { - try { - showVCard(message, viewHolder); - } catch (Exception e) { - e.printStackTrace(); - } - } else if (mimeType != null && message.getMimeType().contains("calendar")) { - final Drawable icon = activity.getResources().getDrawable(R.drawable.ic_calendar_grey600_48dp); - final Drawable drawable = DrawableCompat.wrap(icon); - DrawableCompat.setTint(drawable, StyledAttributes.getColor(getContext(), R.attr.colorAccent)); - viewHolder.download_button.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); - viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity, message))); - } else if (mimeType != null && message.getMimeType().equals("application/vnd.android.package-archive")) { - try { - showAPK(message, viewHolder); - } catch (Exception e) { - e.printStackTrace(); - } - } else if (mimeType != null && message.getMimeType().contains("video")) { - final Drawable icon = activity.getResources().getDrawable(R.drawable.ic_video_grey600_48dp); - final Drawable drawable = DrawableCompat.wrap(icon); - DrawableCompat.setTint(drawable, StyledAttributes.getColor(getContext(), R.attr.colorAccent)); - viewHolder.download_button.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); - viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity, message))); - } else if (mimeType != null && message.getMimeType().contains("image")) { - final Drawable icon = activity.getResources().getDrawable(R.drawable.ic_image_grey600_48dp); - final Drawable drawable = DrawableCompat.wrap(icon); - DrawableCompat.setTint(drawable, StyledAttributes.getColor(getContext(), R.attr.colorAccent)); - viewHolder.download_button.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); - viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity, message))); - } else if (mimeType != null && message.getMimeType().contains("audio")) { - final Drawable icon = activity.getResources().getDrawable(R.drawable.ic_audio_grey600_48dp); - final Drawable drawable = DrawableCompat.wrap(icon); - DrawableCompat.setTint(drawable, StyledAttributes.getColor(getContext(), R.attr.colorAccent)); - viewHolder.download_button.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); - viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity, message))); - } else { - final Drawable icon = activity.getResources().getDrawable(R.drawable.ic_file_grey600_48dp); - final Drawable drawable = DrawableCompat.wrap(icon); - DrawableCompat.setTint(drawable, StyledAttributes.getColor(getContext(), R.attr.colorAccent)); - viewHolder.download_button.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); - viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity, message))); - } - viewHolder.download_button.setOnClickListener(v -> openDownloadable(message)); - } - - private void showAPK(final Message message, final ViewHolder viewHolder) { - String APKName = ""; - if (message.getFileParams().subject.length() != 0) { - try { - byte[] data = Base64.decode(message.getFileParams().subject, Base64.DEFAULT); - APKName = new String(data, "UTF-8"); - } catch (UnsupportedEncodingException e) { - APKName = ""; - e.printStackTrace(); - } - } - final Drawable icon = activity.getResources().getDrawable(R.drawable.ic_android_grey600_48dp); - final Drawable drawable = DrawableCompat.wrap(icon); - DrawableCompat.setTint(drawable, StyledAttributes.getColor(getContext(), R.attr.colorAccent)); - viewHolder.download_button.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); - viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity, message) + APKName)); - } - - private void showVCard(final Message message, ViewHolder viewHolder) { - String VCardName = ""; - if (message.getFileParams().subject.length() != 0) { - try { - byte[] data = Base64.decode(message.getFileParams().subject, Base64.DEFAULT); - VCardName = new String(data, "UTF-8"); - } catch (UnsupportedEncodingException e) { - VCardName = ""; - e.printStackTrace(); - } - } - final Drawable icon = activity.getResources().getDrawable(R.drawable.ic_account_card_details_grey600_48dp); - final Drawable drawable = DrawableCompat.wrap(icon); - DrawableCompat.setTint(drawable, StyledAttributes.getColor(getContext(), R.attr.colorAccent)); - viewHolder.download_button.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); - viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity, message) + VCardName)); - } - - private void displayRichLinkMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground) { - toggleWhisperInfo(viewHolder, message, true, darkBackground); - viewHolder.audioPlayer.setVisibility(View.GONE); - showImages(false, viewHolder); - viewHolder.download_button.setVisibility(View.GONE); - viewHolder.transfer.setVisibility(View.GONE); - String url; - if (message.isWebUri()) { - url = removeTrackingParameter(Uri.parse(message.getBody().trim())).toString(); - } else { - url = removeTrackingParameter(Uri.parse(message.getWebUri())).toString(); - } - final String link = replaceYoutube(activity.getApplicationContext(), url); - Log.d(Config.LOGTAG, "Weburi body for preview: " + link); - final boolean dataSaverDisabled = activity.xmppConnectionService.isDataSaverDisabled(); - viewHolder.richlinkview.setVisibility(mShowLinksInside ? View.VISIBLE : View.GONE); - if (mShowLinksInside) { - final int color = ThemeHelper.messageTextColor(activity); - final float target = activity.getResources().getDimension(R.dimen.image_preview_width); - final int scaledH; - if (Math.max(100, 100) * metrics.density <= target) { - scaledH = (int) (100 * metrics.density); - } else if (Math.max(100, 100) <= target) { - scaledH = 100; - } else { - scaledH = (int) (100 / ((double) 100 / target)); - } - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT); - layoutParams.setMargins(0, (int) (metrics.density * 4), 0, (int) (metrics.density * 4)); - viewHolder.richlinkview.setLayoutParams(layoutParams); - viewHolder.richlinkview.setMinimumHeight(scaledH); - final String weburl; - if (link.startsWith("http://") || link.startsWith("https://")) { - weburl = removeTrailingBracket(link); - } else { - weburl = "http://" + removeTrailingBracket(link); - } - Log.d(Config.LOGTAG, "Weburi for preview: " + weburl); - viewHolder.richlinkview.setLink(weburl, message.getUuid(), dataSaverDisabled, activity.xmppConnectionService, color, new RichPreview.ViewListener() { - - @Override - public void onSuccess(boolean status) { - } - - @Override - public void onError(Exception e) { - e.printStackTrace(); - viewHolder.richlinkview.setVisibility(View.GONE); - } - }); - } - } - - private void displayLocationMessage(final ViewHolder viewHolder, final Message message, final boolean darkBackground, Activity activity) { - toggleWhisperInfo(viewHolder, message, false, darkBackground); - viewHolder.audioPlayer.setVisibility(View.GONE); - final String url = GeoHelper.MapPreviewUri(message, activity); - showImages(false, viewHolder); - viewHolder.richlinkview.setVisibility(View.GONE); - viewHolder.transfer.setVisibility(View.GONE); - if (mShowMapsInside) { - showImages(mShowMapsInside, 0, false, viewHolder); - final double target = activity.getResources().getDimension(R.dimen.image_preview_width); - final int scaledW; - final int scaledH; - if (Math.max(500, 500) * metrics.density <= target) { - scaledW = (int) (500 * metrics.density); - scaledH = (int) (500 * metrics.density); - } else if (Math.max(500, 500) <= target) { - scaledW = 500; - scaledH = 500; - } else { - scaledW = (int) target; - scaledH = (int) (500 / ((double) 500 / target)); - } - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(scaledW, scaledH); - layoutParams.setMargins(0, (int) (metrics.density * 4), 0, (int) (metrics.density * 4)); - viewHolder.images.setLayoutParams(layoutParams); - viewHolder.image.setOnClickListener(v -> showLocation(message)); - Picasso .get() - .load(Uri.parse(url)) - .placeholder(R.drawable.ic_map_marker_grey600_48dp) - .error(R.drawable.ic_map_marker_grey600_48dp) - .into(viewHolder.image); - viewHolder.image.setMaxWidth(500); - viewHolder.image.setAdjustViewBounds(true); - viewHolder.download_button.setVisibility(View.GONE); - } else { - showImages(false, viewHolder); - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setText(R.string.show_location); - final Drawable icon = activity.getResources().getDrawable(R.drawable.ic_map_marker_grey600_48dp); - final Drawable drawable = DrawableCompat.wrap(icon); - DrawableCompat.setTint(drawable, StyledAttributes.getColor(getContext(), R.attr.colorAccent)); - viewHolder.download_button.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); - viewHolder.download_button.setOnClickListener(v -> showLocation(message)); - } - } - - private void displayAudioMessage(ViewHolder viewHolder, Message message, boolean darkBackground) { - final Resources res = activity.getResources(); - viewHolder.messageBody.setWidth((int) res.getDimension(R.dimen.audio_player_width)); - toggleWhisperInfo(viewHolder, message, showTitle(message), darkBackground); - showImages(false, viewHolder); - viewHolder.richlinkview.setVisibility(View.GONE); - viewHolder.transfer.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.GONE); - final RelativeLayout audioPlayer = viewHolder.audioPlayer; - audioPlayer.setVisibility(View.VISIBLE); - AudioPlayer.ViewHolder.get(audioPlayer).setTheme(darkBackground); - this.audioPlayer.init(audioPlayer, message); - } - - private boolean showTitle(Message message) { - boolean show = false; - if (message.getFileParams().subject.length() != 0) { - try { - byte[] data = Base64.decode(message.getFileParams().subject, Base64.DEFAULT); - show = (new String(data, "UTF-8").length() != 0); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - } - return show; - } - - private String getTitle(Message message) { - if (message.getFileParams().subject.length() != 0) { - try { - byte[] data = Base64.decode(message.getFileParams().subject, Base64.DEFAULT); - return new String(data, "UTF-8"); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - } - return ""; - } - - private void displayMediaPreviewMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground) { - toggleWhisperInfo(viewHolder, message, false, darkBackground); - viewHolder.download_button.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.richlinkview.setVisibility(View.GONE); - viewHolder.transfer.setVisibility(View.GONE); - final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); - if (file != null && !file.exists() && !message.isFileDeleted()) { - new Thread(new markFileDeletedFinisher(message, activity)).start(); - displayInfoMessage(viewHolder, activity.getString(R.string.file_deleted), darkBackground, message); - ToastCompat.makeText(activity, R.string.file_deleted, ToastCompat.LENGTH_SHORT).show(); - return; - } - final String mime = file.getMimeType(); - final boolean isGif = mime != null && mime.equals("image/gif"); - final int mediaRuntime = message.getFileParams().runtime; - if (isGif && mPlayGifInside) { - showImages(true, mediaRuntime, true, viewHolder); - Log.d(Config.LOGTAG, "Gif Image file"); - final FileParams params = message.getFileParams(); - final float target = activity.getResources().getDimension(R.dimen.image_preview_width); - final int scaledW; - final int scaledH; - if (Math.max(params.height, params.width) * metrics.density <= target) { - scaledW = (int) (params.width * metrics.density); - scaledH = (int) (params.height * metrics.density); - } else if (Math.max(params.height, params.width) <= target) { - scaledW = params.width; - scaledH = params.height; - } else if (params.width <= params.height) { - scaledW = (int) (params.width / ((double) params.height / target)); - scaledH = (int) target; - } else { - scaledW = (int) target; - scaledH = (int) (params.height / ((double) params.width / target)); - } - final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(scaledW, scaledH); - layoutParams.setMargins(0, (int) (metrics.density * 4), 0, (int) (metrics.density * 4)); - viewHolder.images.setLayoutParams(layoutParams); - activity.loadGif(file, viewHolder.gifImage); - viewHolder.gifImage.setOnClickListener(v -> openDownloadable(message)); - } else { - showImages(true, mediaRuntime, false, viewHolder); - FileParams params = message.getFileParams(); - final float target = activity.getResources().getDimension(R.dimen.image_preview_width); - final int scaledW; - final int scaledH; - if (Math.max(params.height, params.width) * metrics.density <= target) { - scaledW = (int) (params.width * metrics.density); - scaledH = (int) (params.height * metrics.density); - } else if (Math.max(params.height, params.width) <= target) { - scaledW = params.width; - scaledH = params.height; - } else if (params.width <= params.height) { - scaledW = (int) (params.width / ((double) params.height / target)); - scaledH = (int) target; - } else { - scaledW = (int) target; - scaledH = (int) (params.height / ((double) params.width / target)); - } - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(scaledW, scaledH); - layoutParams.setMargins(0, (int) (metrics.density * 4), 0, (int) (metrics.density * 4)); - viewHolder.images.setLayoutParams(layoutParams); - activity.loadBitmap(message, viewHolder.image); - viewHolder.image.setOnClickListener(v -> openDownloadable(message)); - } - } - - private void showImages(final boolean show, final ViewHolder viewHolder) { - showImages(show, 0, false, viewHolder); - } - - private void showImages(final boolean show, final int duration, final boolean isGif, final ViewHolder viewHolder) { - boolean hasDuration = duration > 0; - if (show) { - viewHolder.images.setVisibility(View.VISIBLE); - if (hasDuration) { - viewHolder.mediaduration.setVisibility(View.VISIBLE); - viewHolder.mediaduration.setText(formatTime(safeLongToInt(duration))); - } else { - viewHolder.mediaduration.setVisibility(View.GONE); - } - if (isGif) { - viewHolder.image.setVisibility(View.GONE); - viewHolder.gifImage.setVisibility(View.VISIBLE); - } else { - viewHolder.image.setVisibility(View.VISIBLE); - viewHolder.gifImage.setVisibility(View.GONE); - } - } else { - viewHolder.images.setVisibility(View.GONE); - viewHolder.image.setVisibility(View.GONE); - viewHolder.gifImage.setVisibility(View.GONE); - viewHolder.mediaduration.setVisibility(View.GONE); - } - } - - private void toggleWhisperInfo(ViewHolder viewHolder, final Message message, - final boolean includeBody, final boolean darkBackground) { - SpannableStringBuilder messageBody = new SpannableStringBuilder(replaceYoutube(activity.getApplicationContext(), message.getBody())); - - final String mimeType = message.getMimeType(); - if (mimeType != null && message.getMimeType().contains("audio")) { - messageBody.clear(); - messageBody.append(getTitle(message)); - } - Editable body; - if (darkBackground) { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_OnDark); - } else { - viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1); - } - if (message.isPrivateMessage()) { - final String privateMarker; - if (message.getStatus() <= Message.STATUS_RECEIVED) { - privateMarker = activity.getString(R.string.private_message); - } else { - Jid cp = message.getCounterpart(); - privateMarker = activity.getString(R.string.private_message_to, Strings.nullToEmpty(cp == null ? null : cp.getResource())); - } - body = new SpannableStringBuilder(privateMarker); - viewHolder.messageBody.setVisibility(View.VISIBLE); - if (includeBody) { - body.append("\n"); - body.append(messageBody); - } - body.setSpan(new ForegroundColorSpan(ThemeHelper.getMessageTextColorPrivate(activity)), 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - body.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - MyLinkify.addLinks(body, false); - viewHolder.messageBody.setText(body); - viewHolder.messageBody.setAutoLinkMask(0); - viewHolder.messageBody.setTextIsSelectable(true); - viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance()); - } else { - if (includeBody) { - viewHolder.messageBody.setVisibility(View.VISIBLE); - body = new SpannableStringBuilder(messageBody); - MyLinkify.addLinks(body, false); - viewHolder.messageBody.setText(body); - viewHolder.messageBody.setAutoLinkMask(0); - viewHolder.messageBody.setTextIsSelectable(true); - viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance()); - } else { - viewHolder.messageBody.setVisibility(View.GONE); - } - } - } - - private void loadMoreMessages(Conversation conversation) { - conversation.setLastClearHistory(0, null); - activity.xmppConnectionService.updateConversation(conversation); - conversation.setHasMessagesLeftOnServer(true); - conversation.setFirstMamReference(null); - long timestamp = conversation.getLastMessageTransmitted().getTimestamp(); - if (timestamp == 0) { - timestamp = System.currentTimeMillis(); - } - conversation.messagesLoaded.set(true); - MessageArchiveService.Query query = activity.xmppConnectionService.getMessageArchiveService().query(conversation, new MamReference(0), timestamp, false); - if (query != null) { - ToastCompat.makeText(activity, R.string.fetching_history_from_server, ToastCompat.LENGTH_LONG).show(); - } else { - ToastCompat.makeText(activity, R.string.not_fetching_history_retention_period, ToastCompat.LENGTH_SHORT).show(); - } - } - - @SuppressLint("StringFormatInvalid") - @Override - public View getView(int position, View view, ViewGroup parent) { - final Message message = getItem(position); - final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL; - final boolean isInValidSession = message.isValidInSession() && (!omemoEncryption || message.isTrusted()); - final Conversational conversation = message.getConversation(); - final Account account = conversation.getAccount(); - final int type = getItemViewType(position); - ViewHolder viewHolder; - if (view == null) { - viewHolder = new ViewHolder(); - switch (type) { - case DATE_SEPARATOR: - view = activity.getLayoutInflater().inflate(R.layout.message_date_bubble, parent, false); - viewHolder.status_message = view.findViewById(R.id.status_message); - viewHolder.message_box = view.findViewById(R.id.message_box); - viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); - break; - case RTP_SESSION: - view = activity.getLayoutInflater().inflate(R.layout.message_rtp_session, parent, false); - viewHolder.status_message = view.findViewById(R.id.message_body); - viewHolder.message_box = view.findViewById(R.id.message_box); - viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); - break; - case SENT: - view = activity.getLayoutInflater().inflate(R.layout.message_sent, parent, false); - viewHolder.message_box = view.findViewById(R.id.message_box); - viewHolder.contact_picture = view.findViewById(R.id.message_photo); - viewHolder.username = view.findViewById(R.id.username); - viewHolder.audioPlayer = view.findViewById(R.id.audio_player); - viewHolder.download_button = view.findViewById(R.id.download_button); - viewHolder.resend_button = view.findViewById(R.id.resend_button); - viewHolder.indicator = view.findViewById(R.id.security_indicator); - viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); - viewHolder.retract_indicator = view.findViewById(R.id.retract_indicator); - viewHolder.images = view.findViewById(R.id.images); - viewHolder.mediaduration = view.findViewById(R.id.media_duration); - viewHolder.image = view.findViewById(R.id.message_image); - viewHolder.gifImage = view.findViewById(R.id.message_image_gif); - viewHolder.richlinkview = view.findViewById(R.id.richLinkView); - viewHolder.messageBody = view.findViewById(R.id.message_body); - viewHolder.user = view.findViewById(R.id.message_user); - viewHolder.time = view.findViewById(R.id.message_time); - viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); - viewHolder.transfer = view.findViewById(R.id.transfer); - viewHolder.progressBar = view.findViewById(R.id.progressBar); - viewHolder.cancel_transfer = view.findViewById(R.id.cancel_transfer); - break; - case RECEIVED: - view = activity.getLayoutInflater().inflate(R.layout.message_received, parent, false); - viewHolder.message_box = view.findViewById(R.id.message_box); - viewHolder.contact_picture = view.findViewById(R.id.message_photo); - viewHolder.username = view.findViewById(R.id.username); - viewHolder.audioPlayer = view.findViewById(R.id.audio_player); - viewHolder.download_button = view.findViewById(R.id.download_button); - viewHolder.answer_button = view.findViewById(R.id.answer); - viewHolder.indicator = view.findViewById(R.id.security_indicator); - viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); - viewHolder.retract_indicator = view.findViewById(R.id.retract_indicator); - viewHolder.images = view.findViewById(R.id.images); - viewHolder.mediaduration = view.findViewById(R.id.media_duration); - viewHolder.image = view.findViewById(R.id.message_image); - viewHolder.gifImage = view.findViewById(R.id.message_image_gif); - viewHolder.richlinkview = view.findViewById(R.id.richLinkView); - viewHolder.messageBody = view.findViewById(R.id.message_body); - viewHolder.user = view.findViewById(R.id.message_user); - viewHolder.time = view.findViewById(R.id.message_time); - viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); - viewHolder.encryption = view.findViewById(R.id.message_encryption); - viewHolder.transfer = view.findViewById(R.id.transfer); - viewHolder.progressBar = view.findViewById(R.id.progressBar); - viewHolder.cancel_transfer = view.findViewById(R.id.cancel_transfer); - break; - case STATUS: - view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false); - viewHolder.contact_picture = view.findViewById(R.id.message_photo); - viewHolder.status_message = view.findViewById(R.id.status_message); - viewHolder.load_more_messages = view.findViewById(R.id.load_more_messages); - break; - default: - throw new AssertionError("Unknown view type"); - } - view.setTag(viewHolder); - } else { - viewHolder = (ViewHolder) view.getTag(); - if (viewHolder == null) { - return view; - } - } - - boolean darkBackground = activity.isDarkTheme(); - - if (type == DATE_SEPARATOR) { - if (UIHelper.today(message.getTimeSent())) { - viewHolder.status_message.setText(R.string.today); - } else if (UIHelper.yesterday(message.getTimeSent())) { - viewHolder.status_message.setText(R.string.yesterday); - } else { - viewHolder.status_message.setText(DateUtils.formatDateTime(activity, message.getTimeSent(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR)); - } - viewHolder.message_box.setBackgroundResource(darkBackground ? R.drawable.date_bubble_dark : R.drawable.date_bubble); - int date_bubble_color = ColorUtils.setAlphaComponent(StyledAttributes.getColor(activity, R.attr.colorAccent), 80); //set alpha to date_bubble - activity.setBubbleColor(viewHolder.message_box, (date_bubble_color), -1); // themed color - return view; - } else if (type == RTP_SESSION) { - final boolean isDarkTheme = activity.isDarkTheme(); - final boolean received = message.getStatus() <= Message.STATUS_RECEIVED; - final String formattedTime = UIHelper.readableTimeDifferenceFull(activity, message.getMergedTimeSent()); - final RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody()); - final long duration = rtpSessionStatus.duration; - if (received) { - if (duration > 0) { - viewHolder.status_message.setText(activity.getString(R.string.incoming_call_duration, formattedTime, TimeFrameUtils.resolve(activity, duration))); - } else { - viewHolder.status_message.setText(activity.getString(R.string.missed_call_timestamp, formattedTime)); - } - } else { - if (duration > 0) { - viewHolder.status_message.setText(activity.getString(R.string.outgoing_call_duration, formattedTime, TimeFrameUtils.resolve(activity, duration))); - } else { - viewHolder.status_message.setText(activity.getString(R.string.outgoing_call_time, formattedTime)); - } - } - viewHolder.indicatorReceived.setImageResource(RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful, isDarkTheme)); - viewHolder.indicatorReceived.setAlpha(isDarkTheme ? 0.7f : 0.57f); - viewHolder.message_box.setBackgroundResource(darkBackground ? R.drawable.date_bubble_dark : R.drawable.date_bubble); - int date_bubble_color = ColorUtils.setAlphaComponent(StyledAttributes.getColor(activity, R.attr.colorAccent), 80); //set alpha to date bubble - activity.setBubbleColor(viewHolder.message_box, (date_bubble_color), -1); //themed color - return view; - } else if (type == STATUS) { - if ("LOAD_MORE".equals(message.getBody())) { - viewHolder.status_message.setVisibility(View.GONE); - viewHolder.contact_picture.setVisibility(View.GONE); - viewHolder.load_more_messages.setVisibility(View.VISIBLE); - viewHolder.load_more_messages.setOnClickListener(v -> loadMoreMessages((Conversation) message.getConversation())); - } else { - viewHolder.status_message.setVisibility(View.VISIBLE); - viewHolder.load_more_messages.setVisibility(View.GONE); - viewHolder.status_message.setText(message.getBody()); - boolean showAvatar; - if (conversation.getMode() == Conversation.MODE_SINGLE) { - showAvatar = true; - AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar_on_status_message); - } else if (message.getCounterpart() != null || message.getTrueCounterpart() != null || (message.getCounterparts() != null && message.getCounterparts().size() > 0)) { - showAvatar = true; - AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar_on_status_message); - } else { - showAvatar = false; - } - if (showAvatar) { - viewHolder.contact_picture.setAlpha(0.5f); - viewHolder.contact_picture.setVisibility(View.VISIBLE); - } else { - viewHolder.contact_picture.setVisibility(View.GONE); - } - } - return view; - } else { - AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar); - } - - resetClickListener(viewHolder.message_box, viewHolder.messageBody); - - viewHolder.contact_picture.setOnClickListener(v -> { - if (MessageAdapter.this.mOnContactPictureClickedListener != null) { - MessageAdapter.this.mOnContactPictureClickedListener.onContactPictureClicked(message); - } - - }); - viewHolder.contact_picture.setOnLongClickListener(v -> { - if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) { - MessageAdapter.this.mOnContactPictureLongClickedListener.onContactPictureLongClicked(v, message); - return true; - } else { - return false; - } - }); - - final Transferable transferable = message.getTransferable(); - final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message); - if (unInitiatedButKnownSize || message.isFileDeleted() || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING)) { - if (unInitiatedButKnownSize || transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER) { - displayDownloadableMessage(viewHolder, message, activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)), darkBackground); - } else if (transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) { - displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)), darkBackground); - } else { - /* todo why should we mark a file as deleted? --> causing strange side effects - if (!activity.xmppConnectionService.getFileBackend().getFile(message).exists() && !message.isFileDeleted()) { - new Thread(new markFileDeletedFinisher(message, activity)).start(); - displayInfoMessage(viewHolder, activity.getString(R.string.file_deleted), darkBackground, message); - }*/ - if (checkFileExistence(message, view, viewHolder)) { - new Thread(new markFileExistingFinisher(message, activity)).start(); - } - displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first, darkBackground, message); - } - } else if (message.isFileOrImage() && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) { - if (message.getFileParams().width > 0 && message.getFileParams().height > 0) { - displayMediaPreviewMessage(viewHolder, message, darkBackground); - } else if (message.getFileParams().runtime > 0 && (message.getFileParams().width == 0 && message.getFileParams().height == 0)) { - displayAudioMessage(viewHolder, message, darkBackground); - } else { - displayOpenableMessage(viewHolder, message, darkBackground); - } - } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { - if (account.isPgpDecryptionServiceConnected()) { - if (conversation instanceof Conversation && !account.hasPendingPgpIntent((Conversation) conversation)) { - displayInfoMessage(viewHolder, activity.getString(R.string.message_decrypting), darkBackground, message); - } else { - displayInfoMessage(viewHolder, activity.getString(R.string.pgp_message), darkBackground, message); - } - } else { - displayInfoMessage(viewHolder, activity.getString(R.string.install_openkeychain), darkBackground, message); - viewHolder.message_box.setOnClickListener(this::promptOpenKeychainInstall); - viewHolder.messageBody.setOnClickListener(this::promptOpenKeychainInstall); - } - } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { - displayInfoMessage(viewHolder, activity.getString(R.string.decryption_failed), darkBackground, message); - } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) { - displayInfoMessage(viewHolder, activity.getString(R.string.not_encrypted_for_this_device), darkBackground, message); - } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) { - displayInfoMessage(viewHolder, activity.getString(R.string.omemo_decryption_failed), darkBackground, message); - } else { - if (message.isGeoUri()) { - displayLocationMessage(viewHolder, message, darkBackground, activity); - } else if (message.bodyIsOnlyEmojis() && message.getType() != Message.TYPE_PRIVATE) { - displayEmojiMessage(viewHolder, message.getBody().trim(), darkBackground); - } else if (message.isXmppUri()) { - displayXmppMessage(viewHolder, message.getBody().trim()); - } 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); - } catch (Exception e) { - displayDownloadableMessage(viewHolder, - message, - activity.getString(R.string.check_x_filesize, - UIHelper.getFileDescriptionString(activity, message)), - darkBackground); - } - } else { - displayTextMessage(viewHolder, message, darkBackground, type); - } - } - - if (type == RECEIVED) { - if (message.isPrivateMessage()) { - viewHolder.answer_button.setVisibility(View.VISIBLE); - Drawable icon = activity.getResources().getDrawable(R.drawable.ic_reply_circle_black_24dp); - Drawable drawable = DrawableCompat.wrap(icon); - DrawableCompat.setTint(drawable, StyledAttributes.getColor(getContext(), R.attr.colorAccent)); - viewHolder.answer_button.setImageDrawable(drawable); - viewHolder.answer_button.setOnClickListener(v -> { - try { - if (activity instanceof ConversationsActivity) { - ConversationFragment conversationFragment = ConversationFragment.get(activity); - if (conversationFragment != null) { - activity.invalidateOptionsMenu(); - conversationFragment.privateMessageWith(message.getCounterpart()); - } - } - } catch (Exception e) { - viewHolder.answer_button.setVisibility(View.GONE); - e.printStackTrace(); - } - }); - } else { - viewHolder.answer_button.setVisibility(View.GONE); - } - if (isInValidSession) { - setBubbleBackgroundColor(viewHolder.message_box, type, message.isPrivateMessage(), isInValidSession); - viewHolder.encryption.setVisibility(View.GONE); - viewHolder.encryption.setTextColor(ThemeHelper.getMessageTextColor(activity, darkBackground, false)); - } else { - setBubbleBackgroundColor(viewHolder.message_box, type, message.isPrivateMessage(), isInValidSession); - viewHolder.encryption.setVisibility(View.VISIBLE); - viewHolder.encryption.setTextColor(ThemeHelper.getWarningTextColor(activity, darkBackground)); - if (omemoEncryption && !message.isTrusted()) { - viewHolder.encryption.setText(R.string.not_trusted); - } else { - viewHolder.encryption.setText(CryptoHelper.encryptionTypeToText(message.getEncryption())); - } - } - } - - if (type == SENT) { - setBubbleBackgroundColor(viewHolder.message_box, type, message.isPrivateMessage(), isInValidSession); - } - displayStatus(viewHolder, message, type, darkBackground); - showAvatar(viewHolder, message, view); - return view; - } - private void showAvatar(ViewHolder viewHolder, Message message, View view) { - if (message.isAvatarable()) { - viewHolder.contact_picture.setVisibility(View.VISIBLE); - int left = ThemeHelper.dp2Px(getContext(), 8); - int top = ThemeHelper.dp2Px(getContext(), 0); - int right = ThemeHelper.dp2Px(getContext(), 8); - int bottom = ThemeHelper.dp2Px(getContext(), 16); - view.setPadding(left, top, right, bottom); - } else { - viewHolder.contact_picture.setVisibility(View.INVISIBLE); - int left = ThemeHelper.dp2Px(getContext(), 8); - int top = ThemeHelper.dp2Px(getContext(), 0); - int right = ThemeHelper.dp2Px(getContext(), 8); - int bottom = ThemeHelper.dp2Px(getContext(), 1); - view.setPadding(left, top, right, bottom); - } - } - - private static class markFileExistingFinisher implements Runnable { - private final Message message; - private final WeakReference activityReference; - - private markFileExistingFinisher(Message message, XmppActivity activity) { - this.message = message; - this.activityReference = new WeakReference<>(activity); - } - - @Override - public void run() { - final XmppActivity activity = activityReference.get(); - if (activity == null) { - return; - } - activity.runOnUiThread( - () -> { - Log.d(Config.LOGTAG, "Found and restored orphaned file " + message.getRelativeFilePath()); - message.setFileDeleted(false); - activity.xmppConnectionService.updateMessage(message, false); - activity.xmppConnectionService.updateConversation((Conversation) message.getConversation()); - }); - } - } - - private static class markFileDeletedFinisher implements Runnable { - private final Message message; - private final WeakReference activityReference; - - private markFileDeletedFinisher(Message message, XmppActivity activity) { - this.message = message; - this.activityReference = new WeakReference<>(activity); - } - - @Override - public void run() { - final XmppActivity activity = activityReference.get(); - if (activity == null) { - return; - } - activity.runOnUiThread( - () -> { - Log.d(Config.LOGTAG, "Mark file deleted " + message.getRelativeFilePath()); - message.setFileDeleted(true); - activity.xmppConnectionService.updateMessage(message, false); - activity.xmppConnectionService.updateConversation((Conversation) message.getConversation()); - }); - } - } - - private boolean checkFileExistence(Message message, View view, ViewHolder viewHolder) { - final Rect scrollBounds = new Rect(); - view.getHitRect(scrollBounds); - if (message.isFileDeleted() && viewHolder.messageBody.getLocalVisibleRect(scrollBounds)) { - return activity.xmppConnectionService.getFileBackend().getFile(message).exists(); - } else { - return false; - } - } - - private void promptOpenKeychainInstall(View view) { - activity.showInstallPgpDialog(); - } - - public FileBackend getFileBackend() { - return activity.xmppConnectionService.getFileBackend(); - } - - public void stopAudioPlayer() { - audioPlayer.stop(); - } - - public void unregisterListenerInAudioPlayer() { - audioPlayer.unregisterListener(); - } - - public void startStopPending() { - audioPlayer.startStopPending(); - } - - public void openDownloadable(Message message) { - if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - ConversationFragment.registerPendingMessage(activity, message); - ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, ConversationsActivity.REQUEST_OPEN_MESSAGE); - return; - } - final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); - ViewUtil.view(activity, file); - } - - private void showLocation(Message message) { - for (Intent intent : GeoHelper.createGeoIntentsFromMessage(this.getContext(), message)) { - if (intent.resolveActivity(getContext().getPackageManager()) != null) { - getContext().startActivity(intent); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - return; - } - } - } - - public void updatePreferences() { - SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity); - this.mIndicateReceived = p.getBoolean("indicate_received", activity.getResources().getBoolean(R.bool.indicate_received)); - this.mPlayGifInside = p.getBoolean(PLAY_GIF_INSIDE, activity.getResources().getBoolean(R.bool.play_gif_inside)); - this.mShowLinksInside = p.getBoolean(SHOW_LINKS_INSIDE, activity.getResources().getBoolean(R.bool.show_links_inside)); - this.mShowMapsInside = p.getBoolean(SHOW_MAPS_INSIDE, activity.getResources().getBoolean(R.bool.show_maps_inside)); - } - - public void setHighlightedTerm(List terms) { - this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms); - } - - public interface OnQuoteListener { - void onQuote(String text, String user); - } - - public interface OnContactPictureClicked { - void onContactPictureClicked(Message message); - } - - public interface OnContactPictureLongClicked { - void onContactPictureLongClicked(View v, Message message); - } - - private static class ViewHolder { - - public Button load_more_messages; - public ImageView edit_indicator; - public ImageView retract_indicator; - public RelativeLayout audioPlayer; - public RelativeLayout images; - protected LinearLayout message_box; - protected Button download_button; - protected Button resend_button; - protected ImageButton answer_button; - protected ImageView image; - protected GifImageView gifImage; - protected TextView mediaduration; - protected RichLinkView richlinkview; - protected ImageView indicator; - protected ImageView indicatorReceived; - protected TextView time; - protected TextView messageBody; - protected TextView user; - protected TextView username; - protected ImageView contact_picture; - protected TextView status_message; - protected TextView encryption; - protected RelativeLayout transfer; - protected ProgressBar progressBar; - protected ImageButton cancel_transfer; - } - - public void setBubbleBackgroundColor(final View viewHolder, final int type, - final boolean isPrivateMessage, final boolean isInValidSession) { - if (type == RECEIVED) { - if (isInValidSession) { - if (isPrivateMessage) { - viewHolder.setBackgroundResource(R.drawable.message_bubble_received_light_private); - activity.setBubbleColor(viewHolder, StyledAttributes.getColor(activity, R.attr.color_bubble_light), StyledAttributes.getColor(activity, R.attr.colorAccent)); - } else { - viewHolder.setBackgroundResource(R.drawable.message_bubble_received_light); - activity.setBubbleColor(viewHolder, StyledAttributes.getColor(activity, R.attr.color_bubble_light), -1); - } - } else { - if (isPrivateMessage) { - viewHolder.setBackgroundResource(R.drawable.message_bubble_received_warning_private); - activity.setBubbleColor(viewHolder, StyledAttributes.getColor(activity, R.attr.color_bubble_warning), StyledAttributes.getColor(activity, R.attr.colorAccent)); - } else { - viewHolder.setBackgroundResource(R.drawable.message_bubble_received_warning); - activity.setBubbleColor(viewHolder, StyledAttributes.getColor(activity, R.attr.color_bubble_warning), -1); - } - } - } - - if (type == SENT) { - if (isPrivateMessage) { - viewHolder.setBackgroundResource(R.drawable.message_bubble_sent_private); - activity.setBubbleColor(viewHolder, StyledAttributes.getColor(activity, R.attr.color_bubble_dark), StyledAttributes.getColor(activity, R.attr.colorAccent)); - } else { - viewHolder.setBackgroundResource(R.drawable.message_bubble_sent); - activity.setBubbleColor(viewHolder, StyledAttributes.getColor(activity, R.attr.color_bubble_dark), -1); - } - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageLogAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageLogAdapter.java deleted file mode 100644 index b1d5e67f5..000000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageLogAdapter.java +++ /dev/null @@ -1,97 +0,0 @@ -package eu.siacs.conversations.ui.adapter; - -import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY; -import static eu.siacs.conversations.entities.Message.DELETED_MESSAGE_BODY_OLD; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; -import android.widget.ArrayAdapter; -import android.widget.TextView; - -import java.util.ArrayList; -import java.util.Date; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.ui.adapter.model.MessageLogModel; - -public class MessageLogAdapter extends ArrayAdapter implements View.OnClickListener { - - private final ArrayList dataSet; - Context mContext; - - // View lookup cache - private static class ViewHolder { - TextView txtLineNr; - TextView txtBody; - TextView txtTimeSent; - } - - public MessageLogAdapter(ArrayList data, Context context) { - super(context, R.layout.message_log_item, data); - this.dataSet = data; - this.mContext = context; - } - - @Override - public void onClick(View v) { - - } - - private int lastPosition = -1; - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - // Get the data item for this position - MessageLogModel dataModel = getItem(position); - // Check if an existing view is being reused, otherwise inflate the view - ViewHolder viewHolder; // view lookup cache stored in tag - - final View result; - - if (convertView == null) { - - viewHolder = new ViewHolder(); - LayoutInflater inflater = LayoutInflater.from(getContext()); - convertView = inflater.inflate(R.layout.message_log_item, parent, false); - viewHolder.txtLineNr = convertView.findViewById(R.id.nr); - viewHolder.txtBody = convertView.findViewById(R.id.body); - viewHolder.txtTimeSent = convertView.findViewById(R.id.timeSent); - - result = convertView; - - convertView.setTag(viewHolder); - } else { - viewHolder = (ViewHolder) convertView.getTag(); - result = convertView; - } - - Animation animation = AnimationUtils.loadAnimation(mContext, (position > lastPosition) ? R.anim.ufb : R.anim.dft); - result.startAnimation(animation); - lastPosition = position; - viewHolder.txtLineNr.setText(String.valueOf(position + 1)); - viewHolder.txtBody.setText(preview(dataModel)); - viewHolder.txtTimeSent.setText(getTimeSentFormated(dataModel.getTimeSent())); - // Return the completed view to render on screen - return convertView; - } - - private String getTimeSentFormated(long timeSent) { - return android.text.format.DateFormat.getDateFormat(getContext()).format(new Date(timeSent)) + " " + android.text.format.DateFormat.getTimeFormat(getContext()).format(new Date(timeSent)); - } - - private String preview(MessageLogModel dataModel) { - if (hasDeletedBody(dataModel.getBody())) { - return getContext().getString(R.string.message_deleted); - } else { - return dataModel.getBody(); - } - } - - public boolean hasDeletedBody(String message) { - return message.trim().equals(DELETED_MESSAGE_BODY) || message.trim().equals(DELETED_MESSAGE_BODY_OLD); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/PresenceTemplateAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/PresenceTemplateAdapter.java deleted file mode 100644 index 7f533fd26..000000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/PresenceTemplateAdapter.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui.adapter; - -import android.content.Context; -import android.widget.ArrayAdapter; -import android.widget.Filter; - -import androidx.annotation.NonNull; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -import eu.siacs.conversations.entities.PresenceTemplate; - - -public class PresenceTemplateAdapter extends ArrayAdapter { - - private final List templates; - - private final Filter filter = new Filter() { - - @Override - protected FilterResults performFiltering(CharSequence constraint) { - FilterResults results = new FilterResults(); - if (constraint == null || constraint.length() == 0) { - results.values = new ArrayList<>(templates); - results.count = templates.size(); - } else { - ArrayList suggestions = new ArrayList<>(); - final String needle = constraint.toString().trim().toLowerCase(Locale.getDefault()); - for (PresenceTemplate template : templates) { - final String lc = template.getStatusMessage().toLowerCase(Locale.getDefault()); - if (needle.isEmpty() || lc.contains(needle)) { - suggestions.add(template); - } - } - results.values = suggestions; - results.count = suggestions.size(); - } - return results; - } - - @Override - protected void publishResults(CharSequence constraint, FilterResults results) { - ArrayList filteredList = (ArrayList) results.values; - clear(); - for (Object c : filteredList) { - add((PresenceTemplate) c); - } - notifyDataSetChanged(); - } - }; - - public PresenceTemplateAdapter(@NonNull Context context, int resource, @NonNull List templates) { - super(context, resource, new ArrayList<>()); - this.templates = new ArrayList<>(templates); - } - - @Override - @NonNull - public Filter getFilter() { - return this.filter; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java deleted file mode 100644 index edcbdbf65..000000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/UserAdapter.java +++ /dev/null @@ -1,138 +0,0 @@ -package eu.siacs.conversations.ui.adapter; - -import android.app.PendingIntent; -import android.content.IntentSender; -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.databinding.DataBindingUtil; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.ListAdapter; -import androidx.recyclerview.widget.RecyclerView; - -import org.openintents.openpgp.util.OpenPgpUtils; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.PgpEngine; -import eu.siacs.conversations.databinding.ContactBinding; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.ConferenceDetailsActivity; -import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.ui.util.AvatarWorkerTask; -import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper; -import eu.siacs.conversations.xmpp.Jid; - -public class UserAdapter extends ListAdapter implements View.OnCreateContextMenuListener { - - static final DiffUtil.ItemCallback DIFF = new DiffUtil.ItemCallback() { - @Override - public boolean areItemsTheSame(@NonNull MucOptions.User a, @NonNull MucOptions.User b) { - final Jid fullA = a.getFullJid(); - final Jid fullB = b.getFullJid(); - final Jid realA = a.getRealJid(); - final Jid realB = b.getRealJid(); - if (fullA != null && fullB != null) { - return fullA.equals(fullB); - } else if (realA != null && realB != null) { - return realA.equals(realB); - } else { - return false; - } - } - - @Override - public boolean areContentsTheSame(@NonNull MucOptions.User a, @NonNull MucOptions.User b) { - return a.equals(b); - } - }; - private final boolean advancedMode; - private MucOptions.User selectedUser = null; - - public UserAdapter(final boolean advancedMode) { - super(DIFF); - this.advancedMode = advancedMode; - } - - @NonNull - @Override - public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int position) { - return new ViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.contact, viewGroup, false)); - } - - @Override - public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) { - final MucOptions.User user = getItem(position); - AvatarWorkerTask.loadAvatar(user, viewHolder.binding.contactPhoto, R.dimen.avatar_on_details_screen_size); - viewHolder.binding.getRoot().setOnClickListener(v -> { - final XmppActivity activity = XmppActivity.find(v); - if (activity != null) { - activity.highlightInMuc(user.getConversation(), user.getName()); - } - }); - viewHolder.binding.getRoot().setTag(user); - viewHolder.binding.getRoot().setOnCreateContextMenuListener(this); - viewHolder.binding.getRoot().setOnLongClickListener(v -> { - selectedUser = user; - return false; - }); - final String name = user.getName(); - final Contact contact = user.getContact(); - if (contact != null) { - final String displayName = contact.getDisplayName(); - viewHolder.binding.contactDisplayName.setText(displayName); - if (name != null && !name.equals(displayName)) { - viewHolder.binding.contactJid.setText(String.format("%s \u2022 %s", name, ConferenceDetailsActivity.getStatus(viewHolder.binding.getRoot().getContext(), user, advancedMode))); - } else { - viewHolder.binding.contactJid.setText(ConferenceDetailsActivity.getStatus(viewHolder.binding.getRoot().getContext(), user, advancedMode)); - } - } else { - viewHolder.binding.contactDisplayName.setText(name == null ? "" : name); - viewHolder.binding.contactJid.setText(ConferenceDetailsActivity.getStatus(viewHolder.binding.getRoot().getContext(), user, advancedMode)); - } - if (advancedMode && user.getPgpKeyId() != 0) { - viewHolder.binding.key.setVisibility(View.VISIBLE); - viewHolder.binding.key.setOnClickListener(v -> { - final XmppActivity activity = XmppActivity.find(v); - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; - final PgpEngine pgpEngine = service == null ? null : service.getPgpEngine(); - if (pgpEngine != null) { - PendingIntent intent = pgpEngine.getIntentForKey(user.getPgpKeyId()); - if (intent != null) { - try { - activity.startIntentSenderForResult(intent.getIntentSender(), 0, null, 0, 0, 0); - } catch (IntentSender.SendIntentException ignored) { - - } - } - } - }); - viewHolder.binding.key.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId())); - } else { - viewHolder.binding.key.setVisibility(View.GONE); - } - } - - public MucOptions.User getSelectedUser() { - return selectedUser; - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - MucDetailsContextMenuHelper.onCreateContextMenu(menu, v); - } - - class ViewHolder extends RecyclerView.ViewHolder { - - private final ContactBinding binding; - - private ViewHolder(ContactBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/UserPreviewAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/UserPreviewAdapter.java deleted file mode 100644 index 4a0d490eb..000000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/UserPreviewAdapter.java +++ /dev/null @@ -1,70 +0,0 @@ -package eu.siacs.conversations.ui.adapter; - -import android.view.ContextMenu; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.databinding.DataBindingUtil; -import androidx.recyclerview.widget.ListAdapter; -import androidx.recyclerview.widget.RecyclerView; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.databinding.UserPreviewBinding; -import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.ui.util.AvatarWorkerTask; -import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper; - -public class UserPreviewAdapter extends ListAdapter implements View.OnCreateContextMenuListener { - - private MucOptions.User selectedUser = null; - - public UserPreviewAdapter() { - super(UserAdapter.DIFF); - } - - @NonNull - @Override - public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int position) { - return new ViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.user_preview, viewGroup, false)); - } - - @Override - public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) { - final MucOptions.User user = getItem(position); - AvatarWorkerTask.loadAvatar(user, viewHolder.binding.avatar, R.dimen.media_size); - viewHolder.binding.getRoot().setOnClickListener(v -> { - final XmppActivity activity = XmppActivity.find(v); - if (activity != null) { - activity.highlightInMuc(user.getConversation(), user.getName()); - } - }); - viewHolder.binding.getRoot().setOnCreateContextMenuListener(this); - viewHolder.binding.getRoot().setTag(user); - viewHolder.binding.getRoot().setOnLongClickListener(v -> { - selectedUser = user; - return false; - }); - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - MucDetailsContextMenuHelper.onCreateContextMenu(menu, v); - } - - public MucOptions.User getSelectedUser() { - return selectedUser; - } - - class ViewHolder extends RecyclerView.ViewHolder { - - private final UserPreviewBinding binding; - - private ViewHolder(UserPreviewBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/model/MessageLogModel.java b/src/main/java/eu/siacs/conversations/ui/adapter/model/MessageLogModel.java deleted file mode 100644 index d75db3c91..000000000 --- a/src/main/java/eu/siacs/conversations/ui/adapter/model/MessageLogModel.java +++ /dev/null @@ -1,21 +0,0 @@ -package eu.siacs.conversations.ui.adapter.model; - -public class MessageLogModel { - - String body; - long timeSent; - - public MessageLogModel(String body, long timeSent) { - - this.body = body; - this.timeSent = timeSent; - } - - public String getBody() { - return body; - } - - public long getTimeSent() { - return timeSent; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/forms/FormBooleanFieldWrapper.java b/src/main/java/eu/siacs/conversations/ui/forms/FormBooleanFieldWrapper.java deleted file mode 100644 index f701058ee..000000000 --- a/src/main/java/eu/siacs/conversations/ui/forms/FormBooleanFieldWrapper.java +++ /dev/null @@ -1,80 +0,0 @@ -package eu.siacs.conversations.ui.forms; - -import android.content.Context; -import android.widget.CheckBox; -import android.widget.CompoundButton; - -import java.util.ArrayList; -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.xmpp.forms.Field; - -public class FormBooleanFieldWrapper extends FormFieldWrapper { - - protected CheckBox checkBox; - - protected FormBooleanFieldWrapper(Context context, Field field) { - super(context, field); - checkBox = view.findViewById(R.id.field); - checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - checkBox.setError(null); - invokeOnFormFieldValuesEdited(); - } - }); - } - - @Override - protected void setLabel(String label, boolean required) { - CheckBox checkBox = view.findViewById(R.id.field); - checkBox.setText(createSpannableLabelString(label, required)); - } - - @Override - public List getValues() { - List values = new ArrayList<>(); - values.add(Boolean.toString(checkBox.isChecked())); - return values; - } - - @Override - protected void setValues(List values) { - if (values.size() == 0) { - checkBox.setChecked(false); - } else { - checkBox.setChecked(Boolean.parseBoolean(values.get(0))); - } - } - - @Override - public boolean validates() { - if (checkBox.isChecked() || !field.isRequired()) { - return true; - } else { - checkBox.setError(context.getString(R.string.this_field_is_required)); - checkBox.requestFocus(); - return false; - } - } - - @Override - public boolean edited() { - if (field.getValues().size() == 0) { - return checkBox.isChecked(); - } else { - return super.edited(); - } - } - - @Override - protected int getLayoutResource() { - return R.layout.form_boolean; - } - - @Override - void setReadOnly(boolean readOnly) { - checkBox.setEnabled(!readOnly); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/forms/FormFieldFactory.java b/src/main/java/eu/siacs/conversations/ui/forms/FormFieldFactory.java deleted file mode 100644 index 229080a92..000000000 --- a/src/main/java/eu/siacs/conversations/ui/forms/FormFieldFactory.java +++ /dev/null @@ -1,29 +0,0 @@ -package eu.siacs.conversations.ui.forms; - -import android.content.Context; - -import java.util.Hashtable; - -import eu.siacs.conversations.xmpp.forms.Field; - - -public class FormFieldFactory { - - private static final Hashtable typeTable = new Hashtable<>(); - - static { - typeTable.put("text-single", FormTextFieldWrapper.class); - typeTable.put("text-multi", FormTextFieldWrapper.class); - typeTable.put("text-private", FormTextFieldWrapper.class); - typeTable.put("jid-single", FormJidSingleFieldWrapper.class); - typeTable.put("boolean", FormBooleanFieldWrapper.class); - } - - protected static FormFieldWrapper createFromField(Context context, Field field) { - Class clazz = typeTable.get(field.getType()); - if (clazz == null) { - clazz = FormTextFieldWrapper.class; - } - return FormFieldWrapper.createFromField(clazz, context, field); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/forms/FormFieldWrapper.java b/src/main/java/eu/siacs/conversations/ui/forms/FormFieldWrapper.java deleted file mode 100644 index 18cec2770..000000000 --- a/src/main/java/eu/siacs/conversations/ui/forms/FormFieldWrapper.java +++ /dev/null @@ -1,94 +0,0 @@ -package eu.siacs.conversations.ui.forms; - -import android.content.Context; -import android.text.SpannableString; -import android.text.style.ForegroundColorSpan; -import android.text.style.StyleSpan; -import android.view.LayoutInflater; -import android.view.View; - -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.ui.util.StyledAttributes; -import eu.siacs.conversations.xmpp.forms.Field; - -public abstract class FormFieldWrapper { - - protected final Context context; - protected final Field field; - protected final View view; - OnFormFieldValuesEdited onFormFieldValuesEditedListener; - - FormFieldWrapper(Context context, Field field) { - this.context = context; - this.field = field; - LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - this.view = inflater.inflate(getLayoutResource(), null); - String label = field.getLabel(); - if (label == null) { - label = field.getFieldName(); - } - setLabel(label, field.isRequired()); - } - - public final void submit() { - this.field.setValues(getValues()); - } - - public final View getView() { - return view; - } - - protected abstract void setLabel(String label, boolean required); - - abstract List getValues(); - - protected abstract void setValues(List values); - - abstract boolean validates(); - - abstract protected int getLayoutResource(); - - abstract void setReadOnly(boolean readOnly); - - protected SpannableString createSpannableLabelString(String label, boolean required) { - SpannableString spannableString = new SpannableString(label + (required ? " *" : "")); - if (required) { - int start = label.length(); - int end = label.length() + 2; - spannableString.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), start, end, 0); - spannableString.setSpan(new ForegroundColorSpan(StyledAttributes.getColor(context, R.attr.colorAccent)), start, end, 0); - } - return spannableString; - } - - protected void invokeOnFormFieldValuesEdited() { - if (this.onFormFieldValuesEditedListener != null) { - this.onFormFieldValuesEditedListener.onFormFieldValuesEdited(); - } - } - - public boolean edited() { - return !field.getValues().equals(getValues()); - } - - public void setOnFormFieldValuesEditedListener(OnFormFieldValuesEdited listener) { - this.onFormFieldValuesEditedListener = listener; - } - - protected static FormFieldWrapper createFromField(Class c, Context context, Field field) { - try { - F fieldWrapper = c.getDeclaredConstructor(Context.class, Field.class).newInstance(context, field); - fieldWrapper.setValues(field.getValues()); - return fieldWrapper; - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - - public interface OnFormFieldValuesEdited { - void onFormFieldValuesEdited(); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/forms/FormJidSingleFieldWrapper.java b/src/main/java/eu/siacs/conversations/ui/forms/FormJidSingleFieldWrapper.java deleted file mode 100644 index dc1bbf317..000000000 --- a/src/main/java/eu/siacs/conversations/ui/forms/FormJidSingleFieldWrapper.java +++ /dev/null @@ -1,43 +0,0 @@ -package eu.siacs.conversations.ui.forms; - -import android.content.Context; -import android.text.InputType; - -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.forms.Field; - -public class FormJidSingleFieldWrapper extends FormTextFieldWrapper { - - protected FormJidSingleFieldWrapper(Context context, Field field) { - super(context, field); - editText.setInputType(InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); - editText.setHint(R.string.account_settings_example_jabber_id); - } - - @Override - public boolean validates() { - String value = getValue(); - if (!value.isEmpty()) { - try { - Jid.of(value); - } catch (IllegalArgumentException e) { - editText.setError(context.getString(R.string.invalid_jid)); - editText.requestFocus(); - return false; - } - } - return super.validates(); - } - - @Override - protected void setValues(List values) { - StringBuilder builder = new StringBuilder(""); - for (String value : values) { - builder.append(value); - } - editText.setText(builder.toString()); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/forms/FormTextFieldWrapper.java b/src/main/java/eu/siacs/conversations/ui/forms/FormTextFieldWrapper.java deleted file mode 100644 index a197a0ebe..000000000 --- a/src/main/java/eu/siacs/conversations/ui/forms/FormTextFieldWrapper.java +++ /dev/null @@ -1,97 +0,0 @@ -package eu.siacs.conversations.ui.forms; - -import android.content.Context; -import android.text.Editable; -import android.text.InputType; -import android.text.TextWatcher; -import android.widget.EditText; -import android.widget.TextView; - -import java.util.ArrayList; -import java.util.List; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.xmpp.forms.Field; - -public class FormTextFieldWrapper extends FormFieldWrapper { - - protected EditText editText; - - protected FormTextFieldWrapper(Context context, Field field) { - super(context, field); - editText = view.findViewById(R.id.field); - editText.setSingleLine(!"text-multi".equals(field.getType())); - if ("text-private".equals(field.getType())) { - editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - } - editText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - editText.setError(null); - invokeOnFormFieldValuesEdited(); - } - - @Override - public void afterTextChanged(Editable s) { - } - }); - } - - @Override - protected void setLabel(String label, boolean required) { - TextView textView = view.findViewById(R.id.label); - textView.setText(createSpannableLabelString(label, required)); - } - - protected String getValue() { - return editText.getText().toString(); - } - - @Override - public List getValues() { - List values = new ArrayList<>(); - for (String line : getValue().split("\\n")) { - if (line.length() > 0) { - values.add(line); - } - } - return values; - } - - @Override - protected void setValues(List values) { - StringBuilder builder = new StringBuilder(""); - for (int i = 0; i < values.size(); ++i) { - builder.append(values.get(i)); - if (i < values.size() - 1 && "text-multi".equals(field.getType())) { - builder.append("\n"); - } - } - editText.setText(builder.toString()); - } - - @Override - public boolean validates() { - if (getValue().trim().length() > 0 || !field.isRequired()) { - return true; - } else { - editText.setError(context.getString(R.string.this_field_is_required)); - editText.requestFocus(); - return false; - } - } - - @Override - protected int getLayoutResource() { - return R.layout.form_text; - } - - @Override - void setReadOnly(boolean readOnly) { - editText.setEnabled(!readOnly); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/forms/FormWrapper.java b/src/main/java/eu/siacs/conversations/ui/forms/FormWrapper.java deleted file mode 100644 index 2a27f3c79..000000000 --- a/src/main/java/eu/siacs/conversations/ui/forms/FormWrapper.java +++ /dev/null @@ -1,72 +0,0 @@ -package eu.siacs.conversations.ui.forms; - -import android.content.Context; -import android.widget.LinearLayout; - -import java.util.ArrayList; -import java.util.List; - -import eu.siacs.conversations.xmpp.forms.Data; -import eu.siacs.conversations.xmpp.forms.Field; - -public class FormWrapper { - - private final LinearLayout layout; - - private final Data form; - - private final List fieldWrappers = new ArrayList<>(); - - private FormWrapper(Context context, LinearLayout linearLayout, Data form) { - this.form = form; - this.layout = linearLayout; - this.layout.removeAllViews(); - for (Field field : form.getFields()) { - FormFieldWrapper fieldWrapper = FormFieldFactory.createFromField(context, field); - if (fieldWrapper != null) { - layout.addView(fieldWrapper.getView()); - fieldWrappers.add(fieldWrapper); - } - } - } - - public Data submit() { - for (FormFieldWrapper fieldWrapper : fieldWrappers) { - fieldWrapper.submit(); - } - this.form.submit(); - return this.form; - } - - public boolean validates() { - boolean validates = true; - for (FormFieldWrapper fieldWrapper : fieldWrappers) { - validates &= fieldWrapper.validates(); - } - return validates; - } - - public void setOnFormFieldValuesEditedListener(FormFieldWrapper.OnFormFieldValuesEdited listener) { - for (FormFieldWrapper fieldWrapper : fieldWrappers) { - fieldWrapper.setOnFormFieldValuesEditedListener(listener); - } - } - - public void setReadOnly(boolean b) { - for (FormFieldWrapper fieldWrapper : fieldWrappers) { - fieldWrapper.setReadOnly(b); - } - } - - public boolean edited() { - boolean edited = false; - for (FormFieldWrapper fieldWrapper : fieldWrappers) { - edited |= fieldWrapper.edited(); - } - return edited; - } - - public static FormWrapper createInLayout(Context context, LinearLayout layout, Data form) { - return new FormWrapper(context, layout, form); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/interfaces/OnAvatarPublication.java b/src/main/java/eu/siacs/conversations/ui/interfaces/OnAvatarPublication.java deleted file mode 100644 index 547cd4e25..000000000 --- a/src/main/java/eu/siacs/conversations/ui/interfaces/OnAvatarPublication.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package eu.siacs.conversations.ui.interfaces; - -import androidx.annotation.StringRes; - -public interface OnAvatarPublication { - - void onAvatarPublicationSucceeded(); - - void onAvatarPublicationFailed(@StringRes int res); - -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/interfaces/OnBackendConnected.java b/src/main/java/eu/siacs/conversations/ui/interfaces/OnBackendConnected.java deleted file mode 100644 index 64787e7ce..000000000 --- a/src/main/java/eu/siacs/conversations/ui/interfaces/OnBackendConnected.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package eu.siacs.conversations.ui.interfaces; - -public interface OnBackendConnected { - - void onBackendConnected(); - -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/interfaces/OnConversationArchived.java b/src/main/java/eu/siacs/conversations/ui/interfaces/OnConversationArchived.java deleted file mode 100644 index fa281bfaa..000000000 --- a/src/main/java/eu/siacs/conversations/ui/interfaces/OnConversationArchived.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui.interfaces; - -import eu.siacs.conversations.entities.Conversation; - -public interface OnConversationArchived { - void onConversationArchived(Conversation conversation); -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/interfaces/OnConversationRead.java b/src/main/java/eu/siacs/conversations/ui/interfaces/OnConversationRead.java deleted file mode 100644 index 3cc2be7d6..000000000 --- a/src/main/java/eu/siacs/conversations/ui/interfaces/OnConversationRead.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui.interfaces; - -import eu.siacs.conversations.entities.Conversation; - -public interface OnConversationRead { - void onConversationRead(Conversation conversation, String upToUuid); -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/interfaces/OnConversationSelected.java b/src/main/java/eu/siacs/conversations/ui/interfaces/OnConversationSelected.java deleted file mode 100644 index 14ea9116d..000000000 --- a/src/main/java/eu/siacs/conversations/ui/interfaces/OnConversationSelected.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui.interfaces; - -import eu.siacs.conversations.entities.Conversation; - -public interface OnConversationSelected { - void onConversationSelected(Conversation conversation); -} diff --git a/src/main/java/eu/siacs/conversations/ui/interfaces/OnConversationsListItemUpdated.java b/src/main/java/eu/siacs/conversations/ui/interfaces/OnConversationsListItemUpdated.java deleted file mode 100644 index 5e6f61566..000000000 --- a/src/main/java/eu/siacs/conversations/ui/interfaces/OnConversationsListItemUpdated.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui.interfaces; - -public interface OnConversationsListItemUpdated { - void onConversationsListItemUpdated(); -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/interfaces/OnMediaLoaded.java b/src/main/java/eu/siacs/conversations/ui/interfaces/OnMediaLoaded.java deleted file mode 100644 index 288a76be9..000000000 --- a/src/main/java/eu/siacs/conversations/ui/interfaces/OnMediaLoaded.java +++ /dev/null @@ -1,10 +0,0 @@ -package eu.siacs.conversations.ui.interfaces; - -import java.util.List; - -import eu.siacs.conversations.ui.util.Attachment; - -public interface OnMediaLoaded { - - void onMediaLoaded(List attachments); -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/interfaces/OnSearchResultsAvailable.java b/src/main/java/eu/siacs/conversations/ui/interfaces/OnSearchResultsAvailable.java deleted file mode 100644 index 5dba9614d..000000000 --- a/src/main/java/eu/siacs/conversations/ui/interfaces/OnSearchResultsAvailable.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package eu.siacs.conversations.ui.interfaces; - -import java.util.List; - -import eu.siacs.conversations.entities.Message; - -public interface OnSearchResultsAvailable { - - void onSearchResultsAvailable(List term, List messages); - -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/service/CameraManager.java b/src/main/java/eu/siacs/conversations/ui/service/CameraManager.java deleted file mode 100644 index b63ea052e..000000000 --- a/src/main/java/eu/siacs/conversations/ui/service/CameraManager.java +++ /dev/null @@ -1,321 +0,0 @@ -/* - * Copyright 2012-2015 the original author or authors. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package eu.siacs.conversations.ui.service; - -import android.annotation.SuppressLint; -import android.graphics.Rect; -import android.graphics.RectF; -import android.hardware.Camera; -import android.hardware.Camera.CameraInfo; -import android.util.Log; -import android.view.TextureView; - -import com.google.zxing.PlanarYUVLuminanceSource; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -import eu.siacs.conversations.Config; - -/** - * @author Andreas Schildbach - */ -@SuppressWarnings("deprecation") -public final class CameraManager { - private static final int MIN_FRAME_SIZE = 240; - private static final int MAX_FRAME_SIZE = 600; - private static final int MIN_PREVIEW_PIXELS = 470 * 320; // normal screen - private static final int MAX_PREVIEW_PIXELS = 1280 * 720; - - private Camera camera; - private CameraInfo cameraInfo = new CameraInfo(); - private Camera.Size cameraResolution; - private Rect frame; - private RectF framePreview; - - public Rect getFrame() { - return frame; - } - - public RectF getFramePreview() { - return framePreview; - } - - public int getFacing() { - return cameraInfo.facing; - } - - public int getOrientation() { - return cameraInfo.orientation; - } - - public Camera open(final TextureView textureView, final int displayOrientation, final boolean continuousAutoFocus) - throws IOException { - final int cameraId = determineCameraId(); - Camera.getCameraInfo(cameraId, cameraInfo); - - camera = Camera.open(cameraId); - - if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) - camera.setDisplayOrientation((720 - displayOrientation - cameraInfo.orientation) % 360); - else if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) - camera.setDisplayOrientation((720 - displayOrientation + cameraInfo.orientation) % 360); - else - throw new IllegalStateException("facing: " + cameraInfo.facing); - - camera.setPreviewTexture(textureView.getSurfaceTexture()); - - final int width = textureView.getWidth(); - final int height = textureView.getHeight(); - - final Camera.Parameters parameters = camera.getParameters(); - - cameraResolution = findBestPreviewSizeValue(parameters, width, height); - - final int rawSize = Math.min(width * 2 / 3, height * 2 / 3); - final int frameSize = Math.max(MIN_FRAME_SIZE, Math.min(MAX_FRAME_SIZE, rawSize)); - - final int leftOffset = (width - frameSize) / 2; - final int topOffset = (height - frameSize) / 2; - frame = new Rect(leftOffset, topOffset, leftOffset + frameSize, topOffset + frameSize); - - float widthFactor; - float heightFactor; - Rect orientedFrame; - boolean isTexturePortrait = width < height; - boolean isCameraPortrait = cameraResolution.width < cameraResolution.height; - if (isTexturePortrait == isCameraPortrait) { - widthFactor = (float)cameraResolution.width / width; - heightFactor = (float)cameraResolution.height / height; - orientedFrame = new Rect(frame); - } else { - widthFactor = (float)cameraResolution.width / height; - heightFactor = (float)cameraResolution.height / width; - // Swap X and Y coordinates to flip frame to the same orientation as cameraResolution - orientedFrame = new Rect(frame.top, frame.left, frame.bottom, frame.right); - } - - framePreview = new RectF(orientedFrame.left * widthFactor, orientedFrame.top * heightFactor, - orientedFrame.right * widthFactor, orientedFrame.bottom * heightFactor); - - final String savedParameters = parameters == null ? null : parameters.flatten(); - - try { - setDesiredCameraParameters(camera, cameraResolution, continuousAutoFocus); - } catch (final RuntimeException x) { - if (savedParameters != null) { - final Camera.Parameters parameters2 = camera.getParameters(); - parameters2.unflatten(savedParameters); - try { - camera.setParameters(parameters2); - setDesiredCameraParameters(camera, cameraResolution, continuousAutoFocus); - } catch (final RuntimeException x2) { - Log.d(Config.LOGTAG,"problem setting camera parameters", x2); - } - } - } - - try { - camera.startPreview(); - return camera; - } catch (final RuntimeException x) { - Log.w(Config.LOGTAG,"something went wrong while starting camera preview", x); - camera.release(); - throw x; - } - } - - private int determineCameraId() { - final int cameraCount = Camera.getNumberOfCameras(); - final CameraInfo cameraInfo = new CameraInfo(); - - // prefer back-facing camera - for (int i = 0; i < cameraCount; i++) { - Camera.getCameraInfo(i, cameraInfo); - if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) - return i; - } - - // fall back to front-facing camera - for (int i = 0; i < cameraCount; i++) { - Camera.getCameraInfo(i, cameraInfo); - if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) - return i; - } - - return -1; - } - - public void close() { - if (camera != null) { - try { - camera.stopPreview(); - } catch (final RuntimeException x) { - Log.w(Config.LOGTAG,"something went wrong while stopping camera preview", x); - } - - camera.release(); - } - } - - private static final Comparator numPixelComparator = new Comparator() { - @Override - public int compare(final Camera.Size size1, final Camera.Size size2) { - final int pixels1 = size1.height * size1.width; - final int pixels2 = size2.height * size2.width; - - if (pixels1 < pixels2) - return 1; - else if (pixels1 > pixels2) - return -1; - else - return 0; - } - }; - - private static Camera.Size findBestPreviewSizeValue(final Camera.Parameters parameters, int width, int height) { - if (height > width) { - final int temp = width; - width = height; - height = temp; - } - - final float screenAspectRatio = (float) width / (float) height; - - final List rawSupportedSizes = parameters.getSupportedPreviewSizes(); - if (rawSupportedSizes == null) - return parameters.getPreviewSize(); - - // sort by size, descending - final List supportedPreviewSizes = new ArrayList(rawSupportedSizes); - Collections.sort(supportedPreviewSizes, numPixelComparator); - - Camera.Size bestSize = null; - float diff = Float.POSITIVE_INFINITY; - - for (final Camera.Size supportedPreviewSize : supportedPreviewSizes) { - final int realWidth = supportedPreviewSize.width; - final int realHeight = supportedPreviewSize.height; - final int realPixels = realWidth * realHeight; - if (realPixels < MIN_PREVIEW_PIXELS || realPixels > MAX_PREVIEW_PIXELS) - continue; - - final boolean isCandidatePortrait = realWidth < realHeight; - final int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth; - final int maybeFlippedHeight = isCandidatePortrait ? realWidth : realHeight; - if (maybeFlippedWidth == width && maybeFlippedHeight == height) - return supportedPreviewSize; - - final float aspectRatio = (float) maybeFlippedWidth / (float) maybeFlippedHeight; - final float newDiff = Math.abs(aspectRatio - screenAspectRatio); - if (newDiff < diff) { - bestSize = supportedPreviewSize; - diff = newDiff; - } - } - - if (bestSize != null) - return bestSize; - else - return parameters.getPreviewSize(); - } - - @SuppressLint("InlinedApi") - private static void setDesiredCameraParameters(final Camera camera, final Camera.Size cameraResolution, - final boolean continuousAutoFocus) { - final Camera.Parameters parameters = camera.getParameters(); - if (parameters == null) - return; - - final List supportedFocusModes = parameters.getSupportedFocusModes(); - final String focusMode = continuousAutoFocus - ? findValue(supportedFocusModes, Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE, - Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, Camera.Parameters.FOCUS_MODE_AUTO, - Camera.Parameters.FOCUS_MODE_MACRO) - : findValue(supportedFocusModes, Camera.Parameters.FOCUS_MODE_AUTO, Camera.Parameters.FOCUS_MODE_MACRO); - if (focusMode != null) - parameters.setFocusMode(focusMode); - - parameters.setPreviewSize(cameraResolution.width, cameraResolution.height); - - camera.setParameters(parameters); - } - - public void requestPreviewFrame(final Camera.PreviewCallback callback) { - try { - camera.setOneShotPreviewCallback(callback); - } catch (final RuntimeException x) { - Log.d(Config.LOGTAG,"problem requesting preview frame, callback won't be called", x); - } - } - - public PlanarYUVLuminanceSource buildLuminanceSource(final byte[] data) { - return new PlanarYUVLuminanceSource(data, cameraResolution.width, cameraResolution.height, - (int) framePreview.left, (int) framePreview.top, (int) framePreview.width(), - (int) framePreview.height(), false); - } - - public void setTorch(final boolean enabled) { - if (enabled != getTorchEnabled(camera)) - setTorchEnabled(camera, enabled); - } - - private static boolean getTorchEnabled(final Camera camera) { - final Camera.Parameters parameters = camera.getParameters(); - if (parameters != null) { - final String flashMode = camera.getParameters().getFlashMode(); - return flashMode != null && (Camera.Parameters.FLASH_MODE_ON.equals(flashMode) - || Camera.Parameters.FLASH_MODE_TORCH.equals(flashMode)); - } - - return false; - } - - private static void setTorchEnabled(final Camera camera, final boolean enabled) { - final Camera.Parameters parameters = camera.getParameters(); - - final List supportedFlashModes = parameters.getSupportedFlashModes(); - if (supportedFlashModes != null) { - final String flashMode; - if (enabled) - flashMode = findValue(supportedFlashModes, Camera.Parameters.FLASH_MODE_TORCH, - Camera.Parameters.FLASH_MODE_ON); - else - flashMode = findValue(supportedFlashModes, Camera.Parameters.FLASH_MODE_OFF); - - if (flashMode != null) { - camera.cancelAutoFocus(); // autofocus can cause conflict - - parameters.setFlashMode(flashMode); - camera.setParameters(parameters); - } - } - } - - private static String findValue(final Collection values, final String... valuesToFind) { - for (final String valueToFind : valuesToFind) - if (values.contains(valueToFind)) - return valueToFind; - - return null; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/text/DividerSpan.java b/src/main/java/eu/siacs/conversations/ui/text/DividerSpan.java deleted file mode 100644 index 394e7b3cb..000000000 --- a/src/main/java/eu/siacs/conversations/ui/text/DividerSpan.java +++ /dev/null @@ -1,29 +0,0 @@ -package eu.siacs.conversations.ui.text; - -import android.text.TextPaint; -import android.text.style.MetricAffectingSpan; - -public class DividerSpan extends MetricAffectingSpan { - - private static final float PROPORTION = 0.3f; - - private final boolean large; - - public DividerSpan(boolean large) { - this.large = large; - } - - public boolean isLarge() { - return large; - } - - @Override - public void updateDrawState(TextPaint tp) { - tp.setTextSize(tp.getTextSize() * PROPORTION); - } - - @Override - public void updateMeasureState(TextPaint p) { - p.setTextSize(p.getTextSize() * PROPORTION); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java b/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java deleted file mode 100644 index 60888c26b..000000000 --- a/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2017, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui.text; - -import android.annotation.SuppressLint; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.net.Uri; -import android.text.Editable; -import android.text.Spanned; -import android.text.style.URLSpan; -import android.view.View; - -import java.util.Arrays; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.ui.ConversationsActivity; -import eu.siacs.conversations.ui.util.CustomTab; -import eu.siacs.conversations.utils.ThemeHelper; -import me.drakeet.support.toast.ToastCompat; - -@SuppressLint("ParcelCreator") -public class FixedURLSpan extends URLSpan { - - private FixedURLSpan(String url) { - super(url); - } - - public static void fix(final Editable editable) { - for (final URLSpan urlspan : editable.getSpans(0, editable.length() - 1, URLSpan.class)) { - final int start = editable.getSpanStart(urlspan); - final int end = editable.getSpanEnd(urlspan); - editable.removeSpan(urlspan); - editable.setSpan(new FixedURLSpan(urlspan.getURL()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - - @Override - public void onClick(View widget) { - final Uri uri = Uri.parse(getURL()); - final Context context = widget.getContext(); - final boolean candidateToProcessDirectly = "xmpp".equals(uri.getScheme()) || ("https".equals(uri.getScheme()) && Config.inviteHostURL.equals(uri.getHost()) && uri.getPathSegments().size() > 1 && Arrays.asList("j", "i").contains(uri.getPathSegments().get(0))); - if (candidateToProcessDirectly && context instanceof ConversationsActivity) { - if (((ConversationsActivity) context).onXmppUriClicked(uri)) { - widget.playSoundEffect(0); - return; - } - } - try { - CustomTab.openTab(context, uri, ThemeHelper.isDark(ThemeHelper.find(context))); - widget.playSoundEffect(0); - } catch (ActivityNotFoundException e) { - ToastCompat.makeText(context, R.string.no_application_found_to_open_link, ToastCompat.LENGTH_SHORT).show(); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/text/QuoteSpan.java b/src/main/java/eu/siacs/conversations/ui/text/QuoteSpan.java deleted file mode 100644 index 45768b8e7..000000000 --- a/src/main/java/eu/siacs/conversations/ui/text/QuoteSpan.java +++ /dev/null @@ -1,59 +0,0 @@ -package eu.siacs.conversations.ui.text; - -import android.graphics.Canvas; -import android.graphics.Paint; -import android.text.Layout; -import android.text.TextPaint; -import android.text.style.CharacterStyle; -import android.text.style.LeadingMarginSpan; -import android.util.DisplayMetrics; -import android.util.TypedValue; - -import androidx.annotation.ColorInt; - -public class QuoteSpan extends CharacterStyle implements LeadingMarginSpan { - - private final int color; - - private final int width; - private final int paddingLeft; - private final int paddingRight; - - private static final float WIDTH_SP = 2f; - private static final float PADDING_LEFT_SP = 1.5f; - private static final float PADDING_RIGHT_SP = 8f; - - public QuoteSpan(int color, DisplayMetrics metrics) { - this.color = color; - this.width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, WIDTH_SP, metrics); - this.paddingLeft = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, PADDING_LEFT_SP, metrics); - this.paddingRight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, PADDING_RIGHT_SP, metrics); - } - - @Override - public void updateDrawState(TextPaint tp) { - tp.setColor(this.color); - } - - @Override - public int getLeadingMargin(boolean first) { - return paddingLeft + width + paddingRight; - } - - @Override - public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, - CharSequence text, int start, int end, boolean first, Layout layout) { - Paint.Style style = p.getStyle(); - int color = p.getColor(); - p.setStyle(Paint.Style.FILL); - p.setColor(this.color); - c.drawRect(x + dir * paddingLeft, top, x + dir * (paddingLeft + width), bottom, p); - p.setStyle(style); - p.setColor(color); - } - - @ColorInt - public int getColor() { - return this.color; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/ActionBarUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ActionBarUtil.java deleted file mode 100644 index fb5b45ac7..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/ActionBarUtil.java +++ /dev/null @@ -1,114 +0,0 @@ -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 resetCustomActionBarOnClickListeners(@NonNull View view) { - final View title = view.findViewById(android.R.id.text1); - final View subtitle = view.findViewById(android.R.id.text2); - 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); - } - } - - public static void setCustomActionBarOnClickListener(@NonNull View view, - @NonNull final View.OnClickListener onClickListener) { - final View title = view.findViewById(android.R.id.text1); - final View subtitle = view.findViewById(android.R.id.text2); - 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/ActivityResult.java b/src/main/java/eu/siacs/conversations/ui/util/ActivityResult.java deleted file mode 100644 index f3c90c2a2..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/ActivityResult.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui.util; - -import android.content.Intent; - -public class ActivityResult { - - public final int requestCode; - public final int resultCode; - public final Intent data; - - private ActivityResult(int requestCode, int resultCode, final Intent data) { - this.requestCode = requestCode; - this.resultCode = resultCode; - this.data = data; - } - - public static ActivityResult of(int requestCode, int resultCode, Intent data) { - return new ActivityResult(requestCode, resultCode, data); - } - -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java deleted file mode 100644 index 8f1bfc6e6..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui.util; - -import android.content.ClipData; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; -import android.util.Log; -import org.jetbrains.annotations.NotNull; - -import com.google.common.base.MoreObjects; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.MimeUtils; - -public class Attachment implements Parcelable { - - Attachment(Parcel in) { - uri = in.readParcelable(Uri.class.getClassLoader()); - mime = in.readString(); - uuid = UUID.fromString(in.readString()); - type = Type.valueOf(in.readString()); - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeParcelable(uri, flags); - dest.writeString(mime); - dest.writeString(uuid.toString()); - dest.writeString(type.toString()); - } - - @Override - public int describeContents() { - return 0; - } - - public static final Creator CREATOR = new Creator() { - @Override - public Attachment createFromParcel(Parcel in) { - return new Attachment(in); - } - - @Override - public Attachment[] newArray(int size) { - return new Attachment[size]; - } - }; - - public String getMime() { - return mime; - } - - public Type getType() { - return type; - } - - @NotNull - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("uri", uri) - .add("type", type) - .add("uuid", uuid) - .add("mime", mime) - .toString(); - } - - public enum Type { - FILE, IMAGE, LOCATION, RECORDING - } - - private final Uri uri; - private final Type type; - private final UUID uuid; - private final String mime; - - private Attachment(UUID uuid, Uri uri, Type type, String mime) { - this.uri = uri; - this.type = type; - this.mime = mime; - this.uuid = uuid; - } - - private Attachment(Uri uri, Type type, String mime) { - this.uri = uri; - this.type = type; - this.mime = mime; - this.uuid = UUID.randomUUID(); - } - - public static boolean canBeSendInband(final List attachments) { - for (Attachment attachment : attachments) { - if (attachment.type != Type.LOCATION) { - return false; - } - } - return true; - } - - public static List of(final Context context, Uri uri, Type type) { - final String mime = type == Type.LOCATION ? null : MimeUtils.guessMimeTypeFromUri(context, uri); - return Collections.singletonList(new Attachment(uri, type, mime)); - } - - public static List of(final Context context, List uris, final String type) { - final List attachments = new ArrayList<>(); - for (final Uri uri : uris) { - if (uri == null) { - continue; - } - final String mime = MimeUtils.guessMimeTypeFromUriAndMime(context, uri, type); - attachments.add(new Attachment(uri, mime != null && isImage(mime) ? Type.IMAGE : Type.FILE, mime)); - } - return attachments; - } - - public static Attachment of(UUID uuid, final File file, String mime) { - return new Attachment(uuid, Uri.fromFile(file), mime != null && (isImage(mime) || mime.startsWith("video/")) ? Type.IMAGE : Type.FILE, mime); - } - - public static List extractAttachments(final Context context, final Intent intent, Type type) { - List uris = new ArrayList<>(); - if (intent == null) { - return uris; - } - final String contentType = intent.getType(); - final Uri data = intent.getData(); - if (data == null) { - final ClipData clipData = intent.getClipData(); - if (clipData != null) { - for (int i = 0; i < clipData.getItemCount(); ++i) { - final Uri uri = clipData.getItemAt(i).getUri(); - Log.d(Config.LOGTAG, "uri=" + uri + " contentType=" + contentType); - final String mime = MimeUtils.guessMimeTypeFromUriAndMime(context, uri, contentType); - uris.add(new Attachment(uri, type, mime)); - } - } - } else { - final String mime = MimeUtils.guessMimeTypeFromUriAndMime(context, data, contentType); - uris.add(new Attachment(data, type, mime)); - } - return uris; - } - - public boolean renderThumbnail() { - return type == Type.IMAGE || (type == Type.FILE && mime != null && renderFileThumbnail(mime)); - } - - private static boolean renderFileThumbnail(final String mime) { - return mime.startsWith("video/") - || isImage(mime) - || "application/pdf".equals(mime); - } - - public Uri getUri() { - return uri; - } - - public UUID getUuid() { - return uuid; - } - - private static boolean isImage(final String mime) { - return mime.startsWith("image/") && !mime.equals("image/svg+xml"); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/AvatarWorkerTask.java b/src/main/java/eu/siacs/conversations/ui/util/AvatarWorkerTask.java deleted file mode 100644 index 06f19b1ef..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/AvatarWorkerTask.java +++ /dev/null @@ -1,157 +0,0 @@ -package eu.siacs.conversations.ui.util; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.os.AsyncTask; -import android.widget.ImageView; - -import androidx.annotation.DimenRes; - -import java.lang.ref.WeakReference; -import java.util.concurrent.RejectedExecutionException; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.services.AvatarService; -import eu.siacs.conversations.ui.XmppActivity; - -public class AvatarWorkerTask extends AsyncTask { - private final WeakReference imageViewReference; - private AvatarService.Avatarable avatarable = null; - private @DimenRes - int size; - static final int animationDuration = 100; - - public AvatarWorkerTask(ImageView imageView, @DimenRes int size) { - imageViewReference = new WeakReference<>(imageView); - this.size = size; - } - - @Override - protected Bitmap doInBackground(AvatarService.Avatarable... params) { - this.avatarable = params[0]; - final XmppActivity activity = XmppActivity.find(imageViewReference); - if (activity == null) { - return null; - } - return activity.avatarService().get(avatarable, (int) activity.getResources().getDimension(size), isCancelled()); - } - - @Override - protected void onPostExecute(Bitmap bitmap) { - if (bitmap != null && !isCancelled()) { - final ImageView imageView = imageViewReference.get(); - if (imageView != null) { - imageView.setImageBitmap(bitmap); - imageView.setBackgroundColor(0x00000000); - } - } - } - - public static boolean cancelPotentialWork(AvatarService.Avatarable avatarable, ImageView imageView) { - final AvatarWorkerTask workerTask = getBitmapWorkerTask(imageView); - - if (workerTask != null) { - final AvatarService.Avatarable old = workerTask.avatarable; - if (old == null || avatarable != old) { - workerTask.cancel(true); - } else { - return false; - } - } - return true; - } - - public static AvatarWorkerTask getBitmapWorkerTask(ImageView imageView) { - if (imageView != null) { - final Drawable drawable = imageView.getDrawable(); - if (drawable instanceof AsyncDrawable) { - final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; - return asyncDrawable.getAvatarWorkerTask(); - } - } - return null; - } - - public static void loadAvatar(final AvatarService.Avatarable avatarable, final ImageView imageView, final @DimenRes int size) { - loadAvatar(avatarable, imageView, size, false, null); - } - - public static void loadAvatar(final String JidFromJabberNetwork, final AvatarService.Avatarable avatarable, final ImageView imageView, final @DimenRes int size) { - loadAvatar(avatarable, imageView, size, false, JidFromJabberNetwork); - } - - public static void loadAvatar(final AvatarService.Avatarable avatarable, final ImageView imageView, final @DimenRes int size, final boolean overlay) { - loadAvatar(avatarable, imageView, size, overlay, null); - } - - public static void loadAvatar(final AvatarService.Avatarable avatarable, final ImageView imageView, final @DimenRes int size, final boolean overlay, final String JidFromJabberNetwork) { - if (cancelPotentialWork(avatarable, imageView)) { - final XmppActivity activity = XmppActivity.find(imageView); - if (activity == null) { - return; - } - final Bitmap bm = activity.avatarService().get(avatarable, (int) activity.getResources().getDimension(size), false); - setContentDescription(avatarable, imageView); - if (bm != null && JidFromJabberNetwork == null) { - cancelPotentialWork(avatarable, imageView); - if (overlay) { - activity.xmppConnectionService.fileBackend.drawOverlay(bm, R.drawable.pencil_overlay, 0.35f, true); - imageView.setImageBitmap(bm); - } else { - imageView.setImageBitmap(bm); - } - imageView.setBackgroundColor(0x00000000); - } else if (JidFromJabberNetwork != null) { - try { - new GetAvatarFromJabberNetwork(activity.xmppConnectionService, avatarable, imageView, size, overlay).execute(Config.CHANNEL_DISCOVERY + "/avatar/v1/" + JidFromJabberNetwork); - } catch (Exception e) { - e.printStackTrace(); - loadAvatar(avatarable, imageView, size, overlay, null); - } - } else { - imageView.setBackgroundColor(avatarable.getAvatarBackgroundColor()); - imageView.setImageDrawable(null); - final AvatarWorkerTask task = new AvatarWorkerTask(imageView, size); - final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task); - if (overlay) { - activity.xmppConnectionService.fileBackend.drawOverlayFromDrawable(asyncDrawable, R.drawable.pencil_overlay, 1.0f); - imageView.setImageDrawable(asyncDrawable); - } else { - imageView.setImageDrawable(asyncDrawable); - - } - try { - task.execute(avatarable); - } catch (final RejectedExecutionException ignored) { - } - } - } - } - - private static void setContentDescription(final AvatarService.Avatarable avatarable, final ImageView imageView) { - final Context context = imageView.getContext(); - if (avatarable instanceof Account) { - imageView.setContentDescription(context.getString(R.string.your_avatar)); - } else { - imageView.setContentDescription(context.getString(R.string.avatar_for_x, avatarable.getAvatarName())); - } - } - - static class AsyncDrawable extends BitmapDrawable { - private final WeakReference avatarWorkerTaskReference; - - AsyncDrawable(Resources res, Bitmap bitmap, AvatarWorkerTask workerTask) { - super(res, bitmap); - avatarWorkerTaskReference = new WeakReference<>(workerTask); - } - - AvatarWorkerTask getAvatarWorkerTask() { - return avatarWorkerTaskReference.get(); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/util/CallManager.java b/src/main/java/eu/siacs/conversations/ui/util/CallManager.java deleted file mode 100644 index d706f9e09..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/CallManager.java +++ /dev/null @@ -1,129 +0,0 @@ -package eu.siacs.conversations.ui.util; - -import android.Manifest; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.os.Build; - -import com.google.common.base.Optional; - -import java.util.ArrayList; -import java.util.List; - -import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_START_AUDIO_CALL; -import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_START_VIDEO_CALL; - - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.ui.RtpSessionActivity; -import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; -import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; -import eu.siacs.conversations.xmpp.jingle.Media; -import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; -import eu.siacs.conversations.xmpp.jingle.RtpCapability; -import me.drakeet.support.toast.ToastCompat; - -public class CallManager { - - public static void checkPermissionAndTriggerAudioCall(XmppActivity activity, Conversation conversation) { - if (activity.mUseTor || conversation.getAccount().isOnion()) { - ToastCompat.makeText(activity, R.string.disable_tor_to_make_call, ToastCompat.LENGTH_SHORT).show(); - return; - } - if (hasPermissions(REQUEST_START_AUDIO_CALL, activity, Manifest.permission.RECORD_AUDIO)) { - triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VOICE_CALL, activity, conversation); - } - } - - public static void checkPermissionAndTriggerVideoCall(XmppActivity activity, Conversation conversation) { - if (activity.mUseTor || conversation.getAccount().isOnion()) { - ToastCompat.makeText(activity, R.string.disable_tor_to_make_call, ToastCompat.LENGTH_SHORT).show(); - return; - } - if (hasPermissions(REQUEST_START_VIDEO_CALL, activity, Manifest.permission.CAMERA)) { - triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL, activity, conversation); - } - } - - public static void triggerRtpSession(final String action, XmppActivity activity, Conversation conversation) { - if (activity.xmppConnectionService.getJingleConnectionManager().isBusy()) { - ToastCompat.makeText(activity, R.string.only_one_call_at_a_time, ToastCompat.LENGTH_LONG).show(); - return; - } - - final Contact contact = conversation.getContact(); - if (contact.getPresences().anySupport(Namespace.JINGLE_MESSAGE)) { - triggerRtpSession(contact.getAccount(), contact.getJid().asBareJid(), action, activity); - } else { - final RtpCapability.Capability capability; - if (action.equals(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL)) { - capability = RtpCapability.Capability.VIDEO; - } else { - capability = RtpCapability.Capability.AUDIO; - } - PresenceSelector.selectFullJidForDirectRtpConnection(activity, contact, capability, fullJid -> { - triggerRtpSession(contact.getAccount(), fullJid, action, activity); - }); - } - } - - private static void triggerRtpSession(final Account account, final Jid with, final String action, XmppActivity activity) { - final Intent intent = new Intent(activity, RtpSessionActivity.class); - intent.setAction(action); - intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, account.getJid().toEscapedString()); - intent.putExtra(RtpSessionActivity.EXTRA_WITH, with.toEscapedString()); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - activity.startActivity(intent); - } - - public static void returnToOngoingCall(XmppActivity activity, Conversation conversation) { - final Optional ongoingRtpSession = activity.xmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); - if (ongoingRtpSession.isPresent()) { - final OngoingRtpSession id = ongoingRtpSession.get(); - final Intent intent = new Intent(activity, RtpSessionActivity.class); - intent.putExtra(RtpSessionActivity.EXTRA_ACCOUNT, id.getAccount().getJid().asBareJid().toEscapedString()); - intent.putExtra(RtpSessionActivity.EXTRA_WITH, id.getWith().toEscapedString()); - if (id instanceof AbstractJingleConnection.Id) { - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra(RtpSessionActivity.EXTRA_SESSION_ID, id.getSessionId()); - } else if (id instanceof JingleConnectionManager.RtpSessionProposal) { - if (((JingleConnectionManager.RtpSessionProposal) id).media.contains(Media.VIDEO)) { - intent.setAction(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); - } else { - intent.setAction(RtpSessionActivity.ACTION_MAKE_VOICE_CALL); - } - } - activity.startActivity(intent); - } - } - - private static boolean hasPermissions(int requestCode, XmppActivity activity, String... permissions) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - final List missingPermissions = new ArrayList<>(); - for (String permission : permissions) { - if (Config.ONLY_INTERNAL_STORAGE && permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE) && permission.equals(Manifest.permission.READ_EXTERNAL_STORAGE)) { - continue; - } - if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { - missingPermissions.add(permission); - } - } - if (missingPermissions.size() == 0) { - return true; - } else { - activity.requestPermissions(missingPermissions.toArray(new String[missingPermissions.size()]), requestCode); - return false; - } - } else { - return true; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/util/ChangeWatcher.java b/src/main/java/eu/siacs/conversations/ui/util/ChangeWatcher.java deleted file mode 100644 index 7c5d5014b..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/ChangeWatcher.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui.util; - -public class ChangeWatcher { - - private T object = null; - - public synchronized boolean watch(T object) { - if (this.object == null) { - this.object = object; - return object != null; - } else { - final boolean changed = !this.object.equals(object); - this.object = object; - return changed; - } - } - - public T get() { - return object; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/ConversationMenuConfigurator.java b/src/main/java/eu/siacs/conversations/ui/util/ConversationMenuConfigurator.java deleted file mode 100644 index f7092d200..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/ConversationMenuConfigurator.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui.util; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.view.Menu; -import android.view.MenuItem; - -import androidx.annotation.NonNull; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.OmemoSetting; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Conversational; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.ui.XmppActivity; - -public class ConversationMenuConfigurator { - - private static boolean microphoneAvailable = false; - private static boolean locationAvailable = false; - - public static void reloadFeatures(Context context) { - microphoneAvailable = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_MICROPHONE); - locationAvailable = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS) || context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION_NETWORK); - } - - public static void configureQuickShareAttachmentMenu(@NonNull Conversation conversation, Menu menu, boolean hideVoice) { - final boolean visible = SendButtonTool.AttachmentsVisible(conversation); - if (!visible) { - return; - } - if (hideVoice) { - microphoneAvailable = false; - } - menu.findItem(R.id.attach_record_voice).setVisible(microphoneAvailable); - menu.findItem(R.id.attach_location).setVisible(locationAvailable); - } - - public static void configureAttachmentMenu(@NonNull Conversation conversation, Menu menu, Boolean Quick_share_attachment_choice, boolean hasAttachments) { - if (menu == null) { - return; - } - final MenuItem menuAttach = menu.findItem(R.id.action_attach_file); - boolean isPM = false; - try { - isPM = conversation.getMode() == Conversation.MODE_MULTI && conversation.getNextCounterpart() != null; - } catch (Exception e) { - e.printStackTrace(); - } - if (Quick_share_attachment_choice && !hasAttachments && !isPM) { - menuAttach.setVisible(false); - return; - } - final boolean visible; - if (conversation.getMode() == Conversation.MODE_MULTI) { - visible = conversation.getAccount().httpUploadAvailable() && conversation.getMucOptions().participating() - || conversation.getAccount().httpUploadAvailable() && isPM; - } else { - visible = true; - } - menuAttach.setVisible(visible); - if (!visible) { - return; - } - menu.findItem(R.id.attach_record_voice).setVisible(microphoneAvailable); - menu.findItem(R.id.attach_location).setVisible(locationAvailable); - } - - public static void configureEncryptionMenu(@NonNull Conversation conversation, Menu menu, final XmppActivity activity) { - final MenuItem menuSecure = menu.findItem(R.id.action_security); - final boolean participating = conversation.getMode() == Conversational.MODE_SINGLE || conversation.getMucOptions().participating(); - if (!participating) { - menuSecure.setVisible(false); - return; - } - final MenuItem none = menu.findItem(R.id.encryption_choice_none); - final MenuItem otr = menu.findItem(R.id.encryption_choice_otr); - final MenuItem pgp = menu.findItem(R.id.encryption_choice_pgp); - final MenuItem axolotl = menu.findItem(R.id.encryption_choice_axolotl); - - final int next = conversation.getNextEncryption(); - - boolean visible; - if (OmemoSetting.isAlways() || OmemoSetting.isNever()) { - visible = false; - } else if (conversation.getMode() == Conversation.MODE_MULTI) { - if (next == Message.ENCRYPTION_NONE && !conversation.isPrivateAndNonAnonymous() && !conversation.getBooleanAttribute(Conversation.ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false)) { - visible = false; - } else { - visible = (Config.supportOpenPgp() || Config.supportOmemo()) && Config.multipleEncryptionChoices(); - } - } else { - visible = Config.multipleEncryptionChoices(); - } - - menuSecure.setVisible(visible); - - if (!visible) { - return; - } - - if (conversation.getNextEncryption() != Message.ENCRYPTION_NONE) { - menuSecure.setIcon(R.drawable.ic_lock_white_24dp); - } - otr.setVisible(Config.supportOtr() && activity.enableOTR()); - if (conversation.getMode() == Conversation.MODE_MULTI) { - otr.setVisible(false); - } - pgp.setVisible(Config.supportOpenPgp()); - none.setVisible(Config.supportUnencrypted() || conversation.getMode() == Conversation.MODE_MULTI); - axolotl.setVisible(Config.supportOmemo()); - switch (conversation.getNextEncryption()) { - case Message.ENCRYPTION_OTR: - menuSecure.setTitle(R.string.encryption_choice_otr); - otr.setChecked(true); - break; - case Message.ENCRYPTION_PGP: - menuSecure.setTitle(R.string.encrypted_with_openpgp); - pgp.setChecked(true); - break; - case Message.ENCRYPTION_AXOLOTL: - menuSecure.setTitle(R.string.encrypted_with_omemo); - axolotl.setChecked(true); - break; - default: - menuSecure.setTitle(R.string.not_encrypted); - none.setChecked(true); - break; - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/CustomTab.java b/src/main/java/eu/siacs/conversations/ui/util/CustomTab.java deleted file mode 100644 index fa083dc94..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/CustomTab.java +++ /dev/null @@ -1,30 +0,0 @@ -package eu.siacs.conversations.ui.util; - -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.provider.Browser; - -import androidx.browser.customtabs.CustomTabColorSchemeParams; -import androidx.browser.customtabs.CustomTabsIntent; - -import eu.siacs.conversations.R; - -public class CustomTab { - public static void openTab(Context context, Uri uri, boolean dark) throws ActivityNotFoundException { - CustomTabsIntent.Builder tabBuilder = new CustomTabsIntent.Builder(); - tabBuilder.setShowTitle(true); - tabBuilder.setUrlBarHidingEnabled(false); - tabBuilder.setDefaultColorSchemeParams(new CustomTabColorSchemeParams.Builder() - .setToolbarColor(StyledAttributes.getColor(context, R.attr.colorPrimary)) - .setSecondaryToolbarColor(StyledAttributes.getColor(context, R.attr.colorPrimaryDark)) - .build()); - tabBuilder.setColorScheme(dark ? CustomTabsIntent.COLOR_SCHEME_DARK : CustomTabsIntent.COLOR_SCHEME_LIGHT); - tabBuilder.setShareState(CustomTabsIntent.SHARE_STATE_ON); - CustomTabsIntent customTabsIntent = tabBuilder.build(); - customTabsIntent.intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); - customTabsIntent.intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); - customTabsIntent.launchUrl(context, uri); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/util/DateSeparator.java b/src/main/java/eu/siacs/conversations/ui/util/DateSeparator.java deleted file mode 100644 index b9bf04cd3..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/DateSeparator.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package eu.siacs.conversations.ui.util; - -import java.util.List; - -import eu.siacs.conversations.entities.IndividualMessage; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.utils.UIHelper; - -public class DateSeparator { - - public static void addAll(List messages) { - for (int i = 0; i < messages.size(); ++i) { - final Message current = messages.get(i); - if (i == 0 || !UIHelper.sameDay(messages.get(i - 1).getTimeSent(), current.getTimeSent())) { - messages.add(i, IndividualMessage.createDateSeparator(current)); - i++; - } - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/DelayedHintHelper.java b/src/main/java/eu/siacs/conversations/ui/util/DelayedHintHelper.java deleted file mode 100644 index a36229f6c..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/DelayedHintHelper.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui.util; - -import android.os.Handler; -import android.widget.EditText; - -import androidx.annotation.StringRes; - -public class DelayedHintHelper { - - public static void setHint(@StringRes final int res, EditText editText) { - editText.setOnFocusChangeListener((v, hasFocus) -> { - if (hasFocus) { - new Handler().postDelayed(() -> editText.setHint(res), 200); - } else { - editText.setHint(null); - } - }); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/EditMessageActionModeCallback.java b/src/main/java/eu/siacs/conversations/ui/util/EditMessageActionModeCallback.java deleted file mode 100644 index a3d73e9e9..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/EditMessageActionModeCallback.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui.util; - -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.text.TextUtils; -import android.view.ActionMode; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.ui.widget.EditMessage; - -public class EditMessageActionModeCallback implements ActionMode.Callback { - - private final EditMessage editMessage; - private final ClipboardManager clipboardManager; - - public EditMessageActionModeCallback(EditMessage editMessage) { - this.editMessage = editMessage; - this.clipboardManager = - (ClipboardManager) - editMessage.getContext().getSystemService(Context.CLIPBOARD_SERVICE); - } - - @Override - public boolean onCreateActionMode(final ActionMode mode, final Menu menu) { - final MenuInflater inflater = mode.getMenuInflater(); - inflater.inflate(R.menu.edit_message_actions, menu); - final MenuItem pasteAsQuote = menu.findItem(R.id.paste_as_quote); - final ClipData primaryClip = clipboardManager.getPrimaryClip(); - if (primaryClip != null && primaryClip.getItemCount() > 0) { - final String mimeType; - try { - mimeType = primaryClip.getDescription().getMimeType(0); - } catch (final Exception e) { - pasteAsQuote.setVisible(false); - return true; - } - pasteAsQuote.setVisible( - mimeType.startsWith("text/") - && !TextUtils.isEmpty(primaryClip.getItemAt(0).getText())); - } else { - pasteAsQuote.setVisible(false); - } - return true; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } - - @Override - public boolean onActionItemClicked(final ActionMode mode, final MenuItem item) { - if (item.getItemId() == R.id.paste_as_quote) { - final ClipData primaryClip = clipboardManager.getPrimaryClip(); - if (primaryClip != null && primaryClip.getItemCount() > 0) { - editMessage.insertAsQuote(primaryClip.getItemAt(0).getText().toString()); - return true; - } - } - return false; - } - - @Override - public void onDestroyActionMode(ActionMode mode) {} -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/GetAvatarFromJabberNetwork.java b/src/main/java/eu/siacs/conversations/ui/util/GetAvatarFromJabberNetwork.java deleted file mode 100644 index 7ae6a4d09..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/GetAvatarFromJabberNetwork.java +++ /dev/null @@ -1,127 +0,0 @@ -package eu.siacs.conversations.ui.util; - -import static eu.siacs.conversations.Config.MILLISECONDS_IN_DAY; - -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.os.AsyncTask; -import android.util.Log; -import android.widget.ImageView; - -import androidx.annotation.DimenRes; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.ref.WeakReference; -import java.util.HashMap; -import java.util.Map; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.services.AvatarService; -import eu.siacs.conversations.services.XmppConnectionService; - -public class GetAvatarFromJabberNetwork extends AsyncTask { - private static Map linkMap = new HashMap<>(); - private final WeakReference imageViewReference; - private Bitmap bitmap = null; - private AvatarService.Avatarable avatarable; - ImageView imageView; - int size; - boolean overlay; - XmppConnectionService xmppConnectionService; - - public GetAvatarFromJabberNetwork(final XmppConnectionService xmppConnectionService, final AvatarService.Avatarable avatarable, final ImageView imageView, final @DimenRes int size, final boolean overlay) { - imageViewReference = new WeakReference<>(imageView); - this.avatarable = avatarable; - this.imageView = imageView; - this.size = size; - this.overlay = overlay; - this.xmppConnectionService = xmppConnectionService; - } - - @Override - protected Bitmap doInBackground(String... url) { - String stringUrl = url[0]; - long lastUpdate = xmppConnectionService.getPreferences().getLong("lastAvatarUpdate", 0L); - final long now = System.currentTimeMillis(); - final File data = linkMap.get(stringUrl); - if (data == null) { - final File cachedBitmap = new File(xmppConnectionService.getCacheDir(), "avatars" + File.separator + stringUrl.hashCode()); - if (cachedBitmap.exists() && cachedBitmap.length() > 0 && now <= lastUpdate + MILLISECONDS_IN_DAY) { - if (!loadCachedBitmap(cachedBitmap)) { - downloadBitmap(cachedBitmap, stringUrl, now); - linkMap.put(stringUrl, cachedBitmap); - } - } else { - cachedBitmap.getParentFile().mkdirs(); - xmppConnectionService.getFileBackend().deleteFile(cachedBitmap); - downloadBitmap(cachedBitmap, stringUrl, now); - } - } else { - loadCachedBitmap(data); - } - return bitmap; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - AvatarWorkerTask.loadAvatar(avatarable, imageView, size, overlay, null); - } - - private boolean downloadBitmap(final File cachedBitmap, final String stringUrl, final long now) { - xmppConnectionService.getPreferences().edit().putLong("lastAvatarUpdate", now).commit(); - try (InputStream inputStream = new java.net.URL(stringUrl).openStream()) { - bitmap = BitmapFactory.decodeStream(inputStream); - OutputStream os; - try { - os = new FileOutputStream(cachedBitmap); - bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os); - os.flush(); - os.close(); - Log.d(Config.LOGTAG, "GetAvatarFromJabberNetwork: Load avatar from url: " + stringUrl); - return true; - } catch (Exception e) { - Log.d(Config.LOGTAG, "GetAvatarFromJabberNetwork: Error caching avatar bitmap: ", e); - xmppConnectionService.getFileBackend().deleteFile(cachedBitmap); - } - } catch (Exception e) { - //igrnore - } - return false; - } - - private boolean loadCachedBitmap(final File cachedBitmap) { - try { - InputStream is; - try { - is = new FileInputStream(cachedBitmap); - bitmap = BitmapFactory.decodeStream(is); - is.close(); - Log.d(Config.LOGTAG, "GetAvatarFromJabberNetwork: Load avatar from cache: " + cachedBitmap.getPath()); - return true; - } catch (Exception e) { - Log.d(Config.LOGTAG, "GetAvatarFromJabberNetwork: Error fetching cached avatar bitmap: ", e); - } - } catch (Exception e) { - // ignore - } - return false; - } - - @Override - protected void onPostExecute(Bitmap bitmap) { - super.onPostExecute(bitmap); - if (imageViewReference != null && bitmap != null && !isCancelled()) { - final ImageView imageView = imageViewReference.get(); - if (imageView != null) { - imageView.setImageBitmap(bitmap); - } - } else { - AvatarWorkerTask.loadAvatar(avatarable, imageView, size, overlay, null); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/util/GridManager.java b/src/main/java/eu/siacs/conversations/ui/util/GridManager.java deleted file mode 100644 index bc369b364..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/GridManager.java +++ /dev/null @@ -1,78 +0,0 @@ -package eu.siacs.conversations.ui.util; - -import android.content.Context; -import android.util.Log; -import android.view.ViewTreeObserver; - -import androidx.annotation.DimenRes; -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.ui.adapter.MediaAdapter; - -public class GridManager { - - public static void setupLayoutManager(final Context context, RecyclerView recyclerView, @DimenRes int desiredSize) { - int maxWidth = context.getResources().getDisplayMetrics().widthPixels; - ColumnInfo columnInfo = calculateColumnCount(context, maxWidth, desiredSize); - Log.d(Config.LOGTAG, "preliminary count=" + columnInfo.count); - MediaAdapter.setMediaSize(recyclerView, columnInfo.width); - recyclerView.setLayoutManager(new GridLayoutManager(context, columnInfo.count)); - recyclerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - final int availableWidth = recyclerView.getMeasuredWidth(); - if (availableWidth == 0) { - Log.e(Config.LOGTAG, "GridManager: available width was 0; probably because layout was hidden"); - return; - } - final ColumnInfo columnInfo = calculateColumnCount(context, recyclerView.getMeasuredWidth(), desiredSize); - Log.d(Config.LOGTAG, "final count " + columnInfo.count); - final RecyclerView.Adapter adapter = recyclerView.getAdapter(); - if (adapter != null && adapter.getItemCount() != 0) { - Log.e(Config.LOGTAG, "adapter already has items; just go with it now"); - return; - } - setupLayoutManagerInternal(recyclerView, columnInfo); - MediaAdapter.setMediaSize(recyclerView, columnInfo.width); - } - }); - } - - private static void setupLayoutManagerInternal(RecyclerView recyclerView, final ColumnInfo columnInfo) { - RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); - if (layoutManager instanceof GridLayoutManager) { - ((GridLayoutManager) layoutManager).setSpanCount(columnInfo.count); - } - } - - private static ColumnInfo calculateColumnCount(Context context, int availableWidth, @DimenRes int desiredSize) { - final float desiredWidth = context.getResources().getDimension(desiredSize); - final int columns = Math.round(availableWidth / desiredWidth); - final int realWidth = availableWidth / columns; - Log.d(Config.LOGTAG, "desired=" + desiredWidth + " real=" + realWidth); - return new ColumnInfo(columns, realWidth); - } - - public static int getCurrentColumnCount(RecyclerView recyclerView) { - RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); - if (layoutManager instanceof GridLayoutManager) { - return ((GridLayoutManager) layoutManager).getSpanCount(); - } else { - return 0; - } - } - - public static class ColumnInfo { - private final int count; - private final int width; - - private ColumnInfo(int count, int width) { - this.count = count; - this.width = width; - } - } - -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/IntroHelper.java b/src/main/java/eu/siacs/conversations/ui/util/IntroHelper.java deleted file mode 100644 index ef451314a..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/IntroHelper.java +++ /dev/null @@ -1,63 +0,0 @@ -package eu.siacs.conversations.ui.util; - -import static eu.siacs.conversations.ui.IntroActivity.ACTIVITY; -import static eu.siacs.conversations.ui.IntroActivity.MULTICHAT; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; - -import java.lang.ref.WeakReference; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.ui.IntroActivity; - -public class IntroHelper { - public static void showIntro(Activity activity, boolean mode_multi) { - new Thread(new showIntoFinisher(activity, mode_multi)).start(); - } - - private static class showIntoFinisher implements Runnable { - - private final WeakReference activityReference; - private final boolean mode_multi; - - private showIntoFinisher(Activity activity, boolean mode_multi) { - this.activityReference = new WeakReference<>(activity); - this.mode_multi = mode_multi; - } - - @Override - public void run() { - final Activity activity = activityReference.get(); - if (activity == null) { - return; - } - activity.runOnUiThread( - () -> { - SharedPreferences getPrefs = PreferenceManager.getDefaultSharedPreferences(activity.getBaseContext()); - String activityname = activity.getClass().getSimpleName(); - String INTRO = "intro_shown_on_activity_" + activityname + "_MultiMode_" + mode_multi; - boolean SHOW_INTRO = getPrefs.getBoolean(INTRO, true); - - - if (SHOW_INTRO && Config.SHOW_INTRO) { - final Intent i = new Intent(activity, IntroActivity.class); - i.putExtra(ACTIVITY, activityname); - i.putExtra(MULTICHAT, mode_multi); - activity.runOnUiThread(() -> activity.startActivity(i)); - } - }); - } - } - - public static void SaveIntroShown(Context context, String activity, boolean mode_multi) { - SharedPreferences getPrefs = PreferenceManager.getDefaultSharedPreferences(context); - String INTRO = "intro_shown_on_activity_" + activity + "_MultiMode_" + mode_multi; - SharedPreferences.Editor e = getPrefs.edit(); - e.putBoolean(INTRO, false); - e.apply(); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/util/JidDialog.java b/src/main/java/eu/siacs/conversations/ui/util/JidDialog.java deleted file mode 100644 index c506be513..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/JidDialog.java +++ /dev/null @@ -1,23 +0,0 @@ -package eu.siacs.conversations.ui.util; - -import android.content.Context; -import android.text.SpannableString; -import android.text.Spanned; -import android.text.style.TypefaceSpan; - -import androidx.annotation.StringRes; - -public class JidDialog { - - public static SpannableString style(Context context, @StringRes int res, String... args) { - SpannableString spannable = new SpannableString(context.getString(res, (Object[]) args)); - if (args.length >= 1) { - final String value = args[0]; - int start = spannable.toString().indexOf(value); - if (start >= 0) { - spannable.setSpan(new TypefaceSpan("monospace"), start, start + value.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - return spannable; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/KeyboardUtils.java b/src/main/java/eu/siacs/conversations/ui/util/KeyboardUtils.java deleted file mode 100644 index 6c4859ad0..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/KeyboardUtils.java +++ /dev/null @@ -1,121 +0,0 @@ -package eu.siacs.conversations.ui.util; - -import android.app.Activity; -import android.content.Context; -import android.graphics.Rect; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.view.inputmethod.InputMethodManager; - -import java.util.HashMap; - -/** - * Based on the following Stackoverflow answer: - * http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android - * https://github.com/ravindu1024/android-keyboardlistener/tree/master/keyboard-listener/src/main/java/com/rw/keyboardlistener - */ -@SuppressWarnings("WeakerAccess") -public class KeyboardUtils implements ViewTreeObserver.OnGlobalLayoutListener { - private final static int MAGIC_NUMBER = 200; - - private SoftKeyboardToggleListener mCallback; - private View mRootView; - private Boolean prevValue = null; - private float mScreenDensity; - private static HashMap sListenerMap = new HashMap<>(); - - public interface SoftKeyboardToggleListener { - void onToggleSoftKeyboard(boolean isVisible); - } - - - @Override - public void onGlobalLayout() { - Rect r = new Rect(); - mRootView.getWindowVisibleDisplayFrame(r); - - int heightDiff = mRootView.getRootView().getHeight() - (r.bottom - r.top); - float dp = heightDiff / mScreenDensity; - boolean isVisible = dp > MAGIC_NUMBER; - - if (mCallback != null && (prevValue == null || isVisible != prevValue)) { - prevValue = isVisible; - mCallback.onToggleSoftKeyboard(isVisible); - } - } - - /** - * Add a new keyboard listener - * - * @param act calling activity - * @param listener callback - */ - public static void addKeyboardToggleListener(Activity act, SoftKeyboardToggleListener listener) { - removeKeyboardToggleListener(listener); - - sListenerMap.put(listener, new KeyboardUtils(act, listener)); - } - - /** - * Remove a registered listener - * - * @param listener {@link SoftKeyboardToggleListener} - */ - public static void removeKeyboardToggleListener(SoftKeyboardToggleListener listener) { - if (sListenerMap.containsKey(listener)) { - KeyboardUtils k = sListenerMap.get(listener); - k.removeListener(); - - sListenerMap.remove(listener); - } - } - - /** - * Remove all registered keyboard listeners - */ - public static void removeAllKeyboardToggleListeners() { - for (SoftKeyboardToggleListener l : sListenerMap.keySet()) - sListenerMap.get(l).removeListener(); - - sListenerMap.clear(); - } - - /** - * Manually toggle soft keyboard visibility - * - * @param context calling context - */ - public static void toggleKeyboardVisibility(Context context) { - InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); - if (inputMethodManager != null) - inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); - } - - /** - * Force closes the soft keyboard - * - * @param activeView the view with the keyboard focus - */ - public static void forceCloseKeyboard(View activeView) { - InputMethodManager inputMethodManager = (InputMethodManager) activeView.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - if (inputMethodManager != null) - inputMethodManager.hideSoftInputFromWindow(activeView.getWindowToken(), 0); - } - - private void removeListener() { - mCallback = null; - - mRootView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - } - - private KeyboardUtils(Activity act, SoftKeyboardToggleListener listener) { - mCallback = listener; - - mRootView = ((ViewGroup) act.findViewById(android.R.id.content)).getChildAt(0); - mRootView.getViewTreeObserver().addOnGlobalLayoutListener(this); - - mScreenDensity = act.getResources().getDisplayMetrics().density; - } -} - diff --git a/src/main/java/eu/siacs/conversations/ui/util/ListViewUtils.java b/src/main/java/eu/siacs/conversations/ui/util/ListViewUtils.java deleted file mode 100644 index 2a4463a2d..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/ListViewUtils.java +++ /dev/null @@ -1,52 +0,0 @@ -/* -* Copyright (c) 2018, Daniel Gultsch All rights reserved. -* -* Redistribution and use in source and binary forms, with or without modification, -* are permitted provided that the following conditions are met: -* -* 1. Redistributions of source code must retain the above copyright notice, this -* list of conditions and the following disclaimer. -* -* 2. Redistributions in binary form must reproduce the above copyright notice, -* this list of conditions and the following disclaimer in the documentation and/or -* other materials provided with the distribution. -* -* 3. Neither the name of the copyright holder nor the names of its contributors -* may be used to endorse or promote products derived from this software without -* specific prior written permission. -* -* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -package eu.siacs.conversations.ui.util; - -import android.view.View; -import android.widget.ListView; - -public class ListViewUtils { - public static void scrollToBottom(final ListView listView) { - final int count = listView.getAdapter().getCount(); - if (count > 0) { - setSelection(listView, count - 1, true); - } - } - - public static void setSelection(final ListView listView, int pos, boolean jumpToBottom) { - if (jumpToBottom) { - final View lastChild = listView.getChildAt(listView.getChildCount() - 1); - if (lastChild != null) { - listView.setSelectionFromTop(pos, -lastChild.getHeight()); - return; - } - } - listView.setSelection(pos); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/LocationHelper.java b/src/main/java/eu/siacs/conversations/ui/util/LocationHelper.java deleted file mode 100644 index 7164e0769..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/LocationHelper.java +++ /dev/null @@ -1,69 +0,0 @@ -package eu.siacs.conversations.ui.util; - -import android.location.Location; - -import org.osmdroid.util.GeoPoint; - -import eu.siacs.conversations.Config; - -public final class LocationHelper { - /** - * Parses a lat long string in the form "lat,long". - * - * @param latlong A string in the form "lat,long" - * @return A GeoPoint representing the lat,long string. - * @throws NumberFormatException If an invalid lat or long is specified. - */ - public static GeoPoint parseLatLong(final String latlong) throws NumberFormatException { - if (latlong == null || latlong.isEmpty()) { - return null; - } - - final String[] parts = latlong.split(","); - if (parts[1].contains("?")) { - parts[1] = parts[1].substring(0, parts[1].indexOf("?")); - } - return new GeoPoint(Double.valueOf(parts[0]), Double.valueOf(parts[1])); - } - - private static boolean isSameProvider(final String provider1, final String provider2) { - if (provider1 == null) { - return provider2 == null; - } - return provider1.equals(provider2); - } - - public static boolean isBetterLocation(final Location location, final Location prevLoc) { - if (prevLoc == null) { - return true; - } - - // Check whether the new location fix is newer or older - final long timeDelta = location.getTime() - prevLoc.getTime(); - final boolean isSignificantlyNewer = timeDelta > Config.Map.LOCATION_FIX_SIGNIFICANT_TIME_DELTA; - final boolean isSignificantlyOlder = timeDelta < -Config.Map.LOCATION_FIX_SIGNIFICANT_TIME_DELTA; - final boolean isNewer = timeDelta > 0; - - if (isSignificantlyNewer) { - return true; - } else if (isSignificantlyOlder) { - return false; - } - - // Check whether the new location fix is more or less accurate - final int accuracyDelta = (int) (location.getAccuracy() - prevLoc.getAccuracy()); - final boolean isLessAccurate = accuracyDelta > 0; - final boolean isMoreAccurate = accuracyDelta < 0; - final boolean isSignificantlyLessAccurate = accuracyDelta > 200; - - // Check if the old and new location are from the same provider - final boolean isFromSameProvider = isSameProvider(location.getProvider(), prevLoc.getProvider()); - - // Determine location quality using a combination of timeliness and accuracy - if (isMoreAccurate) { - return true; - } else if (isNewer && !isLessAccurate) { - return true; - } else return isNewer && !isSignificantlyLessAccurate && isFromSameProvider; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/MainThreadExecutor.java b/src/main/java/eu/siacs/conversations/ui/util/MainThreadExecutor.java deleted file mode 100644 index 49f8ffdf4..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/MainThreadExecutor.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2019 Daniel Gultsch - * 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.ui.util; - -import android.os.Handler; -import android.os.Looper; - -import java.util.concurrent.Executor; - -public class MainThreadExecutor implements Executor { - - private static final MainThreadExecutor INSTANCE = new MainThreadExecutor(); - - private final Handler handler = new Handler(Looper.myLooper()); - - @Override - public void execute(final Runnable command) { - handler.post(command); - } - - public static MainThreadExecutor getInstance() { - return INSTANCE; - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/util/MucConfiguration.java b/src/main/java/eu/siacs/conversations/ui/util/MucConfiguration.java deleted file mode 100644 index 7201eb1e4..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/MucConfiguration.java +++ /dev/null @@ -1,133 +0,0 @@ -package eu.siacs.conversations.ui.util; - -import android.content.Context; -import android.os.Bundle; - -import androidx.annotation.StringRes; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.MucOptions; - -public class MucConfiguration { - - public final @StringRes - int title; - public final String[] names; - public final boolean[] values; - public final Option[] options; - - private MucConfiguration(@StringRes int title, String[] names, boolean[] values, Option[] options) { - this.title = title; - this.names = names; - this.values = values; - this.options = options; - } - - public static MucConfiguration get(Context context, boolean advanced, MucOptions mucOptions) { - if (mucOptions.isPrivateAndNonAnonymous()) { - String[] names = new String[]{ - context.getString(R.string.allow_participants_to_edit_subject), - context.getString(R.string.allow_participants_to_invite_others) - }; - boolean[] values = new boolean[]{ - mucOptions.participantsCanChangeSubject(), - mucOptions.allowInvites() - }; - final Option[] options = new Option[]{ - new Option("muc#roomconfig_changesubject"), - new Option("muc#roomconfig_allowinvites") - }; - return new MucConfiguration(R.string.conference_options, names, values, options); - } else { - final String[] names; - final boolean[] values; - final Option[] options; - if (advanced) { - names = new String[]{ - context.getString(R.string.non_anonymous), - context.getString(R.string.allow_participants_to_edit_subject), - context.getString(R.string.moderated) - }; - values = new boolean[]{ - mucOptions.nonanonymous(), - mucOptions.participantsCanChangeSubject(), - mucOptions.moderated() - }; - options = new Option[]{ - new Option("muc#roomconfig_whois", "anyone", "moderators"), - new Option("muc#roomconfig_changesubject"), - new Option("muc#roomconfig_moderatedroom") - }; - } else { - names = new String[]{ - context.getString(R.string.non_anonymous), - context.getString(R.string.allow_participants_to_edit_subject), - }; - values = new boolean[]{ - mucOptions.nonanonymous(), - mucOptions.participantsCanChangeSubject() - }; - options = new Option[]{ - new Option("muc#roomconfig_whois", "anyone", "moderators"), - new Option("muc#roomconfig_changesubject") - }; - } - return new MucConfiguration(R.string.channel_options, names, values, options); - } - } - - public static String describe(final Context context, final MucOptions mucOptions) { - final StringBuilder builder = new StringBuilder(); - if (mucOptions.isPrivateAndNonAnonymous()) { - if (mucOptions.participantsCanChangeSubject()) { - builder.append(context.getString(R.string.anyone_can_edit_subject)); - } else { - builder.append(context.getString(R.string.owners_can_edit_subject)); - } - builder.append(' '); - if (mucOptions.allowInvites()) { - builder.append(context.getString(R.string.anyone_can_invite_others)); - } else { - builder.append(context.getString(R.string.owners_can_invite_others)); - } - } else { - if (mucOptions.nonanonymous()) { - builder.append(context.getString(R.string.jabber_ids_are_visible_to_anyone)); - } else { - builder.append(context.getString(R.string.jabber_ids_are_visible_to_admins)); - } - builder.append(' '); - if (mucOptions.participantsCanChangeSubject()) { - builder.append(context.getString(R.string.anyone_can_edit_subject)); - } else { - builder.append(context.getString(R.string.admins_can_edit_subject)); - } - } - return builder.toString(); - } - - public Bundle toBundle(boolean[] values) { - Bundle bundle = new Bundle(); - for (int i = 0; i < values.length; ++i) { - final Option option = options[i]; - bundle.putString(option.name, option.values[values[i] ? 0 : 1]); - } - return bundle; - } - - private static class Option { - public final String name; - public final String[] values; - - private Option(String name) { - this.name = name; - this.values = new String[]{"1", "0"}; - } - - private Option(String name, String on, String off) { - this.name = name; - this.values = new String[]{on, off}; - } - } - -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java b/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java deleted file mode 100644 index 5c7b3f973..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java +++ /dev/null @@ -1,313 +0,0 @@ -package eu.siacs.conversations.ui.util; - -import android.app.Activity; -import android.graphics.Typeface; -import android.preference.PreferenceManager; -import android.text.SpannableString; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.style.ForegroundColorSpan; -import android.text.style.RelativeSizeSpan; -import android.text.style.StyleSpan; -import android.text.style.TypefaceSpan; -import android.view.ContextMenu; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; - -import androidx.appcompat.app.AlertDialog; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.entities.MucOptions.User; -import eu.siacs.conversations.entities.RawBlockable; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.ConferenceDetailsActivity; -import eu.siacs.conversations.ui.ConversationFragment; -import eu.siacs.conversations.ui.ConversationsActivity; -import eu.siacs.conversations.ui.MucUsersActivity; -import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.xmpp.Jid; - -public final class MucDetailsContextMenuHelper { - - private static int titleColor = 0xff0091ea; - - public static void onCreateContextMenu(ContextMenu menu, View v) { - final XmppActivity activity = XmppActivity.find(v); - final Object tag = v.getTag(); - if (tag instanceof MucOptions.User && activity != null) { - activity.getMenuInflater().inflate(R.menu.muc_details_context, menu); - final MucOptions.User user = (MucOptions.User) tag; - String name; - final Contact contact = user.getContact(); - if (contact != null && contact.showInContactList()) { - name = contact.getDisplayName(); - } else if (user.getRealJid() != null) { - name = user.getRealJid().asBareJid().toEscapedString(); - } else { - name = user.getName(); - } - menu.setHeaderTitle(name); - MucDetailsContextMenuHelper.configureMucDetailsContextMenu(activity, menu, user.getConversation(), user); - } - } - - public static void configureMucDetailsContextMenu(Activity activity, Menu menu, Conversation conversation, User user) { - configureMucDetailsContextMenu(activity, menu, conversation, user, false, null); - } - - public static void configureMucDetailsContextMenu(Activity activity, Menu menu, Conversation conversation, User user, boolean forceContextMenu, String username) { - final boolean advancedMode = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean("advanced_muc_mode", false); - final MucOptions mucOptions = conversation.getMucOptions(); - final boolean isGroupChat = mucOptions.isPrivateAndNonAnonymous(); - MenuItem title = menu.findItem(R.id.title); - MenuItem showAvatar = menu.findItem(R.id.action_show_avatar); - showAvatar.setVisible(user != null); - MenuItem showMucContactDetails = menu.findItem(R.id.action_muc_contact_details); - showMucContactDetails.setVisible(user != null && user.getRealJid() == null); - if (forceContextMenu && username != null) { - SpannableStringBuilder menuTitle = new SpannableStringBuilder(username); - menuTitle.setSpan(new ForegroundColorSpan(titleColor), 0, menuTitle.length(), 0); - menuTitle.setSpan(new StyleSpan(Typeface.BOLD), 0, menuTitle.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - menuTitle.setSpan(new RelativeSizeSpan(0.875f), 0, menuTitle.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - title.setTitle(menuTitle); - title.setVisible(true); - } else { - title.setVisible(false); - } - MenuItem sendPrivateMessage = menu.findItem(R.id.send_private_message); - MenuItem blockUnblockMUCUser = menu.findItem(R.id.context_muc_contact_block_unblock); - if (user != null && user.getRealJid() != null) { - MenuItem showContactDetails = menu.findItem(R.id.action_contact_details); - MenuItem startConversation = menu.findItem(R.id.start_conversation); - MenuItem addToRoster = menu.findItem(R.id.add_contact); - MenuItem giveMembership = menu.findItem(R.id.give_membership); - MenuItem removeMembership = menu.findItem(R.id.remove_membership); - MenuItem giveAdminPrivileges = menu.findItem(R.id.give_admin_privileges); - MenuItem removeAdminPrivileges = menu.findItem(R.id.remove_admin_privileges); - MenuItem giveOwnerPrivileges = menu.findItem(R.id.give_owner_privileges); - MenuItem removeOwnerPrivileges = menu.findItem(R.id.revoke_owner_privileges); - MenuItem managePermissions = menu.findItem(R.id.manage_permissions); - MenuItem removeFromRoom = menu.findItem(R.id.kick_from_room); - removeFromRoom.setTitle(isGroupChat ? R.string.kick_from_room : R.string.remove_from_channel); - MenuItem banFromConference = menu.findItem(R.id.ban_from_room); - banFromConference.setTitle(isGroupChat ? R.string.ban_from_conference : R.string.ban_from_channel); - MenuItem invite = menu.findItem(R.id.invite); - MenuItem highlightInMuc = menu.findItem(R.id.highlight_in_muc); - startConversation.setVisible(true); - final Jid jid = user.getRealJid(); - final Account account = conversation.getAccount(); - final Contact contact = jid == null ? null : account.getRoster().getContact(jid); - final User self = conversation.getMucOptions().getSelf(); - addToRoster.setVisible(contact != null && !contact.showInRoster()); - showContactDetails.setVisible(contact == null || !contact.isSelf()); - if ((activity instanceof ConferenceDetailsActivity || activity instanceof MucUsersActivity) && user.getRole() == MucOptions.Role.NONE) { - invite.setVisible(true); - } - if (activity instanceof ConversationsActivity) { - highlightInMuc.setVisible(false); - } else if (activity instanceof ConferenceDetailsActivity) { - highlightInMuc.setVisible(true); - } - boolean managePermissionsVisible = false; - if ((self.getAffiliation().ranks(MucOptions.Affiliation.ADMIN) && self.getAffiliation().outranks(user.getAffiliation())) || self.getAffiliation() == MucOptions.Affiliation.OWNER) { - if (advancedMode) { - if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) { - managePermissionsVisible = true; - giveMembership.setVisible(true); - } else if (user.getAffiliation() == MucOptions.Affiliation.MEMBER) { - managePermissionsVisible = true; - removeMembership.setVisible(true); - } - if (!Config.DISABLE_BAN) { - managePermissionsVisible = true; - banFromConference.setVisible(true); - } - } - if (!Config.DISABLE_BAN) { - removeFromRoom.setVisible(true); - } - - } - if (self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) { - if (isGroupChat || advancedMode || user.getAffiliation() == MucOptions.Affiliation.OWNER) { - if (!user.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) { - managePermissionsVisible = true; - giveOwnerPrivileges.setVisible(true); - } else if (user.getAffiliation() == MucOptions.Affiliation.OWNER) { - managePermissionsVisible = true; - removeOwnerPrivileges.setVisible(true); - } - } - if (!isGroupChat || advancedMode || user.getAffiliation() == MucOptions.Affiliation.ADMIN) { - if (!user.getAffiliation().ranks(MucOptions.Affiliation.ADMIN)) { - managePermissionsVisible = true; - giveAdminPrivileges.setVisible(true); - } else if (user.getAffiliation() == MucOptions.Affiliation.ADMIN) { - managePermissionsVisible = true; - removeAdminPrivileges.setVisible(true); - } - } - } - managePermissions.setVisible(managePermissionsVisible); - sendPrivateMessage.setVisible(true); - sendPrivateMessage.setEnabled(mucOptions.allowPm()); - blockUnblockMUCUser.setVisible(true); - } else { - sendPrivateMessage.setVisible(true); - sendPrivateMessage.setEnabled(user != null && mucOptions.allowPm() && user.getRole().ranks(MucOptions.Role.VISITOR)); - blockUnblockMUCUser.setVisible(user != null); - } - } - - public static boolean onContextItemSelected(MenuItem item, User user, XmppActivity activity) { - return onContextItemSelected(item, user, activity, null); - } - - public static boolean onContextItemSelected(MenuItem item, User user, XmppActivity activity, final String fingerprint) { - final Conversation conversation = user.getConversation(); - final XmppConnectionService.OnAffiliationChanged onAffiliationChanged = activity instanceof XmppConnectionService.OnAffiliationChanged ? (XmppConnectionService.OnAffiliationChanged) activity : null; - final Jid jid = user.getRealJid(); - final Account account = conversation.getAccount(); - final Contact contact = jid == null ? null : account.getRoster().getContact(jid); - switch (item.getItemId()) { - case R.id.action_show_avatar: - activity.ShowAvatarPopup(activity, user); - return true; - case R.id.action_contact_details: - if (contact != null) { - activity.switchToContactDetails(contact, fingerprint); - } - return true; - case R.id.action_muc_contact_details: - if (user != null) { - activity.switchToMucContactDetails(user); - } - return true; - case R.id.start_conversation: - startConversation(user, activity); - return true; - case R.id.add_contact: - activity.showAddToRosterDialog(contact); - return true; - case R.id.give_admin_privileges: - activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.ADMIN, onAffiliationChanged); - return true; - case R.id.give_membership: - case R.id.remove_admin_privileges: - case R.id.revoke_owner_privileges: - activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.MEMBER, onAffiliationChanged); - return true; - case R.id.give_owner_privileges: - activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.OWNER, onAffiliationChanged); - return true; - case R.id.remove_membership: - activity.xmppConnectionService.changeAffiliationInConference(conversation, jid, MucOptions.Affiliation.NONE, onAffiliationChanged); - return true; - case R.id.kick_from_room: - kickFromRoom(user, activity, onAffiliationChanged); - return true; - case R.id.ban_from_room: - banFromRoom(user, activity, onAffiliationChanged); - return true; - case R.id.send_private_message: - if (activity instanceof ConversationsActivity) { - ConversationFragment conversationFragment = ConversationFragment.get(activity); - if (conversationFragment != null) { - activity.invalidateOptionsMenu(); - conversationFragment.privateMessageWith(user.getFullJid()); - return true; - } - } - activity.privateMsgInMuc(conversation, user.getAvatarName()); - return true; - case R.id.invite: - if (user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) { - activity.xmppConnectionService.directInvite(conversation, jid.asBareJid()); - } else { - activity.xmppConnectionService.invite(conversation, jid); - } - return true; - case R.id.context_muc_contact_block_unblock: - try { - activity.xmppConnectionService.sendBlockRequest(new RawBlockable(account, user.getFullJid()), false); - activity.xmppConnectionService.leaveMuc(conversation); - activity.xmppConnectionService.joinMuc(conversation); - } catch (Exception e) { - e.printStackTrace(); - } - return true; - case R.id.highlight_in_muc: - activity.highlightInMuc(conversation, user.getName()); - return true; - default: - return false; - } - } - - private static void kickFromRoom(final User user, XmppActivity - activity, XmppConnectionService.OnAffiliationChanged onAffiliationChanged) { - final Conversation conversation = user.getConversation(); - final AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(R.string.kick_from_conference); - String jid = user.getRealJid().asBareJid().toEscapedString(); - SpannableString message; - if (conversation.getMucOptions().membersOnly()) { - message = new SpannableString(activity.getString(R.string.kicking_from_conference, jid)); - } else { - message = new SpannableString(activity.getString(R.string.kicking_from_public_conference, jid)); - } - int start = message.toString().indexOf(jid); - if (start >= 0) { - message.setSpan(new TypefaceSpan("monospace"), start, start + jid.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - builder.setMessage(message); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.kick_now, (dialog, which) -> { - activity.xmppConnectionService.changeAffiliationInConference(conversation, user.getRealJid(), MucOptions.Affiliation.NONE, onAffiliationChanged); - if (user.getRole() != MucOptions.Role.NONE) { - activity.xmppConnectionService.changeRoleInConference(conversation, user.getName(), MucOptions.Role.NONE); - } - }); - builder.create().show(); - } - - private static void banFromRoom(final User user, XmppActivity - activity, XmppConnectionService.OnAffiliationChanged onAffiliationChanged) { - final Conversation conversation = user.getConversation(); - final AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(R.string.ban_from_conference); - String jid = user.getRealJid().asBareJid().toString(); - SpannableString message; - if (conversation.getMucOptions().membersOnly()) { - message = new SpannableString(activity.getString(R.string.ban_from_conference_message, jid)); - } else { - message = new SpannableString(activity.getString(R.string.ban_from_public_conference_message, jid)); - } - int start = message.toString().indexOf(jid); - if (start >= 0) { - message.setSpan(new TypefaceSpan("monospace"), start, start + jid.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - builder.setMessage(message); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.ban_now, (dialog, which) -> { - activity.xmppConnectionService.changeAffiliationInConference(conversation, user.getRealJid(), MucOptions.Affiliation.OUTCAST, onAffiliationChanged); - if (user.getRole() != MucOptions.Role.NONE) { - activity.xmppConnectionService.changeRoleInConference(conversation, user.getName(), MucOptions.Role.NONE); - } - }); - builder.create().show(); - } - - private static void startConversation(User user, XmppActivity activity) { - if (user.getRealJid() != null) { - Conversation newConversation = activity.xmppConnectionService.findOrCreateConversation(user.getAccount(), user.getRealJid().asBareJid(), false, true); - activity.switchToConversation(newConversation); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java deleted file mode 100644 index db416253f..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java +++ /dev/null @@ -1,413 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui.util; - -import android.content.Context; -import android.content.SharedPreferences; -import android.net.Uri; -import android.preference.PreferenceManager; -import android.text.Editable; -import android.text.SpannableString; -import android.text.SpannableStringBuilder; -import android.text.TextUtils; -import android.text.style.URLSpan; -import android.text.util.Linkify; -import android.util.Base64; -import android.util.Log; -import android.webkit.URLUtil; - -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.ListItem; -import eu.siacs.conversations.entities.Roster; -import eu.siacs.conversations.ui.SettingsActivity; -import eu.siacs.conversations.ui.text.FixedURLSpan; -import eu.siacs.conversations.utils.GeoHelper; -import eu.siacs.conversations.utils.Patterns; -import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.xmpp.Jid; - -public class MyLinkify { - - private final static Pattern youtubePattern = Pattern.compile("(www\\.|m\\.)?(youtube\\.com|youtu\\.be|youtube-nocookie\\.com)/(((?!([\"'<])).)*)"); - private final static String youtubeURLPattern = "(?:youtube(?:-nocookie)?\\.com\\/(?:[^\\/\\n\\s]+\\/\\S+\\/|(?:v|e(?:mbed)?)\\/|\\S*?[?&]v=)|youtu\\.be\\/)([a-zA-Z0-9_-]{11})"; - - public static boolean isYoutubeUrl(String url) { - return !url.isEmpty() && url.matches("(?i:http|https):\\/\\/" + youtubePattern); - } - - public static String getYoutubeVideoId(String url) { - if (url == null || url.trim().length() <= 0) { - return null; - } - final Pattern pattern = Pattern.compile(youtubeURLPattern, Pattern.CASE_INSENSITIVE); - final Matcher matcher = pattern.matcher(url); - if (matcher.find()) { - return matcher.group(1); - } - return null; - } - - public static String getYoutubeImageUrl(String url) { - return "https://img.youtube.com/vi/" + getYoutubeVideoId(url) + "/0.jpg"; - } - - public static String replaceYoutube(Context context, String content) { - return replaceYoutube(context, new SpannableStringBuilder(content)).toString(); - } - - public static SpannableStringBuilder replaceYoutube(Context context, SpannableStringBuilder content) { - Matcher matcher = youtubePattern.matcher(content); - if (useInvidious(context)) { - while (matcher.find()) { - final String youtubeId = matcher.group(3); - if (matcher.group(2) != null && matcher.group(2).equals("youtu.be")) { - content = new SpannableStringBuilder(content.toString().replaceAll("https://" + Pattern.quote(matcher.group()), Matcher.quoteReplacement("https://" + invidiousHost(context) + "/watch?v=" + youtubeId + "&local=true"))); - } else { - content = new SpannableStringBuilder(content.toString().replaceAll("https://" + Pattern.quote(matcher.group()), Matcher.quoteReplacement("https://" + invidiousHost(context) + "/" + youtubeId + "&local=true"))); - } - } - } - return content; - } - - // https://github.com/M66B/FairEmail/blob/master/app/src/main/java/eu/faircode/email/UriHelper.java - // https://github.com/newhouse/url-tracking-stripper - private static final List PARANOID_QUERY = Collections.unmodifiableList(Arrays.asList( - // own paramaters - "feed_id", - "sd", - "ncid", - "ref", - "ref_", - "sfnsn", // Facebook - "s", "fs", // Facebook, may produce false positives - "utm_source", "utm_medium", "utm_term", "utm_campaign", "utm_content", "utm_name", // Google - "utm_cid", "utm_reader", "utm_viz_id", "utm_pubreferrer", "utm_swu", // Google - "_hsmi", // Hubspot - "mkt_tok", // Marketo - "sr_share", // SimpleReach - "nr_email_referer", - "t_ref", // Bild Zeitung - "oft_id", "oft_k", "oft_lk", "oft_d", "oft_c", "oft_ck", "oft_ids", "oft_sk", // ofsys.com - "ss_email_id", // Squarespace Newsletter tracker - "bsft_uid", "bsft_clkid", // Blueshift Mail Tracker - // end of own paramaters - - // https://en.wikipedia.org/wiki/UTM_parameters - "awt_a", // AWeber - "awt_l", // AWeber - "awt_m", // AWeber - - "icid", // Adobe - "ef_id", // https://experienceleague.adobe.com/docs/advertising-cloud/integrations/analytics/mc/mc-ids.html - "_ga", // Google Analytics - "gclid", // Google - "gclsrc", // Google ads - "dclid", // DoubleClick (Google) - "fbclid", // Facebook - "igshid", // Instagram - "msclkid", // https://help.ads.microsoft.com/apex/index/3/en/60000 - - "mc_cid", // MailChimp - "mc_eid", // MailChimp - - "zanpid", // Zanox (Awin) - - "kclickid", // https://support.freespee.com/hc/en-us/articles/202577831-Kenshoo-integration - - // https://github.com/brave/brave-core/blob/master/browser/net/brave_site_hacks_network_delegate_helper.cc - "oly_anon_id", "oly_enc_id", // https://training.omeda.com/knowledge-base/olytics-product-outline/ - "_openstat", // https://yandex.com/support/direct/statistics/url-tags.html - "vero_conv", "vero_id", // https://help.getvero.com/cloud/articles/what-is-vero_id/ - "wickedid", // https://help.wickedreports.com/how-to-manually-tag-a-facebook-ad-with-wickedid - "yclid", // https://ads-help.yahoo.co.jp/yahooads/ss/articledetail?lan=en&aid=20442 - "__s", // https://ads-help.yahoo.co.jp/yahooads/ss/articledetail?lan=en&aid=20442 - "rb_clickid", // Russian - "s_cid", // https://help.goacoustic.com/hc/en-us/articles/360043311613-Track-lead-sources - "ml_subscriber", "ml_subscriber_hash", // https://www.mailerlite.com/help/how-to-integrate-your-forms-to-a-wix-website - "twclid", // https://business.twitter.com/en/blog/performance-advertising-on-twitter.html - "gbraid", "wbraid", // https://support.google.com/google-ads/answer/10417364 - "_hsenc", "__hssc", "__hstc", "__hsfp", "hsCtaTracking" // https://knowledge.hubspot.com/reports/what-cookies-does-hubspot-set-in-a-visitor-s-browser - )); - - // https://github.com/snarfed/granary/blob/master/granary/facebook.py#L1789 - - private static final List FACEBOOK_WHITELIST_PATH = Collections.unmodifiableList(Arrays.asList( - "/nd/", "/n/", "/story.php" - )); - - private static final List FACEBOOK_WHITELIST_QUERY = Collections.unmodifiableList(Arrays.asList( - "story_fbid", "fbid", "id", "comment_id" - )); - - public static SpannableString removeTrackingParameter(Uri uri) { - if (uri.isOpaque()) { - return new SpannableString(uri.toString()); - } - boolean changed = false; - Uri url; - Uri.Builder builder; - if (uri.getHost() != null && - uri.getHost().endsWith("safelinks.protection.outlook.com") && - !TextUtils.isEmpty(uri.getQueryParameter("url"))) { - changed = true; - url = Uri.parse(uri.getQueryParameter("url")); - } else if ("https".equals(uri.getScheme()) && - "smex-ctp.trendmicro.com".equals(uri.getHost()) && - "/wis/clicktime/v1/query".equals(uri.getPath()) && - !TextUtils.isEmpty(uri.getQueryParameter("url"))) { - changed = true; - url = Uri.parse(uri.getQueryParameter("url")); - } else if ("https".equals(uri.getScheme()) && - "www.google.com".equals(uri.getHost()) && - uri.getPath() != null && - uri.getPath().startsWith("/amp/")) { - // https://blog.amp.dev/2017/02/06/whats-in-an-amp-url/ - Uri result = null; - String u = uri.toString(); - u = u.replace("https://www.google.com/amp/", ""); - int p = u.indexOf("/"); - while (p > 0) { - String segment = u.substring(0, p); - if (segment.contains(".")) { - result = Uri.parse("https://" + u); - break; - } - u = u.substring(p + 1); - p = u.indexOf("/"); - } - changed = (result != null); - url = (result == null ? uri : result); - } else if ("https".equals(uri.getScheme()) && - uri.getHost() != null && - uri.getHost().startsWith("www.google.") && - uri.getQueryParameter("url") != null) { - // Google non-com redirects - Uri result = Uri.parse(uri.getQueryParameter("url")); - changed = (result != null); - url = (result == null ? uri : result); - } else if (uri.getQueryParameterNames().size() == 1) { - // Sophos Email Appliance - Uri result = null; - String key = uri.getQueryParameterNames().iterator().next(); - if (TextUtils.isEmpty(uri.getQueryParameter(key))) - try { - String data = new String(Base64.decode(key, Base64.DEFAULT)); - int v = data.indexOf("ver="); - int u = data.indexOf("&&url="); - if (v == 0 && u > 0) - result = Uri.parse(URLDecoder.decode(data.substring(u + 6), StandardCharsets.UTF_8.name())); - } catch (Throwable ex) { - ex.printStackTrace(); - } - changed = (result != null); - url = (result == null ? uri : result); - } else if (uri.getQueryParameter("redirectUrl") != null) { - // https://.../link-tracker?redirectUrl=&sig=...&iat=...&a=...&account=...&email=...&s=...&i=... - try { - byte[] bytes = Base64.decode(uri.getQueryParameter("redirectUrl"), 0); - String u = URLDecoder.decode(new String(bytes), StandardCharsets.UTF_8.name()); - Uri result = Uri.parse(u); - changed = (result != null); - url = (result == null ? uri : result); - } catch (Throwable ex) { - ex.printStackTrace(); - url = uri; - } - } else { - url = uri; - } - if (url.isOpaque()) { - return new SpannableString(uri.toString()); - } - builder = url.buildUpon(); - builder.clearQuery(); - String host = uri.getHost(); - String path = uri.getPath(); - if (host != null) - host = host.toLowerCase(Locale.ROOT); - if (path != null) - path = path.toLowerCase(Locale.ROOT); - boolean first = "www.facebook.com".equals(host); - for (String key : url.getQueryParameterNames()) { - // https://en.wikipedia.org/wiki/UTM_parameters - // https://docs.oracle.com/en/cloud/saas/marketing/eloqua-user/Help/EloquaAsynchronousTrackingScripts/EloquaTrackingParameters.htm - String lkey = key.toLowerCase(Locale.ROOT); - if (PARANOID_QUERY.contains(lkey) || - lkey.startsWith("utm_") || - lkey.startsWith("elq") || - ((host != null && host.endsWith("facebook.com")) && - !first && - FACEBOOK_WHITELIST_PATH.contains(path) && - !FACEBOOK_WHITELIST_QUERY.contains(lkey)) || - ("store.steampowered.com".equals(host) && - "snr".equals(lkey))) { - changed = true; - } else if (!TextUtils.isEmpty(key)) { - for (String value : url.getQueryParameters(key)) { - Log.d(Config.LOGTAG, "Query " + key + "=" + value); - Uri suri = Uri.parse(value); - if ("http".equals(suri.getScheme()) || "https".equals(suri.getScheme())) { - Uri s = Uri.parse(removeTrackingParameter(suri).toString()); - if (s != null) { - changed = true; - value = s.toString(); - } - } - builder.appendQueryParameter(key, value); - } - } - first = false; - } - return (changed ? new SpannableString(builder.build().toString()) : new SpannableString(uri.toString())); - } - - private static boolean isValid(String url) { - String urlstring = url; - if (!urlstring.toLowerCase(Locale.US).startsWith("http://") && !urlstring.toLowerCase(Locale.US).startsWith("https://")) { - urlstring = "https://" + url; - } - try { - return URLUtil.isValidUrl(urlstring) && Patterns.WEB_URL.matcher(urlstring).matches(); - } catch (Exception e) { - Log.d(Config.LOGTAG, "Could not use invidious host and using youtube-nocookie " + e); - } - return false; - } - - private static final Linkify.TransformFilter WEBURL_TRANSFORM_FILTER = (matcher, url) -> { - if (url == null) { - return null; - } - final String lcUrl = url.toLowerCase(Locale.US); - if (lcUrl.startsWith("http://") || lcUrl.startsWith("https://")) { - return removeTrailingBracket(removeTrackingParameter(Uri.parse(url)).toString()); - } else { - return "http://" + removeTrailingBracket(removeTrackingParameter(Uri.parse(url)).toString()); - } - }; - - public static String removeTrailingBracket(final String url) { - int numOpenBrackets = 0; - for (char c : url.toCharArray()) { - if (c == '(') { - ++numOpenBrackets; - } else if (c == ')') { - --numOpenBrackets; - } - } - if (numOpenBrackets != 0 && url.charAt(url.length() - 1) == ')') { - return url.substring(0, url.length() - 1); - } else { - return url; - } - } - - private static final Linkify.MatchFilter WEBURL_MATCH_FILTER = (cs, start, end) -> { - if (start > 0) { - if (cs.charAt(start - 1) == '@' || cs.charAt(start - 1) == '.' - || cs.subSequence(Math.max(0, start - 3), start).equals("://")) { - return false; - } - } - if (end < cs.length()) { - // Reject strings that were probably matched only because they contain a dot followed by - // by some known TLD (see also comment for WORD_BOUNDARY in Patterns.java) - if (isAlphabetic(cs.charAt(end - 1)) && isAlphabetic(cs.charAt(end))) { - return false; - } - } - return true; - }; - - private static final Linkify.MatchFilter XMPPURI_MATCH_FILTER = (s, start, end) -> { - XmppUri uri = new XmppUri(s.subSequence(start, end).toString()); - return uri.isValidJid(); - }; - - private static boolean isAlphabetic(final int code) { - return Character.isAlphabetic(code); - } - - private static String invidiousHost(Context context) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - String invidioushost = sharedPreferences.getString(SettingsActivity.INVIDIOUS_HOST, context.getResources().getString(R.string.invidious_host)); - if (invidioushost.length() == 0) { - invidioushost = context.getResources().getString(R.string.invidious_host); - } - return invidioushost; - } - - private static boolean useInvidious(Context context) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - return sharedPreferences.getBoolean(SettingsActivity.USE_INVIDIOUS, context.getResources().getBoolean(R.bool.use_invidious)); - } - - public static void addLinks(Editable body, boolean includeGeo) { - Linkify.addLinks(body, Patterns.XMPP_PATTERN, "xmpp", XMPPURI_MATCH_FILTER, null); - Linkify.addLinks(body, Patterns.AUTOLINK_WEB_URL, "http", WEBURL_MATCH_FILTER, WEBURL_TRANSFORM_FILTER); - Linkify.addLinks(body, Patterns.PHONE, "tel:", Linkify.sPhoneNumberMatchFilter, Linkify.sPhoneNumberTransformFilter); - if (includeGeo) { - Linkify.addLinks(body, GeoHelper.GEO_URI, "geo"); - } - FixedURLSpan.fix(body); - } - - public static void addLinks(Editable body, Account account) { - addLinks(body, true); - Roster roster = account.getRoster(); - for (final URLSpan urlspan : body.getSpans(0, body.length() - 1, URLSpan.class)) { - Uri uri = Uri.parse(urlspan.getURL()); - if ("xmpp".equals(uri.getScheme())) { - try { - Jid jid = new XmppUri(uri).getJid(); - ListItem item = account.getBookmark(jid); - if (item == null) item = roster.getContact(jid); - body.replace( - body.getSpanStart(urlspan), - body.getSpanEnd(urlspan), - item.getDisplayName() - ); - } catch (final IllegalArgumentException e) { /* bad JID */ } - } - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/util/PendingActionHelper.java b/src/main/java/eu/siacs/conversations/ui/util/PendingActionHelper.java deleted file mode 100644 index aa73c186b..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/PendingActionHelper.java +++ /dev/null @@ -1,29 +0,0 @@ -package eu.siacs.conversations.ui.util; - -/** - * Created by mxf on 2018/4/3. - */ - -public class PendingActionHelper { - - private PendingAction pendingAction; - - public void push(PendingAction pendingAction) { - this.pendingAction = pendingAction; - } - - public void execute() { - if (pendingAction != null) { - pendingAction.execute(); - pendingAction = null; - } - } - - public void undo() { - pendingAction = null; - } - - public interface PendingAction { - void execute(); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/PresenceSelector.java b/src/main/java/eu/siacs/conversations/ui/util/PresenceSelector.java deleted file mode 100644 index b03840626..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/PresenceSelector.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui.util; - -import android.app.Activity; -import android.content.Context; -import android.util.Pair; - -import androidx.appcompat.app.AlertDialog; - -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Presences; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.jingle.RtpCapability; - -public class PresenceSelector { - - public static void showPresenceSelectionDialog(Activity activity, final Conversation conversation, final OnPresenceSelected listener) { - final Contact contact = conversation.getContact(); - final String[] resourceArray = contact.getPresences().toResourceArray(); - showPresenceSelectionDialog(activity, contact, resourceArray, fullJid -> { - conversation.setNextCounterpart(fullJid); - listener.onPresenceSelected(); - }); - } - - public static void selectFullJidForDirectRtpConnection(final Activity activity, final Contact contact, final RtpCapability.Capability required, final OnFullJidSelected onFullJidSelected) { - final String[] resources = RtpCapability.filterPresences(contact, required); - if (resources.length == 1) { - onFullJidSelected.onFullJidSelected(contact.getJid().withResource(resources[0])); - } else { - showPresenceSelectionDialog(activity, contact, resources, onFullJidSelected); - } - } - - private static void showPresenceSelectionDialog(final Activity activity, final Contact contact, final String[] resourceArray, final OnFullJidSelected onFullJidSelected) { - final Presences presences = contact.getPresences(); - final AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(activity.getString(R.string.choose_presence)); - Pair, Map> typeAndName = presences.toTypeAndNameMap(); - final Map resourceTypeMap = typeAndName.first; - final Map resourceNameMap = typeAndName.second; - final String[] readableIdentities = new String[resourceArray.length]; - final AtomicInteger selectedResource = new AtomicInteger(0); - for (int i = 0; i < resourceArray.length; ++i) { - String resource = resourceArray[i]; - if (resource.equals(contact.getLastResource())) { - selectedResource.set(i); - } - String type = resourceTypeMap.get(resource); - String name = resourceNameMap.get(resource); - if (type != null) { - if (Collections.frequency(resourceTypeMap.values(), type) == 1) { - readableIdentities[i] = translateType(activity, type); - } else if (name != null) { - if (Collections.frequency(resourceNameMap.values(), name) == 1 - || CryptoHelper.UUID_PATTERN.matcher(resource).matches()) { - readableIdentities[i] = translateType(activity, type) + " (" + name + ")"; - } else { - readableIdentities[i] = translateType(activity, type) + " (" + name + " / " + resource + ")"; - } - } else { - readableIdentities[i] = translateType(activity, type) + " (" + resource + ")"; - } - } else { - readableIdentities[i] = resource; - } - } - builder.setSingleChoiceItems(readableIdentities, - selectedResource.get(), - (dialog, which) -> selectedResource.set(which)); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton( - R.string.ok, - (dialog, which) -> onFullJidSelected.onFullJidSelected( - getNextCounterpart(contact, resourceArray[selectedResource.get()]) - ) - ); - builder.create().show(); - } - - public static Jid getNextCounterpart(final Contact contact, final String resource) { - return getNextCounterpart(contact.getJid(), resource); - } - - public static Jid getNextCounterpart(final Jid jid, final String resource) { - if (resource.isEmpty()) { - return jid.asBareJid(); - } else { - return jid.withResource(resource); - } - } - - public static void warnMutualPresenceSubscription(Activity activity, final Conversation conversation, final OnPresenceSelected listener) { - final AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(conversation.getContact().getJid().toString()); - builder.setMessage(R.string.without_mutual_presence_updates); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.ignore, (dialog, which) -> { - conversation.setNextCounterpart(null); - if (listener != null) { - listener.onPresenceSelected(); - } - }); - builder.create().show(); - } - - public static String translateType(Context context, String type) { - switch (type.toLowerCase()) { - case "pc": - return context.getString(R.string.type_pc); - case "phone": - return context.getString(R.string.type_phone); - case "tablet": - return context.getString(R.string.type_tablet); - case "web": - return context.getString(R.string.type_web); - case "console": - return context.getString(R.string.type_console); - default: - return type; - } - } - - public interface OnPresenceSelected { - void onPresenceSelected(); - } - - public interface OnFullJidSelected { - void onFullJidSelected(Jid jid); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java b/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java deleted file mode 100644 index 040d034a8..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java +++ /dev/null @@ -1,118 +0,0 @@ -package eu.siacs.conversations.ui.util; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.utils.MessageUtils; -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 (isPositionQuoteCharacter(line, i)) { - nestingDepth++; - } else if (line.charAt(i) != ' ') { - break; - } - } - return nestingDepth >= (Config.QUOTING_MAX_DEPTH); - } - 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; - } - - public static boolean isMessageQuoteable(Message m){ - if (m.isTypeText() && MessageUtils.prepareQuote(m).length() > 0) { - return true; - } - if (m.isFileOrImage()){ - return true; - } - return false; - } -} \ 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 deleted file mode 100644 index 31155cd6e..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/Rationals.java +++ /dev/null @@ -1,26 +0,0 @@ -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/ScrollState.java b/src/main/java/eu/siacs/conversations/ui/util/ScrollState.java deleted file mode 100644 index 4f37c87ae..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/ScrollState.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui.util; - -import android.os.Parcel; -import android.os.Parcelable; - -public class ScrollState implements Parcelable { - - public static final Creator CREATOR = new Creator() { - @Override - public ScrollState createFromParcel(Parcel in) { - return new ScrollState(in); - } - - @Override - public ScrollState[] newArray(int size) { - return new ScrollState[size]; - } - }; - public final int position; - public final int offset; - - private ScrollState(Parcel in) { - position = in.readInt(); - offset = in.readInt(); - } - - public ScrollState(int position, int offset) { - this.position = position; - this.offset = offset; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(position); - dest.writeInt(offset); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/SendButtonAction.java b/src/main/java/eu/siacs/conversations/ui/util/SendButtonAction.java deleted file mode 100644 index 3ca63c1d3..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/SendButtonAction.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui.util; - -import static eu.siacs.conversations.ui.ConversationFragment.ATTACHMENT_CHOICE; -import static eu.siacs.conversations.ui.ConversationFragment.ATTACHMENT_CHOICE_CHOOSE_IMAGE; -import static eu.siacs.conversations.ui.ConversationFragment.ATTACHMENT_CHOICE_LOCATION; -import static eu.siacs.conversations.ui.ConversationFragment.ATTACHMENT_CHOICE_RECORD_VIDEO; -import static eu.siacs.conversations.ui.ConversationFragment.ATTACHMENT_CHOICE_RECORD_VOICE; -import static eu.siacs.conversations.ui.ConversationFragment.ATTACHMENT_CHOICE_TAKE_PHOTO; - -public enum SendButtonAction { - TEXT, TAKE_PHOTO, SEND_LOCATION, RECORD_VOICE, CANCEL, CHOOSE_PICTURE, RECORD_VIDEO, CHOOSE_ATTACHMENT; - - public static SendButtonAction valueOfOrDefault(final String setting) { - if (setting == null) { - return TEXT; - } - try { - return valueOf(setting); - } catch (IllegalArgumentException e) { - return TEXT; - } - } - - public static SendButtonAction of(int attachmentChoice) { - switch (attachmentChoice) { - case ATTACHMENT_CHOICE_LOCATION: - return SEND_LOCATION; - case ATTACHMENT_CHOICE_RECORD_VOICE: - return RECORD_VOICE; - case ATTACHMENT_CHOICE_RECORD_VIDEO: - return RECORD_VIDEO; - case ATTACHMENT_CHOICE_TAKE_PHOTO: - return TAKE_PHOTO; - case ATTACHMENT_CHOICE_CHOOSE_IMAGE: - return CHOOSE_PICTURE; - case ATTACHMENT_CHOICE: - return CHOOSE_ATTACHMENT; - default: - throw new IllegalArgumentException("Not a known attachment choice"); - } - } - - public int toChoice() { - switch (this) { - case TAKE_PHOTO: - return ATTACHMENT_CHOICE_TAKE_PHOTO; - case RECORD_VIDEO: - return ATTACHMENT_CHOICE_RECORD_VIDEO; - case SEND_LOCATION: - return ATTACHMENT_CHOICE_LOCATION; - case RECORD_VOICE: - return ATTACHMENT_CHOICE_RECORD_VOICE; - case CHOOSE_PICTURE: - return ATTACHMENT_CHOICE_CHOOSE_IMAGE; - case CHOOSE_ATTACHMENT: - return ATTACHMENT_CHOICE; - default: - return 0; - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java b/src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java deleted file mode 100644 index 2c178b9df..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui.util; - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.TypedArray; -import android.graphics.Color; -import android.graphics.drawable.Drawable; -import android.preference.PreferenceManager; - -import androidx.appcompat.content.res.AppCompatResources; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.drawable.DrawableCompat; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Presence; -import eu.siacs.conversations.ui.ConversationFragment; -import eu.siacs.conversations.ui.SettingsActivity; -import eu.siacs.conversations.utils.UIHelper; - -public class SendButtonTool { - - public static SendButtonAction getAction(final Activity activity, final Conversation c, final String text) { - if (activity == null) { - return SendButtonAction.TEXT; - } - final boolean empty = text.length() == 0; - final boolean conference = c.getMode() == Conversation.MODE_MULTI; - if (c.getCorrectingMessage() != null && (empty || text.equals(c.getCorrectingMessage().getBody()))) { - return SendButtonAction.CANCEL; - } else if (conference && !c.getAccount().httpUploadAvailable()) { - if (empty && c.getNextCounterpart() != null) { - return SendButtonAction.CANCEL; - } else { - return SendButtonAction.TEXT; - } - } else { - if (empty) { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); - if (conference && c.getNextCounterpart() != null) { - return SendButtonAction.CANCEL; - } else { - String setting = preferences.getString("quick_action", activity.getResources().getString(R.string.quick_action)); - if (quickShareChoice(activity) && AttachmentsVisible(c)) { - return SendButtonAction.CHOOSE_ATTACHMENT; - } else if (quickShareChoice(activity) && !AttachmentsVisible(c)) { - return SendButtonAction.TEXT; - } else { - if (!"none".equals(setting) && UIHelper.receivedLocationQuestion(c.getLatestMessage())) { - return SendButtonAction.SEND_LOCATION; - } else { - if ("recent".equals(setting)) { - setting = preferences.getString(ConversationFragment.RECENTLY_USED_QUICK_ACTION, SendButtonAction.TEXT.toString()); - return SendButtonAction.valueOfOrDefault(setting); - } else { - return SendButtonAction.valueOfOrDefault(setting); - } - } - } - } - } else { - return SendButtonAction.TEXT; - } - } - } - - public static boolean AttachmentsVisible(Conversation conversation) { - final boolean visible; - visible = conversation.getMode() != Conversation.MODE_MULTI || conversation.getAccount().httpUploadAvailable() && conversation.getMucOptions().participating(); - return visible; - } - - public static int getSendButtonImageResource(Activity activity, SendButtonAction action, Presence.Status status) { - switch (action) { - case TEXT: - switch (status) { - case CHAT: - case ONLINE: - return R.drawable.ic_send_text_online; - case AWAY: - return R.drawable.ic_send_text_away; - case XA: - case DND: - return R.drawable.ic_send_text_dnd; - default: - setSendButtonColor(activity, R.drawable.ic_send_text_offline_white); - return R.drawable.ic_send_text_offline_white; - } - case RECORD_VIDEO: - switch (status) { - case CHAT: - case ONLINE: - return R.drawable.ic_send_videocam_online; - case AWAY: - return R.drawable.ic_send_videocam_away; - case XA: - case DND: - return R.drawable.ic_send_videocam_dnd; - default: - setSendButtonColor(activity, R.drawable.ic_send_videocam_offline_white); - return R.drawable.ic_send_videocam_offline_white; } - case TAKE_PHOTO: - switch (status) { - case CHAT: - case ONLINE: - return R.drawable.ic_send_photo_online; - case AWAY: - return R.drawable.ic_send_photo_away; - case XA: - case DND: - return R.drawable.ic_send_photo_dnd; - default: - setSendButtonColor(activity, R.drawable.ic_send_photo_offline_white); - return R.drawable.ic_send_photo_offline_white; } - case RECORD_VOICE: - switch (status) { - case CHAT: - case ONLINE: - return R.drawable.ic_send_voice_online; - case AWAY: - return R.drawable.ic_send_voice_away; - case XA: - case DND: - return R.drawable.ic_send_voice_dnd; - default: - setSendButtonColor(activity, R.drawable.ic_send_voice_offline_white); - return R.drawable.ic_send_voice_offline_white; } - case SEND_LOCATION: - switch (status) { - case CHAT: - case ONLINE: - return R.drawable.ic_send_location_online; - case AWAY: - return R.drawable.ic_send_location_away; - case XA: - case DND: - return R.drawable.ic_send_location_dnd; - default: - setSendButtonColor(activity, R.drawable.ic_send_location_offline_white); - return R.drawable.ic_send_location_offline_white; } - case CANCEL: - switch (status) { - case CHAT: - case ONLINE: - return R.drawable.ic_send_cancel_online; - case AWAY: - return R.drawable.ic_send_cancel_away; - case XA: - case DND: - return R.drawable.ic_send_cancel_dnd; - default: - setSendButtonColor(activity, R.drawable.ic_send_cancel_offline_white); - return R.drawable.ic_send_cancel_offline_white; } - case CHOOSE_PICTURE: - switch (status) { - case CHAT: - case ONLINE: - return R.drawable.ic_send_picture_online; - case AWAY: - return R.drawable.ic_send_picture_away; - case XA: - case DND: - return R.drawable.ic_send_picture_dnd; - default: - setSendButtonColor(activity, R.drawable.ic_send_picture_offline_white); - return R.drawable.ic_send_picture_offline_white; } - - case CHOOSE_ATTACHMENT: - switch (status) { - case CHAT: - case ONLINE: - return R.drawable.ic_send_attachment_online; - case AWAY: - return R.drawable.ic_send_attachment_away; - case XA: - case DND: - return R.drawable.ic_send_attachment_dnd; - default: - setSendButtonColor(activity, R.drawable.ic_send_attachment_offline_white); - return R.drawable.ic_send_attachment_offline_white; } - } - return getThemeResource(activity, R.attr.ic_send_text_offline, R.drawable.ic_send_text_offline); - } - - private static int getThemeResource(Activity activity, int r_attr_name, int r_drawable_def) { - int[] attrs = {r_attr_name}; - TypedArray ta = activity.getTheme().obtainStyledAttributes(attrs); - - int res = ta.getResourceId(0, r_drawable_def); - ta.recycle(); - - return res; - } - - private static void setSendButtonColor(Activity activity, int ic_send_button_offline_white) - { - // takes a white drawable, and fills accent color in it - Drawable unwrappedDrawable = AppCompatResources.getDrawable(activity.getBaseContext(), ic_send_button_offline_white); - Drawable wrappedDrawable = DrawableCompat.wrap(unwrappedDrawable); - DrawableCompat.setTint(wrappedDrawable, StyledAttributes.getColor(activity, R.attr.colorAccent)); - } - - public static boolean quickShareChoice(Activity activity) { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); - return preferences.getBoolean(SettingsActivity.QUICK_SHARE_ATTACHMENT_CHOICE, activity.getResources().getBoolean(R.bool.quick_share_attachment_choice)); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java deleted file mode 100644 index 0501595ce..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui.util; - -import android.content.ActivityNotFoundException; -import android.content.Intent; - -import java.util.regex.Matcher; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.ui.ConversationsActivity; -import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.utils.Patterns; -import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.xmpp.Jid; -import me.drakeet.support.toast.ToastCompat; -import android.net.Uri; -import android.text.SpannableStringBuilder; -import android.text.style.URLSpan; -import android.widget.Toast; - -public class ShareUtil { - - public static void share(XmppActivity activity, Message message, String user) { - Intent shareIntent = new Intent(); - shareIntent.setAction(Intent.ACTION_SEND); - if (message.isGeoUri()) { - shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody()); - shareIntent.setType("text/plain"); - shareIntent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, false); - } else if (!message.isFileOrImage()) { - shareIntent.putExtra(Intent.EXTRA_TEXT, message.getMergedBody().toString()); - shareIntent.setType("text/plain"); - shareIntent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, true); - shareIntent.putExtra(ConversationsActivity.EXTRA_USER, user); - } else { - final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); - try { - shareIntent.putExtra(Intent.EXTRA_STREAM, FileBackend.getUriForFile(activity, file)); - } catch (SecurityException e) { - ToastCompat.makeText(activity, activity.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), ToastCompat.LENGTH_SHORT).show(); - return; - } - shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - String mime = message.getMimeType(); - if (mime == null) { - mime = "*/*"; - } - shareIntent.setType(mime); - } - try { - activity.startActivity(Intent.createChooser(shareIntent, activity.getText(R.string.share_with))); - activity.overridePendingTransition(R.animator.fade_in, R.animator.fade_out); - } catch (ActivityNotFoundException e) { - //This should happen only on faulty androids because normally chooser is always available - ToastCompat.makeText(activity, R.string.no_application_found_to_open_file, ToastCompat.LENGTH_SHORT).show(); - } - } - - public static void copyToClipboard(XmppActivity activity, Message message) { - if (activity.copyTextToClipboard(message.getMergedBody().toString(), R.string.message)) { - ToastCompat.makeText(activity, R.string.message_copied_to_clipboard, ToastCompat.LENGTH_SHORT).show(); - } - } - - public static void copyUrlToClipboard(XmppActivity activity, Message message) { - final String url; - final int resId; - if (message.isGeoUri()) { - resId = R.string.location; - url = message.getBody(); - } else if (message.hasFileOnRemoteHost()) { - resId = R.string.file_url; - url = message.getFileParams().url; - } else { - final Message.FileParams fileParams = message.getFileParams(); - url = (fileParams != null && fileParams.url != null) ? fileParams.url : message.getBody().trim(); - resId = R.string.file_url; - } - if (activity.copyTextToClipboard(url, resId)) { - ToastCompat.makeText(activity, R.string.url_copied_to_clipboard, ToastCompat.LENGTH_SHORT).show(); - } - } - - public static void copyLinkToClipboard(final XmppActivity activity, final String url) { - final Uri uri = Uri.parse(url); - if ("xmpp".equals(uri.getScheme())) { - try { - final Jid jid = new XmppUri(uri).getJid(); - if (activity.copyTextToClipboard(jid.asBareJid().toString(), R.string.account_settings_jabber_id)) { - Toast.makeText(activity, R.string.jabber_id_copied_to_clipboard, Toast.LENGTH_SHORT).show(); - } - } catch (final Exception e) { } - } else { - if (activity.copyTextToClipboard(url, R.string.web_address)) { - Toast.makeText(activity, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show(); - } - } - } - - - public static void copyLinkToClipboard(final XmppActivity activity, final Message message) { - final SpannableStringBuilder body = message.getMergedBody(); - MyLinkify.addLinks(body, true); - for (final URLSpan urlspan : body.getSpans(0, body.length() - 1, URLSpan.class)) { - copyLinkToClipboard(activity, urlspan.getURL()); - return; - } - } - - public static boolean containsXmppUri(String body) { - Matcher xmppPatternMatcher = Patterns.XMPP_PATTERN.matcher(body); - if (xmppPatternMatcher.find()) { - try { - return new XmppUri(body.substring(xmppPatternMatcher.start(), xmppPatternMatcher.end())).isValidJid(); - } catch (Exception e) { - return false; - } - } - return false; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/SoftKeyboardUtils.java b/src/main/java/eu/siacs/conversations/ui/util/SoftKeyboardUtils.java deleted file mode 100644 index 75570894c..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/SoftKeyboardUtils.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package eu.siacs.conversations.ui.util; - -import android.app.Activity; -import android.content.Context; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; - -import androidx.annotation.NonNull; - -public class SoftKeyboardUtils { - - public static void hideSoftKeyboard(final Activity activity) { - InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm == null) { - return; - } - View view = activity.getCurrentFocus(); - if (view == null) { - view = new View(activity); - } - imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); - } - - public static void hideSoftKeyboard(@NonNull final EditText editText) { - InputMethodManager imm = (InputMethodManager) editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm == null) { - return; - } - imm.hideSoftInputFromWindow(editText.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); - } - - public static void showKeyboard(EditText editText) { - editText.requestFocus(); - try { - final InputMethodManager inputMethodManager = (InputMethodManager) editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0); - } catch (Exception e) { - e.printStackTrace(); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/StyledAttributes.java b/src/main/java/eu/siacs/conversations/ui/util/StyledAttributes.java deleted file mode 100644 index 0e162cfa7..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/StyledAttributes.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.ui.util; - - -import android.content.Context; -import android.content.res.TypedArray; - -import androidx.annotation.AttrRes; -import androidx.annotation.ColorInt; - -public class StyledAttributes { - public static android.graphics.drawable.Drawable getDrawable(Context context, @AttrRes int id) { - TypedArray typedArray = context.obtainStyledAttributes(new int[]{id}); - android.graphics.drawable.Drawable drawable = typedArray.getDrawable(0); - typedArray.recycle(); - return drawable; - } - - public static float getFloat(Context context, @AttrRes int id) { - TypedArray typedArray = context.obtainStyledAttributes(new int[]{id}); - float value = typedArray.getFloat(0, 0f); - typedArray.recycle(); - return value; - } - - public static @ColorInt - int getColor(Context context, @AttrRes int attr) { - TypedArray typedArray = context.obtainStyledAttributes(new int[]{attr}); - int color = typedArray.getColor(0, 0); - typedArray.recycle(); - return color; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/UpdateHelper.java b/src/main/java/eu/siacs/conversations/ui/util/UpdateHelper.java deleted file mode 100644 index e269240d5..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/UpdateHelper.java +++ /dev/null @@ -1,298 +0,0 @@ -package eu.siacs.conversations.ui.util; - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Environment; -import android.preference.PreferenceManager; -import android.util.Log; - -import androidx.appcompat.app.AlertDialog; - -import java.io.File; -import java.lang.ref.WeakReference; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.ui.ConversationsActivity; -import eu.siacs.conversations.ui.WelcomeActivity; -import eu.siacs.conversations.utils.ThemeHelper; -import me.drakeet.support.toast.ToastCompat; - -public class UpdateHelper { - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); - private static final String INSTALL_DATE = "2020-11-01"; - private static final String monocles_message = "MONOCLES.IM_UPDATE_MESSAGE"; - private static boolean moveData = true; - private static boolean dataMoved = false; - - private static final File PAM_MainDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/"); - private static final File monocles_MainDirectory = new File(Environment.getExternalStorageDirectory() + "/monocles chat/"); - private static final File PAM_PicturesDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Media/Monocles Messenger Images/"); - private static final File PAM_FilesDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Media/Monocles Messenger Files/"); - private static final File PAM_AudiosDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Media/Monocles Messenger Audios/"); - private static final File PAM_VideosDirectory = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Monocles Messenger/Media/Monocles Messenger Videos/"); - - public static void showPopup(Activity activity) { - new Thread(new showPopupFinisher(activity)).start(); - } - - private static class showPopupFinisher implements Runnable { - - private final WeakReference activityReference; - - private showPopupFinisher(Activity activity) { - - this.activityReference = new WeakReference<>(activity); - } - - @Override - public void run() { - final Activity activity = activityReference.get(); - if (activity == null) { - return; - } - updateInstalled(activity); - final SharedPreferences getPrefs = PreferenceManager.getDefaultSharedPreferences(activity.getBaseContext()); - final String Message = "message_shown_" + monocles_message; - final boolean SHOW_MESSAGE = getPrefs.getBoolean(Message, true); - if (activity instanceof ConversationsActivity && (SHOW_MESSAGE && updateInstalled(activity) && Config.SHOW_MIGRATION_INFO)) { - Log.d(Config.LOGTAG, "UpdateHelper: installed update from Monocles Messenger to monocles chat"); - activity.runOnUiThread(() -> { - final AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(activity.getString(R.string.title_activity_updater)); - builder.setMessage(activity.getString(R.string.updated_to_monocles)); - builder.setCancelable(false); - builder.setPositiveButton(R.string.ok, (dialog, which) -> SaveMessageShown(activity, monocles_message) - ); - builder.create().show(); - }); - } else if (activity instanceof WelcomeActivity && (SHOW_MESSAGE && newInstalled(activity) && !Config.SHOW_MIGRATION_INFO && PAMInstalled(activity))) { - Log.d(Config.LOGTAG, "UpdateHelper: new installed monocles chat"); - showNewInstalledDialog(activity); - } - } - } - - private static void showNewInstalledDialog(Activity activity) { - checkOldData(); - activity.runOnUiThread(() -> { - if (dataMoved) { - ToastCompat.makeText(activity, R.string.data_successfully_moved, ToastCompat.LENGTH_LONG).show(); - } - final AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(activity.getString(R.string.title_activity_updater)); - builder.setMessage(activity.getString(R.string.updated_to_monocles_google)); - builder.setCancelable(false); - builder.setPositiveButton(R.string.link, (dialog, which) -> { - SaveMessageShown(activity, monocles_message); - try { - final Uri uri = Uri.parse(Config.migrationURL); - try { - CustomTab.openTab(activity, uri, ThemeHelper.isDark(ThemeHelper.find(activity))); - } catch (Exception e) { - ToastCompat.makeText(activity, R.string.no_application_found_to_open_link, ToastCompat.LENGTH_SHORT).show(); - } - showNewInstalledDialog(activity); - } catch (Exception e) { - ToastCompat.makeText(activity, R.string.no_application_found_to_open_link, ToastCompat.LENGTH_LONG).show(); - showNewInstalledDialog(activity); - } - } - ); - builder.setNegativeButton(R.string.move_data, (dialog, which) -> { - SaveMessageShown(activity, monocles_message); - try { - if (!moveData) { - ToastCompat.makeText(activity, R.string.error_moving_data, ToastCompat.LENGTH_LONG).show(); - } else { - moveData_PAM_monocles(); - } - showNewInstalledDialog(activity); - } catch (Exception e) { - ToastCompat.makeText(activity, R.string.error_moving_data, ToastCompat.LENGTH_LONG).show(); - showNewInstalledDialog(activity); - } - } - ); - builder.setNeutralButton(R.string.done, (dialog, which) -> SaveMessageShown(activity, monocles_message) - ); - AlertDialog dialog = builder.create(); - dialog.show(); - dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(!dataMoved); - }); - } - - private static void checkOldData() { - if (PAM_MainDirectory.exists() && PAM_MainDirectory.isDirectory()) { - if (monocles_MainDirectory.exists() && monocles_MainDirectory.isDirectory()) { - moveData = false; - } else { - moveData = true; - } - } else { - moveData = false; - } - Log.d(Config.LOGTAG, "UpdateHelper: old data available: " + moveData); - } - - public static void moveData_PAM_monocles() { - if (PAM_PicturesDirectory.exists() && PAM_PicturesDirectory.isDirectory()) { - final File newPicturesDirectory = new File(Environment.getExternalStorageDirectory() + "/Monocles Messenger/Media/monocles chat Images/"); - newPicturesDirectory.getParentFile().mkdirs(); - final File[] files = PAM_PicturesDirectory.listFiles(); - if (files == null) { - return; - } - if (PAM_PicturesDirectory.renameTo(newPicturesDirectory)) { - Log.d(Config.LOGTAG, "moved " + PAM_PicturesDirectory.getAbsolutePath() + " to " + newPicturesDirectory.getAbsolutePath()); - } else { - Log.d(Config.LOGTAG, "could not move " + PAM_PicturesDirectory.getAbsolutePath() + " to " + newPicturesDirectory.getAbsolutePath()); - } - } - if (PAM_FilesDirectory.exists() && PAM_FilesDirectory.isDirectory()) { - final File newFilesDirectory = new File(Environment.getExternalStorageDirectory() + "/Monocles Messenger/Media/monocles chat Files/"); - newFilesDirectory.mkdirs(); - final File[] files = PAM_FilesDirectory.listFiles(); - if (files == null) { - return; - } - if (PAM_FilesDirectory.renameTo(newFilesDirectory)) { - Log.d(Config.LOGTAG, "moved " + PAM_FilesDirectory.getAbsolutePath() + " to " + newFilesDirectory.getAbsolutePath()); - } else { - Log.d(Config.LOGTAG, "could not move " + PAM_FilesDirectory.getAbsolutePath() + " to " + newFilesDirectory.getAbsolutePath()); - } - } - if (PAM_AudiosDirectory.exists() && PAM_AudiosDirectory.isDirectory()) { - final File newAudiosDirectory = new File(Environment.getExternalStorageDirectory() + "/Monocles Messenger/Media/monocles chat Audios/"); - newAudiosDirectory.mkdirs(); - final File[] files = PAM_AudiosDirectory.listFiles(); - if (files == null) { - return; - } - if (PAM_AudiosDirectory.renameTo(newAudiosDirectory)) { - Log.d(Config.LOGTAG, "moved " + PAM_AudiosDirectory.getAbsolutePath() + " to " + newAudiosDirectory.getAbsolutePath()); - } else { - Log.d(Config.LOGTAG, "could not move " + PAM_AudiosDirectory.getAbsolutePath() + " to " + newAudiosDirectory.getAbsolutePath()); - } - } - if (PAM_VideosDirectory.exists() && PAM_VideosDirectory.isDirectory()) { - final File newVideosDirectory = new File(Environment.getExternalStorageDirectory() + "/Monocles Messenger/Media/monocles chat Videos/"); - newVideosDirectory.mkdirs(); - final File[] files = PAM_VideosDirectory.listFiles(); - if (files == null) { - return; - } - if (PAM_VideosDirectory.renameTo(newVideosDirectory)) { - Log.d(Config.LOGTAG, "moved " + PAM_VideosDirectory.getAbsolutePath() + " to " + newVideosDirectory.getAbsolutePath()); - } else { - Log.d(Config.LOGTAG, "could not move " + PAM_VideosDirectory.getAbsolutePath() + " to " + newVideosDirectory.getAbsolutePath()); - } - } - if (PAM_MainDirectory.exists() && PAM_MainDirectory.isDirectory()) { - monocles_MainDirectory.mkdirs(); - final File[] files = PAM_MainDirectory.listFiles(); - if (files == null) { - return; - } - if (PAM_MainDirectory.renameTo(monocles_MainDirectory)) { - dataMoved = true; - Log.d(Config.LOGTAG, "moved " + PAM_MainDirectory.getAbsolutePath() + " to " + monocles_MainDirectory.getAbsolutePath()); - } else { - Log.d(Config.LOGTAG, "could not move " + PAM_MainDirectory.getAbsolutePath() + " to " + monocles_MainDirectory.getAbsolutePath()); - } - } - } - - private static boolean updateInstalled(Activity activity) { - PackageManager pm = activity.getPackageManager(); - PackageInfo packageInfo; - String firstInstalled = null; - String lastUpdate = null; - Date updateDate = null; - Date lastUpdateDate = null; - try { - packageInfo = pm.getPackageInfo(activity.getPackageName(), PackageManager.GET_SIGNATURES); - firstInstalled = DATE_FORMAT.format(new Date(packageInfo.firstInstallTime)); - lastUpdate = DATE_FORMAT.format(new Date(packageInfo.lastUpdateTime)); - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } - try { - updateDate = DATE_FORMAT.parse(INSTALL_DATE); - if (lastUpdate != null) { - lastUpdateDate = DATE_FORMAT.parse(lastUpdate); - } - } catch (ParseException e) { - e.printStackTrace(); - } - if (updateDate != null) { - if (lastUpdateDate != null) { - if (firstInstalled.equals(lastUpdate)) { - SaveMessageShown(activity, monocles_message); - return false; - } else { - if (lastUpdateDate.getTime() <= updateDate.getTime()) { - return true; - } else { - SaveMessageShown(activity, monocles_message); - return false; - } - } - } else { - SaveMessageShown(activity, monocles_message); - return false; - } - } else { - SaveMessageShown(activity, monocles_message); - return false; - } - } - - private static boolean newInstalled(Activity activity) { - PackageManager pm = activity.getPackageManager(); - PackageInfo packageInfo; - String firstInstalled = null; - Date installDate = null; - Date firstInstalledDate = null; - try { - packageInfo = pm.getPackageInfo(activity.getPackageName(), PackageManager.GET_SIGNATURES); - firstInstalled = DATE_FORMAT.format(new Date(packageInfo.firstInstallTime)); - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } - try { - installDate = DATE_FORMAT.parse(INSTALL_DATE); - if (firstInstalled != null) { - firstInstalledDate = DATE_FORMAT.parse(firstInstalled); - } - } catch (ParseException e) { - e.printStackTrace(); - } - return installDate != null && firstInstalledDate != null && firstInstalledDate.getTime() >= installDate.getTime(); - } - - private static boolean PAMInstalled(Activity activity) { - PackageManager pm = activity.getPackageManager(); - try { - return pm.getApplicationLabel(pm.getApplicationInfo("de.pixart.messenger", 0)).equals("Monocles Messenger"); - } catch (PackageManager.NameNotFoundException e) { - return false; - } - } - - public static void SaveMessageShown(Context context, String message) { - SharedPreferences getPrefs = PreferenceManager.getDefaultSharedPreferences(context); - String Message = "message_shown_" + message; - SharedPreferences.Editor e = getPrefs.edit(); - e.putBoolean(Message, false); - e.apply(); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/UriHelper.java b/src/main/java/eu/siacs/conversations/ui/util/UriHelper.java deleted file mode 100644 index 2022cdf59..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/UriHelper.java +++ /dev/null @@ -1,30 +0,0 @@ -package eu.siacs.conversations.ui.util; - -import java.util.HashMap; - -/** - * Helper methods for parsing URI's. - */ -public final class UriHelper { - /** - * Parses a query string into a hashmap. - * - * @param q The query string to split. - * @return A hashmap containing the key-value pairs from the query string. - */ - public static HashMap parseQueryString(final String q) { - if (q == null || q.isEmpty()) { - return null; - } - - final String[] query = q.split("&"); - // TODO: Look up the HashMap implementation and figure out what the load factor is and make sure we're not reallocating here. - final HashMap queryMap = new HashMap<>(query.length); - for (final String param : query) { - final String[] pair = param.split("="); - queryMap.put(pair[0], pair.length == 2 && !pair[1].isEmpty() ? pair[1] : null); - } - - return queryMap; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java deleted file mode 100644 index 5a7346cb9..000000000 --- a/src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java +++ /dev/null @@ -1,76 +0,0 @@ -package eu.siacs.conversations.ui.util; - -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.util.Log; - -import java.io.File; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.ui.MediaViewerActivity; -import me.drakeet.support.toast.ToastCompat; - -public class ViewUtil { - - public static void view(Context context, Attachment attachment) { - File file = new File(attachment.getUri().getPath()); - final String mime = attachment.getMime() == null ? "*/*" : attachment.getMime(); - view(context, file, mime); - } - - public static void view(Context context, DownloadableFile file) { - if (!file.exists()) { - ToastCompat.makeText(context, R.string.file_deleted, ToastCompat.LENGTH_SHORT).show(); - return; - } - String mime = file.getMimeType(); - if (mime == null) { - mime = "*/*"; - } - view(context, file, mime); - } - - public static void view(Context context, File file, String mime) { - Log.d(Config.LOGTAG, "viewing " + file.getAbsolutePath() + " " + mime); - final Uri uri; - try { - uri = FileBackend.getUriForFile(context, file); - } catch (SecurityException e) { - Log.d(Config.LOGTAG, "No permission to access " + file.getAbsolutePath(), e); - ToastCompat.makeText(context, context.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), ToastCompat.LENGTH_SHORT).show(); - return; - } - // use internal viewer for images and videos - if (mime.startsWith("image/")) { - final Intent intent = new Intent(context, MediaViewerActivity.class); - intent.putExtra("image", Uri.fromFile(file)); - try { - context.startActivity(intent); - } catch (ActivityNotFoundException e) { - //ignored - } - } else if (mime.startsWith("video/")) { - final Intent intent = new Intent(context, MediaViewerActivity.class); - intent.putExtra("video", Uri.fromFile(file)); - try { - context.startActivity(intent); - } catch (ActivityNotFoundException e) { - //ignored - } - } else { - final Intent openIntent = new Intent(Intent.ACTION_VIEW); - openIntent.setDataAndType(uri, mime); - openIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - try { - context.startActivity(openIntent); - } catch (final ActivityNotFoundException e) { - ToastCompat.makeText(context, R.string.no_application_found_to_open_file, ToastCompat.LENGTH_SHORT).show(); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/widget/ClickableMovementMethod.java b/src/main/java/eu/siacs/conversations/ui/widget/ClickableMovementMethod.java deleted file mode 100644 index 01c32b675..000000000 --- a/src/main/java/eu/siacs/conversations/ui/widget/ClickableMovementMethod.java +++ /dev/null @@ -1,42 +0,0 @@ -package eu.siacs.conversations.ui.widget; - -import android.text.Layout; -import android.text.Spannable; -import android.text.method.ArrowKeyMovementMethod; -import android.text.style.ClickableSpan; -import android.view.MotionEvent; -import android.widget.TextView; - -public class ClickableMovementMethod extends ArrowKeyMovementMethod { - - @Override - public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { - // Just copied from android.text.method.LinkMovementMethod - if (event.getAction() == MotionEvent.ACTION_UP) { - int x = (int) event.getX(); - int y = (int) event.getY(); - x -= widget.getTotalPaddingLeft(); - y -= widget.getTotalPaddingTop(); - x += widget.getScrollX(); - y += widget.getScrollY(); - Layout layout = widget.getLayout(); - int line = layout.getLineForVertical(y); - int off = layout.getOffsetForHorizontal(line, x); - ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class); - if (link.length != 0) { - link[0].onClick(widget); - return true; - } - } - return super.onTouchEvent(widget, buffer, event); - } - - public static ClickableMovementMethod getInstance() { - if (sInstance == null) { - sInstance = new ClickableMovementMethod(); - } - return sInstance; - } - - private static ClickableMovementMethod sInstance; -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/widget/DisabledActionModeCallback.java b/src/main/java/eu/siacs/conversations/ui/widget/DisabledActionModeCallback.java deleted file mode 100644 index 861859d9c..000000000 --- a/src/main/java/eu/siacs/conversations/ui/widget/DisabledActionModeCallback.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package eu.siacs.conversations.ui.widget; - -import android.view.ActionMode; -import android.view.Menu; -import android.view.MenuItem; - -public class DisabledActionModeCallback implements ActionMode.Callback { - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - return false; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - return false; - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - return false; - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java b/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java deleted file mode 100644 index edbd58d20..000000000 --- a/src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java +++ /dev/null @@ -1,215 +0,0 @@ -package eu.siacs.conversations.ui.widget; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.text.Editable; -import android.text.InputFilter; -import android.text.InputType; -import android.text.Spanned; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; - -import androidx.appcompat.widget.AppCompatEditText; -import androidx.core.view.inputmethod.EditorInfoCompat; -import androidx.core.view.inputmethod.InputConnectionCompat; -import androidx.core.view.inputmethod.InputContentInfoCompat; - -import java.util.concurrent.ExecutorService; -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 AppCompatEditText { - - private static final InputFilter SPAN_FILTER = (source, start, end, dest, dstart, dend) -> source instanceof Spanned ? source.toString() : source; - private final ExecutorService executor = Executors.newSingleThreadExecutor(); - protected Handler mTypingHandler = new Handler(); - protected KeyboardListener keyboardListener; - private OnCommitContentListener mCommitContentListener = null; - private String[] mimeTypes = null; - private boolean isUserTyping = false; - private final Runnable mTypingTimeout = new Runnable() { - @Override - public void run() { - if (isUserTyping && keyboardListener != null) { - keyboardListener.onTypingStopped(); - isUserTyping = false; - } - } - }; - private boolean lastInputWasTab = false; - - public EditMessage(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public EditMessage(Context context) { - super(context); - } - - @Override - public boolean onKeyDown(final int keyCode, final KeyEvent e) { - final boolean isCtrlPressed = e.isCtrlPressed(); - if (keyCode == KeyEvent.KEYCODE_ENTER && !e.isShiftPressed()) { - lastInputWasTab = false; - if (keyboardListener != null && keyboardListener.onEnterPressed(isCtrlPressed)) { - return true; - } - } else if (keyCode == KeyEvent.KEYCODE_TAB && !e.isAltPressed() && !isCtrlPressed) { - if (keyboardListener != null && keyboardListener.onTabPressed(this.lastInputWasTab)) { - lastInputWasTab = true; - return true; - } - } else { - lastInputWasTab = false; - } - return super.onKeyDown(keyCode, e); - } - - @Override - public int getAutofillType() { - return AUTOFILL_TYPE_NONE; - } - - @Override - public void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { - super.onTextChanged(text, start, lengthBefore, lengthAfter); - lastInputWasTab = false; - if (this.mTypingHandler != null && this.keyboardListener != null) { - executor.execute(() -> triggerKeyboardEvents(text.length())); - } - } - - private void triggerKeyboardEvents(final int length) { - final KeyboardListener listener = this.keyboardListener; - if (listener == null) { - return; - } - this.mTypingHandler.removeCallbacks(mTypingTimeout); - this.mTypingHandler.postDelayed(mTypingTimeout, Config.TYPING_TIMEOUT * 1000); - if (!isUserTyping && length > 0) { - this.isUserTyping = true; - listener.onTypingStarted(); - } else if (length == 0) { - this.isUserTyping = false; - listener.onTextDeleted(); - } - listener.onTextChanged(); - } - - public void setKeyboardListener(KeyboardListener listener) { - this.keyboardListener = listener; - if (listener != null) { - this.isUserTyping = false; - } - } - - @Override - public boolean onTextContextMenuItem(int id) { - if (id == android.R.id.paste) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - return super.onTextContextMenuItem(android.R.id.pasteAsPlainText); - } else { - Editable editable = getEditableText(); - InputFilter[] filters = editable.getFilters(); - InputFilter[] tempFilters = new InputFilter[filters != null ? filters.length + 1 : 1]; - if (filters != null) { - System.arraycopy(filters, 0, tempFilters, 1, filters.length); - } - tempFilters[0] = SPAN_FILTER; - editable.setFilters(tempFilters); - try { - return super.onTextContextMenuItem(id); - } finally { - editable.setFilters(filters); - } - } - } else { - return super.onTextContextMenuItem(id); - } - } - - public void setRichContentListener(String[] mimeTypes, OnCommitContentListener listener) { - this.mimeTypes = mimeTypes; - this.mCommitContentListener = listener; - } - - public void insertAsQuote(String text) { - text = QuoteHelper.replaceAltQuoteCharsInText(text); - text = text - // first replace all '>' at the beginning of the line with nice and tidy '>>' - // for nested quoting - .replaceAll("(^|\n)(" + QuoteHelper.QUOTE_CHAR + ")", "$1$2$2") - // then find all other lines and have them start with a '> ' - .replaceAll("(^|\n)(?!" + QuoteHelper.QUOTE_CHAR + ")(.*)", "$1> $2") - ; - Editable editable = getEditableText(); - int position = getSelectionEnd(); - if (position == -1) position = editable.length(); - if (position > 0 && editable.charAt(position - 1) != '\n') { - editable.insert(position++, "\n"); - } - editable.insert(position, text); - position += text.length(); - editable.insert(position++, "\n"); - if (position < editable.length() && editable.charAt(position) != '\n') { - editable.insert(position, "\n"); - } - setSelection(position); - } - - @Override - public InputConnection onCreateInputConnection(EditorInfo editorInfo) { - final InputConnection ic = super.onCreateInputConnection(editorInfo); - - if (mimeTypes != null && mCommitContentListener != null && ic != null) { - EditorInfoCompat.setContentMimeTypes(editorInfo, mimeTypes); - return InputConnectionCompat.createWrapper(ic, editorInfo, (inputContentInfo, flags, opts) -> EditMessage.this.mCommitContentListener.onCommitContent(inputContentInfo, flags, opts, mimeTypes)); - } else { - return ic; - } - } - - public void refreshIme() { - SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(getContext()); - final boolean usingEnterKey = p.getBoolean("display_enter_key", getResources().getBoolean(R.bool.display_enter_key)); - final boolean enterIsSend = p.getBoolean("enter_is_send", getResources().getBoolean(R.bool.enter_is_send)); - - if (usingEnterKey && enterIsSend) { - setInputType(getInputType() & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE)); - setInputType(getInputType() & (~InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE)); - } else if (usingEnterKey) { - setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE); - setInputType(getInputType() & (~InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE)); - } else { - setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE); - setInputType(getInputType() | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE); - } - } - - public interface OnCommitContentListener { - boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] mimeTypes); - } - - public interface KeyboardListener { - boolean onEnterPressed(boolean isCtrlPressed); - - void onTypingStarted(); - - void onTypingStopped(); - - void onTextDeleted(); - - void onTextChanged(); - - boolean onTabPressed(boolean repeated); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/widget/FailedCountCustomView.java b/src/main/java/eu/siacs/conversations/ui/widget/FailedCountCustomView.java deleted file mode 100644 index bf9f0a208..000000000 --- a/src/main/java/eu/siacs/conversations/ui/widget/FailedCountCustomView.java +++ /dev/null @@ -1,77 +0,0 @@ -package eu.siacs.conversations.ui.widget; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Typeface; -import android.util.AttributeSet; -import android.view.View; - -import androidx.core.content.ContextCompat; - -import eu.siacs.conversations.R; - -public class FailedCountCustomView extends View { - - private int count; - private Paint paint, textPaint; - private int backgroundColor = 0xffd50000; - - public FailedCountCustomView(Context context) { - super(context); - init(); - } - - public FailedCountCustomView(Context context, AttributeSet attrs) { - super(context, attrs); - initXMLAttrs(context, attrs); - init(); - } - - public FailedCountCustomView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - initXMLAttrs(context, attrs); - init(); - } - - private void initXMLAttrs(Context context, AttributeSet attrs) { - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.UnreadCountCustomView); - //setBackgroundColor(a.getColor(a.getIndex(0), ContextCompat.getColor(context, R.color.accent))); - setBackgroundColor(ContextCompat.getColor(context, R.color.red700)); - a.recycle(); - } - - void init() { - paint = new Paint(); - paint.setColor(backgroundColor); - paint.setAntiAlias(true); - textPaint = new Paint(); - textPaint.setColor(Color.WHITE); - textPaint.setTextAlign(Paint.Align.CENTER); - textPaint.setAntiAlias(true); - textPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - float midx = canvas.getWidth() / 2.0f; - float midy = canvas.getHeight() / 2.0f; - float radius = Math.min(canvas.getWidth(), canvas.getHeight()) / 2.0f; - float textOffset = canvas.getWidth() / 6.0f; - textPaint.setTextSize(0.95f * radius); - canvas.drawCircle(midx, midy, radius * 0.94f, paint); - canvas.drawText(count > 999 ? "\u221E" : String.valueOf(count), midx, midy + textOffset, textPaint); - } - - public void setFailedCount(int count) { - this.count = count; - invalidate(); - } - - public void setBackgroundColor(int backgroundColor) { - this.backgroundColor = backgroundColor; - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/widget/ImmediateAutoCompleteTextView.java b/src/main/java/eu/siacs/conversations/ui/widget/ImmediateAutoCompleteTextView.java deleted file mode 100644 index e12d99820..000000000 --- a/src/main/java/eu/siacs/conversations/ui/widget/ImmediateAutoCompleteTextView.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package eu.siacs.conversations.ui.widget; - -import android.content.Context; -import android.util.AttributeSet; - -public class ImmediateAutoCompleteTextView extends androidx.appcompat.widget.AppCompatAutoCompleteTextView { - - public ImmediateAutoCompleteTextView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public ImmediateAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - @Override - public boolean enoughToFilter() { - return true; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/widget/Marker.java b/src/main/java/eu/siacs/conversations/ui/widget/Marker.java deleted file mode 100644 index 9cd7919b9..000000000 --- a/src/main/java/eu/siacs/conversations/ui/widget/Marker.java +++ /dev/null @@ -1,54 +0,0 @@ -package eu.siacs.conversations.ui.widget; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Point; - -import org.osmdroid.util.GeoPoint; -import org.osmdroid.views.MapView; -import org.osmdroid.views.overlay.mylocation.SimpleLocationOverlay; - -/** - * An immutable marker overlay. - */ -public class Marker extends SimpleLocationOverlay { - private final GeoPoint position; - private final Bitmap icon; - private final Point mapPoint; - - /** - * Create a marker overlay which will be drawn at the current Geographical position. - * - * @param icon A bitmap icon for the marker - * @param position The geographic position where the marker will be drawn (if it is inside the view) - */ - public Marker(final Bitmap icon, final GeoPoint position) { - super(icon); - this.icon = icon; - this.position = position; - this.mapPoint = new Point(); - } - - /** - * Create a marker overlay which will be drawn centered in the view. - * - * @param icon A bitmap icon for the marker - */ - public Marker(final Bitmap icon) { - this(icon, null); - } - - @Override - public void draw(final Canvas c, final MapView view, final boolean shadow) { - super.draw(c, view, shadow); - - // If no position was set for the marker, draw it centered in the view. - view.getProjection().toPixels(this.position == null ? view.getMapCenter() : position, mapPoint); - - c.drawBitmap(icon, - mapPoint.x - icon.getWidth() / 2, - mapPoint.y - icon.getHeight(), - null); - - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/widget/MyLocation.java b/src/main/java/eu/siacs/conversations/ui/widget/MyLocation.java deleted file mode 100644 index 02ea0dab4..000000000 --- a/src/main/java/eu/siacs/conversations/ui/widget/MyLocation.java +++ /dev/null @@ -1,52 +0,0 @@ -package eu.siacs.conversations.ui.widget; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Point; -import android.location.Location; - -import org.osmdroid.util.GeoPoint; -import org.osmdroid.util.TileSystem; -import org.osmdroid.views.MapView; -import org.osmdroid.views.overlay.mylocation.SimpleLocationOverlay; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.ui.util.StyledAttributes; - -public class MyLocation extends SimpleLocationOverlay { - private final GeoPoint position; - private final float accuracy; - private final Point mapCenterPoint; - private final Paint fill; - private final Paint outline; - - public MyLocation(final Context ctx, final Bitmap icon, final Location position) { - super(icon); - this.mapCenterPoint = new Point(); - this.fill = new Paint(Paint.ANTI_ALIAS_FLAG); - final int accent = StyledAttributes.getColor(ctx, R.attr.colorAccent); - fill.setColor(accent); - fill.setStyle(Paint.Style.FILL); - this.outline = new Paint(Paint.ANTI_ALIAS_FLAG); - outline.setColor(accent); - outline.setAlpha(50); - outline.setStyle(Paint.Style.FILL); - this.position = new GeoPoint(position); - this.accuracy = position.getAccuracy(); - } - - @Override - public void draw(final Canvas c, final MapView view, final boolean shadow) { - super.draw(c, view, shadow); - - view.getProjection().toPixels(position, mapCenterPoint); - c.drawCircle(mapCenterPoint.x, mapCenterPoint.y, - Math.max(Config.Map.MY_LOCATION_INDICATOR_SIZE + Config.Map.MY_LOCATION_INDICATOR_OUTLINE_SIZE, - accuracy / (float) TileSystem.GroundResolution(position.getLatitude(), view.getZoomLevel()) - ), this.outline); - c.drawCircle(mapCenterPoint.x, mapCenterPoint.y, Config.Map.MY_LOCATION_INDICATOR_SIZE, this.fill); - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/widget/RichLinkView.java b/src/main/java/eu/siacs/conversations/ui/widget/RichLinkView.java deleted file mode 100644 index 0bcbc6af5..000000000 --- a/src/main/java/eu/siacs/conversations/ui/widget/RichLinkView.java +++ /dev/null @@ -1,206 +0,0 @@ -package eu.siacs.conversations.ui.widget; - -import android.content.Context; -import android.net.Uri; -import android.os.Build; -import android.util.AttributeSet; -import android.view.View; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import androidx.annotation.RequiresApi; - -import com.squareup.picasso.Picasso; - -import java.util.HashMap; -import java.util.Map; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.util.CustomTab; -import eu.siacs.conversations.utils.MetaData; -import eu.siacs.conversations.utils.RichPreview; -import eu.siacs.conversations.utils.ThemeHelper; -import me.drakeet.support.toast.ToastCompat; - - -/** - * Created by ponna on 16-01-2018. - */ - -public class RichLinkView extends RelativeLayout { - - private View view; - Context context; - private MetaData meta; - - LinearLayout linearLayout; - ImageView imageView; - TextView textViewTitle; - TextView textViewDesp; - View quoteMessage; - - private String main_url; - - private boolean isDefaultClick = true; - - private RichPreview.RichLinkListener richLinkListener; - - public RichLinkView(Context context) { - super(context); - this.context = context; - } - - public RichLinkView(Context context, AttributeSet attrs) { - super(context, attrs); - this.context = context; - } - - public RichLinkView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - this.context = context; - } - - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - public RichLinkView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - this.context = context; - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - } - - - public void initView(final boolean dataSaverDisabled, final int color) { - if (findLinearLayoutChild() != null) { - this.view = findLinearLayoutChild(); - } else { - this.view = this; - inflate(context, R.layout.link_layout, this); - } - linearLayout = findViewById(R.id.rich_link_card); - imageView = findViewById(R.id.rich_link_image); - textViewTitle = findViewById(R.id.rich_link_title); - textViewTitle.setTextColor(color); - textViewDesp = findViewById(R.id.rich_link_desp); - textViewDesp.setTextColor(color); - quoteMessage = findViewById(R.id.quote_message); - quoteMessage.setBackgroundColor(color); - imageView.setAdjustViewBounds(true); - if (meta.getImageurl() != null && !meta.getImageurl().equals("") && !meta.getImageurl().isEmpty()) { - if (!dataSaverDisabled) { - Picasso.get() - .load(R.drawable.ic_web_grey600_48) - .into(imageView); - } else { - imageView.setVisibility(VISIBLE); - Picasso.get() - .load(meta.getImageurl()) - .resize(80, 80) - .centerInside() - .placeholder(R.drawable.ic_web_grey600_48) - .error(R.drawable.ic_web_grey600_48) - .into(imageView); - } - } else { - imageView.setVisibility(VISIBLE); - Picasso.get() - .load(R.drawable.ic_web_grey600_48) - .into(imageView); - } - if (meta.getTitle().isEmpty() || meta.getTitle().equals("")) { - textViewTitle.setVisibility(VISIBLE); - textViewTitle.setText(meta.getUrl()); - } else { - textViewTitle.setVisibility(VISIBLE); - textViewTitle.setText(meta.getTitle()); - } - if (meta.getDescription().isEmpty() || meta.getDescription().equals("")) { - textViewDesp.setVisibility(GONE); - } else { - textViewDesp.setVisibility(VISIBLE); - textViewDesp.setText(meta.getDescription()); - } - - linearLayout.setOnClickListener(view -> { - if (isDefaultClick) { - richLinkClicked(); - } else { - if (richLinkListener != null) { - richLinkListener.onClicked(view, meta); - } else { - richLinkClicked(); - } - } - }); - } - - private void richLinkClicked() { - try { - CustomTab.openTab(this.context, Uri.parse(main_url), ThemeHelper.isDark(ThemeHelper.find(this.context))); - } catch (Exception e) { - ToastCompat.makeText(this.context, R.string.no_application_found_to_open_link, ToastCompat.LENGTH_SHORT).show(); - } - } - - public void setDefaultClickListener(boolean isDefault) { - isDefaultClick = isDefault; - } - - public void setClickListener(RichPreview.RichLinkListener richLinkListener1) { - richLinkListener = richLinkListener1; - } - - protected LinearLayout findLinearLayoutChild() { - if (getChildCount() > 0 && getChildAt(0) instanceof LinearLayout) { - return (LinearLayout) getChildAt(0); - } - return null; - } - - /* - public void setLinkFromMeta(MetaData metaData) { - meta = metaData; - initView(true); - } - */ - - public MetaData getMetaData() { - return meta; - } - - private static Map linkMap = new HashMap<>(); - - public void setLink(final String url, final String filename, final boolean dataSaverDisabled, final XmppConnectionService mXmppConnectionService, final int color, final RichPreview.ViewListener viewListener) { - MetaData data = linkMap.get(url); - if (data == null) { - main_url = url; - RichPreview richPreview = new RichPreview(new RichPreview.ResponseListener() { - @Override - public void onData(MetaData metaData) { - meta = metaData; - if (!meta.getTitle().isEmpty() || !meta.getTitle().equals("")) { - viewListener.onSuccess(true); - linkMap.put(url, metaData); - } - initView(dataSaverDisabled, color); - } - - @Override - public void onError(Exception e) { - viewListener.onError(e); - } - }); - richPreview.getPreview(url, filename, mXmppConnectionService); - } else { - main_url = url; - meta = data; - viewListener.onSuccess(true); - initView(dataSaverDisabled, color); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/ui/widget/ScannerView.java b/src/main/java/eu/siacs/conversations/ui/widget/ScannerView.java deleted file mode 100644 index 0cef7815c..000000000 --- a/src/main/java/eu/siacs/conversations/ui/widget/ScannerView.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright 2012-2015 the original author or authors. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package eu.siacs.conversations.ui.widget; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.Matrix; -import android.graphics.Matrix.ScaleToFit; -import android.graphics.Paint; -import android.graphics.Paint.Style; -import android.graphics.Rect; -import android.graphics.RectF; -import android.util.AttributeSet; -import android.view.View; - -import com.google.zxing.ResultPoint; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -import eu.siacs.conversations.R; - -/** - * @author Andreas Schildbach - */ - -public class ScannerView extends View { - private static final long LASER_ANIMATION_DELAY_MS = 100l; - private static final int DOT_OPACITY = 0xa0; - private static final int DOT_TTL_MS = 500; - - private final Paint maskPaint; - private final Paint laserPaint; - private final Paint dotPaint; - private final int maskColor, maskResultColor; - private final int laserColor; - private final int dotColor, dotResultColor; - private final Map dots = new HashMap(16); - private final Matrix matrix = new Matrix(); - private boolean isResult; - private Rect frame; - - public ScannerView(final Context context, final AttributeSet attrs) { - super(context, attrs); - - final Resources res = getResources(); - maskColor = res.getColor(R.color.black54); - maskResultColor = res.getColor(R.color.black87); - laserColor = res.getColor(R.color.red500); - dotColor = res.getColor(R.color.orange500); - dotResultColor = res.getColor(R.color.scan_result_dots); - - maskPaint = new Paint(); - maskPaint.setStyle(Style.FILL); - - laserPaint = new Paint(); - laserPaint.setStrokeWidth(res.getDimensionPixelSize(R.dimen.scan_laser_width)); - laserPaint.setStyle(Style.STROKE); - - dotPaint = new Paint(); - dotPaint.setAlpha(DOT_OPACITY); - dotPaint.setStyle(Style.STROKE); - dotPaint.setStrokeWidth(res.getDimension(R.dimen.scan_dot_size)); - dotPaint.setAntiAlias(true); - } - - public void setFraming(final Rect frame, final RectF framePreview, final int displayRotation, - final int cameraRotation, final boolean cameraFlip) { - this.frame = frame; - matrix.setRectToRect(framePreview, new RectF(frame), ScaleToFit.FILL); - matrix.postRotate(-displayRotation, frame.exactCenterX(), frame.exactCenterY()); - matrix.postScale(cameraFlip ? -1 : 1, 1, frame.exactCenterX(), frame.exactCenterY()); - matrix.postRotate(cameraRotation, frame.exactCenterX(), frame.exactCenterY()); - - invalidate(); - } - - public void setIsResult(final boolean isResult) { - this.isResult = isResult; - - invalidate(); - } - - public void addDot(final ResultPoint dot) { - dots.put(new float[]{dot.getX(), dot.getY()}, System.currentTimeMillis()); - - invalidate(); - } - - @Override - public void onDraw(final Canvas canvas) { - if (frame == null) - return; - - final long now = System.currentTimeMillis(); - - final int width = canvas.getWidth(); - final int height = canvas.getHeight(); - - final float[] point = new float[2]; - - // draw mask darkened - maskPaint.setColor(isResult ? maskResultColor : maskColor); - canvas.drawRect(0, 0, width, frame.top, maskPaint); - canvas.drawRect(0, frame.top, frame.left, frame.bottom + 1, maskPaint); - canvas.drawRect(frame.right + 1, frame.top, width, frame.bottom + 1, maskPaint); - canvas.drawRect(0, frame.bottom + 1, width, height, maskPaint); - - if (isResult) { - laserPaint.setColor(dotResultColor); - laserPaint.setAlpha(160); - - dotPaint.setColor(dotResultColor); - } else { - laserPaint.setColor(laserColor); - final boolean laserPhase = (now / 600) % 2 == 0; - laserPaint.setAlpha(laserPhase ? 160 : 255); - - dotPaint.setColor(dotColor); - - // schedule redraw - postInvalidateDelayed(LASER_ANIMATION_DELAY_MS); - } - - canvas.drawRect(frame, laserPaint); - - // draw points - for (final Iterator> i = dots.entrySet().iterator(); i.hasNext(); ) { - final Map.Entry entry = i.next(); - final long age = now - entry.getValue(); - if (age < DOT_TTL_MS) { - dotPaint.setAlpha((int) ((DOT_TTL_MS - age) * 256 / DOT_TTL_MS)); - - matrix.mapPoints(point, entry.getKey()); - canvas.drawPoint(point[0], point[1], dotPaint); - } else { - i.remove(); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/widget/SquareFrameLayout.java b/src/main/java/eu/siacs/conversations/ui/widget/SquareFrameLayout.java deleted file mode 100644 index 38e548fd6..000000000 --- a/src/main/java/eu/siacs/conversations/ui/widget/SquareFrameLayout.java +++ /dev/null @@ -1,25 +0,0 @@ -package eu.siacs.conversations.ui.widget; - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.FrameLayout; - -public class SquareFrameLayout extends FrameLayout { - public SquareFrameLayout(Context context) { - super(context); - } - - public SquareFrameLayout(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public SquareFrameLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - //noinspection SuspiciousNameCombination - super.onMeasure(widthMeasureSpec, widthMeasureSpec); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/widget/SurfaceViewRenderer.java b/src/main/java/eu/siacs/conversations/ui/widget/SurfaceViewRenderer.java deleted file mode 100644 index 06f604076..000000000 --- a/src/main/java/eu/siacs/conversations/ui/widget/SurfaceViewRenderer.java +++ /dev/null @@ -1,48 +0,0 @@ -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/ui/widget/SwipeRefreshListFragment.java b/src/main/java/eu/siacs/conversations/ui/widget/SwipeRefreshListFragment.java deleted file mode 100644 index b1ef6165a..000000000 --- a/src/main/java/eu/siacs/conversations/ui/widget/SwipeRefreshListFragment.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2014 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.ui.widget; - -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ListView; - -import androidx.fragment.app.ListFragment; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.ui.util.StyledAttributes; - -/** - * Subclass of {@link androidx.fragment.app.ListFragment} which provides automatic support for - * providing the 'swipe-to-refresh' UX gesture by wrapping the the content view in a - * {@link androidx.swiperefreshlayout.widget.SwipeRefreshLayout}. - */ -public class SwipeRefreshListFragment extends ListFragment { - - private boolean enabled = false; - private boolean refreshing = false; - - private SwipeRefreshLayout.OnRefreshListener onRefreshListener; - - private SwipeRefreshLayout mSwipeRefreshLayout; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - - // Create the list fragment's content view by calling the super method - final View listFragmentView = super.onCreateView(inflater, container, savedInstanceState); - - // Now create a SwipeRefreshLayout to wrap the fragment's content view - mSwipeRefreshLayout = new ListFragmentSwipeRefreshLayout(container.getContext()); - mSwipeRefreshLayout.setEnabled(enabled); - mSwipeRefreshLayout.setRefreshing(refreshing); - - final Context context = getActivity(); - if (context != null) { - mSwipeRefreshLayout.setColorSchemeColors(StyledAttributes.getColor(context, R.attr.colorAccent)); - } - - if (onRefreshListener != null) { - mSwipeRefreshLayout.setOnRefreshListener(onRefreshListener); - } - - // Add the list fragment's content view to the SwipeRefreshLayout, making sure that it fills - // the SwipeRefreshLayout - mSwipeRefreshLayout.addView(listFragmentView, - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - - // Make sure that the SwipeRefreshLayout will fill the fragment - mSwipeRefreshLayout.setLayoutParams( - new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); - - // Now return the SwipeRefreshLayout as this fragment's content view - return mSwipeRefreshLayout; - } - - /** - * Set the {@link androidx.core.widget.SwipeRefreshLayout.OnRefreshListener} to listen for - * initiated refreshes. - * - * @see androidx.core.widget.SwipeRefreshLayout#setOnRefreshListener(androidx.core.widget.SwipeRefreshLayout.OnRefreshListener) - */ - public void setOnRefreshListener(SwipeRefreshLayout.OnRefreshListener listener) { - onRefreshListener = listener; - enabled = true; - if (mSwipeRefreshLayout != null) { - mSwipeRefreshLayout.setEnabled(true); - mSwipeRefreshLayout.setOnRefreshListener(listener); - } - } - - /** - * Set whether the {@link androidx.core.widget.SwipeRefreshLayout} should be displaying - * that it is refreshing or not. - * - * @see androidx.core.widget.SwipeRefreshLayout#setRefreshing(boolean) - */ - public void setRefreshing(boolean refreshing) { - this.refreshing = refreshing; - if (mSwipeRefreshLayout != null) { - mSwipeRefreshLayout.setRefreshing(refreshing); - } - } - - - /** - * Sub-class of {@link androidx.core.widget.SwipeRefreshLayout} for use in this - * {@link androidx.core.app.ListFragment}. The reason that this is needed is because - * {@link androidx.core.widget.SwipeRefreshLayout} only supports a single child, which it - * expects to be the one which triggers refreshes. In our case the layout's child is the content - * view returned from - * {@link androidx.core.app.ListFragment#onCreateView(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle)} - * which is a {@link android.view.ViewGroup}. - * - *

To enable 'swipe-to-refresh' support via the {@link android.widget.ListView} we need to - * override the default behavior and properly signal when a gesture is possible. This is done by - * overriding {@link #canChildScrollUp()}. - */ - private class ListFragmentSwipeRefreshLayout extends SwipeRefreshLayout { - - public ListFragmentSwipeRefreshLayout(Context context) { - super(context); - } - - /** - * As mentioned above, we need to override this method to properly signal when a - * 'swipe-to-refresh' is possible. - * - * @return true if the {@link android.widget.ListView} is visible and can scroll up. - */ - @Override - public boolean canChildScrollUp() { - final ListView listView = getListView(); - if (listView.getVisibility() == View.VISIBLE) { - return listView.canScrollVertically(-1); - } else { - return false; - } - } - - } - -} diff --git a/src/main/java/eu/siacs/conversations/ui/widget/TextInputEditText.java b/src/main/java/eu/siacs/conversations/ui/widget/TextInputEditText.java deleted file mode 100644 index 96b825427..000000000 --- a/src/main/java/eu/siacs/conversations/ui/widget/TextInputEditText.java +++ /dev/null @@ -1,47 +0,0 @@ -package eu.siacs.conversations.ui.widget; - -import android.content.Context; -import android.os.Build; -import android.util.AttributeSet; -import android.widget.TextView; - -import java.lang.reflect.Field; - -/** - * A wrapper class to fix some weird fuck ups on Meizu devices - * credit goes to the people in this thread https://github.com/android-in-china/Compatibility/issues/11 - */ -public class TextInputEditText extends com.google.android.material.textfield.TextInputEditText { - - public TextInputEditText(Context context) { - super(context); - } - - public TextInputEditText(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public TextInputEditText(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - @Override - public CharSequence getHint() { - String manufacturer = Build.MANUFACTURER.toUpperCase(); - if (!manufacturer.contains("MEIZU") || Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - return super.getHint(); - } else { - try { - return getSuperHintHack(); - } catch (Exception e) { - return super.getHint(); - } - } - } - - private CharSequence getSuperHintHack() throws NoSuchFieldException, IllegalAccessException { - Field hintField = TextView.class.getDeclaredField("mHint"); - hintField.setAccessible(true); - return (CharSequence) hintField.get(this); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/widget/UnreadCountCustomView.java b/src/main/java/eu/siacs/conversations/ui/widget/UnreadCountCustomView.java deleted file mode 100644 index 459dcf5fc..000000000 --- a/src/main/java/eu/siacs/conversations/ui/widget/UnreadCountCustomView.java +++ /dev/null @@ -1,77 +0,0 @@ -package eu.siacs.conversations.ui.widget; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Typeface; -import android.util.AttributeSet; -import android.view.View; - -import eu.siacs.conversations.R; -import eu.siacs.conversations.ui.util.StyledAttributes; - -public class UnreadCountCustomView extends View { - - private int unreadCount; - private Paint paint, textPaint; - private int backgroundColor = 0xff0091ea; - - public UnreadCountCustomView(Context context) { - super(context); - init(context); - } - - public UnreadCountCustomView(Context context, AttributeSet attrs) { - super(context, attrs); - initXMLAttrs(context, attrs); - init(context); - } - - public UnreadCountCustomView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - initXMLAttrs(context, attrs); - init(context); - } - - private void initXMLAttrs(Context context, AttributeSet attrs) { - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.UnreadCountCustomView); - //setBackgroundColor(a.getColor(a.getIndex(0), ContextCompat.getColor(context, R.color.accent))); - setBackgroundColor(StyledAttributes.getColor(context, R.attr.colorAccent)); - a.recycle(); - } - - void init(Context context) { - paint = new Paint(); - paint.setColor(StyledAttributes.getColor(context, R.attr.colorAccent)); - paint.setAntiAlias(true); - textPaint = new Paint(); - textPaint.setColor(Color.WHITE); - textPaint.setTextAlign(Paint.Align.CENTER); - textPaint.setAntiAlias(true); - textPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - float midx = canvas.getWidth() / 2.0f; - float midy = canvas.getHeight() / 2.0f; - float radius = Math.min(canvas.getWidth(), canvas.getHeight()) / 2.0f; - float textOffset = canvas.getWidth() / 6.0f; - textPaint.setTextSize(0.95f * radius); - canvas.drawCircle(midx, midy, radius * 0.94f, paint); - canvas.drawText(unreadCount > 999 ? "\u221E" : String.valueOf(unreadCount), midx, midy + textOffset, textPaint); - - } - - public void setUnreadCount(int unreadCount) { - this.unreadCount = unreadCount; - invalidate(); - } - - public void setBackgroundColor(int backgroundColor) { - this.backgroundColor = backgroundColor; - } -} diff --git a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java b/src/main/java/eu/siacs/conversations/utils/AccountUtils.java deleted file mode 100644 index 98bfa8ab6..000000000 --- a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java +++ /dev/null @@ -1,116 +0,0 @@ -package eu.siacs.conversations.utils; - -import android.app.Activity; -import android.content.Intent; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.XmppActivity; -import me.drakeet.support.toast.ToastCompat; - -public class AccountUtils { - - public static final Class MANAGE_ACCOUNT_ACTIVITY; - - static { - MANAGE_ACCOUNT_ACTIVITY = getManageAccountActivityClass(); - } - - public static boolean hasEnabledAccounts(final XmppConnectionService service) { - final List accounts = service.getAccounts(); - for(Account account : accounts) { - if (account.isOptionSet(Account.OPTION_DISABLED)) { - return false; - } - } - return false; - } - - public static String publicDeviceId(final Account account) { - final UUID uuid; - try { - uuid = UUID.fromString(account.getUuid()); - } catch (final IllegalArgumentException e) { - return account.getUuid(); - } - final UUID publicDeviceId = getUuid(uuid.getLeastSignificantBits(), uuid.getLeastSignificantBits()); - return publicDeviceId.toString(); - } - - protected static UUID getUuid(final long msb, final long lsb) { - final long msb0 = (msb & 0xffffffffffff0fffL) | 4; // set version - final long lsb0 = (lsb & 0x3fffffffffffffffL) | 0x8000000000000000L; // set variant - return new UUID(msb0, lsb0); - } - - public static List getEnabledAccounts(final XmppConnectionService service) { - ArrayList accounts = new ArrayList<>(); - for (Account account : service.getAccounts()) { - if (account.getStatus() != Account.State.DISABLED) { - if (Config.DOMAIN_LOCK != null) { - accounts.add(account.getJid().getEscapedLocal()); - } else { - accounts.add(account.getJid().asBareJid().toEscapedString()); - } - } - } - return accounts; - } - - public static Account getFirstEnabled(XmppConnectionService service) { - final List accounts = service.getAccounts(); - for (Account account : accounts) { - if (!account.isOptionSet(Account.OPTION_DISABLED)) { - return account; - } - } - return null; - } - - public static Account getFirst(XmppConnectionService service) { - final List accounts = service.getAccounts(); - for (Account account : accounts) { - return account; - } - return null; - } - - public static Account getPendingAccount(XmppConnectionService service) { - Account pending = null; - for (Account account : service.getAccounts()) { - if (!account.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)) { - pending = account; - } else { - return null; - } - } - return pending; - } - - public static void launchManageAccounts(final Activity activity) { - if (MANAGE_ACCOUNT_ACTIVITY != null) { - activity.startActivity(new Intent(activity, MANAGE_ACCOUNT_ACTIVITY)); - } else { - ToastCompat.makeText(activity, R.string.feature_not_implemented, ToastCompat.LENGTH_SHORT).show(); - } - } - - public static void launchManageAccount(final XmppActivity xmppActivity) { - final Account account = getFirst(xmppActivity.xmppConnectionService); - xmppActivity.switchToAccount(account); - } - - private static Class getManageAccountActivityClass() { - try { - return Class.forName("eu.siacs.conversations.ui.ManageAccountActivity"); - } catch (final ClassNotFoundException e) { - return null; - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/AppRTCUtils.java b/src/main/java/eu/siacs/conversations/utils/AppRTCUtils.java deleted file mode 100644 index 1b6e43f75..000000000 --- a/src/main/java/eu/siacs/conversations/utils/AppRTCUtils.java +++ /dev/null @@ -1,55 +0,0 @@ - - -/* - * Copyright 2014 The WebRTC Project Authors. All rights reserved. - * - * Use of this source code is governed by a BSD-style license - * that can be found in the LICENSE file in the root of the source - * tree. An additional intellectual property rights grant can be found - * in the file PATENTS. All contributing project authors may - * be found in the AUTHORS file in the root of the source tree. - */ -package eu.siacs.conversations.utils; - -import android.os.Build; -import android.util.Log; - -/** - * AppRTCUtils provides helper functions for managing thread safety. - */ -public final class AppRTCUtils { - private AppRTCUtils() { - } - - /** - * Helper method which throws an exception when an assertion has failed. - */ - public static void assertIsTrue(boolean condition) { - if (!condition) { - throw new AssertionError("Expected condition to be true"); - } - } - - /** - * Helper method for building a string of thread information. - */ - public static String getThreadInfo() { - return "@[name=" + Thread.currentThread().getName() + ", id=" + Thread.currentThread().getId() - + "]"; - } - - /** - * Information about the current build, taken from system properties. - */ - public static void logDeviceInfo(String tag) { - Log.d(tag, "Android SDK: " + Build.VERSION.SDK_INT + ", " - + "Release: " + Build.VERSION.RELEASE + ", " - + "Brand: " + Build.BRAND + ", " - + "Device: " + Build.DEVICE + ", " - + "Id: " + Build.ID + ", " - + "Hardware: " + Build.HARDWARE + ", " - + "Manufacturer: " + Build.MANUFACTURER + ", " - + "Model: " + Build.MODEL + ", " - + "Product: " + Build.PRODUCT); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/AsciiArmor.java b/src/main/java/eu/siacs/conversations/utils/AsciiArmor.java deleted file mode 100644 index a1203ede1..000000000 --- a/src/main/java/eu/siacs/conversations/utils/AsciiArmor.java +++ /dev/null @@ -1,34 +0,0 @@ -package eu.siacs.conversations.utils; - -import com.google.common.base.Joiner; -import com.google.common.base.Splitter; -import com.google.common.base.Strings; -import com.google.common.collect.Iterables; -import com.google.common.io.BaseEncoding; - -import java.util.List; - -public class AsciiArmor { - - public static byte[] decode(final String input) { - final List lines = Splitter.on('\n').splitToList(Strings.nullToEmpty(input).trim()); - if (lines.size() == 1) { - final String line = lines.get(0); - if (line.length() > 1) { - final int end = line.lastIndexOf('='); - if (end >= 1) { - final String cleaned = line.substring(0, end); - return BaseEncoding.base64().decode(cleaned); - } - } - } - final String withoutChecksum; - if (Iterables.getLast(lines).charAt(0) == '=') { - withoutChecksum = Joiner.on("").join(lines.subList(0, lines.size() - 1)); - } else { - withoutChecksum = Joiner.on("").join(lines); - } - return BaseEncoding.base64().decode(withoutChecksum); - } - -} diff --git a/src/main/java/eu/siacs/conversations/utils/BackupFileHeader.java b/src/main/java/eu/siacs/conversations/utils/BackupFileHeader.java deleted file mode 100644 index 10ef07107..000000000 --- a/src/main/java/eu/siacs/conversations/utils/BackupFileHeader.java +++ /dev/null @@ -1,84 +0,0 @@ -package eu.siacs.conversations.utils; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -import eu.siacs.conversations.xmpp.Jid; - -public class BackupFileHeader { - - private static final int VERSION = 1; - - private String app; - private Jid jid; - private long timestamp; - private byte[] iv; - private byte[] salt; - - - @Override - public String toString() { - return "BackupFileHeader{" + - "app='" + app + '\'' + - ", jid=" + jid + - ", timestamp=" + timestamp + - ", iv=" + CryptoHelper.bytesToHex(iv) + - ", salt=" + CryptoHelper.bytesToHex(salt) + - '}'; - } - - public BackupFileHeader(String app, Jid jid, long timestamp, byte[] iv, byte[] salt) { - this.app = app; - this.jid = jid; - this.timestamp = timestamp; - this.iv = iv; - this.salt = salt; - } - - public void write(DataOutputStream dataOutputStream) throws IOException { - dataOutputStream.writeInt(VERSION); - dataOutputStream.writeUTF(app); - dataOutputStream.writeUTF(jid.asBareJid().toEscapedString()); - dataOutputStream.writeLong(timestamp); - dataOutputStream.write(iv); - dataOutputStream.write(salt); - } - - public static BackupFileHeader read(DataInputStream inputStream) throws IOException { - final int version = inputStream.readInt(); - if (version > VERSION) { - throw new IllegalArgumentException("Backup File version was " + version + " but app only supports up to version " + VERSION); - } - String app = inputStream.readUTF(); - String jid = inputStream.readUTF(); - long timestamp = inputStream.readLong(); - byte[] iv = new byte[12]; - inputStream.readFully(iv); - byte[] salt = new byte[16]; - inputStream.readFully(salt); - - return new BackupFileHeader(app, Jid.of(jid), timestamp, iv, salt); - - } - - public byte[] getSalt() { - return salt; - } - - public byte[] getIv() { - return iv; - } - - public Jid getJid() { - return jid; - } - - public String getApp() { - return app; - } - - public long getTimestamp() { - return timestamp; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/CameraUtils.java b/src/main/java/eu/siacs/conversations/utils/CameraUtils.java deleted file mode 100644 index 8c712c728..000000000 --- a/src/main/java/eu/siacs/conversations/utils/CameraUtils.java +++ /dev/null @@ -1,221 +0,0 @@ -package eu.siacs.conversations.utils; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.Dialog; -import android.content.ComponentName; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.graphics.drawable.Drawable; -import android.preference.PreferenceManager; -import android.provider.MediaStore; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.ui.SettingsActivity; -import fr.xgouchet.axml.CompressedXmlParser; - -public class CameraUtils { - - public PackageInfo packageInfo; - public List componentNames; - - public CameraUtils(PackageInfo packageInfo, List componentNames) { - this.packageInfo = packageInfo; - this.componentNames = componentNames; - } - - - public static List getCameraApps(Context context) { - //Step 1 - Get apps with Camera permission - List cameraPermissionPackages = getPackageInfosWithCameraPermission(context); - //Step 2 - Filter out apps with the correct intent-filter(s) - List cameraApps = new ArrayList(); - for (PackageInfo somePackage : cameraPermissionPackages) { - try { - //Step 2a - Get the AndroidManifest.xml - Document doc = readAndroidManifestFromPackageInfo(somePackage); - //Step 2b - Get Camera ComponentNames from Manifest - List componentNames = getCameraComponentNamesFromDocument(doc); - if (componentNames.size() == 0) { - continue; //This is not a Camera app - } - //Step 2c - Create CameraAppModel - CameraUtils cameraApp = new CameraUtils(somePackage, componentNames); - //Step 3 - check if app is enabled - ApplicationInfo ai = context.getPackageManager().getApplicationInfo(cameraApp.packageInfo.packageName, 0); - if (ai.enabled) { - cameraApps.add(cameraApp); - } - } catch (Exception e) { - //ignore - } - } - return cameraApps; - } - - public static List getPackageInfosWithCameraPermission(Context context) { - //Get a list of compatible apps - PackageManager pm = context.getPackageManager(); - List installedPackages = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS); - ArrayList cameraPermissionPackages = new ArrayList(); - //filter out only camera apps - for (PackageInfo somePackage : installedPackages) { - //- A camera app should have the Camera permission - boolean hasCameraPermission = false; - if (somePackage.requestedPermissions == null || somePackage.requestedPermissions.length == 0) { - continue; - } - for (String requestPermission : somePackage.requestedPermissions) { - if (requestPermission.equals(Manifest.permission.CAMERA)) { - //Ask for Camera permission, now see if it's granted. - if (pm.checkPermission(Manifest.permission.CAMERA, somePackage.packageName) == PackageManager.PERMISSION_GRANTED) { - hasCameraPermission = true; - break; - } - } - } - if (hasCameraPermission) { - cameraPermissionPackages.add(somePackage); - } - } - return cameraPermissionPackages; - } - - public static Document readAndroidManifestFromPackageInfo(PackageInfo packageInfo) throws IOException { - File apkFile = new File(packageInfo.applicationInfo.publicSourceDir); - //Get AndroidManifest.xml from APK - ZipFile apkZipFile = new ZipFile(apkFile, ZipFile.OPEN_READ); - ZipEntry manifestEntry = apkZipFile.getEntry("AndroidManifest.xml"); - InputStream manifestInputStream = apkZipFile.getInputStream(manifestEntry); - try { - return new CompressedXmlParser().parseDOM(manifestInputStream); - } catch (Exception e) { - throw new IOException("Error reading AndroidManifest", e); - } - } - - public static List getCameraComponentNamesFromDocument(Document doc) { - @SuppressLint("InlinedApi") - String[] correctActions = {MediaStore.ACTION_IMAGE_CAPTURE, MediaStore.ACTION_IMAGE_CAPTURE_SECURE, MediaStore.ACTION_VIDEO_CAPTURE}; - ArrayList componentNames = new ArrayList(); - Element manifestElement = (Element) doc.getElementsByTagName("manifest").item(0); - String packageName = manifestElement.getAttribute("package"); - Element applicationElement = (Element) manifestElement.getElementsByTagName("application").item(0); - NodeList activities = applicationElement.getElementsByTagName("activity"); - for (int i = 0; i < activities.getLength(); i++) { - Element activityElement = (Element) activities.item(i); - String activityName = activityElement.getAttribute("android:name"); - NodeList intentFiltersList = activityElement.getElementsByTagName("intent-filter"); - for (int j = 0; j < intentFiltersList.getLength(); j++) { - Element intentFilterElement = (Element) intentFiltersList.item(j); - NodeList actionsList = intentFilterElement.getElementsByTagName("action"); - for (int k = 0; k < actionsList.getLength(); k++) { - Element actionElement = (Element) actionsList.item(k); - String actionName = actionElement.getAttribute("android:name"); - for (String correctAction : correctActions) { - if (actionName.equals(correctAction)) { - //this activity has an intent filter with a correct action, add this to the list. - componentNames.add(new ComponentName(packageName, activityName)); - } - } - } - } - } - return componentNames; - } - - public static void showCameraChooser(final Activity activity, final List cameraAppModels) { - final Dialog dialog = new Dialog(activity); - dialog.setContentView(R.layout.dialog_camera_chooser); - dialog.setCancelable(true); - ListView listView = dialog.findViewById(R.id.chooserDialogListView); - listView.setAdapter(new CameraAppListViewAdapter(activity, cameraAppModels)); - listView.setOnItemClickListener((parent, view, position, id) -> { - SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity); - p.edit().putInt(SettingsActivity.CAMERA_CHOICE, position).apply(); - dialog.dismiss(); - }); - dialog.show(); - } - - public static ComponentName getCameraApp(CameraUtils cameraApp) { - Log.d(Config.LOGTAG, "CameraApp " + cameraApp.componentNames.get(0)); - return cameraApp.componentNames.get(0); - } - - static class CameraAppListViewAdapter extends ArrayAdapter { - private Activity activity; - private PackageManager pm; - - CameraAppListViewAdapter(Activity activity, List cameraApps) { - super(activity, R.layout.camera_chooser_item, cameraApps); - this.activity = activity; - this.pm = activity.getPackageManager(); - } - - @NonNull - @Override - public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { - //Get info - CameraUtils cameraApp = getItem(position); - ComponentName componentName = cameraApp.componentNames.get(0); - CharSequence appName = pm.getApplicationLabel(cameraApp.packageInfo.applicationInfo); - CharSequence appDesc = null; //cameraApp.componentNames.get(0).getShortClassName(); - Drawable appIcon = null; - try { - appIcon = pm.getActivityIcon(componentName); - } catch (Exception e) { - //ignore - } - - //Set UI - if (convertView == null) { - convertView = activity.getLayoutInflater().inflate(R.layout.camera_chooser_item, parent, false); - } - TextView firstLine = convertView.findViewById(R.id.firstLine); - firstLine.setText(appName); - TextView secondLine = convertView.findViewById(R.id.secondLine); - if (appDesc == null) { - secondLine.setVisibility(View.GONE); - secondLine.setText(""); - } else { - secondLine.setVisibility(View.VISIBLE); - secondLine.setText(appDesc); - } - ImageView imageView = convertView.findViewById(R.id.icon); - imageView.setImageResource(R.drawable.ic_android_black_48dp); //reset - if (appIcon != null) { - imageView.setImageDrawable(appIcon); - } - return convertView; - } - } - -} diff --git a/src/main/java/eu/siacs/conversations/utils/Cancellable.java b/src/main/java/eu/siacs/conversations/utils/Cancellable.java deleted file mode 100644 index 281380686..000000000 --- a/src/main/java/eu/siacs/conversations/utils/Cancellable.java +++ /dev/null @@ -1,34 +0,0 @@ -/* -* Copyright (c) 2018, Daniel Gultsch All rights reserved. -* -* Redistribution and use in source and binary forms, with or without modification, -* are permitted provided that the following conditions are met: -* -* 1. Redistributions of source code must retain the above copyright notice, this -* list of conditions and the following disclaimer. -* -* 2. Redistributions in binary form must reproduce the above copyright notice, -* this list of conditions and the following disclaimer in the documentation and/or -* other materials provided with the distribution. -* -* 3. Neither the name of the copyright holder nor the names of its contributors -* may be used to endorse or promote products derived from this software without -* specific prior written permission. -* -* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -package eu.siacs.conversations.utils; - - -public interface Cancellable { - void cancel(); -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/CharSequenceUtils.java b/src/main/java/eu/siacs/conversations/utils/CharSequenceUtils.java deleted file mode 100644 index 25e52f53f..000000000 --- a/src/main/java/eu/siacs/conversations/utils/CharSequenceUtils.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.utils; - -import android.text.Spannable; - -import java.util.ArrayList; -import java.util.List; - -public class CharSequenceUtils { - - private static int getStartIndex(CharSequence input) { - int length = input.length(); - int index = 0; - while (Character.isWhitespace(input.charAt(index))) { - ++index; - if (index >= length) { - break; - } - } - return index; - } - - private static int getEndIndex(CharSequence input) { - int index = input.length() - 1; - while (Character.isWhitespace(input.charAt(index))) { - --index; - if (index < 0) { - break; - } - } - return index; - } - - public static CharSequence trim(CharSequence input) { - int begin = getStartIndex(input); - int end = getEndIndex(input); - if (begin > end) { - return ""; - } else { - return StylingHelper.subSequence(input, begin, end + 1); - } - } - - public static List split(Spannable charSequence, char c) { - List out = new ArrayList<>(); - int begin = 0; - for (int i = 0; i < charSequence.length(); ++i) { - if (charSequence.charAt(i) == c) { - out.add(StylingHelper.subSequence(charSequence, begin, i)); - begin = ++i; - } - } - if (begin < charSequence.length()) { - out.add(StylingHelper.subSequence(charSequence, begin, charSequence.length())); - } - return out; - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/Checksum.java b/src/main/java/eu/siacs/conversations/utils/Checksum.java deleted file mode 100644 index 20415e098..000000000 --- a/src/main/java/eu/siacs/conversations/utils/Checksum.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.utils; - -import android.util.Base64; - -import java.io.IOException; -import java.io.InputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -public class Checksum { - - public static String md5(InputStream inputStream) throws IOException { - byte[] buffer = new byte[4096]; - MessageDigest messageDigest; - try { - messageDigest = MessageDigest.getInstance("MD5"); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); - } - - int count; - do { - count = inputStream.read(buffer); - if (count > 0) { - messageDigest.update(buffer, 0, count); - } - } while (count != -1); - inputStream.close(); - return Base64.encodeToString(messageDigest.digest(), Base64.NO_WRAP); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java deleted file mode 100644 index bf3ce8d54..000000000 --- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java +++ /dev/null @@ -1,228 +0,0 @@ -package eu.siacs.conversations.utils; - -import static eu.siacs.conversations.services.EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.os.Build; -import android.preference.Preference; -import android.preference.PreferenceCategory; -import android.preference.PreferenceManager; -import android.preference.PreferenceScreen; -import android.util.Log; -import android.net.ConnectivityManager; -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; - -import androidx.annotation.BoolRes; -import androidx.core.content.ContextCompat; - -import java.util.Arrays; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.ui.SettingsActivity; -import eu.siacs.conversations.ui.SettingsFragment; - -public class Compatibility { - private static final List UNUSED_SETTINGS_POST_TWENTYSIX = Arrays.asList( - SettingsActivity.SHOW_FOREGROUND_SERVICE, - "led", - "notification_ringtone", - "notification_headsup", - "vibrate_on_notification", - "call_ringtone" - ); - - private static final List UNUSED_SETTINGS_PRE_TWENTYSIX = Arrays.asList( - "message_notification_settings", - "call_notification_settings", - "remove_all_individual_notifications" - ); - - private static final List UNUSED_SETTINGS_PRE_THIRTY = Arrays.asList( - SettingsActivity.CAMERA_CHOICE - ); - - public static boolean hasStoragePermission(Context context) { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || (ContextCompat.checkSelfPermission(context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED); - } - - public static boolean runsTwentyThree() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; - } - - public static boolean runsTwentyFour() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; - } - - public static boolean runsTwentySix() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; - } - - public static boolean runsTwentyEight() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P; - } - - public static boolean runsTwentyNine() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; - } - - public static boolean runsThirty() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R; - } - - public static boolean s() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S; - } - - private static boolean getBooleanPreference(Context context, String name, @BoolRes int res) { - return getPreferences(context).getBoolean(name, context.getResources().getBoolean(res)); - } - - private static SharedPreferences getPreferences(final Context context) { - return PreferenceManager.getDefaultSharedPreferences(context); - } - - private static boolean targetsTwentySix(Context context) { - try { - final PackageManager packageManager = context.getPackageManager(); - final ApplicationInfo applicationInfo = packageManager.getApplicationInfo(context.getPackageName(), 0); - return applicationInfo == null || applicationInfo.targetSdkVersion >= 26; - } catch (PackageManager.NameNotFoundException e) { - return true; //when in doubt… - } catch (RuntimeException e) { - return true; //when in doubt… - } - } - - private static boolean targetsThirty(Context context) { - try { - final PackageManager packageManager = context.getPackageManager(); - final ApplicationInfo applicationInfo = packageManager.getApplicationInfo(context.getPackageName(), 0); - return applicationInfo == null || applicationInfo.targetSdkVersion >= 30; - } catch (PackageManager.NameNotFoundException e) { - return true; //when in doubt… - } catch (RuntimeException e) { - return true; //when in doubt… - } - } - - private static boolean targetsTwentyFour(Context context) { - try { - final PackageManager packageManager = context.getPackageManager(); - final ApplicationInfo applicationInfo = packageManager.getApplicationInfo(context.getPackageName(), 0); - return applicationInfo == null || applicationInfo.targetSdkVersion >= 24; - } catch (PackageManager.NameNotFoundException e) { - return true; //when in doubt… - } catch (RuntimeException e) { - return true; //when in doubt… - } - } - - public static boolean runsAndTargetsTwentyFour(Context context) { - return runsTwentyFour() && targetsTwentyFour(context); - } - - public static boolean runsAndTargetsTwentySix(Context context) { - return runsTwentySix() && targetsTwentySix(context); - } - public static boolean runsAndTargetsThirty(Context context) { - return runsThirty() && targetsThirty(context); - } - - public static boolean keepForegroundService(Context context) { - return runsTwentySix() || getBooleanPreference(context, SettingsActivity.SHOW_FOREGROUND_SERVICE, R.bool.show_foreground_service); - } - - public static void removeUnusedPreferences(SettingsFragment settingsFragment) { - List screens = Arrays.asList( - (PreferenceScreen) settingsFragment.findPreference("notifications"), - (PreferenceScreen) settingsFragment.findPreference("attachments")); - List categories = Arrays.asList( - (PreferenceCategory) settingsFragment.findPreference("general")); - for (String key : (runsTwentySix() ? UNUSED_SETTINGS_POST_TWENTYSIX : UNUSED_SETTINGS_PRE_TWENTYSIX)) { - Preference preference = settingsFragment.findPreference(key); - if (preference != null) { - for (PreferenceScreen screen : screens) { - if (screen != null) { - screen.removePreference(preference); - } - } - for (PreferenceCategory category : categories) { - if (category != null) { - category.removePreference(preference); - } - } - } - } - if (Compatibility.runsTwentySix()) { - if (targetsTwentySix(settingsFragment.getContext())) { - Preference preference = settingsFragment.findPreference(SettingsActivity.SHOW_FOREGROUND_SERVICE); - if (preference != null) { - for (PreferenceCategory category : categories) { - if (category != null) { - category.removePreference(preference); - } - } - } - } - } - if (!Compatibility.runsThirty()) { - for (String key : UNUSED_SETTINGS_PRE_THIRTY) { - Preference preference = settingsFragment.findPreference(key); - if (preference != null) { - for (PreferenceScreen screen : screens) { - if (screen != null) { - screen.removePreference(preference); - } - } - for (PreferenceCategory category : categories) { - if (category != null) { - category.removePreference(preference); - } - } - } - } - } - } - - public static void startService(Context context, Intent intent) { - try { - if (Compatibility.runsAndTargetsTwentySix(context)) { - intent.putExtra(EXTRA_NEEDS_FOREGROUND_SERVICE, true); - ContextCompat.startForegroundService(context, intent); - } else { - context.startService(intent); - } - } catch (RuntimeException e) { - Log.d(Config.LOGTAG, context.getClass().getSimpleName() + " was unable to start service"); - } - } - - @SuppressLint("UnsupportedChromeOsCameraSystemFeature") - public static boolean hasFeatureCamera(final Context context) { - final PackageManager packageManager = context.getPackageManager(); - return packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); - } - - @RequiresApi(api = Build.VERSION_CODES.N) - public static int getRestrictBackgroundStatus( - @NonNull final ConnectivityManager connectivityManager) { - try { - return connectivityManager.getRestrictBackgroundStatus(); - } catch (final Exception e) { - Log.d( - Config.LOGTAG, - "platform bug detected. Unable to get restrict background status", - e); - return ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED; - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/ConversationsFileObserver.java b/src/main/java/eu/siacs/conversations/utils/ConversationsFileObserver.java deleted file mode 100644 index 6de1a0011..000000000 --- a/src/main/java/eu/siacs/conversations/utils/ConversationsFileObserver.java +++ /dev/null @@ -1,142 +0,0 @@ -package eu.siacs.conversations.utils; - - -import android.os.FileObserver; -import android.util.Log; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Stack; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicBoolean; - -import eu.siacs.conversations.Config; - -/** - * Copyright (C) 2012 Bartek Przybylski - * Copyright (C) 2015 ownCloud Inc. - * Copyright (C) 2016 Daniel Gultsch - */ - -public abstract class ConversationsFileObserver { - private static final Executor EVENT_EXECUTOR = Executors.newSingleThreadExecutor(); - private static final int MASK = FileObserver.DELETE | FileObserver.MOVED_FROM | FileObserver.CREATE; - - - private final String path; - private final List mObservers = new ArrayList<>(); - private final AtomicBoolean shouldStop = new AtomicBoolean(true); - - protected ConversationsFileObserver(String path) { - this.path = path; - } - - public void startWatching() { - shouldStop.set(false); - startWatchingInternal(); - } - - private synchronized void startWatchingInternal() { - final Stack stack = new Stack<>(); - stack.push(path); - - while (!stack.empty()) { - if (shouldStop.get()) { - Log.d(Config.LOGTAG, "file observer received command to stop"); - return; - } - final String parent = stack.pop(); - final File path = new File(parent); - mObservers.add(new SingleFileObserver(path, MASK)); - final File[] files = path.listFiles(); - for (final File file : (files == null ? new File[0] : files)) { - if (shouldStop.get()) { - Log.d(Config.LOGTAG, "file observer received command to stop"); - return; - } - if (file.isDirectory() && file.getName().charAt(0) != '.') { - final String currentPath = file.getAbsolutePath(); - if (depth(file) <= 8 && !stack.contains(currentPath) && !observing(file)) { - stack.push(currentPath); - } - } - } - } - for (FileObserver observer : mObservers) { - observer.startWatching(); - } - } - - private static int depth(File file) { - int depth = 0; - while ((file = file.getParentFile()) != null) { - depth++; - } - return depth; - } - - private boolean observing(final File path) { - for (final SingleFileObserver observer : mObservers) { - if (path.equals(observer.path)) { - return true; - } - } - return false; - } - - public void stopWatching() { - shouldStop.set(true); - stopWatchingInternal(); - } - - private synchronized void stopWatchingInternal() { - for (FileObserver observer : mObservers) { - observer.stopWatching(); - } - mObservers.clear(); - } - - abstract public void onEvent(final int event, File path); - - public void restartWatching() { - stopWatching(); - startWatching(); - } - - private class SingleFileObserver extends FileObserver { - private final File path; - - SingleFileObserver(final File path, final int mask) { - super(path.getAbsolutePath(), mask); - this.path = path; - } - - @Override - public void onEvent(final int event, final String filename) { - if (filename == null) { - Log.d(Config.LOGTAG, "ignored file event with NULL filename (event=" + event + ")"); - return; - } - EVENT_EXECUTOR.execute(() -> { - final File file = new File(this.path, filename); - if ((event & FileObserver.ALL_EVENTS) == FileObserver.CREATE) { - if (file.isDirectory()) { - Log.d(Config.LOGTAG, "file observer observed new directory creation " + file); - if (!observing(file)) { - final SingleFileObserver observer = new SingleFileObserver(file, MASK); - observer.startWatching(); - } - } - return; - } - try { - ConversationsFileObserver.this.onEvent(event, file); - } catch (Exception e) { - e.printStackTrace(); - } - }); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java deleted file mode 100644 index 53700ac99..000000000 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ /dev/null @@ -1,286 +0,0 @@ -package eu.siacs.conversations.utils; - -import android.os.Bundle; -import android.util.Base64; -import android.util.Pair; -import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; - -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.bouncycastle.asn1.x500.style.IETFUtils; -import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateParsingException; -import java.security.cert.X509Certificate; -import java.text.Normalizer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.regex.Pattern; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.xmpp.Jid; - -public final class CryptoHelper { - - public static final Pattern UUID_PATTERN = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"); - final public static byte[] ONE = new byte[]{0, 0, 0, 1}; - private static final char[] CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz123456789+-/#$!?".toCharArray(); - private static final int PW_LENGTH = 12; - private static final char[] VOWELS = "aeiou".toCharArray(); - private static final char[] CONSONANTS = "bcfghjklmnpqrstvwxyz".toCharArray(); - public static final String FILETRANSFER = "?FILETRANSFERv1:"; - private final static char[] hexArray = "0123456789abcdef".toCharArray(); - - public static String bytesToHex(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = hexArray[v >>> 4]; - hexChars[j * 2 + 1] = hexArray[v & 0x0F]; - } - return new String(hexChars); - } - - public static String createPassword(SecureRandom random) { - StringBuilder builder = new StringBuilder(PW_LENGTH); - for (int i = 0; i < PW_LENGTH; ++i) { - builder.append(CHARS[random.nextInt(CHARS.length - 1)]); - } - return builder.toString(); - } - - public static String pronounceable() { - final int rand = SECURE_RANDOM.nextInt(4); - char[] output = new char[rand * 2 + (5 - rand)]; - boolean vowel = SECURE_RANDOM.nextBoolean(); - for (int i = 0; i < output.length; ++i) { - output[i] = vowel ? VOWELS[SECURE_RANDOM.nextInt(VOWELS.length)] : CONSONANTS[SECURE_RANDOM.nextInt(CONSONANTS.length)]; - vowel = !vowel; - } - return String.valueOf(output); - } - - public static byte[] hexToBytes(String hexString) { - int len = hexString.length(); - byte[] array = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - array[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character - .digit(hexString.charAt(i + 1), 16)); - } - return array; - } - - public static String hexToString(final String hexString) { - return new String(hexToBytes(hexString)); - } - - public static byte[] concatenateByteArrays(byte[] a, byte[] b) { - byte[] result = new byte[a.length + b.length]; - System.arraycopy(a, 0, result, 0, a.length); - System.arraycopy(b, 0, result, a.length, b.length); - return result; - } - - /** - * Escapes usernames or passwords for SASL. - */ - public static String saslEscape(final String s) { - final StringBuilder sb = new StringBuilder((int) (s.length() * 1.1)); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - switch (c) { - case ',': - sb.append("=2C"); - break; - case '=': - sb.append("=3D"); - break; - default: - sb.append(c); - break; - } - } - return sb.toString(); - } - - public static String saslPrep(final String s) { - return Normalizer.normalize(s, Normalizer.Form.NFKC); - } - - public static String random(final int length) { - final byte[] bytes = new byte[length]; - SECURE_RANDOM.nextBytes(bytes); - return Base64.encodeToString(bytes, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE); - } - - public static String prettifyFingerprint(String fingerprint) { - if (fingerprint == null) { - return ""; - } else if (fingerprint.length() < 40) { - return fingerprint; - } - StringBuilder builder = new StringBuilder(fingerprint); - for (int i = 8; i < builder.length(); i += 9) { - builder.insert(i, ' '); - } - return builder.toString(); - } - - public static String prettifyFingerprintCert(String fingerprint) { - StringBuilder builder = new StringBuilder(fingerprint); - for (int i = 2; i < builder.length(); i += 3) { - builder.insert(i, ':'); - } - return builder.toString(); - } - - public static String[] getOrderedCipherSuites(final String[] platformSupportedCipherSuites) { - final Collection cipherSuites = new LinkedHashSet<>(Arrays.asList(Config.ENABLED_CIPHERS)); - final List platformCiphers = Arrays.asList(platformSupportedCipherSuites); - cipherSuites.retainAll(platformCiphers); - cipherSuites.addAll(platformCiphers); - filterWeakCipherSuites(cipherSuites); - cipherSuites.remove("TLS_FALLBACK_SCSV"); - return cipherSuites.toArray(new String[cipherSuites.size()]); - } - - private static void filterWeakCipherSuites(final Collection cipherSuites) { - final Iterator it = cipherSuites.iterator(); - while (it.hasNext()) { - String cipherName = it.next(); - // remove all ciphers with no or very weak encryption or no authentication - for (String weakCipherPattern : Config.WEAK_CIPHER_PATTERNS) { - if (cipherName.contains(weakCipherPattern)) { - it.remove(); - break; - } - } - } - } - - public static Pair extractJidAndName(X509Certificate certificate) throws CertificateEncodingException, IllegalArgumentException, CertificateParsingException { - Collection> alternativeNames = certificate.getSubjectAlternativeNames(); - List emails = new ArrayList<>(); - if (alternativeNames != null) { - for (List san : alternativeNames) { - Integer type = (Integer) san.get(0); - if (type == 1) { - emails.add((String) san.get(1)); - } - } - } - X500Name x500name = new JcaX509CertificateHolder(certificate).getSubject(); - if (emails.size() == 0 && x500name.getRDNs(BCStyle.EmailAddress).length > 0) { - emails.add(IETFUtils.valueToString(x500name.getRDNs(BCStyle.EmailAddress)[0].getFirst().getValue())); - } - String name = x500name.getRDNs(BCStyle.CN).length > 0 ? IETFUtils.valueToString(x500name.getRDNs(BCStyle.CN)[0].getFirst().getValue()) : null; - if (emails.size() >= 1) { - return new Pair<>(Jid.of(emails.get(0)), name); - } else if (name != null) { - try { - Jid jid = Jid.of(name); - if (jid.isBareJid() && jid.getLocal() != null) { - return new Pair<>(jid, null); - } - } catch (IllegalArgumentException e) { - return null; - } - } - return null; - } - - public static Bundle extractCertificateInformation(X509Certificate certificate) { - Bundle information = new Bundle(); - try { - JcaX509CertificateHolder holder = new JcaX509CertificateHolder(certificate); - X500Name subject = holder.getSubject(); - try { - information.putString("subject_cn", subject.getRDNs(BCStyle.CN)[0].getFirst().getValue().toString()); - } catch (Exception e) { - //ignored - } - try { - information.putString("subject_o", subject.getRDNs(BCStyle.O)[0].getFirst().getValue().toString()); - } catch (Exception e) { - //ignored - } - - X500Name issuer = holder.getIssuer(); - try { - information.putString("issuer_cn", issuer.getRDNs(BCStyle.CN)[0].getFirst().getValue().toString()); - } catch (Exception e) { - //ignored - } - try { - information.putString("issuer_o", issuer.getRDNs(BCStyle.O)[0].getFirst().getValue().toString()); - } catch (Exception e) { - //ignored - } - try { - information.putString("sha1", getFingerprintCert(certificate.getEncoded())); - } catch (Exception e) { - - } - return information; - } catch (CertificateEncodingException e) { - return information; - } - } - - public static String getFingerprintCert(byte[] input) throws NoSuchAlgorithmException { - MessageDigest md = MessageDigest.getInstance("SHA-1"); - byte[] fingerprint = md.digest(input); - return prettifyFingerprintCert(bytesToHex(fingerprint)); - } - - public static String getFingerprint(Jid jid, String androidId) { - return getFingerprint(jid.toEscapedString() + "\00" + androidId); - } - - public static String getAccountFingerprint(Account account, String androidId) { - return getFingerprint(account.getJid().asBareJid(), androidId); - } - - public static String getFingerprint(String value) { - try { - MessageDigest md = MessageDigest.getInstance("SHA-1"); - return bytesToHex(md.digest(value.getBytes("UTF-8"))); - } catch (Exception e) { - return ""; - } - } - - public static int encryptionTypeToText(int encryption) { - switch (encryption) { - case Message.ENCRYPTION_OTR: - return R.string.encryption_choice_otr; - case Message.ENCRYPTION_AXOLOTL: - case Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE: - case Message.ENCRYPTION_AXOLOTL_FAILED: - return R.string.encryption_choice_omemo; - case Message.ENCRYPTION_NONE: - return R.string.encryption_choice_unencrypted; - default: - return R.string.encryption_choice_pgp; - } - } - - public static boolean isPgpEncryptedUrl(String url) { - if (url == null) { - return false; - } - final String u = url.toLowerCase(); - return !u.contains(" ") && (u.startsWith("https://") || u.startsWith("http://") || u.startsWith("p1s3://")) && u.endsWith(".pgp"); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/CursorUtils.java b/src/main/java/eu/siacs/conversations/utils/CursorUtils.java deleted file mode 100644 index bbcc9533d..000000000 --- a/src/main/java/eu/siacs/conversations/utils/CursorUtils.java +++ /dev/null @@ -1,21 +0,0 @@ -package eu.siacs.conversations.utils; - -import android.database.AbstractWindowedCursor; -import android.database.Cursor; -import android.database.CursorWindow; -import android.database.sqlite.SQLiteCursor; - -public class CursorUtils { - - public static void upgradeCursorWindowSize(final Cursor cursor) { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { - if (cursor instanceof AbstractWindowedCursor) { - final AbstractWindowedCursor windowedCursor = (AbstractWindowedCursor) cursor; - windowedCursor.setWindow(new CursorWindow("4M", 4 * 1024 * 1024)); - } - if (cursor instanceof SQLiteCursor) { - ((SQLiteCursor) cursor).setFillWindowForwardOnly(true); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/EasyOnboardingInvite.java b/src/main/java/eu/siacs/conversations/utils/EasyOnboardingInvite.java deleted file mode 100644 index fa43da14a..000000000 --- a/src/main/java/eu/siacs/conversations/utils/EasyOnboardingInvite.java +++ /dev/null @@ -1,102 +0,0 @@ -package eu.siacs.conversations.utils; - -import android.os.Parcel; -import android.os.Parcelable; - -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; - -import java.util.Collections; -import java.util.List; - -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.services.QuickConversationsService; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xmpp.XmppConnection; - -public class EasyOnboardingInvite implements Parcelable { - - private final String domain; - private final String uri; - private final String landingUrl; - - protected EasyOnboardingInvite(Parcel in) { - domain = in.readString(); - uri = in.readString(); - landingUrl = in.readString(); - } - - public EasyOnboardingInvite(String domain, String uri, String landingUrl) { - this.domain = domain; - this.uri = uri; - this.landingUrl = landingUrl; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(domain); - dest.writeString(uri); - dest.writeString(landingUrl); - } - - @Override - public int describeContents() { - return 0; - } - - public static final Creator CREATOR = new Creator() { - @Override - public EasyOnboardingInvite createFromParcel(Parcel in) { - return new EasyOnboardingInvite(in); - } - - @Override - public EasyOnboardingInvite[] newArray(int size) { - return new EasyOnboardingInvite[size]; - } - }; - - public static boolean anyHasSupport(final XmppConnectionService service) { - if (QuickConversationsService.isQuicksy()) { - return false; - } - return getSupportingAccounts(service).size() > 0; - } - - public static List getSupportingAccounts(final XmppConnectionService service) { - final ImmutableList.Builder supportingAccountsBuilder = new ImmutableList.Builder<>(); - final List accounts = service == null ? Collections.emptyList() : service.getAccounts(); - for(Account account : accounts) { - final XmppConnection xmppConnection = account.getXmppConnection(); - if (xmppConnection != null && xmppConnection.getFeatures().easyOnboardingInvites()) { - supportingAccountsBuilder.add(account); - } - } - return supportingAccountsBuilder.build(); - } - - public static boolean hasAccountSupport(final Account account) { - final XmppConnection xmppConnection = account.getXmppConnection(); - if (xmppConnection != null && xmppConnection.getFeatures().easyOnboardingInvites()) { - return true; - } - return false; - } - - public String getShareableLink() { - return Strings.isNullOrEmpty(landingUrl) ? uri : landingUrl; - } - - public String getShareableUri() { - return uri; - } - - public String getDomain() { - return domain; - } - - public interface OnInviteRequested { - void inviteRequested(EasyOnboardingInvite invite); - void inviteRequestFailed(String message); - } -} diff --git a/src/main/java/eu/siacs/conversations/utils/Emoticons.java b/src/main/java/eu/siacs/conversations/utils/Emoticons.java deleted file mode 100644 index 4f3582acd..000000000 --- a/src/main/java/eu/siacs/conversations/utils/Emoticons.java +++ /dev/null @@ -1,309 +0,0 @@ -/* - * Copyright (c) 2017, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.utils; - -import android.util.LruCache; - -import androidx.annotation.NonNull; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.regex.Pattern; - -public class Emoticons { - - private static final UnicodeRange MISC_SYMBOLS_AND_PICTOGRAPHS = new UnicodeRange(0x1F300, 0x1F5FF); - private static final UnicodeRange SUPPLEMENTAL_SYMBOLS = new UnicodeRange(0x1F900, 0x1F9FF); - private static final UnicodeRange EMOTICONS = new UnicodeRange(0x1F600, 0x1F64F); - private static final UnicodeRange TRANSPORT_SYMBOLS = new UnicodeRange(0x1F680, 0x1F6FF); - private static final UnicodeRange MISC_SYMBOLS = new UnicodeRange(0x2600, 0x26FF); - private static final UnicodeRange DINGBATS = new UnicodeRange(0x2700, 0x27BF); - private static final UnicodeRange ENCLOSED_ALPHANUMERIC_SUPPLEMENT = new UnicodeRange(0x1F100, 0x1F1FF); - private static final UnicodeRange ENCLOSED_IDEOGRAPHIC_SUPPLEMENT = new UnicodeRange(0x1F200, 0x1F2FF); - private static final UnicodeRange REGIONAL_INDICATORS = new UnicodeRange(0x1F1E6, 0x1F1FF); - private static final UnicodeRange GEOMETRIC_SHAPES = new UnicodeRange(0x25A0, 0x25FF); - private static final UnicodeRange LATIN_SUPPLEMENT = new UnicodeRange(0x80, 0xFF); - private static final UnicodeRange MISC_TECHNICAL = new UnicodeRange(0x2300, 0x23FF); - private static final UnicodeRange TAGS = new UnicodeRange(0xE0020, 0xE007F); - private static final UnicodeList CYK_SYMBOLS_AND_PUNCTUATION = new UnicodeList(0x3030, 0x303D); - private static final UnicodeList LETTERLIKE_SYMBOLS = new UnicodeList(0x2122, 0x2139); - - private static final UnicodeBlocks KEYCAP_COMBINEABLE = new UnicodeBlocks(new UnicodeList(0x23), new UnicodeList(0x2A), new UnicodeRange(0x30, 0x39)); - - private static final UnicodeBlocks SYMBOLIZE = new UnicodeBlocks( - GEOMETRIC_SHAPES, - LATIN_SUPPLEMENT, - CYK_SYMBOLS_AND_PUNCTUATION, - LETTERLIKE_SYMBOLS, - KEYCAP_COMBINEABLE); - private static final UnicodeBlocks EMOJIS = new UnicodeBlocks( - MISC_SYMBOLS_AND_PICTOGRAPHS, - SUPPLEMENTAL_SYMBOLS, - EMOTICONS, - TRANSPORT_SYMBOLS, - MISC_SYMBOLS, - DINGBATS, - ENCLOSED_ALPHANUMERIC_SUPPLEMENT, - ENCLOSED_IDEOGRAPHIC_SUPPLEMENT, - MISC_TECHNICAL); - - private static final int MAX_EMOIJS = 42; - private static final int ZWJ = 0x200D; - private static final int VARIATION_16 = 0xFE0F; - private static final int COMBINING_ENCLOSING_KEYCAP = 0x20E3; - private static final int BLACK_FLAG = 0x1F3F4; - private static final UnicodeRange FITZPATRICK = new UnicodeRange(0x1F3FB, 0x1F3FF); - - private static final LruCache CACHE = new LruCache<>(256); - - private static List parse(String input) { - List symbols = new ArrayList<>(); - Builder builder = new Builder(); - boolean needsFinalBuild = false; - for (int cp, i = 0; i < input.length(); i += Character.charCount(cp)) { - cp = input.codePointAt(i); - if (builder.offer(cp)) { - needsFinalBuild = true; - } else { - symbols.add(builder.build()); - builder = new Builder(); - if (builder.offer(cp)) { - needsFinalBuild = true; - } - } - } - if (needsFinalBuild) { - symbols.add(builder.build()); - } - return symbols; - } - - public static Pattern getEmojiPattern(final CharSequence input) { - Pattern pattern = CACHE.get(input); - if (pattern == null) { - pattern = generatePattern(input); - CACHE.put(input, pattern); - } - return pattern; - } - - private static Pattern generatePattern(CharSequence input) { - final HashSet emojis = new HashSet<>(); - int i = 0; - for (final Symbol symbol : parse(input.toString())) { - if (symbol instanceof Emoji) { - emojis.add(symbol.toString()); - if (++i >= MAX_EMOIJS) { - return Pattern.compile(""); - } - } - } - final StringBuilder pattern = new StringBuilder(); - for (String emoji : emojis) { - if (pattern.length() != 0) { - pattern.append('|'); - } - pattern.append(Pattern.quote(emoji)); - } - return Pattern.compile(pattern.toString()); - } - - public static boolean isEmoji(String input) { - List symbols = parse(input); - return symbols.size() == 1 && symbols.get(0).isEmoji(); - } - - public static boolean isOnlyEmoji(String input) { - List symbols = parse(input); - for (Symbol symbol : symbols) { - if (!symbol.isEmoji()) { - return false; - } - } - return symbols.size() > 0; - } - - private static abstract class Symbol { - - private final String value; - - Symbol(List codepoints) { - final StringBuilder builder = new StringBuilder(); - for (final Integer codepoint : codepoints) { - builder.appendCodePoint(codepoint); - } - this.value = builder.toString(); - } - - abstract boolean isEmoji(); - - @NonNull - @Override - public String toString() { - return value; - } - } - - public static class Emoji extends Symbol { - - public Emoji(List codepoints) { - super(codepoints); - } - - @Override - boolean isEmoji() { - return true; - } - } - - public static class Other extends Symbol { - - public Other(List codepoints) { - super(codepoints); - } - - @Override - boolean isEmoji() { - return false; - } - } - - private static class Builder { - private final List codepoints = new ArrayList<>(); - - - public boolean offer(int codepoint) { - boolean add = false; - if (this.codepoints.size() == 0) { - if (SYMBOLIZE.contains(codepoint)) { - add = true; - } else if (REGIONAL_INDICATORS.contains(codepoint)) { - add = true; - } else if (EMOJIS.contains(codepoint) && !FITZPATRICK.contains(codepoint) && codepoint != ZWJ) { - add = true; - } - } else { - int previous = codepoints.get(codepoints.size() - 1); - if (codepoints.get(0) == BLACK_FLAG) { - add = TAGS.contains(codepoint); - } else if (COMBINING_ENCLOSING_KEYCAP == codepoint) { - add = KEYCAP_COMBINEABLE.contains(previous) || previous == VARIATION_16; - } else if (SYMBOLIZE.contains(previous)) { - add = codepoint == VARIATION_16; - } else if (REGIONAL_INDICATORS.contains(previous) && REGIONAL_INDICATORS.contains(codepoint)) { - add = codepoints.size() == 1; - } else if (previous == VARIATION_16) { - add = isMerger(codepoint) || codepoint == VARIATION_16; - } else if (FITZPATRICK.contains(previous)) { - add = codepoint == ZWJ; - } else if (ZWJ == previous) { - add = EMOJIS.contains(codepoint); - } else if (isMerger(codepoint)) { - add = true; - } else if (codepoint == VARIATION_16 && EMOJIS.contains(previous)) { - add = true; - } - } - if (add) { - codepoints.add(codepoint); - return true; - } else { - return false; - } - } - - static boolean isMerger(int codepoint) { - return codepoint == ZWJ || FITZPATRICK.contains(codepoint); - } - - public Symbol build() { - if (codepoints.size() > 0 && SYMBOLIZE.contains(codepoints.get(codepoints.size() - 1))) { - return new Other(codepoints); - } else if (codepoints.size() > 1 && KEYCAP_COMBINEABLE.contains(codepoints.get(0)) && codepoints.get(codepoints.size() - 1) != COMBINING_ENCLOSING_KEYCAP) { - return new Other(codepoints); - } - return codepoints.size() == 0 ? new Other(codepoints) : new Emoji(codepoints); - } - } - - public static class UnicodeBlocks implements UnicodeSet { - final UnicodeSet[] unicodeSets; - - UnicodeBlocks(final UnicodeSet... sets) { - this.unicodeSets = sets; - } - - @Override - public boolean contains(int codepoint) { - for (UnicodeSet unicodeSet : unicodeSets) { - if (unicodeSet.contains(codepoint)) { - return true; - } - } - return false; - } - } - - public interface UnicodeSet { - boolean contains(int codepoint); - } - - public static class UnicodeList implements UnicodeSet { - - private final List list; - - UnicodeList(final Integer... codes) { - this.list = Arrays.asList(codes); - } - - @Override - public boolean contains(int codepoint) { - return this.list.contains(codepoint); - } - } - - - public static class UnicodeRange implements UnicodeSet { - - private final int lower; - private final int upper; - - UnicodeRange(int lower, int upper) { - this.lower = lower; - this.upper = upper; - } - - public boolean contains(int codePoint) { - return codePoint >= lower && codePoint <= upper; - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/EncryptDecryptFile.java b/src/main/java/eu/siacs/conversations/utils/EncryptDecryptFile.java deleted file mode 100644 index af116a863..000000000 --- a/src/main/java/eu/siacs/conversations/utils/EncryptDecryptFile.java +++ /dev/null @@ -1,73 +0,0 @@ -package eu.siacs.conversations.utils; - -import android.util.Log; - -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.security.InvalidKeyException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; - -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.SecretKeySpec; - -import eu.siacs.conversations.Config; - -public class EncryptDecryptFile { - private static String cipher_mode = "AES/CBC/PKCS5Padding"; - - public static void encrypt(FileInputStream iFile, FileOutputStream oFile, String iKey) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException { - byte[] key = iKey.getBytes("UTF-8"); - Log.d(Config.LOGTAG, "Cipher key: " + Arrays.toString(key)); - MessageDigest sha = MessageDigest.getInstance("SHA-1"); - Log.d(Config.LOGTAG, "Cipher sha: " + sha.toString()); - key = sha.digest(key); - Log.d(Config.LOGTAG, "Cipher sha key: " + Arrays.toString(key)); - key = Arrays.copyOf(key, 16); // use only first 128 bit - Log.d(Config.LOGTAG, "Cipher sha key 16 bytes: " + Arrays.toString(key)); - SecretKeySpec sks = new SecretKeySpec(key, "AES"); - Cipher cipher = Cipher.getInstance(cipher_mode); - cipher.init(Cipher.ENCRYPT_MODE, sks); - Log.d(Config.LOGTAG, "Cipher IV: " + Arrays.toString(cipher.getIV())); - CipherOutputStream cos = new CipherOutputStream(oFile, cipher); - Log.d(Config.LOGTAG, "Encryption with: " + cos.toString()); - int b; - byte[] d = new byte[8]; - while ((b = iFile.read(d)) != -1) { - cos.write(d, 0, b); - } - cos.flush(); - cos.close(); - iFile.close(); - } - - public static void decrypt(FileInputStream iFile, FileOutputStream oFile, String iKey) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException { - byte[] key = iKey.getBytes("UTF-8"); - Log.d(Config.LOGTAG, "Cipher key: " + Arrays.toString(key)); - MessageDigest sha = MessageDigest.getInstance("SHA-1"); - Log.d(Config.LOGTAG, "Cipher sha: " + sha.toString()); - key = sha.digest(key); - Log.d(Config.LOGTAG, "Cipher sha key: " + Arrays.toString(key)); - key = Arrays.copyOf(key, 16); // use only first 128 bit - Log.d(Config.LOGTAG, "Cipher sha key 16 bytes: " + Arrays.toString(key)); - SecretKeySpec sks = new SecretKeySpec(key, "AES"); - Cipher cipher = Cipher.getInstance(cipher_mode); - cipher.init(Cipher.DECRYPT_MODE, sks); - Log.d(Config.LOGTAG, "Cipher IV: " + Arrays.toString(cipher.getIV())); - CipherInputStream cis = new CipherInputStream(iFile, cipher); - Log.d(Config.LOGTAG, "Decryption with: " + cis.toString()); - int b; - byte[] d = new byte[8]; - while ((b = cis.read(d)) != -1) { - oFile.write(d, 0, b); - } - oFile.flush(); - oFile.close(); - cis.close(); - } -} diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java deleted file mode 100644 index b8210d6b8..000000000 --- a/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -package eu.siacs.conversations.utils; - -import android.content.Context; - -import androidx.annotation.NonNull; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.io.Writer; -import java.lang.Thread.UncaughtExceptionHandler; - -import eu.siacs.conversations.services.NotificationService; - -public class ExceptionHandler implements UncaughtExceptionHandler { - - private final UncaughtExceptionHandler defaultHandler; - private final Context context; - - ExceptionHandler(final Context context) { - this.context = context; - this.defaultHandler = Thread.getDefaultUncaughtExceptionHandler(); - } - - @Override - public void uncaughtException(@NonNull Thread thread, final Throwable throwable) { - NotificationService.cancelIncomingCallNotification(context); - final Writer stringWriter = new StringWriter(); - final PrintWriter printWriter = new PrintWriter(stringWriter); - throwable.printStackTrace(printWriter); - final String stacktrace = stringWriter.toString(); - printWriter.close(); - ExceptionHelper.writeToStacktraceFile(context, stacktrace); - this.defaultHandler.uncaughtException(thread, throwable); - } - -} diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java deleted file mode 100644 index 987b98dca..000000000 --- a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java +++ /dev/null @@ -1,126 +0,0 @@ -package eu.siacs.conversations.utils; - -import android.content.Context; -import android.content.SharedPreferences; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.Signature; -import android.os.Build; -import android.preference.PreferenceManager; -import android.util.Log; - -import androidx.appcompat.app.AlertDialog; - -import java.io.BufferedReader; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -import eu.siacs.conversations.BuildConfig; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.XmppActivity; - -public class ExceptionHelper { - private static final String FILENAME = "stacktrace.txt"; - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); - - public static void init(Context context) { - if (Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler) { - return; - } - Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(context)); - } - - public static boolean checkForCrash(XmppActivity activity) { - try { - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; - if (service == null) { - return false; - } - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); - boolean crashreport = preferences.getBoolean("crashreport", activity.getResources().getBoolean(R.bool.send_crashreport)); - if (!crashreport || Config.BUG_REPORTS == null) { - return false; - } - final Account account = AccountUtils.getFirstEnabled(service); - if (account == null) { - return false; - } - FileInputStream file = activity.openFileInput(FILENAME); - InputStreamReader inputStreamReader = new InputStreamReader(file); - BufferedReader stacktrace = new BufferedReader(inputStreamReader); - final StringBuilder report = new StringBuilder(); - PackageManager pm = activity.getPackageManager(); - PackageInfo packageInfo; - String release = Build.VERSION.RELEASE; - int sdkVersion = Build.VERSION.SDK_INT; - String deviceName = getDeviceName(); - try { - packageInfo = pm.getPackageInfo(activity.getPackageName(), PackageManager.GET_SIGNATURES); - report.append("Device: ").append(deviceName).append('\n'); - report.append("Android SDK: ").append(sdkVersion).append(" (").append(release).append(")").append('\n'); - report.append("Version: ").append(packageInfo.versionName).append('\n'); - report.append(String.format(Locale.ROOT, "Version: %s(%d) ", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)).append('\n'); - report.append("Last Update: ").append(DATE_FORMAT.format(new Date(packageInfo.lastUpdateTime))).append('\n'); - Signature[] signatures = packageInfo.signatures; - if (signatures != null && signatures.length >= 1) { - report.append("SHA-1: ").append(CryptoHelper.getFingerprintCert(packageInfo.signatures[0].toByteArray())).append('\n'); - } - report.append('\n'); - } catch (Exception e) { - e.printStackTrace(); - return false; - } - String line; - while ((line = stacktrace.readLine()) != null) { - report.append(line); - report.append('\n'); - } - file.close(); - activity.deleteFile(FILENAME); - final AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(activity.getString(R.string.crash_report_title)); - builder.setMessage(activity.getText(R.string.crash_report_message)); - builder.setPositiveButton(activity.getText(R.string.send_now), (dialog, which) -> { - Log.d(Config.LOGTAG, "using account=" + account.getJid().asBareJid() + " to send in stack trace"); - final Conversation conversation = service.findOrCreateConversation(account, Config.BUG_REPORTS, false, true); - final Message message = new Message(conversation, report.toString(), Message.ENCRYPTION_NONE); - service.sendMessage(message); - }); - builder.setNegativeButton(activity.getText(R.string.send_never), (dialog, which) -> preferences.edit().putBoolean("never_send", true).apply()); - builder.create().show(); - return true; - } catch (final IOException ignored) { - return false; - } - } - - public static String getDeviceName() { - String manufacturer = Build.MANUFACTURER; - String model = Build.MODEL; - if (model.startsWith(manufacturer)) { - return model; - } else { - return manufacturer + " " + model; - } - } - - static void writeToStacktraceFile(Context context, String msg) { - try { - OutputStream os = context.openFileOutput(FILENAME, Context.MODE_PRIVATE); - os.write(msg.getBytes()); - os.flush(); - os.close(); - } catch (IOException ignored) { - } - } -} 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 e69de29bb..000000000 diff --git a/src/main/java/eu/siacs/conversations/utils/FileUtils.java b/src/main/java/eu/siacs/conversations/utils/FileUtils.java deleted file mode 100644 index e8acda32d..000000000 --- a/src/main/java/eu/siacs/conversations/utils/FileUtils.java +++ /dev/null @@ -1,189 +0,0 @@ -package eu.siacs.conversations.utils; - -import android.annotation.SuppressLint; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.Environment; -import android.provider.DocumentsContract; -import android.provider.MediaStore; -import android.util.Log; -import android.webkit.MimeTypeMap; - -import java.io.File; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.persistance.FileBackend; - -public class FileUtils { - - private static final Uri PUBLIC_DOWNLOADS = Uri.parse("content://downloads/public_downloads"); - - /** - * Get a file path from a Uri. This will get the the path for Storage Access - * Framework Documents, as well as the _data field for the MediaStore and - * other file-based ContentProviders. - * - * @param context The context. - * @param uri The Uri to query. - * @author paulburke - */ - @SuppressLint("NewApi") - public static String getPath(final Context context, final Uri uri) { - if (uri == null) { - return null; - } - - // DocumentProvider - if (DocumentsContract.isDocumentUri(context, uri) && !Compatibility.runsThirty()) { - Log.d(Config.LOGTAG, "FileUtils is KitKat"); - // ExternalStorageProvider - if (isExternalStorageDocument(uri)) { - Log.d(Config.LOGTAG, "FileUtils is external storage document"); - final String docId = DocumentsContract.getDocumentId(uri); - final String[] split = docId.split(":"); - final String type = split[0]; - - if ("primary".equalsIgnoreCase(type)) { - return Environment.getExternalStorageDirectory() + "/" + split[1]; - } - - // TODO handle non-primary volumes - } - // DownloadsProvider - else if (isDownloadsDocument(uri)) { - Log.d(Config.LOGTAG, "FileUtils is downloads document"); - final String id = DocumentsContract.getDocumentId(uri); - try { - final Uri contentUri = ContentUris.withAppendedId(PUBLIC_DOWNLOADS, Long.valueOf(id)); - return getDataColumn(context, contentUri, null, null); - } catch (NumberFormatException e) { - return null; - } - } - // MediaProvider - else if (isMediaDocument(uri)) { - Log.d(Config.LOGTAG, "FileUtils is media provider"); - final String docId = DocumentsContract.getDocumentId(uri); - final String[] split = docId.split(":"); - final String type = split[0]; - - Uri contentUri = null; - if ("image".equals(type)) { - contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; - } else if ("video".equals(type)) { - contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; - } else if ("audio".equals(type)) { - contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; - } - - final String selection = "_id=?"; - final String[] selectionArgs = new String[]{ - split[1] - }; - - return getDataColumn(context, contentUri, selection, selectionArgs); - } - } - // MediaStore (and general) - else if ("content".equalsIgnoreCase(uri.getScheme())) { - Log.d(Config.LOGTAG, "FileUtils is media store"); - List segments = uri.getPathSegments(); - String path; - if (FileBackend.getAuthority(context).equals(uri.getAuthority()) && segments.size() > 1 && segments.get(0).equals("external")) { - path = Environment.getExternalStorageDirectory().getAbsolutePath() + uri.getPath().substring(segments.get(0).length() + 1); - } else { - path = getDataColumn(context, uri, null, null); - } - if (path != null) { - File file = new File(path); - if (!file.canRead()) { - return null; - } - } - return path; - } - // File - else if ("file".equalsIgnoreCase(uri.getScheme())) { - Log.d(Config.LOGTAG, "FileUtils is file"); - return uri.getPath(); - } - - return null; - } - - /** - * Get the value of the data column for this Uri. This is useful for - * MediaStore Uris, and other file-based ContentProviders. - * - * @param context The context. - * @param uri The Uri to query. - * @param selection (Optional) Filter used in the query. - * @param selectionArgs (Optional) Selection arguments used in the query. - * @return The value of the _data column, which is typically a file path. - */ - public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { - Cursor cursor = null; - final String column = "_data"; - final String[] projection = { - column - }; - try { - cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); - if (cursor != null && cursor.moveToFirst()) { - final int column_index = cursor.getColumnIndexOrThrow(column); - return cursor.getString(column_index); - } - } catch (Exception e) { - return null; - } finally { - if (cursor != null) { - cursor.close(); - } - } - return null; - } - - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is ExternalStorageProvider. - */ - public static boolean isExternalStorageDocument(Uri uri) { - return "com.android.externalstorage.documents".equals(uri.getAuthority()); - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is DownloadsProvider. - */ - public static boolean isDownloadsDocument(Uri uri) { - return "com.android.providers.downloads.documents".equals(uri.getAuthority()); - } - - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is MediaProvider. - */ - public static boolean isMediaDocument(Uri uri) { - return "com.android.providers.media.documents".equals(uri.getAuthority()); - } - - public static String getExtension(Context context, Uri uri) { - String extension; - //Check uri format to avoid null - if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { - //If scheme is a content - final MimeTypeMap mime = MimeTypeMap.getSingleton(); - extension = mime.getExtensionFromMimeType(context.getContentResolver().getType(uri)); - } else { - //If scheme is a File - //This will replace white spaces with %20 and also other special characters. This will avoid returning null values on file name with spaces and special characters. - extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(new File(uri.getPath())).toString()); - } - return extension; - } -} diff --git a/src/main/java/eu/siacs/conversations/utils/FileWriterException.java b/src/main/java/eu/siacs/conversations/utils/FileWriterException.java deleted file mode 100644 index 024c5ba1f..000000000 --- a/src/main/java/eu/siacs/conversations/utils/FileWriterException.java +++ /dev/null @@ -1,14 +0,0 @@ -package eu.siacs.conversations.utils; - -import java.io.File; - -public class FileWriterException extends Exception { - - public FileWriterException(File file) { - super(String.format("Could not write to %s", file.getAbsolutePath())); - } - - FileWriterException() { - - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/FirstStartManager.java b/src/main/java/eu/siacs/conversations/utils/FirstStartManager.java deleted file mode 100644 index 39bfe80c2..000000000 --- a/src/main/java/eu/siacs/conversations/utils/FirstStartManager.java +++ /dev/null @@ -1,29 +0,0 @@ -package eu.siacs.conversations.utils; - -import android.content.Context; -import android.content.SharedPreferences; - -import static android.content.Context.MODE_PRIVATE; - -public class FirstStartManager { - Context context; - private SharedPreferences pref; - private SharedPreferences.Editor editor; - private static final String PREF_NAME = "de.monocles.chat"; - private static final String IS_FIRST_TIME_LAUNCH = "IsFirstTimeLaunch"; - - public FirstStartManager(Context context) { - this.context = context; - pref = this.context.getSharedPreferences(PREF_NAME, MODE_PRIVATE); - } - - public void setFirstTimeLaunch(boolean isFirstTime) { - editor = pref.edit(); - editor.putBoolean(IS_FIRST_TIME_LAUNCH, isFirstTime); - editor.commit(); - } - - public boolean isFirstTimeLaunch() { - return pref.getBoolean(IS_FIRST_TIME_LAUNCH, true); - } -} diff --git a/src/main/java/eu/siacs/conversations/utils/FtsUtils.java b/src/main/java/eu/siacs/conversations/utils/FtsUtils.java deleted file mode 100644 index ffdb54f95..000000000 --- a/src/main/java/eu/siacs/conversations/utils/FtsUtils.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (c) 2018, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.utils; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; - -public class FtsUtils { - - private static List KEYWORDS = Arrays.asList("OR", "AND"); - - public static List parse(String input) { - List term = new ArrayList<>(); - for (String part : input.replace('"', ' ').split("\\s+")) { - if (part.isEmpty()) { - continue; - } - final String cleaned = clean(part); - if (isKeyword(cleaned) || cleaned.contains("*")) { - term.add(part); - } else if (!cleaned.isEmpty()) { - term.add(cleaned); - } - } - return term; - } - - public static String toMatchString(List terms) { - StringBuilder builder = new StringBuilder(); - for (String term : terms) { - if (builder.length() != 0) { - builder.append(' '); - } - if (isKeyword(term)) { - builder.append(term.toUpperCase(Locale.ENGLISH)); - } else if (term.contains("*") || term.startsWith("-")) { - builder.append(term); - } else { - builder.append(term).append('*'); - } - } - return builder.toString(); - } - - static boolean isKeyword(String term) { - return KEYWORDS.contains(term.toUpperCase(Locale.ENGLISH)); - } - - private static int getStartIndex(String term) { - int length = term.length(); - int index = 0; - while (term.charAt(index) == '*') { - ++index; - if (index >= length) { - break; - } - } - return index; - } - - private static int getEndIndex(String term) { - int index = term.length() - 1; - while (term.charAt(index) == '*') { - --index; - if (index < 0) { - break; - } - } - return index; - } - - private static String clean(String input) { - int begin = getStartIndex(input); - int end = getEndIndex(input); - if (begin > end) { - return ""; - } else { - return input.substring(begin, end + 1); - } - } - - public static String toUserEnteredString(List term) { - final StringBuilder builder = new StringBuilder(); - for (String part : term) { - if (builder.length() != 0) { - builder.append(' '); - } - builder.append(part); - } - return builder.toString(); - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/GeoHelper.java b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java deleted file mode 100644 index 86b7ccb1e..000000000 --- a/src/main/java/eu/siacs/conversations/utils/GeoHelper.java +++ /dev/null @@ -1,173 +0,0 @@ -package eu.siacs.conversations.utils; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.preference.PreferenceManager; -import android.util.Log; -import android.webkit.URLUtil; - -import org.osmdroid.util.GeoPoint; - -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Conversational; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.ui.SettingsActivity; -import eu.siacs.conversations.ui.ShowLocationActivity; -import eu.siacs.conversations.ui.ShareLocationActivity; - -public class GeoHelper { - - public static Pattern GEO_URI = Pattern.compile("geo:(-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?)(?:,-?\\d+(?:\\.\\d+)?)?(?:;crs=[\\w-]+)?(?:;u=\\d+(?:\\.\\d+)?)?(?:;[\\w-]+=(?:[\\w-_.!~*'()]|%[\\da-f][\\da-f])+)*(\\?z=\\d+)?", Pattern.CASE_INSENSITIVE); - - public static String MapPreviewUri(Message message, Activity activity) { - Matcher matcher = GEO_URI.matcher(message.getBody()); - if (!matcher.matches()) { - return null; - } - double latitude; - double longitude; - try { - latitude = Double.parseDouble(matcher.group(1)); - if (latitude > 90.0 || latitude < -90.0) { - return null; - } - longitude = Double.parseDouble(matcher.group(2)); - if (longitude > 180.0 || longitude < -180.0) { - return null; - } - } catch (NumberFormatException nfe) { - return null; - } - return getMappreviewHost(activity) + "?center=" + latitude + "," + longitude + "&size=500x500&markers=" + latitude + "," + longitude + "&zoom=" + Config.Map.FINAL_ZOOM_LEVEL; - } - - private static String getMappreviewHost(Activity activity) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity); - String mapprevieHost = sharedPreferences.getString(SettingsActivity.MAPPREVIEW_HOST, activity.getResources().getString(R.string.mappreview_url)); - if (mapprevieHost.length() == 0) { - return activity.getResources().getString(R.string.mappreview_url); - } else if ((mapprevieHost.length() > 0) && isValid(mapprevieHost)) { - return mapprevieHost; - } else { - return activity.getResources().getString(R.string.mappreview_url); - } - } - - private static boolean isValid(String url) { - String urlstring = url; - if (!urlstring.toLowerCase(Locale.US).startsWith("http://") && !urlstring.toLowerCase(Locale.US).startsWith("https://")) { - urlstring = "https://" + url; - } - try { - return URLUtil.isValidUrl(urlstring) && Patterns.WEB_URL.matcher(urlstring).matches(); - } catch (Exception e) { - Log.d(Config.LOGTAG, "Could not use custom mappreview host and using blabber.im for mappreview " + e); - } - return false; - } - public static Intent getFetchIntent(Context context) { - return new Intent(context, ShareLocationActivity.class); - } - - private static GeoPoint parseGeoPoint(String body) throws IllegalArgumentException { - Matcher matcher = GEO_URI.matcher(body); - if (!matcher.matches()) { - throw new IllegalArgumentException("Invalid geo uri"); - } - double latitude; - double longitude; - try { - latitude = Double.parseDouble(matcher.group(1)); - if (latitude > 90.0 || latitude < -90.0) { - throw new IllegalArgumentException("Invalid geo uri"); - } - longitude = Double.parseDouble(matcher.group(2)); - if (longitude > 180.0 || longitude < -180.0) { - throw new IllegalArgumentException("Invalid geo uri"); - } - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid geo uri", e); - } - return new GeoPoint(latitude, longitude); - } - - public static ArrayList createGeoIntentsFromMessage(Context context, Message message) { - final ArrayList intents = new ArrayList<>(); - final GeoPoint geoPoint; - try { - geoPoint = parseGeoPoint(message.getBody()); - } catch (IllegalArgumentException e) { - return intents; - } - final Conversational conversation = message.getConversation(); - final String label = getLabel(context, message); - - final Intent locationPluginIntent = new Intent(context, ShowLocationActivity.class); - locationPluginIntent.putExtra("latitude", geoPoint.getLatitude()); - locationPluginIntent.putExtra("longitude", geoPoint.getLongitude()); - if (message.getStatus() != Message.STATUS_RECEIVED) { - locationPluginIntent.putExtra("jid", conversation.getAccount().getJid().toString()); - locationPluginIntent.putExtra("name", context.getString(R.string.me)); - } else { - final Contact contact = message.getContact(); - if (contact != null) { - locationPluginIntent.putExtra("name", contact.getDisplayName()); - locationPluginIntent.putExtra("jid", contact.getJid().toString()); - } else { - locationPluginIntent.putExtra("name", UIHelper.getDisplayedMucCounterpart(message.getCounterpart())); - } - } - intents.add(locationPluginIntent); - - Intent geoIntent = new Intent(Intent.ACTION_VIEW); - geoIntent.setData(Uri.parse("geo:" + String.valueOf(geoPoint.getLatitude()) + "," + String.valueOf(geoPoint.getLongitude()) + "?q=" + String.valueOf(geoPoint.getLatitude()) + "," + String.valueOf(geoPoint.getLongitude()) + label)); - intents.add(geoIntent); - return intents; - } - - public static void view(Context context, Message message) { - final GeoPoint geoPoint = parseGeoPoint(message.getBody()); - final String label = getLabel(context, message); - context.startActivity(geoIntent(geoPoint, label)); - } - - private static Intent geoIntent(GeoPoint geoPoint, String label) { - Intent geoIntent = new Intent(Intent.ACTION_VIEW); - geoIntent.setData(Uri.parse("geo:" + String.valueOf(geoPoint.getLatitude()) + "," + String.valueOf(geoPoint.getLongitude()) + "?q=" + String.valueOf(geoPoint.getLatitude()) + "," + String.valueOf(geoPoint.getLongitude()) + "(" + label + ")")); - return geoIntent; - } - - public static boolean openInOsmAnd(Context context, Message message) { - try { - final GeoPoint geoPoint = parseGeoPoint(message.getBody()); - final String label = getLabel(context, message); - return geoIntent(geoPoint, label).resolveActivity(context.getPackageManager()) != null; - } catch (IllegalArgumentException e) { - return false; - } - } - - private static String getLabel(Context context, Message message) { - if (message.getStatus() == Message.STATUS_RECEIVED) { - try { - return URLEncoder.encode(UIHelper.getMessageDisplayName(message), "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); - } - } else { - return context.getString(R.string.me); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/IP.java b/src/main/java/eu/siacs/conversations/utils/IP.java deleted file mode 100644 index 254cc12d5..000000000 --- a/src/main/java/eu/siacs/conversations/utils/IP.java +++ /dev/null @@ -1,30 +0,0 @@ -package eu.siacs.conversations.utils; - -import java.util.regex.Pattern; - -public class IP { - - private static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - private static final Pattern PATTERN_IPV6_6HEX4DEC = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z"); - private static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z"); - - public static boolean matches(String server) { - return server != null && ( - PATTERN_IPV4.matcher(server).matches() - || PATTERN_IPV6.matcher(server).matches() - || PATTERN_IPV6_6HEX4DEC.matcher(server).matches() - || PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches() - || PATTERN_IPV6_HEXCOMPRESSED.matcher(server).matches()); - } - - public static String wrapIPv6(final String host) { - if (matches(host)) { - return String.format("[%s]", host); - } else { - return host; - } - } - -} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/ImStyleParser.java b/src/main/java/eu/siacs/conversations/utils/ImStyleParser.java deleted file mode 100644 index a3da29a05..000000000 --- a/src/main/java/eu/siacs/conversations/utils/ImStyleParser.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (c) 2017, Daniel Gultsch All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation and/or - * other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package eu.siacs.conversations.utils; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public class ImStyleParser { - - private final static List KEYWORDS = Arrays.asList('*', '_', '~', '`'); - private final static List NO_SUB_PARSING_KEYWORDS = Arrays.asList('`'); - private final static List BLOCK_KEYWORDS = Arrays.asList('`'); - private final static boolean ALLOW_EMPTY = false; - private final static boolean PARSE_HIGHER_ORDER_END = true; - - public static List - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml deleted file mode 100644 index f8c647442..000000000 --- a/src/main/res/values/themes.xml +++ /dev/null @@ -1,1547 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/res/xml/automotive_app_desc.xml b/src/main/res/xml/automotive_app_desc.xml deleted file mode 100644 index ef8093892..000000000 --- a/src/main/res/xml/automotive_app_desc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/main/res/xml/backup_content.xml b/src/main/res/xml/backup_content.xml deleted file mode 100644 index 863e280e4..000000000 --- a/src/main/res/xml/backup_content.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/main/res/xml/file_paths.xml b/src/main/res/xml/file_paths.xml deleted file mode 100644 index 723d268b1..000000000 --- a/src/main/res/xml/file_paths.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/res/xml/network_security_configuration.xml b/src/main/res/xml/network_security_configuration.xml deleted file mode 100644 index 7448506ab..000000000 --- a/src/main/res/xml/network_security_configuration.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml deleted file mode 100644 index 11d3f06d5..000000000 --- a/src/main/res/xml/preferences.xml +++ /dev/null @@ -1,619 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/playstore/AndroidManifest.xml b/src/playstore/AndroidManifest.xml deleted file mode 100644 index c6adf5356..000000000 --- a/src/playstore/AndroidManifest.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/playstore/java/eu/siacs/conversations/services/EmojiInitializationService.java b/src/playstore/java/eu/siacs/conversations/services/EmojiInitializationService.java deleted file mode 100644 index 554c8c2fe..000000000 --- a/src/playstore/java/eu/siacs/conversations/services/EmojiInitializationService.java +++ /dev/null @@ -1,10 +0,0 @@ -package eu.siacs.conversations.services; - -import android.content.Context; - -public class EmojiInitializationService { - - public static void execute(final Context context) { - - } -} \ No newline at end of file diff --git a/src/playstore/java/eu/siacs/conversations/services/MaintenanceReceiver.java b/src/playstore/java/eu/siacs/conversations/services/MaintenanceReceiver.java deleted file mode 100644 index d6b21a6e9..000000000 --- a/src/playstore/java/eu/siacs/conversations/services/MaintenanceReceiver.java +++ /dev/null @@ -1,31 +0,0 @@ -package eu.siacs.conversations.services; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -import com.google.firebase.installations.FirebaseInstallations; - -import eu.siacs.conversations.BuildConfig; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.Compatibility; - -public class MaintenanceReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - Log.d(Config.LOGTAG, "received intent in maintenance receiver"); - final String string = BuildConfig.APPLICATION_ID + ".RENEW_INSTANCE_ID"; - if (string.equals(intent.getAction())) { - renewInstanceToken(context); - } - } - - private void renewInstanceToken(final Context context) { - FirebaseInstallations.getInstance().delete().addOnSuccessListener(unused -> { - final Intent intent = new Intent(context, XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_FCM_TOKEN_REFRESH); - Compatibility.startService(context, intent); - }); - } -} \ No newline at end of file diff --git a/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java b/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java deleted file mode 100644 index c4ae6fc24..000000000 --- a/src/playstore/java/eu/siacs/conversations/services/PushManagementService.java +++ /dev/null @@ -1,122 +0,0 @@ -package eu.siacs.conversations.services; - -import android.util.Log; - -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.GoogleApiAvailabilityLight; -import com.google.firebase.FirebaseApp; -import com.google.firebase.messaging.FirebaseMessaging; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.utils.PhoneHelper; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.XmppConnection; -import eu.siacs.conversations.xmpp.forms.Data; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; - -public class PushManagementService { - - protected final XmppConnectionService mXmppConnectionService; - - PushManagementService(XmppConnectionService service) { - this.mXmppConnectionService = service; - } - - private static Data findResponseData(IqPacket response) { - final Element command = response.findChild("command", Namespace.COMMANDS); - final Element x = command == null ? null : command.findChild("x", Namespace.DATA); - return x == null ? null : Data.parse(x); - } - - private Jid getAppServer() { - return Jid.of(mXmppConnectionService.getString(R.string.app_server)); - } - - void registerPushTokenOnServer(final Account account) { - FirebaseApp.initializeApp(mXmppConnectionService); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": has push support"); - retrieveFcmInstanceToken(token -> { - final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService); - final IqPacket packet = mXmppConnectionService.getIqGenerator().pushTokenToAppServer(getAppServer(), token, androidId); - mXmppConnectionService.sendIqPacket(account, packet, (a, response) -> { - final Data data = findResponseData(response); - if (response.getType() == IqPacket.TYPE.RESULT && data != null) { - try { - String node = data.getValue("node"); - String secret = data.getValue("secret"); - Jid jid = Jid.of(data.getValue("jid")); - if (node != null && secret != null) { - enablePushOnServer(a, jid, node, secret); - } - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } - } else { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": failed to enable push. invalid response from app server " + response); - } - }); - }); - } - - private void enablePushOnServer(final Account account, final Jid appServer, final String node, final String secret) { - final IqPacket enable = mXmppConnectionService.getIqGenerator().enablePush(appServer, node, secret); - mXmppConnectionService.sendIqPacket(account, enable, (a, p) -> { - if (p.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": successfully enabled push on server"); - } else if (p.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": enabling push on server failed"); - } - }); - } - - private void retrieveFcmInstanceToken(final OnGcmInstanceTokenRetrieved instanceTokenRetrieved) { - final FirebaseMessaging firebaseMessaging; - try { - firebaseMessaging = FirebaseMessaging.getInstance(); - } catch (IllegalStateException e) { - Log.d(Config.LOGTAG, "unable to get firebase instance token ", e); - return; - } - firebaseMessaging.getToken().addOnCompleteListener(task -> { - if (!task.isSuccessful()) { - Log.d(Config.LOGTAG, "unable to get Firebase instance token", task.getException()); - } - final String result; - try { - result = task.getResult(); - } catch (Exception e) { - Log.d(Config.LOGTAG, "unable to get Firebase instance token due to bug in library ", e); - return; - } - if (result != null) { - instanceTokenRetrieved.onGcmInstanceTokenRetrieved(result); - } - }); - - } - - - public boolean available(Account account) { - final XmppConnection connection = account.getXmppConnection(); - return connection != null - && connection.getFeatures().sm() - && connection.getFeatures().push() - && playServicesAvailable(); - } - - private boolean playServicesAvailable() { - return GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(mXmppConnectionService) == ConnectionResult.SUCCESS; - } - - public boolean isStub() { - return false; - } - - interface OnGcmInstanceTokenRetrieved { - void onGcmInstanceTokenRetrieved(String token); - } -} diff --git a/src/playstore/java/eu/siacs/conversations/services/PushMessageReceiver.java b/src/playstore/java/eu/siacs/conversations/services/PushMessageReceiver.java deleted file mode 100644 index 09ca4fb27..000000000 --- a/src/playstore/java/eu/siacs/conversations/services/PushMessageReceiver.java +++ /dev/null @@ -1,39 +0,0 @@ -package eu.siacs.conversations.services; - -import android.content.Intent; -import android.util.Log; - -import com.google.firebase.messaging.FirebaseMessagingService; -import com.google.firebase.messaging.RemoteMessage; - -import java.util.Map; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.Compatibility; - -public class PushMessageReceiver extends FirebaseMessagingService { - - @Override - public void onMessageReceived(RemoteMessage message) { - if (!EventReceiver.hasEnabledAccounts(this)) { - Log.d(Config.LOGTAG, "PushMessageReceiver ignored message because no accounts are enabled"); - return; - } - final Map data = message.getData(); - final Intent intent = new Intent(this, XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_FCM_MESSAGE_RECEIVED); - intent.putExtra("account", data.get("account")); - Compatibility.startService(this, intent); - } - - @Override - public void onNewToken(String token) { - if (!EventReceiver.hasEnabledAccounts(this)) { - Log.d(Config.LOGTAG, "PushMessageReceiver ignored new token because no accounts are enabled"); - return; - } - final Intent intent = new Intent(this, XmppConnectionService.class); - intent.setAction(XmppConnectionService.ACTION_FCM_TOKEN_REFRESH); - Compatibility.startService(this, intent); - } -} \ No newline at end of file diff --git a/src/playstore/java/eu/siacs/conversations/utils/InstallReferrerUtils.java b/src/playstore/java/eu/siacs/conversations/utils/InstallReferrerUtils.java deleted file mode 100644 index d7daca93b..000000000 --- a/src/playstore/java/eu/siacs/conversations/utils/InstallReferrerUtils.java +++ /dev/null @@ -1,69 +0,0 @@ -package eu.siacs.conversations.utils; - -import android.app.Activity; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.RemoteException; -import android.preference.PreferenceManager; -import android.util.Log; - -import com.android.installreferrer.api.InstallReferrerClient; -import com.android.installreferrer.api.InstallReferrerStateListener; -import com.android.installreferrer.api.ReferrerDetails; -import com.google.common.base.Strings; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.ui.WelcomeActivity; - -public class InstallReferrerUtils implements InstallReferrerStateListener { - - private static final String PROCESSED_INSTALL_REFERRER = "processed_install_referrer"; - - - private final WelcomeActivity welcomeActivity; - private final InstallReferrerClient installReferrerClient; - - - public InstallReferrerUtils(WelcomeActivity welcomeActivity) { - this.welcomeActivity = welcomeActivity; - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(welcomeActivity); - if (preferences.getBoolean(PROCESSED_INSTALL_REFERRER, false)) { - Log.d(Config.LOGTAG, "install referrer already processed"); - this.installReferrerClient = null; - return; - } - this.installReferrerClient = InstallReferrerClient.newBuilder(welcomeActivity).build(); - try { - this.installReferrerClient.startConnection(this); - } catch (SecurityException e) { - Log.e(Config.LOGTAG, "unable to start connection to InstallReferrerClient", e); - } - } - - public static void markInstallReferrerExecuted(final Activity context) { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - preferences.edit().putBoolean(PROCESSED_INSTALL_REFERRER, true).apply(); - } - - @Override - public void onInstallReferrerSetupFinished(int responseCode) { - if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) { - try { - final ReferrerDetails referrerDetails = installReferrerClient.getInstallReferrer(); - final String referrer = referrerDetails.getInstallReferrer(); - if (Strings.isNullOrEmpty(referrer)) { - return; - } - welcomeActivity.onInstallReferrerDiscovered(Uri.parse(referrer)); - } catch (final RemoteException | IllegalArgumentException e) { - Log.d(Config.LOGTAG, "unable to get install referrer", e); - } - } else { - Log.d(Config.LOGTAG, "unable to setup install referrer client. code=" + responseCode); - } - } - - @Override - public void onInstallReferrerServiceDisconnected() { - } -} \ No newline at end of file diff --git a/src/quicksy/java/eu/siacs/conversations/utils/ProvisioningUtils.java b/src/quicksy/java/eu/siacs/conversations/utils/ProvisioningUtils.java deleted file mode 100644 index 5b6cfae07..000000000 --- a/src/quicksy/java/eu/siacs/conversations/utils/ProvisioningUtils.java +++ /dev/null @@ -1,9 +0,0 @@ -package eu.siacs.conversations.utils; - -import eu.siacs.conversations.ui.UriHandlerActivity; - -public class ProvisioningUtils { - public static void provision(UriHandlerActivity uriHandlerActivity, String result) { - throw new IllegalStateException("Quicksy does not support provisioning"); - } -}