Merge branch 'master' of https://codeberg.org/iNPUTmice/Conversations

Stephen Paul Weber created

* 'master' of https://codeberg.org/iNPUTmice/Conversations: (57 commits)
  synchronize renegotiate to avoid race condition when that fails
  move PushMessageReceiver to receiver package
  Translated using Weblate (Japanese)
  Translated using Weblate (French)
  Translated using Weblate (Italian)
  Translated using Weblate (Dutch)
  Translated using Weblate (Italian)
  Translated using Weblate (Albanian)
  Translated using Weblate (Albanian)
  support mute via call integration
  deep link into FSI setting if not granted
  fix opening ringtone chooser when channel sound was set to null
  add ability to cancel in-progress one-off backup
  refactor ExportBackupService to worker
  guard unregister phone account by system feature check
  ignore quickLog startService exception
  set message input cursor color to text color
  use onSurface as link color
  exclude all realme devices up to Android 11
  Translated using Weblate (Turkish)
  ...

Change summary

build.gradle                                                                                 |   1 
fastlane/metadata/android/de-DE/changelogs/4210904.txt                                       |   2 
fastlane/metadata/android/de-DE/changelogs/4211004.txt                                       |   2 
fastlane/metadata/android/es-ES/changelogs/4210804.txt                                       |   2 
fastlane/metadata/android/es-ES/changelogs/4210904.txt                                       |   2 
fastlane/metadata/android/es-ES/changelogs/4211004.txt                                       |   2 
fastlane/metadata/android/gl-ES/changelogs/4210904.txt                                       |   2 
fastlane/metadata/android/gl-ES/changelogs/4211004.txt                                       |   2 
fastlane/metadata/android/it-IT/changelogs/4210904.txt                                       |   2 
fastlane/metadata/android/it-IT/changelogs/4211004.txt                                       |   2 
fastlane/metadata/android/pl-PL/changelogs/4210904.txt                                       |   2 
fastlane/metadata/android/pl-PL/changelogs/4211004.txt                                       |   2 
fastlane/metadata/android/sq/changelogs/4210804.txt                                          |   2 
fastlane/metadata/android/sq/changelogs/4210904.txt                                          |   2 
fastlane/metadata/android/sq/changelogs/4211004.txt                                          |   2 
fastlane/metadata/android/uk/changelogs/4210904.txt                                          |   2 
fastlane/metadata/android/uk/changelogs/4211004.txt                                          |   2 
fastlane/metadata/android/zh-CN/changelogs/4210904.txt                                       |   2 
fastlane/metadata/android/zh-CN/changelogs/4211004.txt                                       |   2 
fastlane/metadata/android/zh-TW/changelogs/42015.txt                                         |   1 
fastlane/metadata/android/zh-TW/changelogs/42018.txt                                         |   3 
fastlane/metadata/android/zh-TW/changelogs/42022.txt                                         |   2 
fastlane/metadata/android/zh-TW/changelogs/42023.txt                                         |   2 
fastlane/metadata/android/zh-TW/changelogs/42037.txt                                         |  11 
fastlane/metadata/android/zh-TW/changelogs/42038.txt                                         |   2 
fastlane/metadata/android/zh-TW/changelogs/42041.txt                                         |   5 
fastlane/metadata/android/zh-TW/changelogs/42042.txt                                         |   2 
fastlane/metadata/android/zh-TW/changelogs/42043.txt                                         |   1 
fastlane/metadata/android/zh-TW/changelogs/42044.txt                                         |   3 
fastlane/metadata/android/zh-TW/changelogs/42046.txt                                         |   1 
fastlane/metadata/android/zh-TW/changelogs/42059.txt                                         |   2 
fastlane/metadata/android/zh-TW/changelogs/42060.txt                                         |   1 
fastlane/metadata/android/zh-TW/changelogs/42061.txt                                         |   1 
fastlane/metadata/android/zh-TW/changelogs/42062.txt                                         |   1 
fastlane/metadata/android/zh-TW/changelogs/42065.txt                                         |   1 
fastlane/metadata/android/zh-TW/changelogs/42068.txt                                         |   2 
fastlane/metadata/android/zh-TW/changelogs/42072.txt                                         |   3 
fastlane/metadata/android/zh-TW/changelogs/4207704.txt                                       |   3 
fastlane/metadata/android/zh-TW/changelogs/4208104.txt                                       |   4 
fastlane/metadata/android/zh-TW/changelogs/4208804.txt                                       |   3 
fastlane/metadata/android/zh-TW/changelogs/4209004.txt                                       |   2 
fastlane/metadata/android/zh-TW/changelogs/4209204.txt                                       |   2 
fastlane/metadata/android/zh-TW/changelogs/4209404.txt                                       |   1 
fastlane/metadata/android/zh-TW/changelogs/4210104.txt                                       |   1 
fastlane/metadata/android/zh-TW/changelogs/4210404.txt                                       |   3 
fastlane/metadata/android/zh-TW/changelogs/4210504.txt                                       |   2 
fastlane/metadata/android/zh-TW/changelogs/4210704.txt                                       |   3 
fastlane/metadata/android/zh-TW/changelogs/4210804.txt                                       |   2 
fastlane/metadata/android/zh-TW/changelogs/4210904.txt                                       |   2 
fastlane/metadata/android/zh-TW/changelogs/4211004.txt                                       |   2 
src/cheogram/java/com/cheogram/android/ConnectionService.java                                |   6 
src/cheogram/java/eu/siacs/conversations/services/ImportBackupService.java                   |   3 
src/conversations/fastlane/metadata/android/tr-TR/full_description.txt                       |  39 
src/conversations/fastlane/metadata/android/tr-TR/short_description.txt                      |   1 
src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java              |   3 
src/conversations/res/values-fr/strings.xml                                                  |   2 
src/conversations/res/values-tr-rTR/strings.xml                                              |  12 
src/conversations/res/values-zh-rTW/strings.xml                                              |   2 
src/main/AndroidManifest.xml                                                                 |  14 
src/main/java/eu/siacs/conversations/receiver/SystemEventReceiver.java                       |   5 
src/main/java/eu/siacs/conversations/receiver/UnifiedPushDistributor.java                    |  41 
src/main/java/eu/siacs/conversations/receiver/WorkManagerEventReceiver.java                  |  32 
src/main/java/eu/siacs/conversations/services/CallIntegration.java                           |  45 
src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java               |   3 
src/main/java/eu/siacs/conversations/services/ExportBackupService.java                       | 469 
src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java                         |   1 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java                     |  17 
src/main/java/eu/siacs/conversations/ui/RecordingActivity.java                               |   9 
src/main/java/eu/siacs/conversations/ui/activity/result/PickRingtone.java                    |  10 
src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java                            |   4 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java                          |  22 
src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java        | 166 
src/main/java/eu/siacs/conversations/ui/fragment/settings/MainSettingsFragment.java          |  63 
src/main/java/eu/siacs/conversations/ui/fragment/settings/NotificationsSettingsFragment.java |  48 
src/main/java/eu/siacs/conversations/ui/fragment/settings/SecuritySettingsFragment.java      |  24 
src/main/java/eu/siacs/conversations/ui/fragment/settings/UpSettingsFragment.java            |   2 
src/main/java/eu/siacs/conversations/ui/fragment/settings/XmppPreferenceFragment.java        |  30 
src/main/java/eu/siacs/conversations/utils/Compatibility.java                                |  12 
src/main/java/eu/siacs/conversations/utils/MimeUtils.java                                    |   4 
src/main/java/eu/siacs/conversations/utils/UIHelper.java                                     |   5 
src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java                          | 607 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java                |  10 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java                    |  17 
src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java                          |  37 
src/main/res/drawable/cursor_on_tertiary_container.xml                                       |   6 
src/main/res/drawable/ic_calendar_month_24dp.xml                                             |  12 
src/main/res/drawable/ic_smartphone_24dp.xml                                                 |  12 
src/main/res/layout/fragment_conversation.xml                                                |   1 
src/main/res/values-de/strings.xml                                                           |  14 
src/main/res/values-es/strings.xml                                                           |  84 
src/main/res/values-it/strings.xml                                                           |  10 
src/main/res/values-ja/strings.xml                                                           | 119 
src/main/res/values-nl/strings.xml                                                           |   4 
src/main/res/values-pl/strings.xml                                                           |  12 
src/main/res/values-ro-rRO/strings.xml                                                       |   2 
src/main/res/values-sq-rAL/strings.xml                                                       |   6 
src/main/res/values-tr-rTR/strings.xml                                                       | 152 
src/main/res/values-uk/strings.xml                                                           |   2 
src/main/res/values-zh-rCN/strings.xml                                                       |   2 
src/main/res/values-zh-rTW/strings.xml                                                       | 102 
src/main/res/values/arrays.xml                                                               |   7 
src/main/res/values/strings.xml                                                              |   7 
src/main/res/xml/preferences_backup.xml                                                      |  20 
src/main/res/xml/preferences_main.xml                                                        |   7 
src/main/res/xml/preferences_notifications.xml                                               |   5 
src/playstore/AndroidManifest.xml                                                            |   2 
src/playstore/java/eu/siacs/conversations/receiver/PushMessageReceiver.java                  |  13 
src/quicksy/fastlane/metadata/android/tr-TR/short_description.txt                            |   1 
src/quicksy/res/values-pl/strings.xml                                                        |   2 
109 files changed, 1,678 insertions(+), 802 deletions(-)

Detailed changes

build.gradle 🔗

@@ -69,6 +69,7 @@ dependencies {
     implementation "androidx.preference:preference:1.2.1"
     implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
     implementation 'com.google.android.material:material:1.11.0'
+    implementation 'androidx.work:work-runtime:2.9.0'
 
     implementation "androidx.emoji2:emoji2:1.4.0"
     freeImplementation "androidx.emoji2:emoji2-bundled:1.4.0"

fastlane/metadata/android/uk/changelogs/4211004.txt 🔗

@@ -0,0 +1,2 @@
+* Виправлено інтеграцію викликів на деяких пристроях Android 14
+* Додано налаштування «Запрошення від незнайомців»

fastlane/metadata/android/zh-TW/changelogs/42037.txt 🔗

@@ -0,0 +1,11 @@
+版本 2.10.9
+* 進行音視頻通話時請求藍牙權限(如果您不使用藍牙耳機可以拒絕)
+* 修復呼叫 Movim 時的錯誤
+* 修復群組聊天的顯示錯誤頭像的問題
+* 始終要求選擇退出電池優化
+* 在“x 個已連接賬號”通知上設置僅本地標誌
+* 修復與 Google 地圖分享位置插件的交互
+* 移除有關服務器費用的腳註
+* 將文件存儲在適合 Android 11 的位置
+* 網絡切換後嘗試重新連接通話
+* 在來電屏幕中顯示來電者 JID 和帳戶 JID

fastlane/metadata/android/zh-TW/changelogs/42041.txt 🔗

@@ -0,0 +1,5 @@
+* 實現可擴展 SASL Profile、Bind 2.0 和 Fast,以加快重新連接速度
+* 實現通道綁定
+* 增加從音頻通話切換到視頻通話的功能
+* 增加刪除自己頭像的功能
+* 增加未接來電通知功能

src/cheogram/java/com/cheogram/android/ConnectionService.java 🔗

@@ -45,10 +45,9 @@ import io.michaelrocks.libphonenumber.android.NumberParseException;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.services.AvatarService;
 import eu.siacs.conversations.services.CallIntegration;
 import eu.siacs.conversations.services.CallIntegrationConnectionService;
-import eu.siacs.conversations.services.AvatarService;
-import eu.siacs.conversations.services.EventReceiver;
 import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.ui.RtpSessionActivity;
@@ -57,6 +56,7 @@ import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
 import eu.siacs.conversations.xmpp.jingle.Media;
 import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
+import static eu.siacs.conversations.receiver.SystemEventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE;
 
 @RequiresApi(Build.VERSION_CODES.M)
 public class ConnectionService extends android.telecom.ConnectionService {
@@ -79,7 +79,7 @@ public class ConnectionService extends android.telecom.ConnectionService {
 		// From XmppActivity.connectToBackend
 		Intent intent = new Intent(this, XmppConnectionService.class);
 		intent.setAction(XmppConnectionService.ACTION_STARTING_CALL);
-		intent.putExtra(EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE, true);
+		intent.putExtra(EXTRA_NEEDS_FOREGROUND_SERVICE, true);
 		try {
 			startService(intent);
 		} catch (IllegalStateException e) {

src/cheogram/java/eu/siacs/conversations/services/ImportBackupService.java 🔗

@@ -58,6 +58,7 @@ import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.ui.ManageAccountActivity;
 import eu.siacs.conversations.utils.BackupFileHeader;
 import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
+import eu.siacs.conversations.worker.ExportBackupWorker;
 import eu.siacs.conversations.xmpp.Jid;
 
 public class ImportBackupService extends Service {
@@ -239,7 +240,7 @@ public class ImportBackupService extends Service {
                 return false;
             }
 
-            final byte[] key = ExportBackupService.getKey(password, backupFileHeader.getSalt());
+            final byte[] key = ExportBackupWorker.getKey(password, backupFileHeader.getSalt());
 
             final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
             cipher.init(false, new AEADParameters(new KeyParameter(key), 128, backupFileHeader.getIv()));

src/conversations/fastlane/metadata/android/tr-TR/full_description.txt 🔗

@@ -0,0 +1,39 @@
+Kullanımı kolay, güvenilir, pil ömrü dostu. Resimler, gruplar ve uçtan uca şifreleme için yerleşik destek.
+
+Tasarım ilkeleri:
+
+* Gizlilik ve güvenlikten tasarruf etmeden olabildiğince iyi görünümlü ve kolay kullanımlı olmak
+* Halihazırda var olan, köklü protokollere dayanmak
+* Bir Google hesabına, özellikle Google Bulut Mesajlaşması (GCM)'e, gerek duymamak
+* Olabildiğince az izine gerek duymak
+
+Özellikler:
+
+* <a href="http://conversations.im/omemo/">OMEMO</a> veya <a href="http://openpgp.org/about/">OpenPGP</a> ile uçtan uca şifreleme
+* Fotoğraf gönderme ve alma
+* Şifrelenmiş görüntülü ve sesli aramalar (DTLS-SRTP)
+* Android tasarım standartlarına uyan öğrenmesi kolay arayüz
+* Kişileriniz için profil fotoğrafları / Avatarlar
+* Masaüstü uygulamasıyla senkronizasyon
+* Konferanslar (yer imi desteği ile)
+* Kişiler listesiyle entegrasyon
+* Birden fazla hesap / Birleşik gelen kutusu
+* Pil ömrüne çok düşük etki
+
+Conversations, kolayca ve ücretsiz olarak conversations.im sunucusunda hesap oluşturmanıza olanak tanır. Conversations başka herhangi bir XMPP sunucusuyla da çalışır. Çoğu XMPP sunucusu gönüllüler tarafından işletilir ve ücretsizdir.
+
+XMPP Özellikleri:
+
+Conversations var olan bütün XMPP sunucularıyla kullanılabilir. Ancak XMPP, eklentiler ile genişletilebilen bir protokoldür. Bu eklentiler XEP'ler olarak standardize edilmiştir. Conversations kullanıcı deneyimini iyileştirmek için bu eklentilerden birkaçını destekler. Kullandığınız XMPP sunucusu bu eklentileri desteklemiyor olabilir. Bu yüzden Conversations'tan en iyi şekilde faydalanmak için bu eklentileri destekleyen bir sunucuya geçmeli veya, daha da iyisi, siz ve arkadaşlarınız için kendi XMPP sunucunuzu kurmalısınız.
+
+Şimdilik bu XEP'ler:
+
+* XEP-0065: SOCKS5 Bytestreams (mod_proxy65). İki taraf da bir güvenlik duvarı (NAT) arkasında ise dosya aktarımı için kullanılacaktır.
+* XEP-0163: Avatarlar için Kişisel Olay Protokolü (Personal Eventing Protocol)
+* XEP-0191: Engelleme komutu - Spam atanları ve kişilerinizi listenizden kaldırmadan engellemenizi sağlar.
+* XEP-0198: Akış Kontrolü (Stream Management) - XMPP'yi ve altındaki TCP bağlantısını küçük çaplı bağlantı kopmalarına karşı korur.
+* XEP-0280: Mesaj Karbonları - Mesajlarınızı masaüstü uygulamasıyla senkronize ederek cihazlarınız arasında kesintisiz geçiş yapmanızı sağlar.
+* XEP-0237: Roster Versioning (Liste Sürüm Takibi) - Zayıf mobil ağlarda bant aralığından tasarruf etmek amacıyla.
+* XEP-0313: Mesaj Arşivi Yönetimi - Çevrimdışı olduğunuzda bile mesaj almaya devam edebilmeniz için mesajlarınızı sunucuyla senkronize eder.
+* XEP-0352: İstemci Durum Bildirimi - Conversations'un arkaplanda çalıştığını sunucuya bildir. Sunucunun önemsiz paketleri saklayarak veriden tasarruf etmesini sağlar.
+* XEP-0363: HTTP Dosya Yükleme - Konferanslarla ve çevrimdışı kişilerle dosya paylaşabilmenizi sağlar. Sunucunuzda ek bileşen gerektirir.

src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java 🔗

@@ -37,6 +37,7 @@ import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.ui.ManageAccountActivity;
 import eu.siacs.conversations.utils.BackupFileHeader;
 import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
+import eu.siacs.conversations.worker.ExportBackupWorker;
 import eu.siacs.conversations.xmpp.Jid;
 
 import org.bouncycastle.crypto.engines.AESEngine;
@@ -273,7 +274,7 @@ public class ImportBackupService extends Service {
                 return false;
             }
 
-            final byte[] key = ExportBackupService.getKey(password, backupFileHeader.getSalt());
+            final byte[] key = ExportBackupWorker.getKey(password, backupFileHeader.getSalt());
 
             final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
             cipher.init(

src/conversations/res/values-fr/strings.xml 🔗

@@ -12,6 +12,6 @@
     <string name="improperly_formatted_provisioning">Code de provisionnement mal formaté</string>
     <string name="tap_share_button_send_invite">Appuyez sur le bouton partager pour envoyer à votre contact une invitation pour %1$s.</string>
     <string name="if_contact_is_nearby_use_qr">Si vos contacts sont à proximité, ils peuvent aussi scanner le code ci-dessous pour accepter votre invitation.</string>
-    <string name="easy_invite_share_text">Rejoignez %1$set discutez avec moi : %2$s</string>
+    <string name="easy_invite_share_text">Rejoignez %1$s et discutez avec moi : %2$s</string>
     <string name="share_invite_with">Partager une invitation avec …</string>
 </resources>

src/conversations/res/values-tr-rTR/strings.xml 🔗

@@ -3,14 +3,16 @@
     <string name="pick_a_server">XMPP sağlayıcınızı seçin</string>
     <string name="use_conversations.im">conversations.im kullan</string>
     <string name="create_new_account">Yeni hesap oluştur</string>
-    <string name="do_you_have_an_account">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.</string>
-    <string name="server_select_text">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.</string>
-    <string name="magic_create_text_on_x">%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.</string>
+    <string name="do_you_have_an_account">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 yoksa şimdi yeni bir XMPP hesabı oluşturabilirsiniz.
+\nİpucu: Bazı e-posta sağlayıcıları da XMPP hesapları sağlayabilir.</string>
+    <string name="server_select_text">XMPP, sağlayıcıdan bağımsız bir anlık mesajlaşma ağıdır. Bu uygulamayı seçtiğiniz herhangi bir sağlayıcıyla kullanabilirsiniz.
+\nFakat, kullanım kolaylığı için, özellikle Conversations ile kullanılmak için tasarlanmış olan conversations.im sunucusunu da kullanabilirsiniz.</string>
+    <string name="magic_create_text_on_x">%1$s sağlayıcısına davet edildiniz. Sizi hesap oluşturulması konusunda yönlendireceğiz.
+\n%1$s bir sağlayıcı olarak seçildiğinde, başka sağlayıcılar kullanan kullanıcılarla, onlara tam XMPP adresinizi vererek iletişim kurabileceksiniz.</string>
     <string name="magic_create_text_fixed">%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.</string>
     <string name="your_server_invitation">Sunucu davetiyeniz</string>
     <string name="improperly_formatted_provisioning">Yanlış ayarlanmış düzenleme kodu</string>
-    <string name="tap_share_button_send_invite">Kişinize, %1$s grubuna davet etmek için Paylaş düğmesine basın.</string>
+    <string name="tap_share_button_send_invite">Kişiyi %1$s\'e davet etmek için paylaş butonuna dokunun.</string>
     <string name="if_contact_is_nearby_use_qr">Kişiniz yakınınızda ise, aşağıdaki kodu tarayak daveti kabul edebilirler.</string>
     <string name="easy_invite_share_text">%1$s grubuna katıl ve benimle sohbet et: %2$s</string>
     <string name="share_invite_with">Daveti şununla paylaş…</string>

src/conversations/res/values-zh-rTW/strings.xml 🔗

@@ -5,7 +5,7 @@
     <string name="create_new_account">建立新帳戶</string>
     <string name="do_you_have_an_account">您已經擁有一個 XMPP 帳戶了嗎?如果您之前使用過其他 XMPP 用戶端或 Conversations 的話,那麼您已經擁有 XMPP 帳戶了。若沒有,您現在就建立一個新的 XMPP 帳戶。
 \n提示:部分電子郵件提供者也會提供 XMPP 帳戶。</string>
-    <string name="server_select_text">XMPP 是提供者無關的即時訊息網路。任何您選擇的 XMPP 伺服器都可在此用戶端上使用。
+    <string name="server_select_text">XMPP 是獨立於服務提供者的即時訊息網路。任何您選擇的 XMPP 伺服器都可在此用戶端上使用。
 \n不過,我們令它在 Coversations.im 中建立帳戶變得更方便;conversations.im 是特別適合 Conversations 的提供者。</string>
     <string name="magic_create_text_on_x">你已受邀參加 %1$s 。我們將指引您完成建立帳戶的過程。
 \n選擇 %1$s 作為提供者後,您可以將您完整的 XMPP 位址交給使用其他提供者的使用者,以便能與他們進行交流。</string>

src/main/AndroidManifest.xml 🔗

@@ -116,9 +116,9 @@
         </service>
 
         <service
-            android:name=".services.ExportBackupService"
-            android:exported="false"
-            android:foregroundServiceType="dataSync" />
+            android:name="androidx.work.impl.foreground.SystemForegroundService"
+            android:foregroundServiceType="dataSync"
+            tools:node="merge" />
 
         <service
             android:name=".services.ImportBackupService"
@@ -143,7 +143,11 @@
         </service>
 
         <receiver
-            android:name=".services.EventReceiver"
+            android:name=".receiver.WorkManagerEventReceiver"
+            android:exported="false" />
+
+        <receiver
+            android:name=".receiver.SystemEventReceiver"
             android:exported="false">
             <intent-filter>
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
@@ -157,7 +161,7 @@
         </receiver>
 
         <receiver
-            android:name=".services.UnifiedPushDistributor"
+            android:name=".receiver.UnifiedPushDistributor"
             android:enabled="false"
             android:exported="true">
             <intent-filter>

src/main/java/eu/siacs/conversations/services/EventReceiver.java → src/main/java/eu/siacs/conversations/receiver/SystemEventReceiver.java 🔗

@@ -1,4 +1,4 @@
-package eu.siacs.conversations.services;
+package eu.siacs.conversations.receiver;
 
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -10,9 +10,10 @@ import android.util.Log;
 import com.google.common.base.Strings;
 
 import eu.siacs.conversations.Config;
+import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.Compatibility;
 
-public class EventReceiver extends BroadcastReceiver {
+public class SystemEventReceiver extends BroadcastReceiver {
 
     public static final String SETTING_ENABLED_ACCOUNTS = "enabled_accounts";
     public static final String EXTRA_NEEDS_FOREGROUND_SERVICE = "needs_foreground_service";

src/main/java/eu/siacs/conversations/services/UnifiedPushDistributor.java → src/main/java/eu/siacs/conversations/receiver/UnifiedPushDistributor.java 🔗

@@ -1,4 +1,4 @@
-package eu.siacs.conversations.services;
+package eu.siacs.conversations.receiver;
 
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
@@ -25,6 +25,7 @@ import java.util.List;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.persistance.UnifiedPushDatabase;
+import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.Compatibility;
 
 public class UnifiedPushDistributor extends BroadcastReceiver {
@@ -35,9 +36,9 @@ 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";
 
-
     // connector actions (these are actions used for distributor->connector broadcasts)
-    public static final String ACTION_UNREGISTERED = "org.unifiedpush.android.connector.UNREGISTERED";
+    public static final String ACTION_UNREGISTERED =
+            "org.unifiedpush.android.connector.UNREGISTERED";
     public static final String ACTION_BYTE_MESSAGE =
             "org.unifiedpush.android.distributor.feature.BYTES_MESSAGE";
     public static final String ACTION_REGISTRATION_FAILED =
@@ -69,7 +70,7 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
         final Parcelable appVerification = intent.getParcelableExtra("app");
         if (appVerification instanceof PendingIntent pendingIntent) {
             application = pendingIntent.getIntentSender().getCreatorPackage();
-            Log.d(Config.LOGTAG,"received application name via pending intent "+ application);
+            Log.d(Config.LOGTAG, "received application name via pending intent " + application);
         } else {
             application = intent.getStringExtra("application");
         }
@@ -79,10 +80,10 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
         switch (Strings.nullToEmpty(action)) {
             case ACTION_REGISTER -> register(context, application, instance, features, messenger);
             case ACTION_UNREGISTER -> unregister(context, instance);
-            case Intent.ACTION_PACKAGE_FULLY_REMOVED ->
-                    unregisterApplication(context, intent.getData());
-            default ->
-                    Log.d(Config.LOGTAG, "UnifiedPushDistributor received unknown action " + action);
+            case Intent.ACTION_PACKAGE_FULLY_REMOVED -> unregisterApplication(
+                    context, intent.getData());
+            default -> Log.d(
+                    Config.LOGTAG, "UnifiedPushDistributor received unknown action " + action);
         }
     }
 
@@ -111,7 +112,11 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
                 Log.d(
                         Config.LOGTAG,
                         "successfully created UnifiedPush entry. waking up XmppConnectionService");
-                quickLog(context, String.format("successfully registered %s (token = %s) for UnifiedPushed", application, instance));
+                quickLog(
+                        context,
+                        String.format(
+                                "successfully registered %s (token = %s) for UnifiedPushed",
+                                application, instance));
                 final Intent serviceIntent = new Intent(context, XmppConnectionService.class);
                 serviceIntent.setAction(XmppConnectionService.ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS);
                 serviceIntent.putExtra("instance", instance);
@@ -140,7 +145,7 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
             }
         } else {
             if (messenger instanceof Messenger m) {
-                sendRegistrationFailed(m,"Your application is not registered to receive messages");
+                sendRegistrationFailed(m, "Your application is not registered to receive messages");
             }
             Log.d(
                     Config.LOGTAG,
@@ -157,7 +162,7 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
         try {
             messenger.send(message);
         } catch (final RemoteException e) {
-            Log.d(Config.LOGTAG,"unable to tell messenger of failed registration",e);
+            Log.d(Config.LOGTAG, "unable to tell messenger of failed registration", e);
         }
     }
 
@@ -177,7 +182,11 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
         }
         final UnifiedPushDatabase unifiedPushDatabase = UnifiedPushDatabase.getInstance(context);
         if (unifiedPushDatabase.deleteInstance(instance)) {
-            quickLog(context, String.format("successfully unregistered token %s from UnifiedPushed (application requested unregister)", instance));
+            quickLog(
+                    context,
+                    String.format(
+                            "successfully unregistered token %s from UnifiedPushed (application requested unregister)",
+                            instance));
             Log.d(Config.LOGTAG, "successfully removed " + instance + " from UnifiedPush");
             // TODO send UNREGISTERED broadcast back to app?!
         }
@@ -192,7 +201,11 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
             Log.d(Config.LOGTAG, "app " + application + " has been removed from the system");
             final UnifiedPushDatabase database = UnifiedPushDatabase.getInstance(context);
             if (database.deleteApplication(application)) {
-                quickLog(context, String.format("successfully removed %s from UnifiedPushed (ACTION_PACKAGE_FULLY_REMOVED)", application));
+                quickLog(
+                        context,
+                        String.format(
+                                "successfully removed %s from UnifiedPushed (ACTION_PACKAGE_FULLY_REMOVED)",
+                                application));
                 Log.d(Config.LOGTAG, "successfully removed " + application + " from UnifiedPush");
             }
         }
@@ -210,6 +223,6 @@ public class UnifiedPushDistributor extends BroadcastReceiver {
         final Intent intent = new Intent(context, XmppConnectionService.class);
         intent.setAction(XmppConnectionService.ACTION_QUICK_LOG);
         intent.putExtra("message", message);
-        context.startService(intent);
+        Compatibility.startService(context, intent);
     }
 }

src/main/java/eu/siacs/conversations/receiver/WorkManagerEventReceiver.java 🔗

@@ -0,0 +1,32 @@
+package eu.siacs.conversations.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import androidx.work.WorkManager;
+
+import com.google.common.base.Strings;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.ui.fragment.settings.BackupSettingsFragment;
+
+public class WorkManagerEventReceiver extends BroadcastReceiver {
+
+    public static final String ACTION_STOP_BACKUP = "eu.siacs.conversations.receiver.STOP_BACKUP";
+
+    @Override
+    public void onReceive(final Context context, final Intent intent) {
+        final var action = Strings.nullToEmpty(intent == null ? null : intent.getAction());
+        if (action.equals(ACTION_STOP_BACKUP)) {
+            stopBackup(context);
+        }
+    }
+
+    private void stopBackup(final Context context) {
+        Log.d(Config.LOGTAG, "trying to stop one-off backup worker");
+        final var workManager = WorkManager.getInstance(context);
+        workManager.cancelUniqueWork(BackupSettingsFragment.CREATE_ONE_OFF_BACKUP);
+    }
+}

src/main/java/eu/siacs/conversations/services/CallIntegration.java 🔗

@@ -38,15 +38,8 @@ public class CallIntegration extends Connection {
 
     private static final List<String> BROKEN_DEVICE_MODELS =
             Arrays.asList(
-                    "OnePlus6", // OnePlus 6 (Android 8.1-11) Device is buggy and always starts the
-                                // operating system call screen even though we want to be self
-                                // managed
-                    "RMX1921", // Realme XT (Android 9-10) shows "Call not sent" dialog
-                    "RMX1971", // Realme 5 Pro (Android 9-11), show "Call not sent" dialog
-                    "RMX1973", // Realme 5 Pro (see above),
-                    "RMX2071", // Realme X50 Pro 5G (Call not sent)
-                    "RMX2075L1", // Realme X50 Pro 5G
-                    "RMX2076" // Realme X50 Pro 5G
+                    "OnePlus6" // OnePlus 6 (Android 8.1-11) Device is buggy and always starts the
+                    // OS call screen even though we want to be self managed
                     );
 
     public static final int DEFAULT_TONE_VOLUME = 60;
@@ -63,6 +56,7 @@ public class CallIntegration extends Connection {
     private final AtomicBoolean isDestroyed = new AtomicBoolean(false);
 
     private List<CallEndpoint> availableEndpoints = Collections.emptyList();
+    private boolean isMicrophoneEnabled = true;
 
     private Callback callback = null;
 
@@ -81,6 +75,7 @@ public class CallIntegration extends Connection {
             this.appRTCAudioManager.setAudioManagerEvents(this::onAudioDeviceChanged);
         }
         setRingbackRequested(true);
+        setConnectionCapabilities(CAPABILITY_MUTE | CAPABILITY_RESPOND_VIA_TEXT);
     }
 
     public void setCallback(final Callback callback) {
@@ -151,10 +146,26 @@ public class CallIntegration extends Connection {
             Log.d(Config.LOGTAG, "ignoring onCallAudioStateChange() on Upside Down Cake");
             return;
         }
+        setMicrophoneEnabled(!state.isMuted());
         Log.d(Config.LOGTAG, "onCallAudioStateChange(" + state + ")");
         this.onAudioDeviceChanged(getAudioDeviceOreo(state), getAudioDevicesOreo(state));
     }
 
+    @Override
+    public void onMuteStateChanged(final boolean isMuted) {
+        Log.d(Config.LOGTAG, "onMuteStateChanged(" + isMuted + ")");
+        setMicrophoneEnabled(!isMuted);
+    }
+
+    private void setMicrophoneEnabled(final boolean enabled) {
+        this.isMicrophoneEnabled = enabled;
+        this.callback.onCallIntegrationMicrophoneEnabled(enabled);
+    }
+
+    public boolean isMicrophoneEnabled() {
+        return this.isMicrophoneEnabled;
+    }
+
     public Set<AudioDevice> getAudioDevices() {
         if (notSelfManaged(context)) {
             return getAudioDevicesFallback();
@@ -496,7 +507,7 @@ public class CallIntegration extends Connection {
     public static boolean selfManaged(final Context context) {
         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
                 && hasSystemFeature(context)
-                && !BROKEN_DEVICE_MODELS.contains(Build.DEVICE);
+                && isDeviceModelSupported();
     }
 
     public static boolean hasSystemFeature(final Context context) {
@@ -509,6 +520,18 @@ public class CallIntegration extends Connection {
         }
     }
 
+    private static boolean isDeviceModelSupported() {
+        if (BROKEN_DEVICE_MODELS.contains(Build.DEVICE)) {
+            return false;
+        }
+        // all Realme devices at least up to and including Android 11 are broken
+        if ("realme".equalsIgnoreCase(Build.MANUFACTURER)
+                && Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
+            return false;
+        }
+        return true;
+    }
+
     public static boolean notSelfManaged(final Context context) {
         return !selfManaged(context);
     }
@@ -579,6 +602,8 @@ public class CallIntegration extends Connection {
 
         void onCallIntegrationSilence();
 
+        void onCallIntegrationMicrophoneEnabled(boolean enabled);
+
         boolean applyDtmfTone(final String dtmf);
     }
 }

src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java 🔗

@@ -22,6 +22,7 @@ import java.util.List;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.receiver.SystemEventReceiver;
 import eu.siacs.conversations.ui.ConversationsActivity;
 import eu.siacs.conversations.utils.Compatibility;
 
@@ -45,7 +46,7 @@ public class ContactChooserTargetService extends ChooserTargetService implements
     @Override
     public List<ChooserTarget> onGetChooserTargets(
             final ComponentName targetActivityName, final IntentFilter matchedFilter) {
-        if (!EventReceiver.hasEnabledAccounts(this)) {
+        if (!SystemEventReceiver.hasEnabledAccounts(this)) {
             return Collections.emptyList();
         }
         final Intent intent = new Intent(this, XmppConnectionService.class);

src/main/java/eu/siacs/conversations/services/ExportBackupService.java 🔗

@@ -1,469 +0,0 @@
-package eu.siacs.conversations.services;
-
-import static eu.siacs.conversations.utils.Compatibility.s;
-
-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.DatabaseUtils;
-import android.database.sqlite.SQLiteDatabase;
-import android.net.Uri;
-import android.os.IBinder;
-import android.util.Log;
-
-import androidx.core.app.NotificationCompat;
-
-import com.google.common.base.CharMatcher;
-import com.google.common.base.Strings;
-
-import java.io.DataOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-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.Locale;
-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;
-
-public class ExportBackupService extends Service {
-
-    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
-
-    public static final String KEYTYPE = "AES";
-    public static final String CIPHERMODE = "AES/GCM/NoPadding";
-    public static final String PROVIDER = "BC";
-
-    public static final String MIME_TYPE = "application/vnd.conversations.backup";
-
-    private static final int NOTIFICATION_ID = 19;
-    private static final int PAGE_SIZE = 20;
-    private static final AtomicBoolean RUNNING = new AtomicBoolean(false);
-    private DatabaseBackend mDatabaseBackend;
-    private List<Account> mAccounts;
-    private NotificationManager notificationManager;
-
-    private static List<Intent> 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);
-    }
-
-    @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        if (RUNNING.compareAndSet(false, true)) {
-            new Thread(() -> {
-                boolean success;
-                List<File> files;
-                try {
-                    files = export(intent.getBooleanExtra("cheogram_db", true));
-                    success = true;
-                } catch (final Exception e) {
-                    Log.d(Config.LOGTAG, "unable to create backup", e);
-                    success = false;
-                    files = Collections.emptyList();
-                }
-                stopForeground(true);
-                RUNNING.set(false);
-                if (success) {
-                    notifySuccess(files);
-                }
-                stopSelf();
-            }).start();
-            return START_STICKY;
-        } else {
-            Log.d(Config.LOGTAG, "ExportBackupService. ignoring start command because already running");
-        }
-        return START_NOT_STICKY;
-    }
-
-    private void messageExport(SQLiteDatabase db, String uuid, PrintWriter writer, Progress progress) {
-        Cursor cursor = db.rawQuery("select messages.* from messages join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?", new String[]{uuid});
-        int size = cursor != null ? cursor.getCount() : 0;
-        Log.d(Config.LOGTAG, "exporting " + size + " messages for account " + uuid);
-        int i = 0;
-        int p = 0;
-        while (cursor != null && cursor.moveToNext()) {
-            writer.write(cursorToString(Message.TABLENAME, cursor, PAGE_SIZE, false));
-            if (i + PAGE_SIZE > size) {
-                i = size;
-            } else {
-                i += PAGE_SIZE;
-            }
-            final int percentage = i * 100 / size;
-            if (p < percentage) {
-                p = percentage;
-                notificationManager.notify(NOTIFICATION_ID, progress.build(p));
-            }
-        }
-        if (cursor != null) {
-            cursor.close();
-        }
-    }
-
-    private void messageExportCheogram(SQLiteDatabase db, String uuid, PrintWriter writer, Progress progress) {
-        Cursor cursor = db.rawQuery("select cmessages.* from messages join cheogram.messages cmessages using (uuid) 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 + " cheogram messages for account " + uuid);
-        int i = 0;
-        int p = 0;
-        while (cursor != null && cursor.moveToNext()) {
-            writer.write(cursorToString("cheogram." + 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(NOTIFICATION_ID, progress.build(p));
-            }
-        }
-        if (cursor != null) {
-            cursor.close();
-        }
-
-        cursor = db.rawQuery("select webxdc_updates.* from " + Conversation.TABLENAME + " join cheogram.webxdc_updates webxdc_updates on " + Conversation.TABLENAME + ".uuid=webxdc_updates." + Message.CONVERSATION + " where conversations.accountUuid=?", new String[]{uuid});
-        size = cursor != null ? cursor.getCount() : 0;
-        Log.d(Config.LOGTAG, "exporting " + size + " WebXDC updates for account " + uuid);
-        while (cursor != null && cursor.moveToNext()) {
-            writer.write(cursorToString("cheogram.webxdc_updates", 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(NOTIFICATION_ID, progress.build(p));
-            }
-        }
-        if (cursor != null) {
-            cursor.close();
-        }
-    }
-
-    private List<File> export(boolean withCheogramDb) throws Exception {
-        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getBaseContext(), "backup");
-        mBuilder.setContentTitle(getString(R.string.notification_create_backup_title))
-                .setSmallIcon(R.drawable.ic_archive_24dp)
-                .setProgress(1, 0, false);
-        startForeground(NOTIFICATION_ID, mBuilder.build());
-        int count = 0;
-        final int max = this.mAccounts.size();
-        final SecureRandom secureRandom = new SecureRandom();
-        final List<File> files = new ArrayList<>();
-        Log.d(Config.LOGTAG, "starting backup for " + max + " accounts");
-        for (final Account account : this.mAccounts) {
-            final String password = account.getPassword();
-            if (Strings.nullToEmpty(password).trim().isEmpty()) {
-                Log.d(Config.LOGTAG, String.format("skipping backup for %s because password is empty. unable to encrypt", account.getJid().asBareJid()));
-                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 String filename =
-                    String.format(
-                            "%s.%s.ceb",
-                            account.getJid().asBareJid().toEscapedString(),
-                            DATE_FORMAT.format(new Date()));
-            final File file =
-                    new File(
-                            FileBackend.getBackupDirectory(this), filename);
-            files.add(file);
-            final File directory = file.getParentFile();
-            if (directory != null && directory.mkdirs()) {
-                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.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
-            final byte[] key = getKey(password, salt);
-            SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
-            IvParameterSpec ivSpec = new IvParameterSpec(IV);
-            cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
-            CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher);
-
-            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);
-            if (withCheogramDb) messageExportCheogram(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());
-            count++;
-        }
-        return files;
-    }
-
-    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<File> files) {
-        final String path = FileBackend.getBackupDirectory(this).getAbsolutePath();
-
-        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<Uri> 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, FileBackend.getBackupDirectory(this).getAbsolutePath())))
-                .setAutoCancel(true)
-                .setContentIntent(openFolderIntent)
-                .setSmallIcon(R.drawable.ic_archive_24dp);
-
-        if (shareFilesIntent != null) {
-            mBuilder.addAction(
-                    R.drawable.ic_share_24dp,
-                    getString(R.string.share_backup_files),
-                    shareFilesIntent);
-        }
-
-        try { Thread.sleep(500); } catch (final Exception e) { }
-        notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
-    }
-
-    @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();
-        }
-    }
-}

src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java 🔗

@@ -28,6 +28,7 @@ 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.receiver.UnifiedPushDistributor;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -149,6 +149,7 @@ import eu.siacs.conversations.parser.PresenceParser;
 import eu.siacs.conversations.persistance.DatabaseBackend;
 import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.persistance.UnifiedPushDatabase;
+import eu.siacs.conversations.receiver.SystemEventReceiver;
 import eu.siacs.conversations.ui.ChooseAccountForProfilePictureActivity;
 import eu.siacs.conversations.ui.ConversationsActivity;
 import eu.siacs.conversations.ui.RtpSessionActivity;
@@ -855,7 +856,7 @@ public class XmppConnectionService extends Service {
     @Override
     public int onStartCommand(final Intent intent, int flags, int startId) {
         final String action = Strings.nullToEmpty(intent == null ? null : intent.getAction());
-        final boolean needsForegroundService = intent != null && intent.getBooleanExtra(EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE, false);
+        final boolean needsForegroundService = intent != null && intent.getBooleanExtra(SystemEventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE, false);
         if (needsForegroundService) {
             Log.d(Config.LOGTAG, "toggle forced foreground service after receiving event (action=" + action + ")");
             toggleForegroundService(true, action.equals(ACTION_STARTING_CALL));
@@ -1497,7 +1498,7 @@ public class XmppConnectionService extends Service {
         }
         final SharedPreferences.Editor editor = getPreferences().edit();
         final boolean hasEnabledAccounts = hasEnabledAccounts();
-        editor.putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply();
+        editor.putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply();
         editor.apply();
         toggleSetProfilePictureActivity(hasEnabledAccounts);
         reconfigurePushDistributor();
@@ -1795,7 +1796,7 @@ public class XmppConnectionService extends Service {
             return;
         }
         final long triggerAtMillis = SystemClock.elapsedRealtime() + (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL * 1000);
-        final Intent intent = new Intent(this, EventReceiver.class);
+        final Intent intent = new Intent(this, SystemEventReceiver.class);
         intent.setAction(ACTION_POST_CONNECTIVITY_CHANGE);
         try {
             final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1, intent, s()
@@ -1817,7 +1818,7 @@ public class XmppConnectionService extends Service {
         if (alarmManager == null) {
             return;
         }
-        final Intent intent = new Intent(this, EventReceiver.class);
+        final Intent intent = new Intent(this, SystemEventReceiver.class);
         intent.setAction(ACTION_PING);
         try {
             final PendingIntent pendingIntent =
@@ -1836,7 +1837,7 @@ public class XmppConnectionService extends Service {
         if (alarmManager == null) {
             return;
         }
-        final Intent intent = new Intent(this, EventReceiver.class);
+        final Intent intent = new Intent(this, SystemEventReceiver.class);
         intent.setAction(ACTION_IDLE_PING);
         try {
             final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, s()
@@ -3008,7 +3009,7 @@ public class XmppConnectionService extends Service {
 
     private void syncEnabledAccountSetting() {
         final boolean hasEnabledAccounts = hasEnabledAccounts();
-        getPreferences().edit().putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply();
+        getPreferences().edit().putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply();
         toggleSetProfilePictureActivity(hasEnabledAccounts);
     }
 
@@ -3198,7 +3199,9 @@ public class XmppConnectionService extends Service {
             };
             mDatabaseWriterExecutor.execute(runnable);
             this.accounts.remove(account);
-            CallIntegrationConnectionService.unregisterPhoneAccount(this, account);
+            if (CallIntegration.hasSystemFeature(this)) {
+                CallIntegrationConnectionService.unregisterPhoneAccount(this, account);
+            }
             this.mRosterSyncTaskManager.clear(account);
             updateAccountUi();
             mNotificationService.updateErrorNotification();

src/main/java/eu/siacs/conversations/ui/RecordingActivity.java 🔗

@@ -113,6 +113,9 @@ public class RecordingActivity extends BaseActivity implements View.OnClickListe
         mRecorder = new MediaRecorder();
         mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
         final String userChosenCodec = getPreferences().getString("voice_message_codec", "");
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            mRecorder.setPrivacySensitive(true);
+        }
         final int outputFormat;
         if (("opus".equals(userChosenCodec) || ("".equals(userChosenCodec) && Config.USE_OPUS_VOICE_MESSAGES)) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
             outputFormat = MediaRecorder.OutputFormat.OGG;
@@ -122,14 +125,14 @@ public class RecordingActivity extends BaseActivity implements View.OnClickListe
         } else if ("mpeg4".equals(userChosenCodec) || !Config.USE_OPUS_VOICE_MESSAGES) {
             outputFormat = MediaRecorder.OutputFormat.MPEG_4;
             mRecorder.setOutputFormat(outputFormat);
-            if (AAC_SENSITIVE_DEVICES.contains(Build.MODEL)) {
-                // Changing these three settings for AAC sensitive devices might lead to sporadically truncated (cut-off) voice messages.
+            if (AAC_SENSITIVE_DEVICES.contains(Build.MODEL) && Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
+                // Changing these three settings for AAC sensitive devices for Android<=13 might lead to sporadically truncated (cut-off) voice messages.
                 mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC);
                 mRecorder.setAudioSamplingRate(24_000);
                 mRecorder.setAudioEncodingBitRate(28_000);
             } else {
                 mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
-                mRecorder.setAudioSamplingRate(22_050);
+                mRecorder.setAudioSamplingRate(44_100);
                 mRecorder.setAudioEncodingBitRate(64_000);
             }
         } else {

src/main/java/eu/siacs/conversations/ui/activity/result/PickRingtone.java 🔗

@@ -5,6 +5,7 @@ import android.content.Context;
 import android.content.Intent;
 import android.media.RingtoneManager;
 import android.net.Uri;
+
 import androidx.activity.result.contract.ActivityResultContract;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -26,7 +27,7 @@ public class PickRingtone extends ActivityResultContract<Uri, Uri> {
         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, ringToneType);
         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
-        if (existing != null) {
+        if (noneToNull(existing) != null) {
             intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, existing);
         }
         return intent;
@@ -37,11 +38,14 @@ public class PickRingtone extends ActivityResultContract<Uri, Uri> {
         if (resultCode != Activity.RESULT_OK || data == null) {
             return null;
         }
-        final Uri pickedUri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
-        return pickedUri == null ? NONE : pickedUri;
+        return nullToNone(data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI));
     }
 
     public static Uri noneToNull(final Uri uri) {
         return uri == null || NONE.equals(uri) ? null : uri;
     }
+
+    public static @NonNull Uri nullToNone(final Uri uri) {
+        return uri == null ? NONE : uri;
+    }
 }

src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java 🔗

@@ -23,10 +23,10 @@ import com.google.common.base.Strings;
 
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.databinding.ItemMediaBinding;
-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.ViewUtil;
+import eu.siacs.conversations.worker.ExportBackupWorker;
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
@@ -99,7 +99,7 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
         } else if (mime.equals("application/epub+zip")
                 || mime.equals("application/vnd.amazon.mobi8-ebook")) {
             return R.drawable.ic_book_48dp;
-        } else if (mime.equals(ExportBackupService.MIME_TYPE)) {
+        } else if (mime.equals(ExportBackupWorker.MIME_TYPE)) {
             return R.drawable.ic_backup_48dp;
         } else if (DOCUMENT_MIMES.contains(mime)) {
             return R.drawable.ic_description_48dp;

src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java 🔗

@@ -121,6 +121,8 @@ import eu.siacs.conversations.xmpp.mam.MamReference;
 import eu.siacs.conversations.xml.Element;
 
 import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
 import java.util.Locale;
 import java.util.regex.Matcher;
@@ -690,10 +692,11 @@ public class MessageAdapter extends ArrayAdapter<Message> {
             }
 
             StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor());
+            MyLinkify.addLinks(body, message.getConversation().getAccount(), message.getConversation().getJid());
             if (highlightedTerm != null) {
                 StylingHelper.highlight(viewHolder.messageBody, body, highlightedTerm);
             }
-            MyLinkify.addLinks(body, message.getConversation().getAccount(), message.getConversation().getJid());
+
             viewHolder.messageBody.setAutoLinkMask(0);
             viewHolder.messageBody.setText(body);
             BetterLinkMovementMethod method = new BetterLinkMovementMethod() {
@@ -1533,7 +1536,15 @@ public class MessageAdapter extends ArrayAdapter<Message> {
     }
 
     public static void setTextColor(final TextView textView, final BubbleColor bubbleColor) {
-        textView.setTextColor(bubbleToOnSurfaceColor(textView, bubbleColor));
+        final var color = bubbleToOnSurfaceColor(textView, bubbleColor);
+        textView.setTextColor(color);
+        if (BubbleColor.SURFACES.contains(bubbleColor)) {
+            textView.setLinkTextColor(
+                    MaterialColors.getColor(
+                            textView, com.google.android.material.R.attr.colorPrimary));
+        } else {
+            textView.setLinkTextColor(color);
+        }
     }
 
     private static void setTextSize(final TextView textView, final boolean largeFont) {
@@ -1549,7 +1560,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
     private static @ColorInt int bubbleToOnSurfaceVariant(
             final View view, final BubbleColor bubbleColor) {
         final @AttrRes int colorAttributeResId;
-        if (bubbleColor == BubbleColor.SURFACE_HIGH || bubbleColor == BubbleColor.SURFACE) {
+        if (BubbleColor.SURFACES.contains(bubbleColor)) {
             colorAttributeResId = com.google.android.material.R.attr.colorOnSurfaceVariant;
         } else {
             colorAttributeResId = bubbleToOnSurface(bubbleColor);
@@ -1583,7 +1594,10 @@ public class MessageAdapter extends ArrayAdapter<Message> {
         PRIMARY,
         SECONDARY,
         TERTIARY,
-        WARNING
+        WARNING;
+
+        private static final Collection<BubbleColor> SURFACES =
+                Arrays.asList(BubbleColor.SURFACE, BubbleColor.SURFACE_HIGH);
     }
 
     private static class BubbleDesign {

src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java 🔗

@@ -0,0 +1,166 @@
+package eu.siacs.conversations.ui.fragment.settings;
+
+import android.Manifest;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+import androidx.work.Constraints;
+import androidx.work.Data;
+import androidx.work.ExistingPeriodicWorkPolicy;
+import androidx.work.ExistingWorkPolicy;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.OutOfQuotaPolicy;
+import androidx.work.PeriodicWorkRequest;
+import androidx.work.WorkManager;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.common.base.Strings;
+import com.google.common.primitives.Longs;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.worker.ExportBackupWorker;
+
+import java.util.concurrent.TimeUnit;
+
+public class BackupSettingsFragment extends XmppPreferenceFragment {
+
+    public static final String CREATE_ONE_OFF_BACKUP = "create_one_off_backup";
+    private static final String RECURRING_BACKUP = "recurring_backup";
+
+    private final ActivityResultLauncher<String> requestStorageForBackupLauncher =
+            registerForActivityResult(
+                    new ActivityResultContracts.RequestPermission(),
+                    isGranted -> {
+                        if (isGranted) {
+                            startOneOffBackup();
+                        } else {
+                            Toast.makeText(
+                                            requireActivity(),
+                                            getString(
+                                                    R.string.no_storage_permission,
+                                                    getString(R.string.app_name)),
+                                            Toast.LENGTH_LONG)
+                                    .show();
+                        }
+                    });
+
+    @Override
+    public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
+        setPreferencesFromResource(R.xml.preferences_backup, rootKey);
+        final var createOneOffBackup = findPreference(CREATE_ONE_OFF_BACKUP);
+        final ListPreference recurringBackup = findPreference(RECURRING_BACKUP);
+        final var backupDirectory = findPreference("backup_directory");
+        if (createOneOffBackup == null || recurringBackup == null || backupDirectory == null) {
+            throw new IllegalStateException(
+                    "The preference resource file is missing some preferences");
+        }
+        backupDirectory.setSummary(
+                getString(
+                        R.string.pref_create_backup_summary,
+                        FileBackend.getBackupDirectory(requireContext()).getAbsolutePath()));
+        createOneOffBackup.setOnPreferenceClickListener(this::onBackupPreferenceClicked);
+        final int[] choices = getResources().getIntArray(R.array.recurring_backup_values);
+        final CharSequence[] entries = new CharSequence[choices.length];
+        final CharSequence[] entryValues = new CharSequence[choices.length];
+        for (int i = 0; i < choices.length; ++i) {
+            entryValues[i] = String.valueOf(choices[i]);
+            entries[i] = timeframeValueToName(requireContext(), choices[i]);
+        }
+        recurringBackup.setEntries(entries);
+        recurringBackup.setEntryValues(entryValues);
+        recurringBackup.setSummaryProvider(new TimeframeSummaryProvider());
+    }
+
+    @Override
+    protected void onSharedPreferenceChanged(@NonNull String key) {
+        super.onSharedPreferenceChanged(key);
+        if (RECURRING_BACKUP.equals(key)) {
+            final var sharedPreferences = getPreferenceManager().getSharedPreferences();
+            if (sharedPreferences == null) {
+                return;
+            }
+            final Long recurringBackupInterval =
+                    Longs.tryParse(
+                            Strings.nullToEmpty(
+                                    sharedPreferences.getString(RECURRING_BACKUP, null)));
+            if (recurringBackupInterval == null) {
+                return;
+            }
+            Log.d(
+                    Config.LOGTAG,
+                    "recurring backup interval changed to: " + recurringBackupInterval);
+            final var workManager = WorkManager.getInstance(requireContext());
+            if (recurringBackupInterval <= 0) {
+                workManager.cancelUniqueWork(RECURRING_BACKUP);
+            } else {
+                final Constraints constraints =
+                        new Constraints.Builder()
+                                .setRequiresBatteryNotLow(true)
+                                .setRequiresStorageNotLow(true)
+                                .build();
+
+                final PeriodicWorkRequest periodicWorkRequest =
+                        new PeriodicWorkRequest.Builder(
+                                        ExportBackupWorker.class,
+                                        recurringBackupInterval,
+                                        TimeUnit.SECONDS)
+                                .setConstraints(constraints)
+                                .setInputData(
+                                        new Data.Builder()
+                                                .putBoolean("recurring_backup", true)
+                                                .build())
+                                .build();
+                workManager.enqueueUniquePeriodicWork(
+                        RECURRING_BACKUP, ExistingPeriodicWorkPolicy.UPDATE, periodicWorkRequest);
+            }
+        }
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        requireActivity().setTitle(R.string.backup);
+    }
+
+    private boolean onBackupPreferenceClicked(final Preference preference) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+            if (ContextCompat.checkSelfPermission(
+                            requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
+                    != PackageManager.PERMISSION_GRANTED) {
+                requestStorageForBackupLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE);
+            } else {
+                startOneOffBackup();
+            }
+        } else {
+            startOneOffBackup();
+        }
+        return true;
+    }
+
+    private void startOneOffBackup() {
+        final OneTimeWorkRequest exportBackupWorkRequest =
+                new OneTimeWorkRequest.Builder(ExportBackupWorker.class)
+                        .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+                        .build();
+        WorkManager.getInstance(requireContext())
+                .enqueueUniqueWork(
+                        CREATE_ONE_OFF_BACKUP, ExistingWorkPolicy.KEEP, exportBackupWorkRequest);
+        final MaterialAlertDialogBuilder builder =
+                new MaterialAlertDialogBuilder(requireActivity());
+        builder.setMessage(R.string.backup_started_message);
+        builder.setPositiveButton(R.string.ok, null);
+        builder.create().show();
+    }
+}

src/main/java/eu/siacs/conversations/ui/fragment/settings/MainSettingsFragment.java 🔗

@@ -1,63 +1,27 @@
 package eu.siacs.conversations.ui.fragment.settings;
 
-import android.Manifest;
-import android.content.Intent;
-import android.content.pm.PackageManager;
 import android.os.Build;
 import android.os.Bundle;
-import android.widget.Toast;
 
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
 import androidx.annotation.Nullable;
-import androidx.core.content.ContextCompat;
-import androidx.preference.Preference;
 import androidx.preference.PreferenceFragmentCompat;
 
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.common.base.Strings;
 
 import eu.siacs.conversations.BuildConfig;
 import eu.siacs.conversations.R;
-import eu.siacs.conversations.persistance.FileBackend;
-import eu.siacs.conversations.services.ExportBackupService;
 
 public class MainSettingsFragment extends PreferenceFragmentCompat {
 
-    private static final String CREATE_BACKUP = "create_backup";
-
-    private final ActivityResultLauncher<String> requestStorageForBackupLauncher =
-            registerForActivityResult(
-                    new ActivityResultContracts.RequestPermission(),
-                    isGranted -> {
-                        if (isGranted) {
-                            startBackup();
-                        } else {
-                            Toast.makeText(
-                                            requireActivity(),
-                                            getString(
-                                                    R.string.no_storage_permission,
-                                                    getString(R.string.app_name)),
-                                            Toast.LENGTH_LONG)
-                                    .show();
-                        }
-                    });
-
     @Override
     public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
         setPreferencesFromResource(R.xml.preferences_main, rootKey);
         final var about = findPreference("about");
         final var connection = findPreference("connection");
-        final var backup = findPreference(CREATE_BACKUP);
-        if (about == null || connection == null || backup == null) {
+        if (about == null || connection == null) {
             throw new IllegalStateException(
                     "The preference resource file is missing some preferences");
         }
-        backup.setSummary(
-                getString(
-                        R.string.pref_create_backup_summary,
-                        FileBackend.getBackupDirectory(requireContext()).getAbsolutePath()));
-        backup.setOnPreferenceClickListener(this::onBackupPreferenceClicked);
         about.setTitle(getString(R.string.title_activity_about_x, BuildConfig.APP_NAME));
         about.setSummary(
                 String.format(
@@ -74,31 +38,6 @@ public class MainSettingsFragment extends PreferenceFragmentCompat {
         }
     }
 
-    private boolean onBackupPreferenceClicked(final Preference preference) {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
-            if (ContextCompat.checkSelfPermission(
-                            requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
-                    != PackageManager.PERMISSION_GRANTED) {
-                requestStorageForBackupLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE);
-            } else {
-                startBackup();
-            }
-        } else {
-            startBackup();
-        }
-        return true;
-    }
-
-    private void startBackup() {
-        ContextCompat.startForegroundService(
-                requireContext(), new Intent(requireContext(), ExportBackupService.class));
-        final MaterialAlertDialogBuilder builder =
-                new MaterialAlertDialogBuilder(requireActivity());
-        builder.setMessage(R.string.backup_started_message);
-        builder.setPositiveButton(R.string.ok, null);
-        builder.create().show();
-    }
-
     @Override
     public void onStart() {
         super.onStart();

src/main/java/eu/siacs/conversations/ui/fragment/settings/NotificationsSettingsFragment.java 🔗

@@ -1,11 +1,15 @@
 package eu.siacs.conversations.ui.fragment.settings;
 
-import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
 import android.media.RingtoneManager;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
+import android.provider.Settings;
 import android.util.Log;
+import android.widget.Toast;
 
 import androidx.activity.result.ActivityResultLauncher;
 import androidx.annotation.NonNull;
@@ -56,12 +60,14 @@ public class NotificationsSettingsFragment extends XmppPreferenceFragment {
             @Nullable final Bundle savedInstanceState, final @Nullable String rootKey) {
         setPreferencesFromResource(R.xml.preferences_notifications, rootKey);
         final var messageNotificationSettings = findPreference("message_notification_settings");
+        final var fullscreenNotification = findPreference("fullscreen_notification");
         final var notificationRingtone = findPreference(AppSettings.NOTIFICATION_RINGTONE);
         final var notificationHeadsUp = findPreference(AppSettings.NOTIFICATION_HEADS_UP);
         final var notificationVibrate = findPreference(AppSettings.NOTIFICATION_VIBRATE);
         final var notificationLed = findPreference(AppSettings.NOTIFICATION_LED);
         final var foregroundService = findPreference(AppSettings.KEEP_FOREGROUND_SERVICE);
         if (messageNotificationSettings == null
+                || fullscreenNotification == null
                 || notificationRingtone == null
                 || notificationHeadsUp == null
                 || notificationVibrate == null
@@ -78,6 +84,44 @@ public class NotificationsSettingsFragment extends XmppPreferenceFragment {
         } else {
             messageNotificationSettings.setVisible(false);
         }
+        fullscreenNotification.setOnPreferenceClickListener(this::manageAppUseFullScreen);
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+                || requireContext()
+                        .getSystemService(NotificationManager.class)
+                        .canUseFullScreenIntent()) {
+            fullscreenNotification.setVisible(false);
+        }
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        final var fullscreenNotification = findPreference("fullscreen_notification");
+        if (fullscreenNotification == null) {
+            return;
+        }
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+                || requireContext()
+                        .getSystemService(NotificationManager.class)
+                        .canUseFullScreenIntent()) {
+            fullscreenNotification.setVisible(false);
+        }
+    }
+
+    private boolean manageAppUseFullScreen(final Preference preference) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            return false;
+        }
+        final var intent = new Intent(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT);
+        intent.setData(Uri.parse(String.format("package:%s", requireContext().getPackageName())));
+        try {
+            startActivity(intent);
+        } catch (final ActivityNotFoundException e) {
+            Toast.makeText(requireContext(), R.string.unsupported_operation, Toast.LENGTH_SHORT)
+                    .show();
+            return false;
+        }
+        return true;
     }
 
     @Override
@@ -140,7 +184,7 @@ public class NotificationsSettingsFragment extends XmppPreferenceFragment {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             channelRingtone =
                     NotificationService.getCurrentIncomingCallChannel(requireContext())
-                            .transform(NotificationChannel::getSound);
+                            .transform(channel -> PickRingtone.nullToNone(channel.getSound()));
         } else {
             channelRingtone = Optional.absent();
         }

src/main/java/eu/siacs/conversations/ui/fragment/settings/SecuritySettingsFragment.java 🔗

@@ -1,6 +1,5 @@
 package eu.siacs.conversations.ui.fragment.settings;
 
-import android.content.Context;
 import android.content.DialogInterface;
 import android.os.Bundle;
 import android.widget.Toast;
@@ -13,13 +12,11 @@ import androidx.preference.Preference;
 
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.common.base.Strings;
-import com.google.common.primitives.Ints;
 
 import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.OmemoSetting;
 import eu.siacs.conversations.services.MemorizingTrustManager;
-import eu.siacs.conversations.utils.TimeFrameUtils;
 
 import java.security.KeyStoreException;
 import java.util.ArrayList;
@@ -44,20 +41,13 @@ public class SecuritySettingsFragment extends XmppPreferenceFragment {
         final CharSequence[] entryValues = new CharSequence[choices.length];
         for (int i = 0; i < choices.length; ++i) {
             entryValues[i] = String.valueOf(choices[i]);
-            entries[i] = messageDeletionValueToName(requireContext(), choices[i]);
+            entries[i] = timeframeValueToName(requireContext(), choices[i]);
         }
         automaticMessageDeletion.setEntries(entries);
         automaticMessageDeletion.setEntryValues(entryValues);
-        automaticMessageDeletion.setSummaryProvider(new MessageDeletionSummaryProvider());
+        automaticMessageDeletion.setSummaryProvider(new TimeframeSummaryProvider());
     }
 
-    private static String messageDeletionValueToName(final Context context, final int value) {
-        if (value == 0) {
-            return context.getString(R.string.never);
-        } else {
-            return TimeFrameUtils.resolve(context, 1000L * value);
-        }
-    }
 
     @Override
     protected void onSharedPreferenceChanged(@NonNull String key) {
@@ -161,16 +151,6 @@ public class SecuritySettingsFragment extends XmppPreferenceFragment {
                 .show();
     }
 
-    private static class MessageDeletionSummaryProvider
-            implements Preference.SummaryProvider<ListPreference> {
-
-        @Nullable
-        @Override
-        public CharSequence provideSummary(@NonNull ListPreference preference) {
-            final Integer value = Ints.tryParse(Strings.nullToEmpty(preference.getValue()));
-            return messageDeletionValueToName(preference.getContext(), value == null ? 0 : value);
-        }
-    }
 
     private static class OmemoSummaryProvider
             implements Preference.SummaryProvider<ListPreference> {

src/main/java/eu/siacs/conversations/ui/fragment/settings/XmppPreferenceFragment.java 🔗

@@ -1,18 +1,27 @@
 package eu.siacs.conversations.ui.fragment.settings;
 
+import android.content.Context;
 import android.content.SharedPreferences;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
 import androidx.preference.PreferenceFragmentCompat;
 import androidx.preference.Preference;
 
 import com.rarepebble.colorpicker.ColorPreference;
 
+import com.google.common.base.Strings;
+import com.google.common.primitives.Ints;
+
 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 eu.siacs.conversations.utils.TimeFrameUtils;
 
 public abstract class XmppPreferenceFragment extends PreferenceFragmentCompat {
 
@@ -28,7 +37,7 @@ public abstract class XmppPreferenceFragment extends PreferenceFragmentCompat {
                     };
 
     protected void onSharedPreferenceChanged(@NonNull String key) {
-        Log.d(Config.LOGTAG,"onSharedPreferenceChanged("+key+")");
+        Log.d(Config.LOGTAG, "onSharedPreferenceChanged(" + key + ")");
     }
 
     public void onBackendConnected() {}
@@ -93,4 +102,23 @@ public abstract class XmppPreferenceFragment extends PreferenceFragmentCompat {
     protected void runOnUiThread(final Runnable runnable) {
         requireActivity().runOnUiThread(runnable);
     }
+
+    protected static String timeframeValueToName(final Context context, final int value) {
+        if (value == 0) {
+            return context.getString(R.string.never);
+        } else {
+            return TimeFrameUtils.resolve(context, 1000L * value);
+        }
+    }
+
+    protected static class TimeframeSummaryProvider
+            implements Preference.SummaryProvider<ListPreference> {
+
+        @Nullable
+        @Override
+        public CharSequence provideSummary(@NonNull ListPreference preference) {
+            final Integer value = Ints.tryParse(Strings.nullToEmpty(preference.getValue()));
+            return timeframeValueToName(preference.getContext(), value == null ? 0 : value);
+        }
+    }
 }

src/main/java/eu/siacs/conversations/utils/Compatibility.java 🔗

@@ -1,6 +1,6 @@
 package eu.siacs.conversations.utils;
 
-import static eu.siacs.conversations.services.EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE;
+import static eu.siacs.conversations.receiver.SystemEventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE;
 
 import android.annotation.SuppressLint;
 import android.app.ActivityOptions;
@@ -12,8 +12,6 @@ import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
 import android.os.Build;
 import android.os.Bundle;
-import android.preference.Preference;
-import android.preference.PreferenceCategory;
 import android.preference.PreferenceManager;
 import android.util.Log;
 
@@ -26,10 +24,6 @@ import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
 public class Compatibility {
 
     public static boolean hasStoragePermission(final Context context) {
@@ -99,7 +93,7 @@ public class Compatibility {
                         R.bool.enable_foreground_service);
     }
 
-    public static void startService(Context context, Intent intent) {
+    public static void startService(final Context context, final Intent intent) {
         try {
             if (Compatibility.runsAndTargetsTwentySix(context)) {
                 intent.putExtra(EXTRA_NEEDS_FOREGROUND_SERVICE, true);
@@ -107,7 +101,7 @@ public class Compatibility {
             } else {
                 context.startService(intent);
             }
-        } catch (RuntimeException e) {
+        } catch (final RuntimeException e) {
             Log.d(
                     Config.LOGTAG,
                     context.getClass().getSimpleName() + " was unable to start service");

src/main/java/eu/siacs/conversations/utils/MimeUtils.java 🔗

@@ -37,7 +37,7 @@ import java.util.Properties;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Transferable;
-import eu.siacs.conversations.services.ExportBackupService;
+import eu.siacs.conversations.worker.ExportBackupWorker;
 
 /**
  * Utilities for dealing with MIME types.
@@ -91,7 +91,7 @@ public final class MimeUtils {
         add("application/vnd.amazon.mobi8-ebook", "kfx");
         add("application/vnd.android.package-archive", "apk");
         add("application/vnd.cinderella", "cdy");
-        add(ExportBackupService.MIME_TYPE, "ceb");
+        add(ExportBackupWorker.MIME_TYPE, "ceb");
         add("application/vnd.ms-pki.stl", "stl");
         add("application/vnd.oasis.opendocument.database", "odb");
         add("application/vnd.oasis.opendocument.formula", "odf");

src/main/java/eu/siacs/conversations/utils/UIHelper.java 🔗

@@ -35,16 +35,15 @@ 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.ListItem;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.MucOptions;
 import eu.siacs.conversations.entities.Presence;
 import eu.siacs.conversations.entities.RtpSessionStatus;
 import eu.siacs.conversations.entities.Transferable;
-import eu.siacs.conversations.services.ExportBackupService;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.ui.util.MyLinkify;
 import eu.siacs.conversations.ui.util.QuoteHelper;
+import eu.siacs.conversations.worker.ExportBackupWorker;
 import eu.siacs.conversations.xmpp.Jid;
 
 public class UIHelper {
@@ -429,7 +428,7 @@ public class UIHelper {
             return context.getString(R.string.pdf_document);
         } else if (mime.equals("application/vnd.android.package-archive")) {
             return context.getString(R.string.apk);
-        } else if (mime.equals(ExportBackupService.MIME_TYPE)) {
+        } else if (mime.equals(ExportBackupWorker.MIME_TYPE)) {
             return context.getString(R.string.conversations_backup);
         } else if (mime.contains("vcard")) {
             return context.getString(R.string.vcard);

src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java 🔗

@@ -0,0 +1,607 @@
+package eu.siacs.conversations.worker;
+
+import static eu.siacs.conversations.utils.Compatibility.s;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ServiceInfo;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.core.app.NotificationCompat;
+import androidx.work.ForegroundInfo;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.gson.stream.JsonWriter;
+
+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.receiver.WorkManagerEventReceiver;
+import eu.siacs.conversations.utils.BackupFileHeader;
+import eu.siacs.conversations.utils.Compatibility;
+
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.zip.GZIPOutputStream;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class ExportBackupWorker extends Worker {
+
+    private static final SimpleDateFormat DATE_FORMAT =
+            new SimpleDateFormat("yyyy-MM-dd-HH-mm", Locale.US);
+
+    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";
+
+    private static final int NOTIFICATION_ID = 19;
+    private static final int PAGE_SIZE = 50;
+    private static final int BACKUP_CREATED_NOTIFICATION_ID = 23;
+
+    private static final int PENDING_INTENT_FLAGS =
+            s()
+                    ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
+                    : PendingIntent.FLAG_UPDATE_CURRENT;
+
+    private final boolean recurringBackup;
+
+    public ExportBackupWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
+        super(context, workerParams);
+        final var inputData = workerParams.getInputData();
+        this.recurringBackup = inputData.getBoolean("recurring_backup", false);
+    }
+
+    @NonNull
+    @Override
+    public Result doWork() {
+        final List<File> files;
+        try {
+            files = export();
+        } catch (final IOException
+                | InvalidKeySpecException
+                | InvalidAlgorithmParameterException
+                | InvalidKeyException
+                | NoSuchPaddingException
+                | NoSuchAlgorithmException
+                | NoSuchProviderException e) {
+            Log.d(Config.LOGTAG, "could not create backup", e);
+            return Result.failure();
+        } finally {
+            getApplicationContext()
+                    .getSystemService(NotificationManager.class)
+                    .cancel(NOTIFICATION_ID);
+        }
+        Log.d(Config.LOGTAG, "done creating " + files.size() + " backup files");
+        if (files.isEmpty() || recurringBackup) {
+            return Result.success();
+        }
+        notifySuccess(files);
+        return Result.success();
+    }
+
+    @NonNull
+    @Override
+    public ForegroundInfo getForegroundInfo() {
+        Log.d(Config.LOGTAG, "getForegroundInfo()");
+        final NotificationCompat.Builder notification = getNotification();
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
+            return new ForegroundInfo(
+                    NOTIFICATION_ID,
+                    notification.build(),
+                    ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
+        } else {
+            return new ForegroundInfo(NOTIFICATION_ID, notification.build());
+        }
+    }
+
+    private List<File> export()
+            throws IOException,
+                    InvalidKeySpecException,
+                    InvalidAlgorithmParameterException,
+                    InvalidKeyException,
+                    NoSuchPaddingException,
+                    NoSuchAlgorithmException,
+                    NoSuchProviderException {
+        final Context context = getApplicationContext();
+        final var database = DatabaseBackend.getInstance(context);
+        final var accounts = database.getAccounts();
+
+        int count = 0;
+        final int max = accounts.size();
+        final ImmutableList.Builder<File> files = new ImmutableList.Builder<>();
+        Log.d(Config.LOGTAG, "starting backup for " + max + " accounts");
+        for (final Account account : accounts) {
+            if (isStopped()) {
+                Log.d(Config.LOGTAG, "ExportBackupWorker has stopped. Returning what we have");
+                return files.build();
+            }
+            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()));
+                count++;
+                continue;
+            }
+            final String filename =
+                    String.format(
+                            "%s.%s.ceb",
+                            account.getJid().asBareJid().toEscapedString(),
+                            DATE_FORMAT.format(new Date()));
+            final File file = new File(FileBackend.getBackupDirectory(context), filename);
+            try {
+                export(database, account, password, file, max, count);
+            } catch (final WorkStoppedException e) {
+                if (file.delete()) {
+                    Log.d(
+                            Config.LOGTAG,
+                            "deleted in progress backup file " + file.getAbsolutePath());
+                }
+                Log.d(Config.LOGTAG, "ExportBackupWorker has stopped. Returning what we have");
+                return files.build();
+            }
+            files.add(file);
+            count++;
+        }
+        return files.build();
+    }
+
+    private void export(
+            final DatabaseBackend database,
+            final Account account,
+            final String password,
+            final File file,
+            final int max,
+            final int count)
+            throws IOException,
+                    InvalidKeySpecException,
+                    InvalidAlgorithmParameterException,
+                    InvalidKeyException,
+                    NoSuchPaddingException,
+                    NoSuchAlgorithmException,
+                    NoSuchProviderException,
+                    WorkStoppedException {
+        final var context = getApplicationContext();
+        final SecureRandom secureRandom = new SecureRandom();
+        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(
+                        context.getString(R.string.app_name),
+                        account.getJid(),
+                        System.currentTimeMillis(),
+                        IV,
+                        salt);
+        final var notification = getNotification();
+        if (!recurringBackup) {
+            final var cancel = new Intent(context, WorkManagerEventReceiver.class);
+            cancel.setAction(WorkManagerEventReceiver.ACTION_STOP_BACKUP);
+            final var cancelPendingIntent =
+                    PendingIntent.getBroadcast(context, 197, cancel, PENDING_INTENT_FLAGS);
+            notification.addAction(
+                    new NotificationCompat.Action.Builder(
+                                    R.drawable.ic_cancel_24dp,
+                                    context.getString(R.string.cancel),
+                                    cancelPendingIntent)
+                            .build());
+        }
+        final Progress progress = new Progress(notification, max, count);
+        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.twentyEight()
+                        ? Cipher.getInstance(CIPHERMODE)
+                        : Cipher.getInstance(CIPHERMODE, PROVIDER);
+        final byte[] key = getKey(password, salt);
+        SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
+        IvParameterSpec ivSpec = new IvParameterSpec(IV);
+        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
+        CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher);
+
+        final GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream);
+        final SQLiteDatabase db = database.getReadableDatabase();
+        final var writer = new PrintWriter(gzipOutputStream);
+        final String uuid = account.getUuid();
+        accountExport(db, uuid, writer);
+        simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, writer);
+        messageExport(db, uuid, writer, progress);
+        messageExportCheogram(db, uuid, writer, progress);
+        for (final String table :
+                Arrays.asList(
+                        SQLiteAxolotlStore.PREKEY_TABLENAME,
+                        SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
+                        SQLiteAxolotlStore.SESSION_TABLENAME,
+                        SQLiteAxolotlStore.IDENTITIES_TABLENAME)) {
+            throwIfWorkStopped();
+            simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, writer);
+        }
+        writer.flush();
+        writer.close();
+        mediaScannerScanFile(file);
+        Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile());
+    }
+
+    private NotificationCompat.Builder getNotification() {
+        final var context = getApplicationContext();
+        final NotificationCompat.Builder notification =
+                new NotificationCompat.Builder(context, "backup");
+        notification
+                .setContentTitle(context.getString(R.string.notification_create_backup_title))
+                .setSmallIcon(R.drawable.ic_archive_24dp)
+                .setProgress(1, 0, false);
+        notification.setOngoing(true);
+        notification.setLocalOnly(true);
+        return notification;
+    }
+
+    private void throwIfWorkStopped() throws WorkStoppedException {
+        if (isStopped()) {
+            throw new WorkStoppedException();
+        }
+    }
+
+    private void mediaScannerScanFile(final File file) {
+        final Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
+        intent.setData(Uri.fromFile(file));
+        getApplicationContext().sendBroadcast(intent);
+    }
+
+    private void messageExport(SQLiteDatabase db, String uuid, PrintWriter writer, Progress progress) {
+        final var notificationManager = getApplicationContext().getSystemService(NotificationManager.class);
+        Cursor cursor = db.rawQuery("select messages.* from messages join conversations on conversations.uuid=messages.conversationUuid where conversations.accountUuid=?", new String[]{uuid});
+        int size = cursor != null ? cursor.getCount() : 0;
+        Log.d(Config.LOGTAG, "exporting " + size + " messages for account " + uuid);
+        int i = 0;
+        int p = 0;
+        while (cursor != null && cursor.moveToNext()) {
+            writer.write(cursorToString(Message.TABLENAME, cursor, PAGE_SIZE, false));
+            if (i + PAGE_SIZE > size) {
+                i = size;
+            } else {
+                i += PAGE_SIZE;
+            }
+            final int percentage = i * 100 / size;
+            if (p < percentage) {
+                p = percentage;
+                notificationManager.notify(NOTIFICATION_ID, progress.build(p));
+            }
+        }
+        if (cursor != null) {
+            cursor.close();
+        }
+    }
+
+    private void messageExportCheogram(SQLiteDatabase db, String uuid, PrintWriter writer, Progress progress) {
+        final var notificationManager = getApplicationContext().getSystemService(NotificationManager.class);
+        Cursor cursor = db.rawQuery("select cmessages.* from messages join cheogram.messages cmessages using (uuid) 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 + " cheogram messages for account " + uuid);
+        int i = 0;
+        int p = 0;
+        while (cursor != null && cursor.moveToNext()) {
+            writer.write(cursorToString("cheogram." + 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(NOTIFICATION_ID, progress.build(p));
+            }
+        }
+        if (cursor != null) {
+            cursor.close();
+        }
+
+        cursor = db.rawQuery("select webxdc_updates.* from " + Conversation.TABLENAME + " join cheogram.webxdc_updates webxdc_updates on " + Conversation.TABLENAME + ".uuid=webxdc_updates." + Message.CONVERSATION + " where conversations.accountUuid=?", new String[]{uuid});
+        size = cursor != null ? cursor.getCount() : 0;
+        Log.d(Config.LOGTAG, "exporting " + size + " WebXDC updates for account " + uuid);
+        while (cursor != null && cursor.moveToNext()) {
+            writer.write(cursorToString("cheogram.webxdc_updates", 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(NOTIFICATION_ID, progress.build(p));
+            }
+        }
+        if (cursor != null) {
+            cursor.close();
+        }
+    }
+
+    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 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();
+        }
+    }
+
+    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(")");
+
+    }
+
+    private static void appendEscapedSQLString(final StringBuilder sb, final String sqlString) {
+        DatabaseUtils.appendEscapedSQLString(sb, CharMatcher.is('\u0000').removeFrom(sqlString));
+    }
+
+    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 void notifySuccess(final List<File> files) {
+        final var context = getApplicationContext();
+        final String path = FileBackend.getBackupDirectory(context).getAbsolutePath();
+
+        final var openFolderIntent = getOpenFolderIntent(path);
+
+        final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
+        final ArrayList<Uri> uris = new ArrayList<>();
+        for (final File file : files) {
+            uris.add(FileBackend.getUriForFile(context, 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, context.getString(R.string.share_backup_files));
+        final var shareFilesIntent =
+                PendingIntent.getActivity(context, 190, chooser, PENDING_INTENT_FLAGS);
+
+        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context, "backup");
+        mBuilder.setContentTitle(context.getString(R.string.notification_backup_created_title))
+                .setContentText(
+                        context.getString(R.string.notification_backup_created_subtitle, path))
+                .setStyle(
+                        new NotificationCompat.BigTextStyle()
+                                .bigText(
+                                        context.getString(
+                                                R.string.notification_backup_created_subtitle,
+                                                FileBackend.getBackupDirectory(context)
+                                                        .getAbsolutePath())))
+                .setAutoCancel(true)
+                .setSmallIcon(R.drawable.ic_archive_24dp);
+
+        if (openFolderIntent.isPresent()) {
+            mBuilder.setContentIntent(openFolderIntent.get());
+        } else {
+            Log.w(Config.LOGTAG, "no app can display folders");
+        }
+
+        mBuilder.addAction(
+                R.drawable.ic_share_24dp,
+                context.getString(R.string.share_backup_files),
+                shareFilesIntent);
+        final var notificationManager = context.getSystemService(NotificationManager.class);
+        notificationManager.notify(BACKUP_CREATED_NOTIFICATION_ID, mBuilder.build());
+    }
+
+    private Optional<PendingIntent> getOpenFolderIntent(final String path) {
+        final var context = getApplicationContext();
+        for (final Intent intent : getPossibleFileOpenIntents(context, path)) {
+            if (intent.resolveActivityInfo(context.getPackageManager(), 0) != null) {
+                return Optional.of(
+                        PendingIntent.getActivity(context, 189, intent, PENDING_INTENT_FLAGS));
+            }
+        }
+        return Optional.absent();
+    }
+
+    private static List<Intent> 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 class Progress {
+        private final NotificationCompat.Builder notification;
+        private final int max;
+        private final int count;
+
+        private Progress(
+                final NotificationCompat.Builder notification, final int max, final int count) {
+            this.notification = notification;
+            this.max = max;
+            this.count = count;
+        }
+
+        private Notification build(int percentage) {
+            notification.setProgress(max * 100, count * 100 + percentage, false);
+            return notification.build();
+        }
+    }
+
+    private static class WorkStoppedException extends Exception {}
+}

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java 🔗

@@ -648,7 +648,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
     }
 
     public JingleRtpConnection getOngoingRtpConnection() {
-        for(final AbstractJingleConnection jingleConnection : this.connections.values()) {
+        for (final AbstractJingleConnection jingleConnection : this.connections.values()) {
             if (jingleConnection instanceof JingleRtpConnection jingleRtpConnection) {
                 if (jingleRtpConnection.isTerminated()) {
                     continue;
@@ -984,10 +984,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                 this.rtpSessionProposals.remove(sessionProposal);
                 sessionProposal.getCallIntegration().error();
                 mXmppConnectionService.notifyJingleRtpConnectionUpdate(
-                        account,
-                        sessionProposal.with,
-                        sessionProposal.sessionId,
-                        endUserState);
+                        account, sessionProposal.with, sessionProposal.sessionId, endUserState);
                 return;
             }
 
@@ -1225,6 +1222,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
         @Override
         public void onCallIntegrationSilence() {}
 
+        @Override
+        public void onCallIntegrationMicrophoneEnabled(boolean enabled) {}
+
         @Override
         public boolean applyDtmfTone(final String dtmf) {
             return false;

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java 🔗

@@ -2333,6 +2333,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
         this.jingleConnectionManager.ensureConnectionIsRegistered(this);
         this.webRTCWrapper.setup(this.xmppConnectionService);
         this.webRTCWrapper.initializePeerConnection(media, iceServers, trickle);
+        this.webRTCWrapper.setMicrophoneEnabledOrThrow(callIntegration.isMicrophoneEnabled());
     }
 
     private void acceptCallFromProposed() {
@@ -2490,7 +2491,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
         this.webRTCWrapper.execute(this::renegotiate);
     }
 
-    private void renegotiate() {
+    private synchronized void renegotiate() {
         final SessionDescription sessionDescription;
         try {
             sessionDescription = setLocalSessionDescription();
@@ -2539,10 +2540,11 @@ public class JingleRtpConnection extends AbstractJingleConnection
                             + this.webRTCWrapper.getSignalingState());
         }
 
-        if (diff.added.size() > 0) {
-            modifyLocalContentMap(rtpContentMap);
-            sendContentAdd(rtpContentMap, diff.added);
+        if (diff.added.isEmpty()) {
+            return;
         }
+        modifyLocalContentMap(rtpContentMap);
+        sendContentAdd(rtpContentMap, diff.added);
     }
 
     private void initiateIceRestart(final RtpContentMap rtpContentMap) {
@@ -2695,7 +2697,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
     }
 
     public boolean setMicrophoneEnabled(final boolean enabled) {
-        return webRTCWrapper.setMicrophoneEnabled(enabled);
+        return webRTCWrapper.setMicrophoneEnabledOrThrow(enabled);
     }
 
     public boolean isVideoEnabled() {
@@ -2771,6 +2773,11 @@ public class JingleRtpConnection extends AbstractJingleConnection
         xmppConnectionService.getNotificationService().stopSoundAndVibration();
     }
 
+    @Override
+    public void onCallIntegrationMicrophoneEnabled(final boolean enabled) {
+        this.webRTCWrapper.setMicrophoneEnabled(enabled);
+    }
+
     @Override
     public void onAudioDeviceChanged(
             final CallIntegration.AudioDevice selectedAudioDevice,

src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java 🔗

@@ -538,26 +538,33 @@ public class WebRTCWrapper {
         }
     }
 
-    boolean setMicrophoneEnabled(final boolean enabled) {
-        Optional<AudioTrack> audioTrack = null;
+    boolean setMicrophoneEnabledOrThrow(final boolean enabled) {
+        final Optional<AudioTrack> audioTrack =
+                TrackWrapper.get(peerConnection, this.localAudioTrack);
+        if (audioTrack.isPresent()) {
+            return setEnabled(audioTrack.get(), enabled);
+
+        } else {
+            throw new IllegalStateException("Local audio track does not exist (yet)");
+        }
+    }
+
+    private static boolean setEnabled(final AudioTrack audioTrack, final boolean enabled) {
         try {
-            audioTrack = TrackWrapper.get(peerConnection, this.localAudioTrack);
+            audioTrack.setEnabled(enabled);
+            return true;
         } catch (final IllegalStateException e) {
-            Log.d(Config.LOGTAG, "unable to toggle microphone", e);
-            // ignoring race condition in case sender has been disposed
+            Log.d(Config.LOGTAG, "unable to toggle audio track", e);
+            // ignoring race condition in case MediaStreamTrack has been disposed
             return false;
         }
+    }
+
+    void setMicrophoneEnabled(final boolean enabled) {
+        final Optional<AudioTrack> audioTrack =
+                TrackWrapper.get(peerConnection, this.localAudioTrack);
         if (audioTrack.isPresent()) {
-            try {
-                audioTrack.get().setEnabled(enabled);
-                return true;
-            } catch (final IllegalStateException e) {
-                Log.d(Config.LOGTAG, "unable to toggle microphone", e);
-                // ignoring race condition in case MediaStreamTrack has been disposed
-                return false;
-            }
-        } else {
-            throw new IllegalStateException("Local audio track does not exist (yet)");
+            setEnabled(audioTrack.get(), enabled);
         }
     }
 

src/main/res/drawable/cursor_on_tertiary_container.xml 🔗

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <size android:width="2dp" />
+    <!-- width of the original cursor was measured on a Pixel 6a because it’s not documented anywhere -->
+    <solid android:color="?colorOnTertiaryContainer" />
+</shape>

src/main/res/drawable/ic_calendar_month_24dp.xml 🔗

@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?colorControlNormal"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M19,4h-1V2h-2v2H8V2H6v2H5C3.89,4 3.01,4.9 3.01,6L3,20c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V6C21,4.9 20.1,4 19,4zM19,20H5V10h14V20zM9,14H7v-2h2V14zM13,14h-2v-2h2V14zM17,14h-2v-2h2V14zM9,18H7v-2h2V18zM13,18h-2v-2h2V18zM17,18h-2v-2h2V18z" />
+
+</vector>

src/main/res/drawable/ic_smartphone_24dp.xml 🔗

@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?colorControlNormal"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M17,1.01L7,1c-1.1,0 -2,0.9 -2,2v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3c0,-1.1 -0.9,-1.99 -2,-1.99zM17,19H7V5h10v14z" />
+
+</vector>

src/main/res/layout/fragment_conversation.xml 🔗

@@ -130,6 +130,7 @@
                             android:hint="Subject"
                             android:textColor="?colorOnTertiaryContainer"
                             android:textColorHint="@color/hint_on_tertiary_container"
+                            android:textCursorDrawable="@drawable/cursor_on_tertiary_container"
                             android:maxLines="1"
                             android:padding="8dp"
                             android:imeOptions="flagNoExtractUi"

src/main/res/values-de/strings.xml 🔗

@@ -404,7 +404,7 @@
     <string name="pref_input_options">Eingabe</string>
     <string name="pref_enter_is_send">Eingabetaste sendet Nachricht</string>
     <string name="pref_enter_is_send_summary">Nutze die Eingabetaste zum Versenden einer Nachricht. Strg+Eingabetaste sendet die Nachricht unabhängig von dieser Einstellung.</string>
-    <string name="pref_display_enter_key">Zeige Eingabetaste</string>
+    <string name="pref_display_enter_key">Eingabetaste anzeigen</string>
     <string name="pref_display_enter_key_summary">Emoji-Taste durch Eingabetaste ersetzen</string>
     <string name="audio">Audio</string>
     <string name="video">Video</string>
@@ -445,7 +445,7 @@
     <string name="pref_quick_action">Schnell-Tasten</string>
     <string name="none">Keine</string>
     <string name="recently_used">Zuletzt verwendet</string>
-    <string name="choose_quick_action">Wähle Schnell-Taste</string>
+    <string name="choose_quick_action">Schnell-Taste auswählen</string>
     <string name="search_contacts">Kontakte durchsuchen</string>
     <string name="send_private_message">Private Nachricht senden</string>
     <string name="user_has_left_conference">%1$s hat den Gruppenchat verlassen</string>
@@ -465,9 +465,9 @@
     <string name="pref_away_when_screen_off">Abwesend bei gesperrtem Gerät</string>
     <string name="pref_away_when_screen_off_summary">Als abwesend anzeigen, wenn das Gerät gesperrt ist</string>
     <string name="pref_dnd_on_silent_mode">Beschäftigt im lautlosen Modus</string>
-    <string name="pref_dnd_on_silent_mode_summary">Als Beschäftigt anzeigen, wenn sich das Gerät im lautlosen Modus befindet</string>
+    <string name="pref_dnd_on_silent_mode_summary">Als beschäftigt anzeigen, wenn sich das Gerät im lautlosen Modus befindet</string>
     <string name="pref_treat_vibrate_as_silent">Vibration als Lautlos behandeln</string>
-    <string name="pref_treat_vibrate_as_dnd_summary">Als Beschäftigt anzeigen, wenn das Gerät auf Vibration eingestellt ist</string>
+    <string name="pref_treat_vibrate_as_dnd_summary">Als beschäftigt anzeigen, wenn das Gerät auf Vibration eingestellt ist</string>
     <string name="pref_show_connection_options">Hostname &amp; Port</string>
     <string name="pref_show_connection_options_summary">Erweiterte Verbindungseinstellungen beim Einrichten eines Kontos anzeigen</string>
     <string name="hostname_example">xmpp.domain.de</string>
@@ -600,7 +600,7 @@
     <string name="please_wait_for_keys_to_be_fetched">Bitte warten, bis die Schlüssel abgerufen werden</string>
     <string name="share_as_barcode">Als Barcode teilen</string>
     <string name="share_as_uri">Als XMPP-URI teilen</string>
-    <string name="share_as_http">Als HTTP Link teilen</string>
+    <string name="share_as_http">Als HTTP-Link teilen</string>
     <string name="pref_blind_trust_before_verification">Blind vertrauen vor der Überprüfung</string>
     <string name="pref_blind_trust_before_verification_summary">Neuen Geräten von nicht verifizierten Kontakten vertrauen, aber bei verifizierten Kontakten eine manuelle Bestätigung der neuen Geräte verlangen.</string>
     <string name="blindly_trusted_omemo_keys">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.</string>
@@ -728,7 +728,7 @@
     <string name="copy_jabber_id">XMPP-Adresse kopieren</string>
     <string name="p1_s3_filetransfer">HTTP-Dateifreigabe für S3</string>
     <string name="pref_start_search">Direkte Suche </string>
-    <string name="pref_start_search_summary">Beim Dialog \'Neuer Chat\' Tastatur öffnen und den Cursor im Suchfeld platzieren</string>
+    <string name="pref_start_search_summary">Beim Dialog \"Neuer Chat\" Tastatur öffnen und den Cursor im Suchfeld platzieren</string>
     <string name="group_chat_avatar">Gruppenchat-Profilbild</string>
     <string name="host_does_not_support_group_chat_avatars">Host unterstützt keine Gruppenchat-Profilbilder</string>
     <string name="only_the_owner_can_change_group_chat_avatar">Nur der Eigentümer kann das Gruppenchat-Profilbild ändern</string>
@@ -1064,4 +1064,6 @@
     <string name="send_encrypted_message">Verschlüsselt schreiben…</string>
     <string name="pref_large_font_summary">Schriftgröße der Nachrichten erhöhen</string>
     <string name="pref_large_font">Große Schrift</string>
+    <string name="pref_accept_invites_from_strangers">Einladungen von Unbekannten</string>
+    <string name="pref_accept_invites_from_strangers_summary">Einladungen zu Gruppenchats von Unbekannten annehmen</string>
 </resources>

src/main/res/values-es/strings.xml 🔗

@@ -109,11 +109,11 @@
     <string name="pref_accept_files">Aceptar archivos</string>
     <string name="pref_accept_files_summary">De forma automática aceptar archivos menores que…</string>
     <string name="pref_attachments">Adjuntos</string>
-    <string name="pref_notification_settings">Notificaciones</string>
+    <string name="pref_notification_settings">Notificación</string>
     <string name="pref_vibrate">Vibrar</string>
     <string name="pref_vibrate_summary">Vibra cuando llega un nuevo mensaje</string>
-    <string name="pref_led">Luz</string>
-    <string name="pref_led_summary">La luz parpadea cuando llega un nuevo mensaje</string>
+    <string name="pref_led">Led de notificaciones</string>
+    <string name="pref_led_summary">La luz de notificación parpadea cuando llega un nuevo mensaje</string>
     <string name="pref_ringtone">Tono de llamada</string>
     <string name="pref_notification_sound">Sonido de notificación</string>
     <string name="pref_notification_sound_summary">Sonido de notificación para nuevos mensajes</string>
@@ -561,7 +561,7 @@
     <string name="gp_medium">Medio</string>
     <string name="gp_long">Largo</string>
     <string name="pref_broadcast_last_activity">Visto por ultima vez</string>
-    <string name="pref_broadcast_last_activity_summary">Permite que tus contactos sepan cuando usas Conversations</string>
+    <string name="pref_broadcast_last_activity_summary">Permitir que tus contactos vean la última vez que usaste la aplicación</string>
     <string name="pref_privacy">Privacidad</string>
     <string name="pref_theme_options">Tema</string>
     <string name="pref_theme_options_summary">Selecciona el color de la paleta</string>
@@ -702,14 +702,14 @@
     <string name="error_trustkey_device_list">No se ha podido conseguir la lista de dispositivos </string>
     <string name="error_trustkey_bundle">No se han podido conseguir las claves de cifrado</string>
     <string name="error_trustkey_hint_mutual">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.</string>
-    <string name="disable_encryption_message">¿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.</string>
+    <string name="disable_encryption_message">¿Estás seguro de que deseas desactivar el cifrado OMEMO para este chat?
+\nEsto permitirá que el administrador de su servidor lea sus mensajes, pero podría ser la única forma de comunicarse con personas que utilizan clientes obsoletos.</string>
     <string name="disable_now">Deshabilitar ahora</string>
     <string name="draft">Borrador:</string>
     <string name="pref_omemo_setting">Cifrado OMEMO</string>
     <string name="pref_omemo_setting_summary_always">OMEMO siempre será usado para conversaciones uno a uno y en conversaciones en grupo privadas.</string>
-    <string name="pref_omemo_setting_summary_default_on">OMEMO será usado por defecto para nuevas conversaciones.</string>
-    <string name="pref_omemo_setting_summary_default_off">OMEMO tendrá que ser explícitamente activado para nuevas conversaciones.</string>
+    <string name="pref_omemo_setting_summary_default_on">OMEMO será usado por defecto para chats nuevos.</string>
+    <string name="pref_omemo_setting_summary_default_off">OMEMO tendrá que ser activado explícitamente para los nuevos chats.</string>
     <string name="create_shortcut">Crear acceso directo</string>
     <string name="default_on">Activo por defecto</string>
     <string name="default_off">Desactivado por defecto</string>
@@ -730,14 +730,14 @@
     <string name="no_microphone_permission">Permitir a %1$s acceder al micrófono</string>
     <string name="search_messages">Buscar mensajes</string>
     <string name="gif">GIF</string>
-    <string name="view_conversation">Ver conversación</string>
+    <string name="view_conversation">Ver el chat</string>
     <string name="pref_use_share_location_plugin">Plugin para Compartir Ubicación</string>
     <string name="pref_use_share_location_plugin_summary">Usar el Plugin Compartir Ubicación en lugar del propio de la aplicación</string>
     <string name="copy_link">Copiar dirección web</string>
     <string name="copy_jabber_id">Copiar dirección XMPP</string>
     <string name="p1_s3_filetransfer">Compartición de Archivos mediante S3</string>
     <string name="pref_start_search">Búsqueda directa</string>
-    <string name="pref_start_search_summary">En la pantalla de \'Nueva Conversación\' abrir el teclado y poner el cursor en el campo de búsqueda</string>
+    <string name="pref_start_search_summary">En la pantalla \"Nuevo chat\", abra el teclado y coloque el cursor en el campo de búsqueda</string>
     <string name="group_chat_avatar">Avatar de la conversación en grupo</string>
     <string name="host_does_not_support_group_chat_avatars">El servidor no soporta avatares en conversaciones en grupo</string>
     <string name="only_the_owner_can_change_group_chat_avatar">Solo el propietario de la conversación puede cambiar el avatar</string>
@@ -789,20 +789,20 @@
     <string name="verify_x">Verificar %s</string>
     <string name="we_have_sent_you_an_sms_to_x"><![CDATA[Hemos enviado un mensaje SMS a <b>%s</b>.]]></string>
     <string name="we_have_sent_you_another_sms">Hemos enviado otro mensaje SMS con un código de 6 dígitos.</string>
-    <string name="please_enter_pin_below">Por favor, introduce el código de 6 dígitos abajo.</string>
+    <string name="please_enter_pin_below">Por favor, introduzca a continuación el PIN de 6 dígitos.</string>
     <string name="resend_sms">Reenviar SMS</string>
     <string name="resend_sms_in">Reenviar SMS (%s)</string>
     <string name="wait_x">Por favor, espera (%s)</string>
-    <string name="back">atrás</string>
-    <string name="possible_pin">Automáticamente pegar el posible pin del portapapeles.</string>
-    <string name="please_enter_pin">Por favor, introduce tu código de 6 dígitos.</string>
+    <string name="back">Atrás</string>
+    <string name="possible_pin">Pegado automático del posible PIN desde el portapapeles.</string>
+    <string name="please_enter_pin">Por favor, introduzca su PIN de 6 dígitos.</string>
     <string name="abort_registration_procedure">¿Estás seguro de que quieres abortar el proceso de registro?</string>
     <string name="yes">Sí</string>
     <string name="no">No</string>
     <string name="verifying">Verificando…</string>
     <string name="requesting_sms">Solicitando un mensaje de texto…</string>
-    <string name="incorrect_pin">El código que has introducido no es correcto.</string>
-    <string name="pin_expired">El código que te hemos enviado ha expirado.</string>
+    <string name="incorrect_pin">El PIN introducido es incorrecto.</string>
+    <string name="pin_expired">El PIN que te hemos enviado ha caducado.</string>
     <string name="unknown_api_error_network">Error desconocido de red.</string>
     <string name="unknown_api_error_response">Respuesta de servidor desconocida.</string>
     <string name="unable_to_connect_to_server">No se ha podido conectar al servidor. </string>
@@ -951,8 +951,8 @@
     <string name="remove_from_favorites">Desfijar de la parte superior</string>
     <string name="gpx_track">Recorrido GPX</string>
     <string name="could_not_correct_message">No se pudo corregir el mensaje</string>
-    <string name="search_all_conversations">Todas las conversaciones</string>
-    <string name="search_this_conversation">Esta conversación</string>
+    <string name="search_all_conversations">Todos los chats</string>
+    <string name="search_this_conversation">Este chat</string>
     <string name="your_avatar">Tu imagen de perfil</string>
     <string name="avatar_for_x">Imagen de perfil de %s</string>
     <string name="encrypted_with_omemo">Encriptado con OMEMO</string>
@@ -1024,7 +1024,7 @@
     <string name="no_permission_to_place_call">Sin permiso para llamar por teléfono</string>
     <string name="rtp_state_contact_offline">Contacto no disponible</string>
     <string name="call_integration_not_available">¡Sin integración de llamadas!</string>
-    <string name="delete_and_close">Borrar y cerrar</string>
+    <string name="delete_and_close">Borrar y cerrar el chat</string>
     <string name="remove_bookmark">¿Quieres eliminar el marcador de %s ?</string>
     <string name="remove_bookmark_and_close">¿Quieres eliminar el marcador de %s y guardar el chat?</string>
     <string name="pref_send_crash_reports">Enviar informes de errores</string>
@@ -1034,4 +1034,50 @@
     <string name="archive_this_chat">Guardar este chat</string>
     <string name="title_undo_swipe_out_chat">Chat guardado</string>
     <string name="welcome_header">Unirse a Conversation</string>
+    <string name="pref_use_colorful_bubbles">Burbujas de chat de colores</string>
+    <string name="pref_use_colorful_bubbles_summary">Colores de fondo distintos para mensajes enviados y recibidos</string>
+    <string name="barcode_does_not_contain_fingerprints_for_this_chat">El código de barras no contiene huellas digitales para este chat.</string>
+    <string name="pref_keyboard_options">Teclado</string>
+    <string name="pref_category_engagement_notifications">Notificaciones de participación</string>
+    <string name="pref_category_application">Solicitud</string>
+    <string name="pref_category_interaction">Interacción</string>
+    <string name="pref_connection_summary">Nombre del host y puerto, Tor</string>
+    <string name="pref_connection_summary_w_cd">Nombre del host y puerto, Tor, descubrimiento de canales</string>
+    <string name="pref_summary_security">Cifrado E2E, confianza ciega antes de la verificación, detección de MITM</string>
+    <string name="pref_up_long_summary">Al actuar como un Distribuidor de UnifiedPush la conexión XMPP persistente, fiable y de bajo consumo de batería se utilizará para despertar a otras aplicaciones compatibles con UnifiedPush como Tusky, Ltt.rs, FluffyChat y más.</string>
+    <string name="send_encrypted_message">Enviar mensaje cifrado</string>
+    <string name="pref_title_interface">Interfaz</string>
+    <string name="pref_summary_appearance">Tema, Colores, Capturas de pantalla, Entrada</string>
+    <string name="pref_title_security">Seguridad</string>
+    <string name="unified_push_summary">Relé de notificaciones para aplicaciones de terceros compatibles con UnifiedPush</string>
+    <string name="notifications">Notificaciones</string>
+    <string name="pref_notifications_summary">Período de gracia, Tono de llamada, Vibración, Extraños</string>
+    <string name="pref_category_sending">Enviando</string>
+    <string name="pref_category_receiving">Recibiendo</string>
+    <string name="pref_automatic_download">Descarga automática</string>
+    <string name="appearance">Apariencia</string>
+    <string name="pref_light_dark_mode">Modo claro/oscuro</string>
+    <string name="pref_allow_screenshots">Permitir capturas de pantalla</string>
+    <string name="pref_category_e2ee">Cifrado de extremo a extremo</string>
+    <string name="pref_title_trust_system_ca_store">Organismos de certificación</string>
+    <string name="pref_title_trust_system_ca_store_summary">Confiar en los certificados CA del sistema</string>
+    <string name="detect_mim">Requerir enlace al canal</string>
+    <string name="detect_mim_summary">La vinculación de canales puede detectar algunos ataques al intermediario</string>
+    <string name="pref_category_server_connection">Conexión al servidor</string>
+    <string name="pref_category_operating_system">Sistema operativo</string>
+    <string name="pref_category_on_this_device">En el dispositivo</string>
+    <string name="pref_large_font">Texto grande</string>
+    <string name="pref_large_font_summary">Aumentar el tamaño del texto en las burbujas de mensajes</string>
+    <string name="corresponding_chats_closed">Chats correspondientes archivados.</string>
+    <string name="pref_dynamic_colors">Colores dinámicos</string>
+    <string name="pref_dynamic_colors_summary">Colores del sistema (Material You)</string>
+    <string name="switch_to_chat">Cambiar al chat</string>
+    <string name="pref_privacy_summary">Notificaciones de escritura, Visto por última vez, Disponibilidad</string>
+    <string name="start_chat">Iniciar un chat</string>
+    <string name="channel_discover_opt_in_message">El descubrimiento de canales utiliza un servicio de terceros llamado &lt;a href=https://search.jabber.network&gt;search.jabber.network&lt;/a&gt;.&lt;br&gt;&lt;br&gt;Usar esta función transmitirá tu dirección IP y términos de búsqueda a ese servicio. Consulte su &lt;a href=https://search.jabber.network/privacy&gt;Política de privacidad&lt;/a&gt; para obtener más información.</string>
+    <string name="no_certificate_selected">¡No se ha seleccionado ningún certificado de cliente!</string>
+    <string name="pref_attachments_summary">Tamaño de archivo, Compresión de imagen, Calidad de vídeo</string>
+    <string name="pref_allow_screenshots_summary">Mostrar el contenido de la aplicación en el conmutador de aplicaciones y permitir la realización de capturas de pantalla</string>
+    <string name="pref_accept_invites_from_strangers">Invitaciones de extraños</string>
+    <string name="pref_accept_invites_from_strangers_summary">Aceptar invitaciones a chats grupales de extraños</string>
 </resources>

src/main/res/values-it/strings.xml 🔗

@@ -276,7 +276,7 @@
     <string name="ignore">Ignora</string>
     <string name="without_mutual_presence_updates"><b>Attenzione:</b> inviarlo senza aggiornamenti della presenza reciproci può causare problemi inaspettati.\n\n<small>Vai nei dettagli del contatto per verificare le tue sottoscrizioni alla presenza.</small></string>
     <string name="pref_security_settings">Sicurezza</string>
-    <string name="pref_allow_message_correction">Correzione del messaggio</string>
+    <string name="pref_allow_message_correction">Correzione dei messaggi</string>
     <string name="pref_allow_message_correction_summary">Consenti ai tuoi contatti di modificare retroattivamente i loro messaggi</string>
     <string name="pref_expert_options">Impostazioni per esperti</string>
     <string name="pref_expert_options_summary">Fai attenzione con queste impostazioni</string>
@@ -566,8 +566,8 @@
     <string name="pref_theme_options">Tema</string>
     <string name="pref_theme_options_summary">Seleziona il colore</string>
     <string name="pref_theme_automatic">Automatica</string>
-    <string name="pref_theme_light">Chiaro</string>
-    <string name="pref_theme_dark">Scuro</string>
+    <string name="pref_theme_light">Chiara</string>
+    <string name="pref_theme_dark">Scura</string>
     <string name="unable_to_connect_to_keychain">Impossibile connettersi a OpenKeychain</string>
     <string name="this_device_is_no_longer_in_use">Questo dispositivo non è più in uso</string>
     <string name="type_pc">Computer</string>
@@ -1050,11 +1050,11 @@
     <string name="pref_category_operating_system">Sistema operativo</string>
     <string name="pref_connection_summary_w_cd">Nome host e porta, Tor, scoperta dei canali</string>
     <string name="pref_keyboard_options">Tastiera</string>
-    <string name="pref_category_engagement_notifications">Notifiche di coinvolgimento</string>
+    <string name="pref_category_engagement_notifications">Notifiche di partecipazione</string>
     <string name="pref_category_application">Applicazione</string>
     <string name="pref_category_interaction">Interazione</string>
     <string name="pref_category_on_this_device">Sul dispositivo</string>
-    <string name="pref_up_long_summary">Quando si agisce come distributore di UnifiedPush, la connessione XMPP persistente, affidabile e a basso consumo di batteria viene usata per risvegliare altre app compatibili con UnifiedPush, come Tusky, Ltt.rs, FluffyChat ed altre.</string>
+    <string name="pref_up_long_summary">Quando si agisce come distributore di UnifiedPush, viene usata la connessione XMPP persistente, affidabile e a basso consumo di batteria per risvegliare altre app compatibili con UnifiedPush, come Tusky, Ltt.rs, FluffyChat ed altre.</string>
     <string name="contact_list_integration_not_available">L\'integrazione dell\'elenco di contatti non è disponibile</string>
     <string name="privacy_policy">Informativa sulla privacy</string>
     <string name="no_permission_to_place_call">Autorizzazione mancante per effettuare una chiamata</string>

src/main/res/values-ja/strings.xml 🔗

@@ -39,10 +39,10 @@
     <string name="participant">参加者</string>
     <string name="visitor">訪問者</string>
     <string name="remove_contact_text">連絡先リストから%sを削除しますか? この連絡先との会話は削除されません。</string>
-    <string name="block_contact_text">%sからあなたに送信されるメッセージをブロックしますか?</string>
+    <string name="block_contact_text">%sからあなたに送信されるメッセージをブロックしますか ?</string>
     <string name="unblock_contact_text">%s のブロックを解除し、あなたにメッセージを送信できるようにしますか?</string>
-    <string name="block_domain_text">%sの連絡先をすべてブロックしますか?</string>
-    <string name="unblock_domain_text">%sのすべての連絡先のブロックを解除しますか?</string>
+    <string name="block_domain_text">%sの連絡先をすべてブロックしますか ?</string>
+    <string name="unblock_domain_text">%sのすべての連絡先のブロックを解除しますか ?</string>
     <string name="contact_blocked">連絡先をブロックしました</string>
     <string name="blocked">ブロックしました</string>
     <string name="register_account">サーバーに新規アカウントを登録</string>
@@ -69,7 +69,7 @@
     <string name="problem_connecting_to_accounts">複数のアカウントに接続できません</string>
     <string name="touch_to_fix">タップしてアカウントを管理</string>
     <string name="attach_file">ファイルを添付</string>
-    <string name="not_in_roster">連絡先が連絡先リストにありません。リストに追加しますか?</string>
+    <string name="not_in_roster">連絡先が連絡先リストにありません。リストに追加しますか ?</string>
     <string name="add_contact">連絡先を追加</string>
     <string name="send_failed">配信に失敗しました</string>
     <string name="preparing_image">送信用画像の準備中</string>
@@ -83,7 +83,7 @@
     <string name="delete_file_dialog">ファイルを削除</string>
     <string name="delete_file_dialog_msg">このファイルを削除してもよろしいですか?\n\n<b>警告:</b> これは、他のデバイスやサーバーに保存されているファイルのコピーは削除しません。</string>
     <string name="choose_presence">デバイスを選択</string>
-    <string name="send_unencrypted_message">暗号化されていないメッセージを送信</string>
+    <string name="send_unencrypted_message">暗号化されないメッセージを送信</string>
     <string name="send_message">メッセージを送信</string>
     <string name="send_message_to_x">メッセージを %s に送信</string>
     <string name="send_omemo_x509_message">v\\OMEMO 暗号化メッセージを送信</string>
@@ -108,7 +108,7 @@
     <string name="pref_accept_files_summary">自動的に小さいファイルを受取…</string>
     <string name="pref_attachments">添付ファイル</string>
     <string name="pref_notification_settings">通知</string>
-    <string name="pref_vibrate">振動</string>
+    <string name="pref_vibrate">バイブレート</string>
     <string name="pref_vibrate_summary">新着メッセージが届いたときに振動します</string>
     <string name="pref_led">LED 通知</string>
     <string name="pref_led_summary">新着メッセージが届いたときに通知ライトを点滅します</string>
@@ -121,7 +121,7 @@
     <string name="pref_advanced_options">詳細</string>
     <string name="pref_never_send_crash_summary">スタックトレースを送信すると、 開発の助けとなります</string>
     <string name="pref_confirm_messages">メッセージを確認</string>
-    <string name="pref_confirm_messages_summary">あなたがメッセージを受信して読んだときに、連絡先に知らせる</string>
+    <string name="pref_confirm_messages_summary">あなたがメッセージを受信して読んだときに、連絡先に知らせます</string>
     <string name="pref_prevent_screenshots">スクリーンショットを防ぐ</string>
     <string name="pref_prevent_screenshots_summary">アプリスイッチャー内でアプリの内容を隠し、スクリーンショットを防ぐ</string>
     <string name="pref_ui_options">UI</string>
@@ -148,7 +148,7 @@
     <string name="account_status_online">オンライン</string>
     <string name="account_status_connecting">接続中\u2026</string>
     <string name="account_status_offline">オフライン</string>
-    <string name="account_status_unauthorized">許可されていません</string>
+    <string name="account_status_unauthorized">認証されていません</string>
     <string name="account_status_not_found">サーバーが見つかりません</string>
     <string name="account_status_no_internet">接続なし</string>
     <string name="account_status_regis_fail">登録に失敗しました</string>
@@ -175,7 +175,7 @@
     <string name="unpublish_pgp_message">出席情報告知から OpenPGP 公開鍵を削除してもよろしいですか?\n連絡先はあなたに OpenPGP 暗号化メッセージを送信できなくなります。</string>
     <string name="openpgp_has_been_published">OpenPGP 公開鍵を公開しました。</string>
     <string name="mgmt_account_enable">アカウントを有効化</string>
-    <string name="mgmt_account_delete_confirm_text">アカウントを削除してよろしいですか?アカウントを削除すると会話履歴がすべて消去されます</string>
+    <string name="mgmt_account_delete_confirm_text">アカウントを削除してよろしいですか ? アカウントを削除すると会話履歴がすべて消去されます</string>
     <string name="attach_record_voice">音声を録音</string>
     <string name="account_settings_jabber_id">XMPP アドレス</string>
     <string name="block_jabber_id">XMPP アドレスをブロック</string>
@@ -272,7 +272,7 @@
     <string name="ignore">無視</string>
     <string name="without_mutual_presence_updates"><b>警告:</b> 相互の出席情報アップデートなしにこれを送信すると、予期しない問題が発生する可能性があります。\n\n<small>あなたの出席情報サブスクリプションを検証するために、“連絡先の詳細”に移動します。</small></string>
     <string name="pref_security_settings">セキュリティ</string>
-    <string name="pref_allow_message_correction">メッセージの修正を許可</string>
+    <string name="pref_allow_message_correction">メッセージの修正</string>
     <string name="pref_allow_message_correction_summary">連絡先が、遡及的に自分のメッセージを編集することを許可します</string>
     <string name="pref_expert_options">エキスパート設定</string>
     <string name="pref_expert_options_summary">ご利用には注意してください</string>
@@ -401,7 +401,7 @@
     <string name="pref_enter_is_send">Enter で送信</string>
     <string name="pref_enter_is_send_summary">メッセージの送信に Enter キーを使用します。このオプションが無効でも、常に Ctrl+Enter でメッセージを送信できます。</string>
     <string name="pref_display_enter_key">Enter キーを表示</string>
-    <string name="pref_display_enter_key_summary">絵文字キーを Enter キーに変更</string>
+    <string name="pref_display_enter_key_summary">絵文字キーを Enter キーに変更します</string>
     <string name="audio">音声</string>
     <string name="video">ビデオ</string>
     <string name="image">画像</string>
@@ -419,7 +419,7 @@
     <string name="contacts_are_typing">%s さんが入力中…</string>
     <string name="contacts_have_stopped_typing">%s さんが入力を止めました</string>
     <string name="pref_chat_states">入力中通知</string>
-    <string name="pref_chat_states_summary">あなたがメッセージを書いているときに、連絡先に知らせる</string>
+    <string name="pref_chat_states_summary">あなたがメッセージを書いているときに、連絡先に知らせます</string>
     <string name="send_location">位置を送信</string>
     <string name="show_location">位置を表示</string>
     <string name="no_application_found_to_display_location">位置を表示するアプリケーションが見つかりません</string>
@@ -436,7 +436,7 @@
     <plurals name="toast_delete_certificates">
         <item quantity="other">%d個の証明書を削除しました</item>
     </plurals>
-    <string name="pref_quick_action_summary">“送信”ボタンをクイックアクションで置き換える</string>
+    <string name="pref_quick_action_summary">“送信”ボタンをクイックアクションで置き換えます</string>
     <string name="pref_quick_action">クイックアクション</string>
     <string name="none">なし</string>
     <string name="recently_used">最近使用した</string>
@@ -458,20 +458,20 @@
     <string name="server_info_broken">壊れています</string>
     <string name="pref_presence_settings">在席状況</string>
     <string name="pref_away_when_screen_off">デバイスがロックされているときは離席</string>
-    <string name="pref_away_when_screen_off_summary">デバイスがロックされているときは離席と表示</string>
+    <string name="pref_away_when_screen_off_summary">デバイスがロックされているときは離席と表示します</string>
     <string name="pref_dnd_on_silent_mode">サイレントモードのときは取込中</string>
-    <string name="pref_dnd_on_silent_mode_summary">デバイスがサイレントモードのときは取込中と表示</string>
+    <string name="pref_dnd_on_silent_mode_summary">デバイスがサイレントモードのときは取込中と表示します</string>
     <string name="pref_treat_vibrate_as_silent">バイブレートをサイレントモードとして扱う</string>
-    <string name="pref_treat_vibrate_as_dnd_summary">デバイスがバイブレートのときは取込中と表示</string>
-    <string name="pref_show_connection_options">拡張接続設定</string>
-    <string name="pref_show_connection_options_summary">アカウントを設定するときに、ホスト名とポートの設定を表示</string>
+    <string name="pref_treat_vibrate_as_dnd_summary">デバイスがバイブレートのときは取込中と表示します</string>
+    <string name="pref_show_connection_options">ホスト名とポート番号</string>
+    <string name="pref_show_connection_options_summary">アカウントを設定するときに、拡張設定項目を表示します</string>
     <string name="hostname_example">xmpp.example.com</string>
     <string name="action_add_account_with_certificate">証明書でログイン</string>
     <string name="unable_to_parse_certificate">証明書を解析できません</string>
-    <string name="mam_prefs">アーカイブ設定</string>
-    <string name="server_side_mam_prefs">サーバー側のアーカイブ設定</string>
-    <string name="fetching_mam_prefs">アーカイブ設定を取得しています。しばらくお待ちください…</string>
-    <string name="unable_to_fetch_mam_prefs">アーカイブ設定を取得できません</string>
+    <string name="mam_prefs">保管の設定</string>
+    <string name="server_side_mam_prefs">サーバー側の保管の設定</string>
+    <string name="fetching_mam_prefs">保管の設定を取得しています。しばらくお待ちください…</string>
+    <string name="unable_to_fetch_mam_prefs">保管の設定を取得できません</string>
     <string name="captcha_required">キャプチャが要求されました</string>
     <string name="captcha_hint">上の画像からテキストを入力してください</string>
     <string name="certificate_chain_is_not_trusted">信頼できない証明書チェーン</string>
@@ -551,8 +551,8 @@
     <string name="gp_short">短</string>
     <string name="gp_medium">中</string>
     <string name="gp_long">長</string>
-    <string name="pref_broadcast_last_activity">ブロードキャストを使用</string>
-    <string name="pref_broadcast_last_activity_summary">Conversations を使用するときに、連絡先に知らせましょう</string>
+    <string name="pref_broadcast_last_activity">最後に見たのは</string>
+    <string name="pref_broadcast_last_activity_summary">最後にこのアプリを使用したのがいつかを、連絡先に知らせます</string>
     <string name="pref_privacy">プライバシー</string>
     <string name="pref_theme_options">テーマ</string>
     <string name="pref_theme_options_summary">カラーパレットの選択</string>
@@ -674,7 +674,7 @@
     <string name="once">一度だけ</string>
     <string name="qr_code_scanner_needs_access_to_camera">QR コードスキャナーはカメラにアクセスが必要です</string>
     <string name="pref_scroll_to_bottom">一番下へスクロール</string>
-    <string name="pref_scroll_to_bottom_summary">メッセージ送信後に下へスクロール</string>
+    <string name="pref_scroll_to_bottom_summary">メッセージ送信後に下へスクロールします</string>
     <string name="edit_status_message_title">ステータスメッセージを編集</string>
     <string name="edit_status_message">ステータスメッセージを編集</string>
     <string name="disable_encryption">暗号化が無効</string>
@@ -743,7 +743,7 @@
     <string name="delivery_failed_channel_name">配信に失敗</string>
     <string name="pref_message_notification_settings">メッセージ通知設定</string>
     <string name="pref_incoming_call_notification_settings">着信通話の通知設定</string>
-    <string name="pref_more_notification_settings_summary">重要性、音、振動</string>
+    <string name="pref_more_notification_settings_summary">重要性、音、バイブレーション</string>
     <string name="video_compression_channel_name">ビデオの圧縮</string>
     <string name="view_media">メディアを表示</string>
     <string name="group_chat_members">参加者</string>
@@ -768,20 +768,20 @@
     <string name="verify_x">%s を検証</string>
     <string name="we_have_sent_you_an_sms_to_x"><![CDATA[<b>%s</b>にSMSを送りました。]]></string>
     <string name="we_have_sent_you_another_sms">6桁のコードを含む別のSMSを送信しました。</string>
-    <string name="please_enter_pin_below">以下に6桁の pin を入力してください。</string>
+    <string name="please_enter_pin_below">以下に6桁の PIN を入力してください。</string>
     <string name="resend_sms">SMS 再送信</string>
     <string name="resend_sms_in">SMS 再送信(%s)</string>
     <string name="wait_x">しばらくお待ちください(%s)</string>
     <string name="back">戻る</string>
-    <string name="possible_pin">クリップボードから可能な pin を自動的に貼り付ける。</string>
-    <string name="please_enter_pin">6桁の pin を入力してください。</string>
-    <string name="abort_registration_procedure">登録手続きを中止してもよろしいのですか?</string>
+    <string name="possible_pin">クリップボードから可能な PIN を自動的に貼り付ける。</string>
+    <string name="please_enter_pin">6桁の PIN を入力してください。</string>
+    <string name="abort_registration_procedure">登録手続きを中止してもよろしいですか ?</string>
     <string name="yes">はい</string>
     <string name="no">いいえ</string>
     <string name="verifying">検証しています…</string>
     <string name="requesting_sms">SMSを要求しています…</string>
-    <string name="incorrect_pin">入力された pin が正しくありません。</string>
-    <string name="pin_expired">送信した pin の有効期限が切れています。</string>
+    <string name="incorrect_pin">入力された PIN が正しくありません。</string>
+    <string name="pin_expired">送信した PIN の有効期限が切れています。</string>
     <string name="unknown_api_error_network">未知のネットワークエラー。</string>
     <string name="unknown_api_error_response">サーバーからの不明な応答。</string>
     <string name="unable_to_connect_to_server">サーバーに接続できません。</string>
@@ -875,8 +875,8 @@
     <string name="please_enable_an_account">アカウントを有効化してください</string>
     <string name="make_call">通話をする</string>
     <string name="rtp_state_incoming_call">着信通話</string>
-    <string name="rtp_state_incoming_video_call">着信映像通話</string>
-    <string name="rtp_state_content_add_video">ビデオ通話に切り替えますか?</string>
+    <string name="rtp_state_incoming_video_call">ビデオ通話の着信</string>
+    <string name="rtp_state_content_add_video">ビデオ通話に切り替えますか ?</string>
     <string name="rtp_state_connecting">接続中</string>
     <string name="rtp_state_connected">接続しました</string>
     <string name="rtp_state_reconnecting">再接続中</string>
@@ -912,7 +912,7 @@
         <item quantity="other">%2$d人から%1$d件の不在着信</item>
     </plurals>
     <string name="audio_call">音声通話</string>
-    <string name="video_call">映像通話</string>
+    <string name="video_call">ビデオ通話</string>
     <string name="help">ヘルプ</string>
     <string name="microphone_unavailable">マイクが利用できません</string>
     <string name="only_one_call_at_a_time">1度に1回線の通話のみ。</string>
@@ -945,16 +945,16 @@
     <string name="no_application_found">アプリケーションが見つかりません</string>
     <string name="invite_to_app">Conversationsに招待</string>
     <string name="unable_to_parse_invite">招待を解析できません</string>
-    <string name="server_does_not_support_easy_onboarding_invites">サーバーは招待の作成をサポートしていません</string>
+    <string name="server_does_not_support_easy_onboarding_invites">サーバーは招待の作成に対応していません</string>
     <string name="no_active_accounts_support_this">この機能をサポートするアクティブなアカウントがありません</string>
     <string name="backup_started_message">バックアップを開始しました。 バックアップが完了すると通知が届きます。</string>
-    <string name="unable_to_enable_video">映像を有効化できません。</string>
+    <string name="unable_to_enable_video">ビデオを有効化できません。</string>
     <string name="plain_text_document">プレーンテキスト文書</string>
     <string name="account_registrations_are_not_supported">アカウント登録はサポートされていません</string>
     <string name="no_xmpp_adddress_found">XMPPアドレスがみつかりません</string>
     <string name="account_status_temporary_auth_failure">一時的な認証失敗</string>
     <string name="delete_avatar">アバターを削除</string>
-    <string name="audio_video_disabled_tor">Tor使用中のため通話できません</string>
+    <string name="audio_video_disabled_tor">Tor 使用中のため通話できません</string>
     <string name="switch_to_video">ビデオ通話へ切替</string>
     <string name="this_account_is_logged_out">このアカウントをログアウトしました</string>
     <string name="action_directions">ルート</string>
@@ -993,9 +993,48 @@
     <string name="contact_uses_unverified_keys">連絡先は未検証のデバイスを使用しています。 QR コードをスキャンして検証を実行し、アクティブな MITM 攻撃を阻止してください。</string>
     <string name="unverified_devices">未検証のデバイスを使用しています。他のデバイスで QR コードをスキャンして検証を実行し、アクティブな MITM 攻撃を阻止してください。</string>
     <string name="no_permission_to_place_call">電話をかける権限がありません</string>
-    <string name="remove_bookmark_and_close">%s のブックマークを削除して会話を閉じますか ?</string>
+    <string name="remove_bookmark_and_close">%s のブックマークを削除して会話を保管しますか ?</string>
     <string name="remove_bookmark">%s のブックマークを削除しますか ?</string>
     <string name="call_integration_not_available">通話の統合は利用できません。</string>
     <string name="rtp_state_contact_offline">連絡先は利用できません</string>
-    <string name="delete_and_close">削除して閉じる</string>
+    <string name="delete_and_close">削除して会話を保管</string>
+    <string name="pref_send_crash_reports">クラッシュ報告を送信</string>
+    <string name="corresponding_chats_closed">対応する会話は保管されました。</string>
+    <string name="no_certificate_selected">クライアント証明書が選択されていません。</string>
+    <string name="pref_connection_summary">ホスト名とポート番号、Tor</string>
+    <string name="pref_category_on_this_device">このデバイスで</string>
+    <string name="pref_notifications_summary">猶予期間、着信音、バイブレーション、見知らぬ人</string>
+    <string name="pref_summary_security">端末間暗号化、検証前の無条件の信頼、MITM検出</string>
+    <string name="action_archive_chat">会話を保管</string>
+    <string name="title_activity_new_chat">新しい会話</string>
+    <string name="archive_this_chat">この会話を保管</string>
+    <string name="pref_dynamic_colors_summary">システムの配色 (Material You)</string>
+    <string name="send_encrypted_message">暗号化メッセージを送信</string>
+    <string name="title_undo_swipe_out_chat">会話は保管されました</string>
+    <string name="pref_use_colorful_bubbles">多彩な会話の吹き出し</string>
+    <string name="switch_to_chat">会話に切り替え</string>
+    <string name="pref_title_interface">インターフェース</string>
+    <string name="pref_summary_appearance">テーマ、配色、スクリーンショット、入力</string>
+    <string name="appearance">外観</string>
+    <string name="pref_light_dark_mode">明暗モード</string>
+    <string name="pref_allow_screenshots">スクリーンショットを許可</string>
+    <string name="pref_allow_screenshots_summary">アプリのスイッチャーでアプリのコンテンツを表示し、スクリーンショットの撮影を許可します</string>
+    <string name="pref_title_security">セキュリティ</string>
+    <string name="pref_category_e2ee">端末間暗号化</string>
+    <string name="pref_title_trust_system_ca_store">認証局</string>
+    <string name="notifications">通知</string>
+    <string name="pref_attachments_summary">ファイルサイズ、画像圧縮、ビデオ品質</string>
+    <string name="pref_automatic_download">自動ダウンロード</string>
+    <string name="pref_category_receiving">受信時</string>
+    <string name="pref_category_sending">送信時</string>
+    <string name="start_chat">会話を開始</string>
+    <string name="pref_category_application">アプリケーション</string>
+    <string name="pref_category_operating_system">オペレーティングシステム</string>
+    <string name="pref_keyboard_options">キーボード</string>
+    <string name="pref_category_server_connection">サーバー接続</string>
+    <string name="pref_title_trust_system_ca_store_summary">システムの CA を信頼します</string>
+    <string name="pref_connection_summary_w_cd">ホスト名とポート番号、Tor、チャンネル探索</string>
+    <string name="pref_use_colorful_bubbles_summary">送受信メッセージの個別の背景色</string>
+    <string name="pref_large_font_summary">メッセージの吹き出しのフォントサイズを大きくする</string>
+    <string name="pref_large_font">大きなフォント</string>
 </resources>

src/main/res/values-nl/strings.xml 🔗

@@ -798,4 +798,8 @@
     <string name="plain_text_document">Onversleuteld document</string>
     <string name="account_registrations_are_not_supported">Accountregistraties zijn niet ondersteund</string>
     <string name="pref_never_send_crash_summary">Door crashrapportages te versturen help je de ontwikkeling</string>
+    <string name="action_add_account_with_certificate">Inloggen met certificaat</string>
+    <string name="action_archive_chat">Chat archiveren</string>
+    <string name="unable_to_connect_to_keychain">Kan niet verbinden met OpenKeychain</string>
+    <string name="start_chat">Chat starten</string>
 </resources>

src/main/res/values-pl/strings.xml 🔗

@@ -65,7 +65,7 @@
     <string name="save">Zapisz</string>
     <string name="ok">Ok</string>
     <string name="crash_report_title">%1$s uległo awarii</string>
-    <string name="crash_report_message">Używając swojego konta XMPP do wysyłania śladów stosu pomagasz w rozwoju %1$s.</string>
+    <string name="crash_report_message">Używanie Twojego konta XMPP do wysyłania śladów stosu pomaga w ciągłym rozwoju %1$s.</string>
     <string name="send_now">Wyślij teraz</string>
     <string name="send_never">Nie pytaj ponownie</string>
     <string name="problem_connecting_to_account">Nie można połączyć z kontem</string>
@@ -122,7 +122,7 @@
     <string name="pref_notification_grace_period">Czas bez powiadomień</string>
     <string name="pref_notification_grace_period_summary">Długość czasu kiedy powiadomienia są uśpione po wykryciu aktywności na jednym z twoich innych urządzeń.</string>
     <string name="pref_advanced_options">Zaawansowane</string>
-    <string name="pref_never_send_crash_summary">Wysyłając nam ślady stosu pomagasz w rozwoju</string>
+    <string name="pref_never_send_crash_summary">Wysyłając ślady stosu pomagasz w rozwoju</string>
     <string name="pref_confirm_messages">Potwierdzenia wiadomości</string>
     <string name="pref_confirm_messages_summary">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</string>
     <string name="pref_prevent_screenshots">Zapobiegaj zrzutom ekranu</string>
@@ -527,9 +527,9 @@
     <string name="large_images_only">Tylko duże obrazki</string>
     <string name="battery_optimizations_enabled">Optymalizacje zużycia baterii włączone</string>
     <string name="battery_optimizations_enabled_explained">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.</string>
-    <string name="battery_optimizations_enabled_dialog">Twoje urządzenie stosuje agresywne oszczędzanie baterii, przez co %1$s może odbierać wiadomości z opóźnieniem lub je tracić.
+    <string name="battery_optimizations_enabled_dialog">Twoje urządzenie stosuje agresywne oszczędzanie baterii, przez co %1$s może odbierać wiadomości z opóźnieniem lub nawet je tracić.
 \n
-\nZostaniesz poproszony o jego wyłączenie.</string>
+\nZostaniesz poproszony o wyłączenie tej funkcjonalności.</string>
     <string name="disable">Wyłącz</string>
     <string name="selection_too_large">Zaznaczony obszar jest zbyt duży</string>
     <string name="no_accounts">(Brak aktywynych kont)</string>
@@ -1048,7 +1048,7 @@
     <string name="action_archive_chat">Archiwizuj rozmowę</string>
     <string name="title_activity_new_chat">Nowa rozmowa</string>
     <string name="archive_this_chat">Archiwizuj tę rozmowę</string>
-    <string name="pref_use_colorful_bubbles">Kolorowe bańki rozmowy</string>
+    <string name="pref_use_colorful_bubbles">Kolorowe dymki rozmowy</string>
     <string name="barcode_does_not_contain_fingerprints_for_this_chat">Kod kreskowy nie zawiera odcisków palca dla tej rozmowy.</string>
     <string name="corresponding_chats_closed">Powiązane rozmowy zarchiwizowane.</string>
     <string name="start_chat">Rozpocznij rozmowę</string>
@@ -1091,7 +1091,7 @@
     <string name="pref_category_application">Aplikacja</string>
     <string name="pref_category_interaction">Interakcja</string>
     <string name="pref_category_on_this_device">Na urządzeniu</string>
-    <string name="pref_large_font_summary">Zwiększ rozmiar czcionki w bańkach wiadomości</string>
+    <string name="pref_large_font_summary">Zwiększ rozmiar czcionki w dymkach wiadomości</string>
     <string name="pref_use_colorful_bubbles_summary">Różne kolory tła dla wysłanych i odebranych wiadomości</string>
     <string name="send_encrypted_message">Wyślij zaszyfrowaną wiadomość</string>
     <string name="welcome_header">Dołącz do rozmowy</string>

src/main/res/values-ro-rRO/strings.xml 🔗

@@ -1016,7 +1016,7 @@
     <string name="this_account_is_logged_out">V-ați deconectat de la acest cont</string>
     <string name="log_in">Conectați-vă</string>
     <string name="hide_notification">Ascunde notificare</string>
-    <string name="contact_uses_unverified_keys">Persoana de contact utilizează dispozitive neverificate. Scanați codul QR al acestora pentru a efectua verificarea și a împiedica atacurile MITM active.</string>
+    <string name="contact_uses_unverified_keys">Persoana de contact utilizează dispozitive neverificate. Scanați codul QR al acesteia pentru a efectua verificarea și a împiedica atacurile MITM active.</string>
     <string name="log_out">Deconectare</string>
     <string name="account_state_logged_out">Deconectat</string>
     <string name="unverified_devices">Folosiți dispozitive neverificate. Scanați codul QR pe celelalte dispozitive pentru a efectua verificarea și a împiedica atacurile MITM active.</string>

src/main/res/values-sq-rAL/strings.xml 🔗

@@ -1022,7 +1022,7 @@
     <string name="remove_bookmark_and_close">Doni të hiqet faqerojtësi për %s dhe arkivohet fjalosja?</string>
     <string name="delete_and_close">Fshije &amp; Arkivoje fjalosjen</string>
     <string name="remove_bookmark">Doni të hiqet faqerojtësi për %s?</string>
-    <string name="pref_use_colorful_bubbles_summary">Flluska të ngjyrosura fjalosjeje ndihmojnë të dalloni mesazhe të dërguar dhe të marrë</string>
+    <string name="pref_use_colorful_bubbles_summary">Ngjyra të dallueshme sfondi për mesazhe të dërguar dhe të marrë</string>
     <string name="pref_dynamic_colors">Ngjyra dinamike</string>
     <string name="pref_dynamic_colors_summary">Ngjyra Sistemi (Material You)</string>
     <string name="possible_pin">U ngjit automatikisht nga e papastra PIN i mundshëm.</string>
@@ -1074,4 +1074,8 @@
     <string name="pref_up_long_summary">Kur veprohet si një Distributor UnifiedPush, për të zgjuar aplikacion tjetër të përputhshëm me UnifiedPush , fjala vjen, Tusky, Ltt.rs, FluffyChat, etj, do të përdoret lidhja XMPP, e vazhdueshme, e qëndrueshme dhe miqësore ndaj baterisë.</string>
     <string name="unified_push_distributor">Distributor UnifiedPush</string>
     <string name="send_encrypted_message">Dërgo mesazh të fshehtëzuar</string>
+    <string name="pref_accept_invites_from_strangers">Ftesa nga të panjohur</string>
+    <string name="pref_accept_invites_from_strangers_summary">Prano ftesa për fjalosje në grup nga të panjohur</string>
+    <string name="pref_large_font">Shkronja të mëdha</string>
+    <string name="pref_large_font_summary">Rrit madhësi shkronja në flluska mesazhesh</string>
 </resources>

src/main/res/values-tr-rTR/strings.xml 🔗

@@ -26,8 +26,8 @@
     <string name="minute_ago">1 dakika önce</string>
     <string name="minutes_ago">%d dakika önc</string>
     <plurals name="x_unread_conversations">
-        <item quantity="one">%d okunmamış konuşma</item>
-        <item quantity="other">%d okunmamış konuşmalar</item>
+        <item quantity="one">%d okunmamış sohbet</item>
+        <item quantity="other">%d okunmamış sohbet</item>
     </plurals>
     <string name="sending">gönderiyor…</string>
     <string name="message_decrypting">İleti deşifre ediliyor. Lütfen bekleyin…</string>
@@ -39,7 +39,7 @@
     <string name="moderator">Moderatör</string>
     <string name="participant">Katılımcı</string>
     <string name="visitor">Ziyaretçi</string>
-    <string name="remove_contact_text">%s adlı kişiyi listenizden çıkarmak ister misiniz? Bu kişi ile olan konuşmalar silinmeyecektir.</string>
+    <string name="remove_contact_text">%s adlı kişiyi listenizden çıkarmak ister misiniz? Bu kişi ile olan sohbet silinmeyecektir.</string>
     <string name="block_contact_text">%s kişisinin size ileti göndermesini engellemek istiyor musunuz?</string>
     <string name="unblock_contact_text">%s kişisinin size ileti  göndermesine koyduğunuz engellemeyi kaldırmak ve size ileti göndermesine izin vermek istiyor musunuz?</string>
     <string name="block_domain_text">%s üzerinden gelen tüm kişileri engellemek istiyor musunuz? </string>
@@ -77,7 +77,7 @@
     <string name="preparing_images">Görüntüler gönderilmeye hazırlanılıyor</string>
     <string name="sharing_files_please_wait">Dosyalar Paylaşılıyor. Lütfen bekleyin…</string>
     <string name="action_clear_history">Geçmişi sil</string>
-    <string name="clear_conversation_history">Konuşma geçmişini sil</string>
+    <string name="clear_conversation_history">Sohbet geçmişini sil</string>
     <string name="clear_histor_msg">Bu konuşmadaki tüm mesajları silmek istiyor musunuz? 
 \n 
 \n<b>Uyarı:</b> Bu eylem, diğer aygıt ve sunucularda kayıtlı mesajları etkilemeyecektir.</string>
@@ -176,7 +176,7 @@
     <string name="unpublish_pgp_message">OpenPGP genel anahtarınız Çevrim içi durum anonsunuzdan kaldırmak istediğinizden emin misiniz?\nArtık kişileriniz size şifrelenmiş OpenPGP mesajları gönderemeyecek.</string>
     <string name="openpgp_has_been_published">OpenPGP genel anahtarı yayınlandı.</string>
     <string name="mgmt_account_enable">Hesabı etkinleştir</string>
-    <string name="mgmt_account_delete_confirm_text">Hesabınızı silmekten emin misiniz? Hesabınızın silinmesi bütün konuşma geçmişinizi siler</string>
+    <string name="mgmt_account_delete_confirm_text">Hesabınızı silmek istediğinizden emin misiniz? Bütün sohbetleriniz silinecektir</string>
     <string name="attach_record_voice">Ses kaydet</string>
     <string name="account_settings_jabber_id">XMPP adresi</string>
     <string name="block_jabber_id">XMPP adresini engelle</string>
@@ -277,7 +277,7 @@
     <string name="ignore">Yok say</string>
     <string name="without_mutual_presence_updates"><b>Uyarı:</b> Bu mesajı karşılıklı çevrim içi durum bildirimleri olmadan göndermek, beklenmeyen problemlere neden olabilir. <small>\n\n Çevrim içi durum aboneliklerini doğrulamak için \"Kişi Bilgileri\" kısmına gidin.</small></string>
     <string name="pref_security_settings">Güvenlik</string>
-    <string name="pref_allow_message_correction">İleti düzeltmeye izin ver</string>
+    <string name="pref_allow_message_correction">Mesaj düzeltme</string>
     <string name="pref_allow_message_correction_summary">Kişilerinizin geçmiş iletilerini düzeltmelerine izin ver</string>
     <string name="pref_expert_options">Uzman seçenekleri</string>
     <string name="pref_expert_options_summary">Lütfen dikkatli olun</string>
@@ -313,8 +313,8 @@
     <string name="jabber_id_copied_to_clipboard">XMPP adresi panoya kopyalandı</string>
     <string name="error_message_copied_to_clipboard">Hata mesajı panoya kopyalandı</string>
     <string name="web_address">web adresi</string>
-    <string name="scan_qr_code">2B Barkod Tara</string>
-    <string name="show_qr_code">2B Barkod Göster</string>
+    <string name="scan_qr_code">QR Kodu tara</string>
+    <string name="show_qr_code">QR Kodu göster</string>
     <string name="show_block_list">Engellenenler listesini göster</string>
     <string name="account_details">Hesap bilgileri</string>
     <string name="confirm">Doğrula</string>
@@ -357,7 +357,7 @@
     <string name="clear_other_devices_desc">OMEMO bildirimindeki diğer aygıtların hepsini silmek istediğinizden emin misiniz? Aygıtlarınız yeniden bağlandıklarında kendilerini yeniden bildirecekler ama bu süre zarfındaki iletileri alamayabilirler.</string>
     <string name="error_no_keys_to_trust_server_error">Bu kişi için kullanılabilecek bir anahtar bulunmuyor.\nSunucudan yeni anahtarlar alınamıyor. Belki bağlantınızın sunucusunda bir sorun vardır?</string>
     <string name="error_no_keys_to_trust_presence">Bu kişi için kullanılabilir bir anahtar yok.
-\nİkinizin de çevrim içi durum aboneliği oldudğundan emin olun.</string>
+\nİkinizin de çevrimiçi durum aboneliği olduğundan emin olun.</string>
     <string name="error_trustkeys_title">Bir şeyler ters gitti</string>
     <string name="fetching_history_from_server">Sunucudan geçmiş alınıyor</string>
     <string name="no_more_history_on_server">Sunucuda başka geçmiş kalmadı</string>
@@ -470,8 +470,8 @@
     <string name="pref_dnd_on_silent_mode_summary">Telefonunuz sessizdeyken, durum bildiriminizi müsait değil olarak gösterir.</string>
     <string name="pref_treat_vibrate_as_silent">Titreşim kipini sessiz kip olarak değerlendir</string>
     <string name="pref_treat_vibrate_as_dnd_summary">Telefonunuz titreşimdeyken, durum bildiriminizi müsait değil olarak gösterir.</string>
-    <string name="pref_show_connection_options">Genişletilmiş bağlantı seçenekleri</string>
-    <string name="pref_show_connection_options_summary">Hesap oluştururken sunucu adıyla bağlantı noktası seçeneğini göster</string>
+    <string name="pref_show_connection_options">Barındırıcı adı ve Port</string>
+    <string name="pref_show_connection_options_summary">Hesap oluştururken gelişmiş bağlantı ayarlarını göster</string>
     <string name="hostname_example">xmpp.ornek.com</string>
     <string name="action_add_account_with_certificate">Sertifika ile giriş yap</string>
     <string name="unable_to_parse_certificate">Sertifika çözümlenemedi</string>
@@ -507,11 +507,10 @@
     <string name="shared_text_with_x">Metin %s ile paylaşıldı</string>
     <string name="no_storage_permission">%1$s\'ın harici depolama erişimine izin ver</string>
     <string name="no_camera_permission">%1$s\'ın kamera erişimine izin ver</string>
-    <string name="sync_with_contacts">Kişilerle senkronize et</string>
-    <string name="sync_with_contacts_long">%1$s XMPP listenizi telefon rehberinizle eşleştirmek için izin istiyor.
-\nBöylelikle, tüm rehberinizindeki kişilerin tam adları ve avatarlarını görebileceksiniz.
+    <string name="sync_with_contacts">Kişiler entegrasyonu</string>
+    <string name="sync_with_contacts_long">%1$s kişilerinizi yerel olarak, cihazınızda, XMPP\'de bulunan kişilerinizin isimlerini ve profil fotoğraflarını göstermek için işler.
 \n
-\n%1$s kişilerinizi sunucunuza yüklemeyecek olup, sadece cihazınız üzerinden eşleştirme yapacaktır.</string>
+\nKişileriniz hakkında hiçbir bilgi cihazınızdan ayrılmaz!</string>
     <string name="notify_on_all_messages">Tüm iletilerde uyar</string>
     <string name="notify_only_when_highlighted">Yalnızca bahsedilğinde haber ver</string>
     <string name="notify_never">Uyarılar devre dışı</string>
@@ -529,13 +528,14 @@
     <string name="this_field_is_required">Bu alan zorunludur</string>
     <string name="correct_message">ileti düzelt</string>
     <string name="send_corrected_message">Düzeltilmiş iletiyi gönder</string>
-    <string name="no_keys_just_confirm">Bu kişinin güvenini doğrulamak için parmak izini zaten güvenle onayladınız. \"Tamam\"ı seçerek sadece%s kişisinin, grup konuşmasının bir parçası olduğunu doğruluyorsunuz.</string>
+    <string name="no_keys_just_confirm">Bu kişinin parmak izine zaten güvenmiştiniz. \"Tamam\"ı seçerek sadece %s\'in bu grupta olduğunu onaylamış olacaksınız.</string>
     <string name="this_account_is_disabled">Bu hesabı devre dışı bıraktınız</string>
     <string name="security_error_invalid_file_access">Güvenlik hatası: Geçersiz dosya erişimi!</string>
     <string name="no_application_to_share_uri">URL paylaşacak uygulama bulunamadı</string>
     <string name="share_uri_with">URI paylaş ile…</string>
     <string name="agree_and_continue">Kabul et ve devam et</string>
-    <string name="magic_create_text">Conversations\'da hesap kurulum için bir rehber hazırlanmıştır.¹\nConversations.im\'i bir sağlayıcı olarak seçtikten sonra başka sağlayıcılar kullanan kullanıcılarla onlara tam XMPP adresinizi vererek iletişim kurabilirsiniz.</string>
+    <string name="magic_create_text">Conversations.im üzerinde hesap oluşturma rehberi bulunmaktadır. 
+\nConversations.im\'i sağlayıcınız olarak seçtiğinizde diğer sağlayıcıların kullanıcılarıyla tam XMPP adresinizi paylaşarak iletişim kurabileceksiniz.</string>
     <string name="your_full_jid_will_be">Tam XMPP adresiniz %s olacak</string>
     <string name="create_account">Hesap Oluştur</string>
     <string name="use_own_provider">Kendi sağlayıcımı kullan</string>
@@ -559,8 +559,8 @@
     <string name="gp_short">Kısa</string>
     <string name="gp_medium">Orta</string>
     <string name="gp_long">Uzun</string>
-    <string name="pref_broadcast_last_activity">Kullanımı yayınla</string>
-    <string name="pref_broadcast_last_activity_summary">Kişiler Conversations\'ı kullandığınız zaman bundan haberdar olur.</string>
+    <string name="pref_broadcast_last_activity">Son görülme</string>
+    <string name="pref_broadcast_last_activity_summary">Kişilerinizin uygulamayı en son ne zaman kullandığınızı görmesine izin verin</string>
     <string name="pref_privacy">Gizlilik</string>
     <string name="pref_theme_options">Gövde</string>
     <string name="pref_theme_options_summary">Renk paletini seçin</string>
@@ -607,7 +607,7 @@
     <string name="pref_blind_trust_before_verification_summary">Doğrulanmamış kişilerin tüm yani aygıtlarına güven, ama doğrulanmış kişilerden aygıtlarını onaylamalarını iste.</string>
     <string name="blindly_trusted_omemo_keys">OMEMO anahtarlarına körlemisne güvenildi, yani bu kişi bir başkası olabilir veya biri konuşmayı dinleyebilir.</string>
     <string name="not_trusted">Güvenilmeyen</string>
-    <string name="invalid_barcode">Geçersiz 2D barkod</string>
+    <string name="invalid_barcode">Geçersiz QR Kodu</string>
     <string name="pref_clean_cache_summary">Önbellek dizinini temizle (Kamera uygulamasının kullandığı)</string>
     <string name="pref_clean_cache">Önbelleği temizle</string>
     <string name="pref_clean_private_storage">Özel depolama alanını temizle</string>
@@ -696,14 +696,14 @@
     <string name="error_trustkey_device_list">Aygıt listesi alınamadı</string>
     <string name="error_trustkey_bundle">Şifreleme anahtarları alınamadı</string>
     <string name="error_trustkey_hint_mutual">İpucu: Kimi durumlarda bu sorun, birbirinizi kişi listenize eklemenizle çözülebilir.</string>
-    <string name="disable_encryption_message">Bu konuşma için OMEMO şifrelemesini devre dışı bırakmak istediğinizden emin misiniz?
-\nBu, sunucu yöneticinizin mesajlarınızı okumasını mümkün kılsa da, tarihi geçmiş istemcileri kullanan insanlarla iletişim kurmanın tek yolu olabilir.</string>
+    <string name="disable_encryption_message">Bu sohbet için OMEMO şifrelemesini devre dışı bırakmak istediğinizden emin misiniz?
+\nBunu yapmanız sunucu yöneticinizin mesajlarınızı okumasına izin verecektir, fakat güncel olmayan istemcileri kullanan kişilerle konuşmanın tek yolu bu olabilir.</string>
     <string name="disable_now">Şimdi devre dışı bırak</string>
     <string name="draft">Taslak:</string>
     <string name="pref_omemo_setting">OMEMO Şifrelemesi</string>
     <string name="pref_omemo_setting_summary_always">Bire bir ve grup konuşmalarında her zaman OMEMO kullanılacak.</string>
-    <string name="pref_omemo_setting_summary_default_on">Yeni konuşmalarda OMEMO varsayılan olarak kullanılacak.</string>
-    <string name="pref_omemo_setting_summary_default_off">Özellikle yeni konuşmalarda OMEMO aktif hale getirilecek.</string>
+    <string name="pref_omemo_setting_summary_default_on">Yeni sohbetlerde OMEMO varsayılan olarak kullanılacak.</string>
+    <string name="pref_omemo_setting_summary_default_off">Yeni sohbetlerde OMEMO\'nun el ile aktifleştirilmesi gerekecektir.</string>
     <string name="create_shortcut">Kısayol oluştur</string>
     <string name="default_on">Varsayılan olarak aktif</string>
     <string name="default_off">Varsayılan olarak devre dışı</string>
@@ -724,14 +724,14 @@
     <string name="no_microphone_permission">%1$s\'ın mikrofon erişimine izin ver</string>
     <string name="search_messages">İleti ara</string>
     <string name="gif">GIF</string>
-    <string name="view_conversation">Konuşma görüntüle</string>
+    <string name="view_conversation">Sohbeti görüntüle</string>
     <string name="pref_use_share_location_plugin">Konum Eklentisini Paylaş</string>
     <string name="pref_use_share_location_plugin_summary">Varolan harita yerine Konum Eklentisini Paylaş\'ı kullan</string>
     <string name="copy_link">Web adresini kopyala</string>
     <string name="copy_jabber_id">XMPP adresini kopyala</string>
     <string name="p1_s3_filetransfer">S3 için HTTP Dosya Paylaşımı</string>
     <string name="pref_start_search">Doğrudan arama</string>
-    <string name="pref_start_search_summary">\'Konuşma Başlat\" ekranında klavyeyi aç ve arama kısmına imleci getir</string>
+    <string name="pref_start_search_summary">\"Yeni sohbet\" ekranında klavyeyi arama kutusunda aç</string>
     <string name="group_chat_avatar">Grup konuşması avatarı</string>
     <string name="host_does_not_support_group_chat_avatars">Yönetici grup konuşması avatarlarını desteklemiyor</string>
     <string name="only_the_owner_can_change_group_chat_avatar">Yalnızca yönetici grup konuşması avatarını değiştirebilir</string>
@@ -783,13 +783,13 @@
     <string name="verify_x">%s doğrula</string>
     <string name="we_have_sent_you_an_sms_to_x"><![CDATA[<b>%s</b> telefonunuza bir SMS gönderdik.]]></string>
     <string name="we_have_sent_you_another_sms">Size 6 haneli kodun olduğu başka bir SMS  gönderdik.</string>
-    <string name="please_enter_pin_below">Lüfen aşağıya 6 haneli kodu girin.</string>
+    <string name="please_enter_pin_below">Lütfen aşağıya 6 haneli kodu girin.</string>
     <string name="resend_sms">Tekrar sms gönder</string>
     <string name="resend_sms_in">Tekrar sms gönder (%s)</string>
     <string name="wait_x">Lütfen bekleyin  (%s)</string>
-    <string name="back">geri</string>
-    <string name="possible_pin">Olası kod, otomatik olarak panodan yapıştırıldı.</string>
-    <string name="please_enter_pin">Lütfen 6 haneli kodu girin.</string>
+    <string name="back">Geri</string>
+    <string name="possible_pin">Olası kod, otomatik olarak yapıştırıldı.</string>
+    <string name="please_enter_pin">Lütfen 6 haneli kodunuzu girin.</string>
     <string name="abort_registration_procedure">Kayıt sürecini iptal etmek istediğinizden emin misiniz?</string>
     <string name="yes">Evet</string>
     <string name="no">Hayır</string>
@@ -940,8 +940,8 @@
     <string name="remove_from_favorites">En baştan kaldır</string>
     <string name="gpx_track">GPX izi</string>
     <string name="could_not_correct_message">İleti düzeltilemedi</string>
-    <string name="search_all_conversations">Bütün konuşmalar</string>
-    <string name="search_this_conversation">Bu konuşma</string>
+    <string name="search_all_conversations">Tüm sohbetler</string>
+    <string name="search_this_conversation">Bu sohbet</string>
     <string name="your_avatar">Avatarınız</string>
     <string name="avatar_for_x">%s avatarı</string>
     <string name="encrypted_with_omemo">OMEMO ile şifrelendi</string>
@@ -978,4 +978,92 @@
     <string name="switch_to_video">Videoya geç</string>
     <string name="reject_switch_to_video">Videoya geçme isteğini reddet</string>
     <string name="pref_up_push_account_title">XMPP Hesabı</string>
+    <string name="account_state_logged_out">Oturum kapatıldı</string>
+    <string name="pref_up_push_server_title">Bildirim Sunucusu</string>
+    <string name="pref_send_crash_reports">Çökme raporları gönder</string>
+    <string name="welcome_header">Sohbete Katıl</string>
+    <string name="welcome_header_quicksy">Quicksy\'e hoşgeldiniz!</string>
+    <string name="restore_warning_continued">Kendiniz oluşturmadığınız yedekleri geri yüklemeye çalışmayın!</string>
+    <string name="log_out">Oturumu kapat</string>
+    <string name="hide_notification">Bildirimi gizle</string>
+    <string name="report_spam">Spam bildir</string>
+    <string name="no_certificate_selected">İstemci sertifikası seçilmedi!</string>
+    <string name="log_in">Oturum aç</string>
+    <string name="pref_title_interface">Arayüz</string>
+    <string name="pref_summary_appearance">Tema, Renkler, Ekran Görüntüleri, Girdi</string>
+    <string name="pref_light_dark_mode">Açık/Koyu tema</string>
+    <string name="pref_privacy_summary">Yazıyor bildirimi, Son görülme, Müsaitlik</string>
+    <string name="pref_connection_summary">Barındırıcı adı ve Portu, Tor</string>
+    <string name="pref_connection_summary_w_cd">Barındırıcı adı ve Portu, Tor, Kanal Keşfetme</string>
+    <string name="contact_list_integration_not_available">Kişiler entegrasyonu mevcut değil</string>
+    <string name="privacy_policy">Gizlilik politikası</string>
+    <string name="title_activity_share_with">Şununla paylaş…</string>
+    <string name="incoming_call_duration_timestamp">Gelen arama (%s) · %s</string>
+    <string name="outgoing_call_duration_timestamp">Giden arama (%s) · %s</string>
+    <string name="outgoing_call_timestamp">Giden arama · %s</string>
+    <string name="unverified_devices">Doğrulanmamış bir cihaz kullanıyorsunuz. Aktif MITM (Ortadaki Adam) saldırılarını önlemek için diğer cihazlarınızda QR kodunu okutarak doğrulama yapın.</string>
+    <string name="report_spam_and_block">Spam bildir ve kişiyi engelle</string>
+    <string name="delete_and_close">Sohbeti sil ve arşivle</string>
+    <string name="start_chat">Sohbet başlat</string>
+    <string name="contact_uses_unverified_keys">Kişiniz doğrulanmamış bir cihaz kullanıyor. Aktif MITM (Ortadaki Adam) saldırılarını önlemek için kişinizin QR kodunu okutarak doğrulama yapın.</string>
+    <string name="call_integration_not_available">Arama entegrasyonu mevcut değil!</string>
+    <string name="action_archive_chat">Sohbeti arşivle</string>
+    <string name="archive_this_chat">Bu sohbeti arşivle</string>
+    <string name="switch_to_chat">Sohbete git</string>
+    <string name="pref_up_push_account_summary">Bildirimlerin alınacağı hesap.</string>
+    <string name="unified_push_distributor">UnifiedPush dağıtıcısı</string>
+    <string name="no_account_deactivated">Hiçbiri (devre dışı)</string>
+    <string name="title_activity_new_chat">Yeni sohbet</string>
+    <string name="rtp_state_content_add_video">Görüntülü aramaya geçilsin mi?</string>
+    <string name="pref_allow_screenshots_summary">Uygulama değiştiricide içeriği göster ve ekran görüntüsü almaya izin ver</string>
+    <string name="remove_bookmark_and_close">%s için yer imini kaldırmak ve sohbeti arşivlemek ister misiniz?</string>
+    <string name="save_as_group_chat">Grup olarak kaydet</string>
+    <string name="audiobook">Sesli kitap</string>
+    <string name="send_encrypted_message">Şifrelenmiş ileti gönder</string>
+    <string name="group_chats">Gruplar</string>
+    <string name="quicksy_wants_your_consent">Quicksy, verilerinizi kullanmak için izninizi istiyor</string>
+    <string name="this_account_is_logged_out">Bu hesabın oturumunu kapatmışsınız</string>
+    <string name="pref_use_colorful_bubbles">Renkli konuşma balonları</string>
+    <string name="pref_use_colorful_bubbles_summary">Gönderilen ve alınan mesajlar için farklı renkler kullan</string>
+    <string name="no_permission_to_place_call">Arama yapma izni yok</string>
+    <string name="corresponding_chats_closed">Uygun gelen sohbetler arşivlendi</string>
+    <string name="pref_dynamic_colors">Dinamik renklendirme</string>
+    <string name="pref_dynamic_colors_summary">Sistem renkleri (Material You)</string>
+    <string name="rtp_state_contact_offline">Kişi müsait değil</string>
+    <string name="decline">Reddet</string>
+    <string name="delete_from_server">Hesabı sunucudan sil</string>
+    <string name="could_not_delete_account_from_server">Hesap sunucudan silinemedi</string>
+    <string name="pref_title_security">Güvenlik</string>
+    <string name="unified_push_summary">UnifiedPush destekleyen üçüncü parti uygulamalar için bildirim aktarıcısı</string>
+    <string name="notifications">Bildirimler</string>
+    <string name="pref_attachments_summary">Dosya büyüklüğü, Resim sıkıştırma, Video kalitesi</string>
+    <string name="pref_notifications_summary">Yok sayma süresi, Zil sesi, Titreşim, Yabancılar</string>
+    <string name="pref_category_sending">Gönderme</string>
+    <string name="pref_category_receiving">Teslim alma</string>
+    <string name="pref_automatic_download">Otomatik indirme</string>
+    <string name="appearance">Görünüm</string>
+    <string name="pref_allow_screenshots">Ekran görüntüsü almaya izin ver</string>
+    <string name="pref_category_e2ee">Uçtan uca şifreleme</string>
+    <string name="pref_title_trust_system_ca_store">Sertifika yetkilileri</string>
+    <string name="pref_title_trust_system_ca_store_summary">Sistemin CA sertifikalarına güven</string>
+    <string name="detect_mim">Kanal bağlamayı zorunlu kıl</string>
+    <string name="detect_mim_summary">Kanal bağlama bazı aradaki makine saldırılarını tespit edebilir</string>
+    <string name="pref_category_server_connection">Sunucu bağlantısı</string>
+    <string name="pref_category_operating_system">İşletim Sistemi</string>
+    <string name="pref_keyboard_options">Klavye</string>
+    <string name="pref_category_application">Uygulama</string>
+    <string name="pref_category_interaction">Etkileşim</string>
+    <string name="pref_category_on_this_device">Bu Cihazda</string>
+    <string name="pref_large_font">Büyük punto</string>
+    <string name="pref_large_font_summary">Konuşma balonlarındaki yazı büyüklüğünü arttır</string>
+    <string name="remove_bookmark">%s için yer imini kaldırmak ister misiniz?</string>
+    <string name="pref_autojoin_summary">Bir gruba katılırken \"autojoin\" (otomatik katıl) işaretini ayarla ve diğer istemciler tarafından yapılan değişikliklere tepki ver.</string>
+    <string name="title_undo_swipe_out_chat">Sohbet arşivlendi</string>
+    <string name="search_group_chats">Grup ara</string>
+    <string name="barcode_does_not_contain_fingerprints_for_this_chat">Barkod, bu sohbet için parmak izi bilgisi barındırmıyor.</string>
+    <string name="channel_discover_opt_in_message">Grup keşfetme &lt;a href=https://search.jabber.network&gt;search.jabber.network&lt;/a&gt; adlı bir hizmeti kullanır.&lt;br&gt;&lt;br&gt;Bu özelliği kullanmanız IP adresinizi ve aramalarınızı bu hizmete gönderecektir. Daha fazla bilgi için hizmetin &lt;a href=https://search.jabber.network/privacy&gt;Gizlilik Politikasına&lt;/a&gt; göz atın.</string>
+    <string name="outdated_backup_file_format">Artık desteklenmeyen bir yedek dosyası türünü geri yüklemeye çalışıyorsunuz</string>
+    <string name="pref_summary_security">Uçtan Uca Şifreleme, Doğrulamadan Körü Körüne Güven, MITM Algılama</string>
+    <string name="pref_category_engagement_notifications">Etkileşim bildirimleri</string>
+    <string name="pref_up_long_summary">UnifiedPush Dağıtıcısı olarak davranılırken devamlı, güvenli ve pil ömrü dostu olan XMPP bağlantısı diğer UnifiedPush ile uyumlu olan Tusky, Ltt.rs, FluffyChat ve benzeri uygulamaları uyandırmak için kullanılacaktır.</string>
 </resources>

src/main/res/values-uk/strings.xml 🔗

@@ -646,7 +646,7 @@
     <string name="pref_notifications_from_strangers">Сповіщення від незнайомців</string>
     <string name="pref_notifications_from_strangers_summary">Сповіщати про повідомлення і виклики від незнайомців.</string>
     <string name="received_message_from_stranger">Отримано повідомлення від незнайомця</string>
-    <string name="block_stranger">Заблокувати невідомий контакт</string>
+    <string name="block_stranger">Заблокувати незнайомців</string>
     <string name="block_entire_domain">Заблокувати весь домен</string>
     <string name="online_right_now">зараз у мережі</string>
     <string name="retry_decryption">Спробувати знову розшифрувати</string>

src/main/res/values-zh-rCN/strings.xml 🔗

@@ -1022,7 +1022,7 @@
     <string name="channel_discover_opt_in_message">频道发现使用称为 &lt;a href=https://search.jabber.network&gt;search.jabber.network&lt;/a&gt; 的第三方服务。&lt;br&gt;&lt;br&gt;使用此功能会将您的 IP 地址和搜索词传输到此服务。请参阅其 &lt;a href=https://search.jabber.network/privacy&gt;隐私政策&lt;/a&gt; 以获取更多信息。</string>
     <string name="start_chat">开始聊天</string>
     <string name="title_activity_share_with">分享至…</string>
-    <string name="pref_use_colorful_bubbles_summary">已发送和已接收消息的背景颜色各不相同</string>
+    <string name="pref_use_colorful_bubbles_summary">为已发送和已接收消息使用不同的背景颜色</string>
     <string name="no_certificate_selected">未选择客户端证书!</string>
     <string name="pref_use_colorful_bubbles">彩色聊天气泡</string>
     <string name="pref_dynamic_colors">动态色彩</string>

src/main/res/values-zh-rTW/strings.xml 🔗

@@ -78,12 +78,12 @@
     <string name="clear_conversation_history">清除會話記錄</string>
     <string name="delete_file_dialog">刪除檔案</string>
     <string name="choose_presence">選擇裝置</string>
-    <string name="send_unencrypted_message">傳送未加密的訊息</string>
+    <string name="send_unencrypted_message">傳送明文訊息</string>
     <string name="send_message">傳送訊息</string>
     <string name="send_message_to_x">傳送訊息至 %s</string>
     <string name="send_omemo_x509_message">傳送 v\\OMEMO 加密訊息</string>
     <string name="your_nick_has_been_changed">新暱稱已被使用</string>
-    <string name="send_unencrypted">不加密傳送</string>
+    <string name="send_unencrypted">傳送明文</string>
     <string name="decryption_failed">解密失敗,可能是私密金鑰不正確。</string>
     <string name="openkeychain_required">OpenKeychain</string>
     <string name="restart">重新啟動</string>
@@ -254,7 +254,7 @@
     <string name="request_now">立即要求</string>
     <string name="ignore">忽略</string>
     <string name="pref_security_settings">安全性</string>
-    <string name="pref_allow_message_correction">允許訊息修正</string>
+    <string name="pref_allow_message_correction">訊息修正</string>
     <string name="pref_allow_message_correction_summary">允許您的聯絡人追溯編輯他們的訊息</string>
     <string name="pref_expert_options">專家設定</string>
     <string name="pref_expert_options_summary">請謹慎使用</string>
@@ -290,8 +290,8 @@
     <string name="jabber_id_copied_to_clipboard">已複製 XMPP 位址到剪貼簿</string>
     <string name="error_message_copied_to_clipboard">已複製錯誤訊息到剪貼簿</string>
     <string name="web_address">網頁位址</string>
-    <string name="scan_qr_code">掃描二維條碼</string>
-    <string name="show_qr_code">顯示二維條碼</string>
+    <string name="scan_qr_code">掃描 QR 碼</string>
+    <string name="show_qr_code">顯示 QR 碼</string>
     <string name="show_block_list">顯示封鎖清單</string>
     <string name="account_details">帳戶詳細資料</string>
     <string name="confirm">確認</string>
@@ -441,8 +441,8 @@
     <string name="pref_dnd_on_silent_mode_summary">靜音模式時顯示為忙碌</string>
     <string name="pref_treat_vibrate_as_silent">靜音模式開啟震動</string>
     <string name="pref_treat_vibrate_as_dnd_summary">裝置震動時顯示為忙碌</string>
-    <string name="pref_show_connection_options">進階連線設定</string>
-    <string name="pref_show_connection_options_summary">註冊帳戶時顯示主機名稱和連接埠設定</string>
+    <string name="pref_show_connection_options">主機名和端口</string>
+    <string name="pref_show_connection_options_summary">註冊帳戶時顯示進階連線設定</string>
     <string name="hostname_example">xmpp.example.com</string>
     <string name="action_add_account_with_certificate">以憑證登入</string>
     <string name="unable_to_parse_certificate">無法解析憑證</string>
@@ -477,7 +477,7 @@
     <string name="shared_text_with_x">與 %s 分享的文字</string>
     <string name="no_storage_permission">授予 %1$s 外部儲存空間存取權</string>
     <string name="no_camera_permission">授予 %1$s 相機存取權</string>
-    <string name="sync_with_contacts">同步處理聯絡人</string>
+    <string name="sync_with_contacts">整合通訊錄</string>
     <string name="notify_on_all_messages">通知所有訊息</string>
     <string name="notify_only_when_highlighted">僅在被提及時通知</string>
     <string name="notify_never">通知已停用</string>
@@ -564,7 +564,7 @@
     <string name="share_as_http">分享為 HTTP 連結</string>
     <string name="pref_blind_trust_before_verification">驗證前盲目信任</string>
     <string name="not_trusted">未受信任</string>
-    <string name="invalid_barcode">無效的二維條碼</string>
+    <string name="invalid_barcode">無效的 QR 碼</string>
     <string name="pref_clean_cache">清理快取</string>
     <string name="pref_clean_private_storage">清理私人儲存空間</string>
     <string name="pref_clean_private_storage_summary">清理儲存檔案的私人空間 (檔案可從伺服器重新下載)</string>
@@ -641,7 +641,7 @@
     <string name="pref_omemo_setting">OMEMO 加密</string>
     <string name="pref_omemo_setting_summary_always">OMEMO 將一律用於一對一和私人群組聊天。</string>
     <string name="pref_omemo_setting_summary_default_on">OMEMO 將預設用於新會話。</string>
-    <string name="pref_omemo_setting_summary_default_off">OMEMO 將明確用於新會話。</string>
+    <string name="pref_omemo_setting_summary_default_off">OMEMO 將需要明確地用於新會話。</string>
     <string name="create_shortcut">建立捷徑</string>
     <string name="default_on">預設開啟</string>
     <string name="default_off">預設關閉</string>
@@ -669,7 +669,7 @@
     <string name="copy_jabber_id">複製 XMPP 位址</string>
     <string name="p1_s3_filetransfer">用於 S3 的 HTTP 檔案分享</string>
     <string name="pref_start_search">直接搜尋</string>
-    <string name="pref_start_search_summary">在「開始對話」畫面上開啟鍵盤並將遊標放在搜尋欄位</string>
+    <string name="pref_start_search_summary">在「開始會話」畫面上開啟鍵盤並將遊標放在搜尋欄位</string>
     <string name="group_chat_avatar">群組聊天頭像</string>
     <string name="host_does_not_support_group_chat_avatars">主機不支援群組聊天頭像</string>
     <string name="only_the_owner_can_change_group_chat_avatar">只有擁有者才能變更群組聊天頭像</string>
@@ -800,7 +800,7 @@
     <string name="attach">附加</string>
     <string name="discover_channels">探索頻道</string>
     <string name="search_channels">搜尋頻道</string>
-    <string name="channel_discovery_opt_in_title">可能違反隱私權!</string>
+    <string name="channel_discovery_opt_in_title">可能會侵犯隱私!</string>
     <string name="i_already_have_an_account">我已經有一個帳戶</string>
     <string name="add_existing_account">新增現有帳戶</string>
     <string name="register_new_account">註冊新帳戶</string>
@@ -905,7 +905,7 @@
     <string name="account_status_temporary_auth_failure">暫時驗證失敗</string>
     <string name="delete_avatar">刪除頭像</string>
     <string name="audio_video_disabled_tor">使用 Tor 時通話已停用</string>
-    <string name="pref_broadcast_last_activity_summary">在您使用 Conversations 時讓您的聯絡人知道</string>
+    <string name="pref_broadcast_last_activity_summary">讓您的聯絡人知道您最後使用此應用的時間</string>
     <string name="clear_histor_msg">您確定要刪除此會話中的所有訊息嗎? 
 \n 
 \n<b>警告:</b>這將不會影響儲存在其他裝置或伺服器上的訊息。</string>
@@ -935,16 +935,15 @@
 \n<small>前往「聯絡人詳細資料」以驗證您的線上狀態訂閱。</small></string>
     <string name="pref_autojoin_summary">加入或離開多使用者聊天時設定「自動加入」旗標,並回應其他用戶端所做的修改。</string>
     <string name="clear_other_devices_desc">您確定要從 OMEMO 宣告中清除所有裝置嗎?您的裝置在下次連線時將會重新宣告,但可能不會收到您傳送的訊息。</string>
-    <string name="sync_with_contacts_long">%1$s想要您通訊錄的存取權以將其與您的 XMPP 聯絡人清單相符。
-\n這將顯示您聯絡人的完整名稱和頭像。
+    <string name="sync_with_contacts_long">%1$s 在您的設備上本地處理您的通訊錄,以向您顯示 XMPP 上相符的聯絡人名稱和個人資料頭像。
 \n
-\n%1$s僅會讀取您的通訊錄並在本機進行相符處理,不會上傳任何內容至您的伺服器。</string>
+\n任何通訊錄數據都不會離開您的設備!</string>
     <string name="battery_optimizations_enabled_dialog">您的裝置正在為 %1$s 採用強力電池效能最佳化,這可能會導致通知延遲甚至訊息遺失。
 \n
 \n您將被要求將其停用。</string>
     <string name="data_saver_enabled_explained">您的作業系統正在限制 %1$s 在背景存取網際網路。若要接收新訊息的通知,您應該允許 %1$s 在「數據節省」開啟時無限制地存取。
 \n在可能的狀況下,%1$s 仍會努力地節省數據。</string>
-    <string name="pref_broadcast_last_activity">廣播使用</string>
+    <string name="pref_broadcast_last_activity">最後在線</string>
     <string name="delete_file_dialog_msg">您確定要刪除此檔案嗎?
 \n
 \n<b>警告:</b>這將不會影響儲存在其他裝置或伺服器上的檔案複本。 </string>
@@ -957,7 +956,7 @@
     <string name="pref_notification_grace_period_summary">在您的其他裝置上偵測到活動後,通知被靜音的時間長度。</string>
     <string name="pref_never_send_crash_summary">透過傳送堆疊追蹤,您可以協助開發</string>
     <string name="error_security_exception">您用來選取此圖像的應用程式沒有足夠的權限以讀取此檔案。</string>
-    <string name="unified_push_distributor">UnifiedPush 散發者</string>
+    <string name="unified_push_distributor">UnifiedPush 散發程序</string>
     <string name="pref_up_push_account_title">XMPP 帳戶</string>
     <string name="pref_up_push_server_title">推送伺服器</string>
     <string name="no_account_deactivated">無 (已停用)</string>
@@ -1000,11 +999,74 @@
     <string name="log_in">登入</string>
     <string name="hide_notification">隱藏通知</string>
     <string name="reconnect_on_other_host">在其他主機上重新連接</string>
-    <string name="contact_uses_unverified_keys">您的聯絡人使用未驗證的設備。掃描他們的二維條碼進行驗證,以防止主動中間人攻擊。</string>
+    <string name="contact_uses_unverified_keys">您的聯絡人使用未驗證的設備。請掃描他們的 QR 碼進行驗證,以防止主動中間人攻擊。</string>
     <string name="log_out">登出</string>
     <string name="outdated_backup_file_format">您正在嘗試匯入一個過時的備份檔案格式</string>
     <string name="account_state_logged_out">已登出</string>
-    <string name="unverified_devices">您正在使用未驗證的設備。掃描您其他設備上的二維條碼進行驗證,以防止主動中間人攻擊。</string>
+    <string name="unverified_devices">您正在使用未驗證的設備。請掃描您其他設備上的 QR 碼進行驗證,以防止主動中間人攻擊。</string>
     <string name="audiobook">有聲書</string>
     <string name="restore_warning_continued">請勿嘗試還原非您自己建立的備份!</string>
+    <string name="report_spam">報告垃圾訊息</string>
+    <string name="pref_send_crash_reports">發送崩潰報告</string>
+    <string name="corresponding_chats_closed">相應的會話已存檔。</string>
+    <string name="contact_list_integration_not_available">聯絡人整合不可用</string>
+    <string name="privacy_policy">隱私權政策</string>
+    <string name="report_spam_and_block">報告垃圾訊息並封鎖垃圾訊息發送者</string>
+    <string name="delete_and_close">刪除並存檔會話</string>
+    <string name="pref_title_interface">用戶界面</string>
+    <string name="no_certificate_selected">未選擇用戶端證書!</string>
+    <string name="pref_title_security">安全性</string>
+    <string name="pref_attachments_summary">文件大小、圖片壓縮、影片質量</string>
+    <string name="unified_push_summary">兼容 UnifiedPush 的第三方應用的通知轉發器</string>
+    <string name="notifications">通知</string>
+    <string name="pref_title_trust_system_ca_store">證書頒發機構</string>
+    <string name="pref_title_trust_system_ca_store_summary">信任系統的 CA 證書</string>
+    <string name="detect_mim">需要通道綁定</string>
+    <string name="detect_mim_summary">通道綁定可以檢測某些中間人攻擊</string>
+    <string name="pref_category_server_connection">伺服器連接</string>
+    <string name="pref_category_operating_system">作業系統</string>
+    <string name="pref_privacy_summary">輸入通知、最後在線、在線狀態</string>
+    <string name="pref_connection_summary">主機名和端口、Tor</string>
+    <string name="pref_connection_summary_w_cd">主機名和端口、Tor、頻道探索</string>
+    <string name="pref_keyboard_options">鍵盤</string>
+    <string name="pref_category_engagement_notifications">參與通知</string>
+    <string name="pref_category_application">應用程序</string>
+    <string name="pref_category_interaction">互動</string>
+    <string name="pref_summary_security">端對端加密、驗證前盲目信任、中間人攻擊檢測</string>
+    <string name="title_activity_share_with">分享至…</string>
+    <string name="title_activity_new_chat">新會話</string>
+    <string name="action_archive_chat">存檔會話</string>
+    <string name="archive_this_chat">存檔此會話</string>
+    <string name="switch_to_chat">切換到會話</string>
+    <string name="send_encrypted_message">傳送加密消息</string>
+    <string name="quicksy_wants_your_consent">Quicksy 請求您同意使用您的數據</string>
+    <string name="welcome_header">參與會話</string>
+    <string name="welcome_header_quicksy">歡迎使用 Quicksy!</string>
+    <string name="pref_use_colorful_bubbles">彩色聊天氣泡</string>
+    <string name="no_permission_to_place_call">沒有撥打電話的權限</string>
+    <string name="barcode_does_not_contain_fingerprints_for_this_chat">二維碼並不包含此會話的指紋。</string>
+    <string name="pref_dynamic_colors_summary">系統色彩 (Material You)</string>
+    <string name="rtp_state_contact_offline">聯絡人不可用</string>
+    <string name="call_integration_not_available">通話整合不可用!</string>
+    <string name="start_chat">開始會話</string>
+    <string name="pref_summary_appearance">主題、配色、截屏、輸入</string>
+    <string name="pref_notifications_summary">靜默期、鈴聲、震動、陌生人</string>
+    <string name="pref_category_sending">發送</string>
+    <string name="pref_category_receiving">接收</string>
+    <string name="pref_automatic_download">自動下載</string>
+    <string name="appearance">外觀</string>
+    <string name="pref_light_dark_mode">淺色/深色模式</string>
+    <string name="pref_allow_screenshots">允許截屏</string>
+    <string name="pref_allow_screenshots_summary">在切換應用時顯示應用內容並允許截屏</string>
+    <string name="pref_category_e2ee">端對端加密</string>
+    <string name="pref_category_on_this_device">在設備上</string>
+    <string name="pref_up_long_summary">在充當 UnifiedPush 散發者時,將利用持久、可靠且省電的 XMPP 連接來喚醒其他兼容 UnifiedPush 的應用,如 Tusky、Ltt.rs、FluffyChat 等。</string>
+    <string name="title_undo_swipe_out_chat">會話已存檔</string>
+    <string name="pref_use_colorful_bubbles_summary">為已發送和已接收訊息使用個別的背景顏色</string>
+    <string name="pref_large_font">大字體</string>
+    <string name="pref_large_font_summary">增加訊息氣泡中的字體大小</string>
+    <string name="remove_bookmark">是否移除 %s 的書籤?</string>
+    <string name="remove_bookmark_and_close">是否移除 %s 的書籤並存檔會話?</string>
+    <string name="pref_dynamic_colors">自動配色</string>
+    <string name="channel_discover_opt_in_message">頻道探索使用一個名為 &lt;a href=https://search.jabber.network&gt;search.jabber.network&lt;/a&gt; 的第三方服務,&lt;br&gt;&lt;br&gt;使用此功能會將您的 IP 位址和搜尋詞彙傳送至此服務。更多資訊請參見他們的 &lt;a href=https://search.jabber.network/privacy&gt;隱私權政策&lt;/a&gt;。</string>
 </resources>

src/main/res/values/arrays.xml 🔗

@@ -86,6 +86,13 @@
 		<item>2592000</item>
 		<item>15811200</item>
 	</integer-array>
+	<integer-array name="recurring_backup_values">
+		<item>0</item>
+		<item>86400</item>
+		<item>172800</item>
+		<item>604800</item>
+		<item>2592000</item>
+	</integer-array>
 	<string-array name="omemo_setting_entry_values">
 		<item>always</item>
 		<item>default_on</item>

src/main/res/values/strings.xml 🔗

@@ -1066,5 +1066,10 @@
     <string name="pref_up_long_summary">When acting as a UnifiedPush Distributor the persistent, reliable and battery-friendly XMPP connection will be utilized to wake up other UnifiedPush compatible app such as Tusky, Ltt.rs, FluffyChat and more.</string>
     <string name="pref_large_font">Large font</string>
     <string name="pref_large_font_summary">Increase font size in message bubbles</string>
-
+    <string name="pref_backup_summary">Create one-off, Schedule recurring</string>
+    <string name="pref_create_backup_one_off_summary">Create one-off backup</string>
+    <string name="pref_backup_recurring">Recurring backup</string>
+    <string name="pref_fullscreen_notification">Full screen notifications</string>
+    <string name="pref_fullscreen_notification_summary">Allow this app to show incoming call notifications that take up the full screen when the device is locked.</string>
+    <string name="unsupported_operation">Unsupported operation</string>
 </resources>

src/main/res/xml/preferences_backup.xml 🔗

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <ListPreference
+        android:defaultValue="@integer/automatic_message_deletion"
+        android:icon="@drawable/ic_calendar_month_24dp"
+        android:key="recurring_backup"
+        android:title="@string/pref_backup_recurring" />
+
+    <Preference
+        android:icon="@drawable/ic_archive_24dp"
+        android:key="create_one_off_backup"
+        android:summary="@string/pref_create_backup_one_off_summary"
+        android:title="@string/pref_create_backup" />
+
+    <Preference
+        android:key="backup_directory"
+        android:summary="@string/pref_create_backup_summary" />
+
+</PreferenceScreen>

src/main/res/xml/preferences_main.xml 🔗

@@ -34,9 +34,10 @@
         app:title="@string/pref_connection_options" />
     <Preference
         android:icon="@drawable/ic_archive_24dp"
-        android:key="create_backup"
-        android:summary="@string/pref_create_backup_summary"
-        android:title="@string/pref_create_backup" />
+        android:key="backup"
+        app:fragment="eu.siacs.conversations.ui.fragment.settings.BackupSettingsFragment"
+        android:summary="@string/pref_backup_summary"
+        android:title="@string/backup" />
     <Preference
         android:icon="@drawable/ic_cloud_sync_24dp"
         app:fragment="eu.siacs.conversations.ui.fragment.settings.UpSettingsFragment"

src/main/res/xml/preferences_notifications.xml 🔗

@@ -52,6 +52,11 @@
         android:key="dialler_integration_incoming"
         android:summary="@string/pref_dialler_integration_incoming_summary"
         android:title="@string/pref_dialler_integration_incoming" />
+    <Preference
+        android:icon="@drawable/ic_smartphone_24dp"
+        android:key="fullscreen_notification"
+        android:summary="@string/pref_fullscreen_notification_summary"
+        android:title="@string/pref_fullscreen_notification" />
     <ListPreference
         android:defaultValue="@integer/grace_period"
         android:entries="@array/grace_periods"

src/playstore/AndroidManifest.xml 🔗

@@ -24,7 +24,7 @@
         </receiver>
 
         <service
-            android:name=".services.PushMessageReceiver"
+            android:name=".receiver.PushMessageReceiver"
             android:exported="false">
             <intent-filter>
                 <action android:name="com.google.firebase.MESSAGING_EVENT" />

src/playstore/java/eu/siacs/conversations/services/PushMessageReceiver.java → src/playstore/java/eu/siacs/conversations/receiver/PushMessageReceiver.java 🔗

@@ -1,21 +1,24 @@
-package eu.siacs.conversations.services;
+package eu.siacs.conversations.receiver;
 
 import android.content.Intent;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
+
 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.services.XmppConnectionService;
 import eu.siacs.conversations.utils.Compatibility;
 
 public class PushMessageReceiver extends FirebaseMessagingService {
 
 	@Override
-	public void onMessageReceived(RemoteMessage message) {
-		if (!EventReceiver.hasEnabledAccounts(this)) {
+	public void onMessageReceived(@NonNull final RemoteMessage message) {
+		if (!SystemEventReceiver.hasEnabledAccounts(this)) {
 			Log.d(Config.LOGTAG,"PushMessageReceiver ignored message because no accounts are enabled");
 			return;
 		}
@@ -27,8 +30,8 @@ public class PushMessageReceiver extends FirebaseMessagingService {
 	}
 
 	@Override
-	public void onNewToken(String token) {
-		if (!EventReceiver.hasEnabledAccounts(this)) {
+	public void onNewToken(@NonNull final String token) {
+		if (!SystemEventReceiver.hasEnabledAccounts(this)) {
 			Log.d(Config.LOGTAG,"PushMessageReceiver ignored new token because no accounts are enabled");
 			return;
 		}

src/quicksy/res/values-pl/strings.xml 🔗

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
     <string name="pref_notification_grace_period_summary">Czas, przez który Quicksy jest cicho po zobaczeniu aktywności na innym urządzeniu</string>
-    <string name="pref_never_send_crash_summary">Wysyłając nam ślady stosu pomagasz w rozwoju Quicksy</string>
+    <string name="pref_never_send_crash_summary">Wysyłając ślady stosu pomagasz w ciągłym rozwoju Quicksy</string>
     <string name="pref_broadcast_last_activity_summary">Powiadom kontakty o tym że używasz Quicksy</string>
     <string name="huawei_protected_apps_summary">Aby otrzymywać powiadomienia nawet kiedy ekran jest wyłączony musisz dodać Quicksy do listy chronionych aplikacji.</string>
     <string name="set_profile_picture">Obrazek profilowy Quicksy</string>